GPT美化UI

<!DOCTYPE html><html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>股票面板</title><style>*{margin:0;padding:0;box-sizing:border-box;font-family:"Microsoft YaHei","PingFang SC",sans-serif}html,body{ width:100%;height:100%; margin:0;overflow:hidden; background:transparent; user-select:none; -webkit-font-smoothing:antialiased; text-rendering:optimizeLegibility;}body{ color:#eaf2ff;}button,input,select,textarea{ font:inherit;}button{cursor:pointer}#app{ width:100%;height:100%; display:flex;flex-direction:column; padding:10px; gap:7px; overflow:hidden; background: radial-gradient(120% 120% at 0% 0%, rgba(97,215,255,.12), transparent 45%), radial-gradient(120% 120% at 100% 0%, rgba(78,149,255,.15), transparent 42%), linear-gradient(180deg, rgba(11,17,30,.97), rgba(8,12,20,.98)); border-radius:18px; border:1px solid rgba(173,204,255,.16); box-shadow:inset 0 1px 0 rgba(255,255,255,.06), 0 12px 36px rgba(0,0,0,.34);}#app *{box-sizing:border-box}#app::before{ content:""; position:absolute; inset:0; pointer-events:none; border-radius:18px; box-shadow:inset 0 1px 0 rgba(255,255,255,.05), inset 0 -1px 0 rgba(0,0,0,.22);}#indexBar{ position:relative; display:flex; gap:8px; height:58px; padding:0; flex-shrink:0; background:transparent; color:#eaf2ff;}.index-item{ flex:1; display:flex; flex-direction:column; align-items:center; justify-content:center; cursor:pointer; position:relative; padding:5px 6px; border-radius:14px; border:1px solid rgba(173,204,255,.12); background:linear-gradient(180deg, rgba(255,255,255,.05), rgba(255,255,255,.025)); box-shadow:inset 0 1px 0 rgba(255,255,255,.03); transition:transform .15s ease, border-color .15s ease, background .15s ease;}.index-item:hover{ transform:translateY(-1px); border-color:rgba(97,215,255,.28); background:linear-gradient(180deg, rgba(97,215,255,.09), rgba(255,255,255,.03));}.index-item.active{ border-color:rgba(97,215,255,.50); background:linear-gradient(180deg, rgba(97,215,255,.14), rgba(78,149,255,.08));}.index-item:not(:last-child)::after{display:none}.index-name{font-size:9px;color:#93a7c8;margin-bottom:2px;line-height:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.index-price{font-size:15px;font-weight:800;line-height:1.1;color:#f4f8ff}.index-change{font-size:9px;margin-top:1px;font-weight:700}.index-more{ position:absolute;right:4px;top:-2px; font-size:10px;color:#8ca0c0;cursor:pointer; background:rgba(10,16,28,.82); border:1px solid rgba(173,204,255,.14); border-radius:999px; padding:3px 7px; backdrop-filter:blur(4px);}#toolBar{ display:flex;align-items:center;gap:6px; flex-shrink:0; padding:0; background:transparent; border-bottom:none; min-height:26px;}#groupTabs{ display:flex;align-items:center;gap:6px;flex:1;overflow-x:auto;overflow-y:hidden;padding-bottom:2px; scrollbar-width:thin;scrollbar-color:rgba(118,143,182,.72) transparent;}#groupTabs::-webkit-scrollbar{height:4px}#groupTabs::-webkit-scrollbar-thumb{background:rgba(118,143,182,.55);border-radius:999px}.group-tab{ font-size:10px; padding:5px 10px; border:1px solid rgba(173,204,255,.12); border-radius:999px; cursor:pointer; color:#b8c7e4; background:rgba(255,255,255,.03); white-space:nowrap; transition:all .15s ease;}.group-tab:hover{ background:rgba(97,215,255,.08); border-color:rgba(97,215,255,.28); color:#fff; transform:translateY(-1px);}.group-tab.active{ background:linear-gradient(180deg, rgba(97,215,255,.18), rgba(78,149,255,.12)); color:#fff; border-color:rgba(97,215,255,.55); box-shadow:0 0 0 1px rgba(97,215,255,.06) inset;}.group-tab.editing{outline:1px solid rgba(97,215,255,.55);cursor:text;min-width:40px}.group-add{ font-size:14px;width:22px;height:22px;display:flex;align-items:center;justify-content:center; border:1px dashed rgba(173,204,255,.30); border-radius:999px; cursor:pointer; color:#b8c7e4; flex-shrink:0; background:rgba(255,255,255,.02); transition:all .15s ease;}.group-add:hover{border-color:rgba(97,215,255,.45);color:#fff;background:rgba(97,215,255,.08);transform:translateY(-1px)}
#toolBar .btn{ font-size:10px;padding:5px 9px;border:1px solid rgba(173,204,255,.14);border-radius:10px; background:linear-gradient(180deg, rgba(255,255,255,.05), rgba(255,255,255,.025)); color:#d9e5fa;cursor:pointer;transition:all .15s ease;}#toolBar .btn:hover{background:rgba(97,215,255,.08);border-color:rgba(97,215,255,.28);color:#fff;transform:translateY(-1px)}#toolBar .btn.active{background:linear-gradient(180deg, rgba(97,215,255,.18), rgba(78,149,255,.12));color:#fff;border-color:rgba(97,215,255,.45)}#toolBar .btn.primary{background:linear-gradient(180deg, rgba(97,215,255,.20), rgba(78,149,255,.12));color:#fff;border-color:rgba(97,215,255,.42)}#toolBar .btn.primary:hover{filter:brightness(1.08)}
#stockArea{ flex:1; min-height:0; overflow-y:auto; overflow-x:hidden; background:rgba(255,255,255,.02); border:1px solid rgba(173,204,255,.08); border-radius:14px; scrollbar-width:thin; scrollbar-color:rgba(118,143,182,.72) rgba(255,255,255,.02);}#stockArea::-webkit-scrollbar{width:5px}#stockArea::-webkit-scrollbar-thumb{background:rgba(118,143,182,.55);border-radius:999px}#stockArea::-webkit-scrollbar-track{background:rgba(255,255,255,.02)}#listHeader{ display:flex; padding:0 8px; min-height:24px; align-items:center; font-size:10px; color:#92a5c4; background:transparent; border-bottom:1px solid rgba(173,204,255,.08); flex-shrink:0;}#listHeader .col-name{flex:2;min-width:0}#listHeader .col-price{flex:1.2;text-align:right;cursor:pointer}#listHeader .col-price:hover{color:#fff}#listHeader .col-change{flex:1;text-align:right;cursor:pointer}#listHeader .col-change:hover{color:#fff}#listHeader .col-change-pct{flex:1.05;text-align:right;cursor:pointer}#listHeader .col-change-pct:hover{color:#fff}#listHeader .col-turnover{flex:0.9;text-align:right;padding-right:4px}.sort-icon{font-size:7px;color:#7287a8;margin-left:2px;display:inline-block;vertical-align:middle;line-height:1}.stock-row{ display:flex;align-items:center; padding:0 8px; min-height:32px; font-size:11px; border-bottom:1px solid rgba(173,204,255,.06); cursor:pointer; transition:background .12s ease; position:relative;}.stock-row:hover{background:rgba(97,215,255,.06)}.stock-row .col-name{ flex:2;min-width:0;display:flex;align-items:center;gap:4px;overflow:hidden}.stock-row .col-name .expand-btn{ display:inline-flex;align-items:center;justify-content:center;width:13px;height:13px;font-size:10px;color:#9db0cf; transition:transform .2s;flex-shrink:0;cursor:pointer;border-radius:3px;background:rgba(255,255,255,.03);}.stock-row .col-name .expand-btn:hover{background:rgba(97,215,255,.10);color:#fff}.stock-row .col-name .expand-btn.expanded{transform:rotate(90deg);color:#61d7ff}.stock-row .col-name .sname{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#eef5ff}.stock-row .col-price{flex:1.2;text-align:right;font-weight:700;color:#f5f8ff}.stock-row .col-change{flex:1;text-align:right}.stock-row .col-change-pct{flex:1.05;text-align:right}.stock-row .col-turnover{flex:0.9;text-align:right;color:#a7b8d5;padding-right:4px}.stock-row .color-bg{ display:inline-block;padding:0 6px;border-radius:999px;font-size:10px;line-height:16px; min-width:58px;text-align:right;}.stock-row .color-bg.change-val{min-width:54px}.stock-row .color-bg.red{background:rgba(255,106,106,.18);color:#ff8b8b;border:1px solid rgba(255,106,106,.16)}.stock-row .color-bg.green{background:rgba(71,211,139,.18);color:#8fe9b8;border:1px solid rgba(71,211,139,.16)}.stock-row .color-bg.gray{background:rgba(255,255,255,.05);color:#cad7ec;border:1px solid rgba(173,204,255,.08)}.stock-row .text-red{color:#ff8b8b}.stock-row .text-green{color:#8fe9b8}.stock-row .text-gray{color:#8ca0c0}.detail-panel{ background:linear-gradient(180deg, rgba(255,255,255,.03), rgba(255,255,255,.02)); border-bottom:1px solid rgba(173,204,255,.08); display:none; padding:6px 8px 8px;}.detail-panel.active{display:block}.detail-tabs{ display:flex; padding:0; gap:4px; margin-bottom:6px; flex-wrap:wrap;}.detail-tab{ font-size:9px; padding:4px 10px; border:1px solid rgba(173,204,255,.12); border-radius:999px; background:rgba(255,255,255,.03); color:#b8c7e4; cursor:pointer; transition:all .15s ease;}.detail-tab.active{ background:linear-gradient(180deg, rgba(97,215,255,.18), rgba(78,149,255,.12))!important; border-color:rgba(97,215,255,.48)!important; color:#fff!important; font-weight:700;}.detail-tab:hover:not(.active){background:rgba(97,215,255,.08);color:#fff}.detail-chart{ width:100%; height:120px; display:block; border-radius:12px; background:linear-gradient(180deg, rgba(6,10,18,.96), rgba(12,18,32,.92)); border:1px solid rgba(173,204,255,.08);}.profitBar{ display:flex; flex-direction:column; align-items:flex-start; justify-content:center; min-height:28px; background:transparent; border-top:1px solid rgba(173,204,255,.08); font-size:11px; color:#d8e6ff; flex-shrink:0; gap:2px; padding:4px 8px 0;}.edit-stock-row:hover{background:rgba(97,215,255,.06)}.edit-stock-row .drag-handle:hover{color:#61d7ff}#editStockList{ scrollbar-width:thin; scrollbar-color:rgba(118,143,182,.72) transparent;}#editStockList::-webkit-scrollbar{width:5px}#editStockList::-webkit-scrollbar-thumb{background:rgba(118,143,182,.55);border-radius:999px}
.modal-overlay{ display:none;position:fixed;top:0;left:0;right:0;bottom:0; background:rgba(3,7,15,.56); z-index:100;justify-content:center;align-items:flex-start;padding-top:18px; backdrop-filter:blur(4px);}.modal-overlay.active{display:flex}.modal-box{ background:linear-gradient(180deg, rgba(16,25,42,.98), rgba(9,14,24,.99)); border-radius:16px; padding:12px; width:calc(100% - 18px); max-width:none; max-height:calc(100vh - 24px); border:1px solid rgba(173,204,255,.16); box-shadow:0 18px 44px rgba(0,0,0,.34); display:flex; flex-direction:column; overflow:hidden;}.modal-title{ font-size:13px; font-weight:800; color:#fff; margin-bottom:10px;}.modal-input{ width:100%; padding:8px 10px; border:1px solid rgba(173,204,255,.14); border-radius:12px; font-size:12px; outline:none; color:#fff; background:rgba(255,255,255,.03); transition:border-color .2s, box-shadow .2s, background .2s;}.modal-input:focus{ border-color:rgba(97,215,255,.55); box-shadow:0 0 0 3px rgba(97,215,255,.12); background:rgba(255,255,255,.04);}.modal-search-results{ max-height:180px; min-height:40px; overflow-y:auto; margin-top:8px; border:1px solid rgba(173,204,255,.10); border-radius:12px; display:none; background:rgba(255,255,255,.02); scrollbar-width:thin; scrollbar-color:rgba(118,143,182,.72) transparent;}.modal-search-results::-webkit-scrollbar{width:5px}.modal-search-results::-webkit-scrollbar-thumb{background:rgba(118,143,182,.55);border-radius:999px}.modal-search-results.active{display:block}.search-item{ display:flex;align-items:center;padding:8px 10px;font-size:10px; border-bottom:1px solid rgba(173,204,255,.06); cursor:pointer; transition:background .1s;}.search-item:hover{background:rgba(97,215,255,.08)}.search-item .scode{color:#61d7ff;font-weight:700;margin-right:8px;min-width:60px}.search-item .sname{color:#eef5ff;flex:1}.search-item .stag{ font-size:9px;color:#c6d4ea;background:rgba(255,255,255,.04); padding:1px 6px;border-radius:999px;border:1px solid rgba(173,204,255,.08)}.modal-btns{ display:flex;gap:8px;margin-top:10px}.modal-btns .mbtn{ flex:1;padding:8px 0;border:none;border-radius:10px;font-size:10px;cursor:pointer; transition:all .15s ease;}.modal-btns .mbtn:hover{transform:translateY(-1px)}.modal-btns .mbtn.cancel{background:rgba(255,255,255,.05);color:#d8e6ff;border:1px solid rgba(173,204,255,.12)}.modal-btns .mbtn.confirm{background:linear-gradient(180deg, rgba(97,215,255,.20), rgba(78,149,255,.12));color:#fff;border:1px solid rgba(97,215,255,.42)}.modal-btns .mbtn.danger{background:linear-gradient(180deg, rgba(255,106,106,.22), rgba(255,106,106,.10));color:#fff;border:1px solid rgba(255,106,106,.38)}.loading-text{ text-align:left; padding:40px 0; font-size:10px; color:#95a8c9;}.loading-text .spin{ display:inline-block;width:18px;height:18px;border:2px solid rgba(173,204,255,.18); border-top-color:#61d7ff;border-radius:50%;animation:spin .8s linear infinite;margin-bottom:8px}@keyframes spin{to{transform:rotate(360deg)}}.ctx-menu{ display:none;position:fixed;z-index:200; background:linear-gradient(180deg, rgba(16,25,42,.98), rgba(9,14,24,.99)); border:1px solid rgba(173,204,255,.14); border-radius:12px; box-shadow:0 10px 28px rgba(0,0,0,.30); padding:4px 0; min-width:110px; font-size:12px; backdrop-filter:blur(4px);}.ctx-menu.show{display:block}.ctx-menu-item{padding:8px 14px;cursor:pointer;color:#e7f0ff;transition:background .1s}.ctx-menu-item:hover{background:rgba(97,215,255,.08);color:#fff}.ctx-menu-item.danger{color:#ff8b8b}.ctx-menu-item.danger:hover{background:rgba(255,106,106,.10)}.ctx-menu-sep{height:1px;background:rgba(173,204,255,.10);margin:4px 0}.stock-row.liked{ background:linear-gradient(90deg, rgba(255,106,106,.10), rgba(255,255,255,.02)); border-left:3px solid rgba(255,106,106,.85);}.stock-row.liked .sname{color:#ff8b8b;font-weight:700}#addModal .modal-box,#addGroupModal .modal-box,#delGroupModal .modal-box,#alertModal .modal-box,#editModal .modal-box{ width:calc(100% - 18px);}#indexChartBox{ display:none; background:linear-gradient(180deg, rgba(255,255,255,.03), rgba(255,255,255,.02)) !important; padding:8px 10px; border-bottom:1px solid rgba(173,204,255,.08);}#indexChartInfo{color:#91a4c3 !important}#editList3{ max-height:320px; overflow-y:auto; border:1px solid rgba(173,204,255,.10) !important; border-radius:12px !important; background:rgba(255,255,255,.02); scrollbar-width:thin; scrollbar-color:rgba(118,143,182,.72) transparent;}#editList3::-webkit-scrollbar{width:5px}#editList3::-webkit-scrollbar-thumb{background:rgba(118,143,182,.55);border-radius:999px}#editMoveMenu{ display:none; position:absolute; bottom:100%; right:0; margin-bottom:4px; background:linear-gradient(180deg, rgba(16,25,42,.98), rgba(9,14,24,.99)) !important; border:1px solid rgba(173,204,255,.14) !important; border-radius:10px !important; box-shadow:0 10px 28px rgba(0,0,0,.30) !important; padding:4px 0; min-width:100px; z-index:10;}#stockArea .detail-panel .detail-tab#detailBtn_0,#stockArea .detail-panel .detail-tab[id^="detailBtn_"]{ margin-left:4px;}span[style*="color:#999"], div[style*="color:#999"], td[style*="color:#999"]{ color:#93a7c8 !important;}</style></head><body><div id="app"><div class="ctx-menu" id="groupCtxMenu"> <div class="ctx-menu-item" onclick="ctxEditGroup()">编辑</div> <div class="ctx-menu-sep"></div> <div class="ctx-menu-item danger" onclick="ctxDelGroup()">删除</div></div><div class="ctx-menu" id="stockCtxMenu"> <div class="ctx-menu-item" id="ctxLikeBtn" onclick="ctxToggleLike()">★ 关注</div> <div class="ctx-menu-item" onclick="ctxSetAlert()">🔔 提醒</div></div><div class="modal-overlay" id="alertModal"> <div class="modal-box"> <div class="modal-title" id="alertModalTitle">设置提醒 - </div> <div id="alertExistList" style="margin-bottom:8px;max-height:120px;overflow-y:auto"></div> <div style="display:flex;gap:6px;align-items:center;margin-bottom:6px"> <select id="alertType" onchange="updateAlertUnit()" style="flex:1;padding:5px 8px;border:1px solid #ddd;border-radius:4px;font-size:11px"> <option value="price_up">股价高于</option> <option value="price_down">股价低于</option> <option value="pct_up">涨幅超过</option> <option value="pct_down">跌幅超过</option> </select> <input class="modal-input" id="alertValue" type="number" step="0.01" placeholder="数值" style="flex:0.7;padding:5px 8px;font-size:11px"> <span style="font-size:11px;color:#999" id="alertUnit">元</span> </div> <div class="modal-btns"> <button class="mbtn cancel" onclick="hideModal('alert')">取消</button> <button class="mbtn confirm" onclick="doAddAlert()">添加</button> </div> </div></div> <div id="indexBar"> <div class="index-item" data-idx="0"><div class="index-name">上证指数</div><div class="index-price">--</div><div class="index-change">--</div></div> <div class="index-item" data-idx="1"><div class="index-name">深证成指</div><div class="index-price">--</div><div class="index-change">--</div></div> <div class="index-item" data-idx="2"><div class="index-name">创业板指</div><div class="index-price">--</div><div class="index-change">--</div></div> <div class="index-more">更多 ▾</div> </div> <div id="indexChartBox" style="display:none;background:#fff;padding:8px 10px;border-bottom:1px solid #eee"> <div style="display:flex;align-items:center;gap:8px;margin-bottom:4px"> <span id="indexChartName" style="font-size:10px;font-weight:600"></span> <span id="indexChartInfo" style="font-size:9px;color:#999"></span> </div> <canvas id="indexChartCv" width="800" height="320" style="width:100%;height:160px"></canvas> </div> <div id="toolBar"> <div id="groupTabs"></div> <button class="btn" onclick="showAddModal()">+ 添加</button> <button class="btn" onclick="openEdit()">✎ 编辑</button> </div> <div id="listHeader"> <div class="col-name"><span style="font-size:16px">股票名称</span> <span style="font-size:14px;color:#999">股票代码</span></div> <div class="col-price" onclick="sortBy('price')">市价<span class="sort-icon">↕</span></div> <div class="col-change" onclick="sortBy('change')">涨跌额<span class="sort-icon">↕</span></div> <div class="col-change-pct" onclick="sortBy('changePct')">涨跌幅<span class="sort-icon">↕</span></div> <div class="col-turnover">换手率</div> </div> <div id="stockArea"> <div class="loading-text"><div class="spin"></div><div>加载中...</div></div> </div> <div id="profitBar"> <table id="statsTable" style="border:none;border-collapse:collapse;font-size:14px;width:auto;margin-left:5px"> <tr><td id="gsLabel" style="border:none;color:#999">自选概况:</td><td style="border:none;color:#f56c6c">上涨家数:</td><td id="gsUp" style="border:none;color:#f56c6c;text-align:left;min-width:22px">0</td><td style="border:none;color:#67c23a">下跌家数:</td><td id="gsDown" style="border:none;color:#67c23a;text-align:left;min-width:22px">0</td></tr> <tr><td id="mkLabel" style="border:none;color:#999">市场概况:</td><td style="border:none;color:#f56c6c">上涨家数:</td><td id="mkUp" style="border:none;color:#f56c6c;text-align:left;min-width:22px">0</td><td style="border:none;color:#67c23a">下跌家数:</td><td id="mkDown" style="border:none;color:#67c23a;text-align:left;min-width:22px">0</td></tr> </table> </div>
</div><div class="modal-overlay" id="addModal"> <div class="modal-box"> <div class="modal-title" style="display:flex;align-items:center;justify-content:space-between"> <span>添加股票</span> <span class="mbtn" style="font-size:10px;padding:3px 8px;background:#e8f4fd;color:#409eff;border-radius:4px;cursor:pointer" onclick="toggleBatchImport()">批量导入</span> </div> <input class="modal-input" id="searchInput" placeholder="输入股票代码或名称搜索..." autocomplete="off"> <div class="modal-search-results" id="searchResults"></div> <div id="batchImportArea" style="display:none;margin-top:8px"> <textarea id="batchInput" class="modal-input" style="height:120px;resize:vertical;font-size:10px" placeholder="每行一个股票代码或名称,例如: 000001 平安银行 600519 贵州茅台"></textarea> <div style="margin-top:6px;text-align:right"> <button class="mbtn confirm" style="font-size:10px;padding:4px 12px" onclick="doBatchImport()">批量导入</button> </div> </div>
</div></div><div class="modal-overlay" id="addGroupModal"> <div class="modal-box"> <div class="modal-title">新建分组</div> <input class="modal-input" id="newGroupNameInput" placeholder="请输入新分组名称" onkeydown="onAddGroupKey(event)"> <div class="modal-btns"> <button class="mbtn cancel" onclick="hideModal('addGroup')">取消</button> <button class="mbtn confirm" onclick="confirmAddGroup()">确定</button> </div> </div></div>
<div class="modal-overlay" id="delGroupModal"> <div class="modal-box" style="max-width:340px"> <div class="modal-title" id="delGroupTitle">删除分组</div> <div id="delGroupBody" style="font-size:12px;color:#666;margin-bottom:10px"></div> <div id="delGroupMoveArea" style="display:none;margin-bottom:10px"> <div style="font-size:11px;color:#999;margin-bottom:6px">将组内股票移动到:</div> <select id="delGroupTarget" class="modal-input" style="cursor:pointer"></select> </div> <div class="modal-btns"> <button class="mbtn cancel" id="delGroupMoveBtn" onclick="doMoveAndDel()">移动后删除</button> <button class="mbtn danger" id="delGroupConfirmBtn" onclick="doDirectDel()">直接删除</button> </div> </div></div>
<script>// ============ 配置 ============var INDEX_CODES = ['sh000001','sz399001','sz399006'];var STOCKS = [];var STOCK_CACHE = {};var IDX_CACHE = {};var GROUPS = ['默认分组'];var CUR_GROUP = '默认分组';var PENDING = null;var REFRESHING = false;var TIMER = null;
var _toastBackdrop = null;function showToast(msg) { var t = document.getElementById('toast'); if (!t) { try { window.alert(msg); } catch(e){} return; } hideToast(); t.textContent = msg; t.style.display = 'block'; var bd = document.createElement('div'); bd.id = '_toast_bd'; bd.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;z-index:99998;'; bd.onclick = function() { hideToast(); }; document.body.appendChild(bd); _toastBackdrop = bd;}function hideToast() { var t = document.getElementById('toast'); if (t) t.style.display = 'none'; if (_toastBackdrop && _toastBackdrop.parentNode) { _toastBackdrop.parentNode.removeChild(_toastBackdrop); _toastBackdrop = null; }}
var EXPANDED = {};var INDEX_EXPANDED = null;var CHART_TYPE = {};var SORT_FIELD = null;var SORT_ASC = false;var INTERVAL = 1000;var ALERTS = [];// 分时数据缓存var MINUTE_CACHE = {};var MINUTE_TTL = 30000; // 缓存30秒// 行情数据备份(用于快速更新)var LATEST = {};
var DEFAULT_STOCKS = [];;
// ============ JSONP工具(搜索用) ============var _jpCnt = 0;function jsonp(url, timeout) { return new Promise(function(resolve, reject) { timeout = timeout || 10000; _jpCnt++; var cb = 'jp' + _jpCnt + '_' + Date.now(); var sep = url.indexOf('?') >= 0 ? '&' : '?'; var timer = setTimeout(function() { cleanup(); reject(new Error('timeout')); }, timeout); function cleanup() { clearTimeout(timer); delete window[cb]; var s = document.getElementById('_js_' + cb); if (s && s.parentNode) s.parentNode.removeChild(s); } window[cb] = function(d) { cleanup(); resolve(d); }; var s = document.createElement('script'); s.id = '_js_' + cb; s.src = url + sep + 'callback=' + cb; s.onerror = function() { cleanup(); reject(new Error('error')); }; document.head.appendChild(s); });}
// ============ fetch工具(兼容旧WebView2) ============function fetchWithTimeout(url, ms) { return new Promise(function(resolve, reject) { var controller = new AbortController(); var timer = setTimeout(function() { controller.abort(); }, ms || 6000); fetch(url, { signal: controller.signal }).then(function(r) { clearTimeout(timer); resolve(r); }).catch(function(e) { clearTimeout(timer); reject(e); }); });}
// ============ 行情获取:腾讯API(script标签) ============function loadTencentQuotes(codes) { return new Promise(function(resolve) { if (!codes || codes.length === 0) { resolve({}); return; } for (var i = 0; i < codes.length; i++) delete window['v_' + codes[i]]; var url = 'https://qt.gtimg.cn/q=' + codes.join(',') + '&_=' + Date.now(); var timer = setTimeout(function() { cleanup(); resolve(collectData()); }, 10000); function cleanup() { clearTimeout(timer); var s = document.getElementById('_tq_script'); if (s && s.parentNode) s.parentNode.removeChild(s); } function collectData() { var result = {}; for (var i = 0; i < codes.length; i++) { var raw = window['v_' + codes[i]]; if (raw && typeof raw === 'string') { var f = raw.split('~'); if (f.length >= 40) { result[codes[i]] = { name: f[1] || '', price: parseFloat(f[3]) || 0, preClose: parseFloat(f[4]) || 0, change: parseFloat(f[31]) || 0, changePct: parseFloat(f[32]) || 0, high: parseFloat(f[33]) || 0, low: parseFloat(f[34]) || 0, open: parseFloat(f[5]) || 0, volume: parseFloat(f[6]) || 0, turnoverRate: parseFloat(f[38]) || 0 }; } } } return result; } var script = document.createElement('script'); script.id = '_tq_script'; script.src = url; script.onload = function() { cleanup(); resolve(collectData()); }; script.onerror = function() { cleanup(); resolve({}); }; document.head.appendChild(script); });}
// ============ 分时数据:腾讯API(fetch + CORS) ============async function fetchMinuteData(tcode) { try { var resp = await fetchWithTimeout('https://web.ifzq.gtimg.cn/appstock/app/minute/query?code=' + tcode, 6000); if (!resp.ok) return null; var data = await resp.json(); if (!data || !data.data) return null; var sd = data.data[tcode]; if (!sd) { var keys = Object.keys(data.data); for (var i = 0; i < keys.length; i++) { if (keys[i].indexOf(tcode.substring(2)) >= 0) { sd = data.data[keys[i]]; break; } } } if (!sd) return null; var preClose = 0; if (sd.pre_close) preClose = parseFloat(sd.pre_close); if (!preClose && sd.qt) { var qtArr = sd.qt[tcode]; if (qtArr && qtArr.length >= 5) preClose = parseFloat(qtArr[4]) || 0; } var raw = sd.data ? (sd.data.data || []) : (sd.qt_minute_stock || sd.qt_mi || []); if (!raw || !raw.length) return null; var points = []; for (var i = 0; i < raw.length; i++) { var item = raw[i]; var parts = typeof item === 'string' ? item.split(' ') : (Array.isArray(item) ? item : null); if (!parts || parts.length < 2) continue; var ts = String(parts[0] || ''); var p = parseFloat(parts[1]) || 0; var v = parseFloat(parts[2]) || 0; var mins = 0; if (ts.indexOf(':') > 0) { var t = ts.split(':'); mins = parseInt(t[0])*60 + parseInt(t[1]); } else if (ts.length >= 4) { mins = parseInt(ts.substring(0,2))*60 + parseInt(ts.substring(2,4)); } points.push({ time: ts, minutes: mins, price: p, avgPrice: 0, volume: v }); } if (points.length === 0) return null; return { points: points, preClose: preClose }; } catch(e) { return null; }}
// ============ 分时数据缓存 ============async function getMinuteData(idx, tcode) { var now = Date.now(); var cache = MINUTE_CACHE[idx]; if (cache && (now - cache.lastFetch) < MINUTE_TTL) return cache; var r = await fetchMinuteData(tcode); if (r) { MINUTE_CACHE[idx] = { points: r.points, preClose: r.preClose, lastFetch: now }; return MINUTE_CACHE[idx]; } return cache || null;}
// ============ K线数据:腾讯API(fetch + CORS) ============async function fetchKlineData(tcode, period) { var pdMap = { day: 'day', week: 'week', month: 'month' }; var fqMap = { day: 'qfqday', week: 'qfqweek', month: 'qfqmonth' }; try { var resp = await fetchWithTimeout('https://web.ifzq.gtimg.cn/appstock/app/fqkline/get?param=' + tcode + ',' + (pdMap[period]||'day') + ',,,120,qfq', 6000); if (!resp.ok) return []; var data = await resp.json(); if (!data || !data.data) return []; var sd = data.data[tcode]; if (!sd) { var keys = Object.keys(data.data); for (var i = 0; i < keys.length; i++) { if (keys[i].indexOf(tcode.substring(2)) >= 0) { sd = data.data[keys[i]]; break; } } } if (!sd) return []; var fq = fqMap[period] || 'qfqday'; var raw = sd[fq] || []; if (!raw || !raw.length) return []; return raw.map(function(item) { var parts = typeof item === 'string' ? item.split(' ') : (Array.isArray(item) ? item : []); if (!parts || parts.length < 5) return null; return { date: String(parts[0] || ''), open: parseFloat(parts[1]) || 0, close: parseFloat(parts[2]) || 0, high: parseFloat(parts[3]) || 0, low: parseFloat(parts[4]) || 0, volume: parseFloat(parts[5]) || 0 }; }).filter(function(p) { return p !== null; }); } catch(e) { return []; }}
// ============ 搜索(东方财富JSONP) ============async function searchStock(kw) { if (!kw || !kw.trim()) return []; try { return await new Promise(function(resolve) { var timer = setTimeout(function() { cleanup(); resolve([]); }, 5000); function cleanup() { clearTimeout(timer); } var s = document.createElement('script'); s.src = 'https://smartbox.gtimg.cn/s3/?v=2&q=' + encodeURIComponent(kw.trim()) + '&t=all&l=10&_=' + Date.now(); s.onload = function() { cleanup(); try { var raw = window.v_hint || ''; if (!raw || raw === 'N') { resolve([]); return; } var parts = raw.split('^'); var items = []; for (var i = 0; i < parts.length; i++) { var segs = parts[i].split('~'); if (segs.length < 4) continue; var mkt = segs[0]; var code = segs[1]; var name = segs[2]; if (mkt !== 'sh' && mkt !== 'sz') continue; if (!code || !name) continue; items.push({ name: name, code: code, exchange: (mkt === 'sh' ? '\u6caa' : '\u6df1'), index: mkt + code }); } resolve(items); } catch(e) { resolve([]); } }; s.onerror = function() { cleanup(); resolve([]); }; document.head.appendChild(s); }); } catch(e) { console.error('searchStock error:', e); return []; }}
// ============ 配置持久化 ============function loadCfg() { var s = null; // 优先从 Quicker 变量读取 try { if (typeof $quickerSync !== 'undefined') { var qData = $quickerSync.getVar('stockData'); if (qData && qData.length > 0) s = qData; } } catch(e) {} // 回退到 localStorage if (!s) { try { s = localStorage.getItem('stockPanel_config'); } catch(e) {} } try { if (s) { var c = JSON.parse(s); if (c.stocks && c.stocks.length > 0) { STOCKS = c.stocks; if (c.groups) GROUPS = c.groups; if (c.alerts) ALERTS = c.alerts; if (c.currentGroup) CUR_GROUP = c.currentGroup; if (!GROUPS.length) GROUPS = ['默认分组']; if (!STOCKS.some(function(s){ return s.group === CUR_GROUP; })) CUR_GROUP = GROUPS[0]; return; } } } catch(e) {} // 无数据时初始化为空 STOCKS = []; GROUPS = ['默认分组']; CUR_GROUP = '默认分组'; ALERTS = [];}
function saveCfg() { var data = JSON.stringify({stocks: STOCKS, groups: GROUPS, currentGroup: CUR_GROUP, alerts: ALERTS}); try { localStorage.setItem('stockPanel_config', data); } catch(e) {} // Quicker WebView2 桥接:将数据同步到 Quicker 变量 try { if (typeof $quickerSync !== 'undefined') { $quickerSync.setVar('stockData', data); } } catch(e) {} writeCfgToYanmu();}
// 窗口关闭前同步数据window.addEventListener('beforeunload', function() { try { saveCfg(); } catch(e) {}});
// ============ 燕幕桥接 ============ var YANMU_STATE_KEY = 'stockData';function yanmuText(res) { if (res == null) return ''; if (typeof res === 'string') return res; if (typeof res.text === 'string') return res.text; if (typeof res.value === 'string') return res.value; if (typeof res.data === 'string') return res.data; if (typeof res.result === 'string') return res.result; if (res.result && typeof res.result.text === 'string') return res.result.text; return '';}function applyCfgText(s) { try { if (!s) return false; var c = JSON.parse(s); if (c.stocks && c.stocks.length > 0) { STOCKS = c.stocks; if (c.groups) GROUPS = c.groups; if (c.alerts) ALERTS = c.alerts; if (c.currentGroup) CUR_GROUP = c.currentGroup; if (!GROUPS.length) GROUPS = ['默认分组']; if (!STOCKS.some(function(s){ return s.group === CUR_GROUP; })) CUR_GROUP = GROUPS[0]; return true; } } catch(e) {} return false;}async function hydrateCfgFromYanmu() { try { if (!window.yanm || typeof window.yanm.invoke !== 'function') return; var res = await window.yanm.invoke('state.read', { key: YANMU_STATE_KEY, defaultValue: JSON.stringify({stocks: [], groups: ['默认分组'], currentGroup: '默认分组', alerts: []}) }); var s = yanmuText(res); if (applyCfgText(s)) { renderGroups(); rebuildList(); updatePrices(); showToast('已同步燕幕状态'); } } catch(e) {}}function writeCfgToYanmu() { try { if (window.yanm && typeof window.yanm.invoke === 'function') { window.yanm.invoke('state.write', { key: YANMU_STATE_KEY, value: JSON.stringify({stocks: STOCKS, groups: GROUPS, currentGroup: CUR_GROUP, alerts: ALERTS}) }); } } catch(e) {}}
// ============ DOM 构建(一次性) ============function buildStockItem(s) { // 主行 var row = document.createElement('div'); row.className = 'stock-row'; row.dataset.idx = s.idx; row.innerHTML = '<div class="col-name">' + '<span class="expand-btn" data-idx="' + s.idx + '">▶</span>' + '<span class="sname ' + (STOCK_CACHE[s.idx] ? '' : 'text-gray') + '">' + s.name + '<span style="color:#999;margin-left:4px;font-size:11px">' + s.code + '</span></span></div>' + '<div class="col-price">--</div>' + '<div class="col-change"><span class="color-bg gray">--</span></div>' + '<div class="col-change-pct"><span class="color-bg gray">--</span></div>' + '<div class="col-turnover">--</div>'; // liked样式 if (s.liked) row.classList.add('liked'); // 右键菜单 row.oncontextmenu = function(e) { e.preventDefault(); window._ctxStockIdx = s.idx; var menu = document.getElementById('stockCtxMenu'); var btn = document.getElementById('ctxLikeBtn'); var liked = STOCKS.some(function(x){ return x.idx === s.idx && x.liked; }); btn.innerHTML = liked ? '★ 取消关注' : '☆ 关注'; menu.style.left = e.clientX + 'px'; menu.style.top = e.clientY + 'px'; menu.classList.add('show'); }; // 整行点击展开/折叠 row.onclick = function() { toggleExpand(s.idx); }; // 详情面板 var detail = document.createElement('div'); detail.className = 'detail-panel'; detail.id = 'd_' + s.idx; detail.innerHTML = '<div class="detail-tabs">' + '<span class="detail-tab active" data-p="minute">分时</span>' + '<span class="detail-tab" data-p="day">日K</span>' + '<span class="detail-tab" data-p="week">周K</span>' + '<span class="detail-tab" data-p="month">月K</span>' + '<span class="detail-tab" data-p="detail" style="margin-left:4px" id="detailBtn_' + s.idx + '">详情</span>' + '</div><canvas class="detail-chart" id="ch_' + s.idx + '" width="400" height="180"></canvas>'; // tab按钮事件 var tabs = detail.querySelectorAll('.detail-tab'); for (var i = 0; i < tabs.length; i++) { (function(t) { t.onclick = function() { if (t.dataset.p === 'detail') { var all2 = detail.querySelectorAll('.detail-tab'); for (var j = 0; j < all2.length; j++) all2[j].classList.toggle('active', all2[j] === t); showStockDetail(s.idx); return; } CHART_TYPE[s.idx] = t.dataset.p; var all = detail.querySelectorAll('.detail-tab'); for (var j = 0; j < all.length; j++) all[j].classList.toggle('active', all[j] === t); var kkey = s.idx + '_' + t.dataset.p; KLINE_STATE[kkey] = { offset: 0, zoom: 1 }; delete KLINE_DATA[kkey]; var det = document.getElementById('detail_' + s.idx); if (det) det.style.display = 'none'; var cv2 = document.getElementById('ch_' + s.idx); if (cv2) cv2.style.display = 'block'; renderChart(s.idx, t.dataset.p); }; })(tabs[i]); } return { row: row, detail: detail };}
function buildList() { var area = document.getElementById('stockArea'); var gs = getCurrentStocks(); if (!gs.length) { area.innerHTML = '<div class="loading-text" style="padding:60px 0;color:#ccc">暂无股票,点击添加按钮添加自选</div>'; return; } // 清空但保留stockArea元素 area.innerHTML = ''; // 用DocumentFragment批量添加 var frag = document.createDocumentFragment(); for (var i = 0; i < gs.length; i++) { var items = buildStockItem(gs[i]); frag.appendChild(items.row); frag.appendChild(items.detail); } area.appendChild(frag); // 存储所有行和详情面板的引用 window._rows = {}; window._details = {}; for (var i = 0; i < gs.length; i++) { window._rows[gs[i].idx] = document.querySelector('.stock-row[data-idx="' + gs[i].idx + '"]'); window._details[gs[i].idx] = document.getElementById('d_' + gs[i].idx); }}
function getCurrentStocks() { var gs = STOCKS.filter(function(s) { return s.group === CUR_GROUP; }); if (SORT_FIELD) { gs.sort(function(a, b) { var va = STOCK_CACHE[a.idx] ? (STOCK_CACHE[a.idx][SORT_FIELD] || 0) : 0; var vb = STOCK_CACHE[b.idx] ? (STOCK_CACHE[b.idx][SORT_FIELD] || 0) : 0; return SORT_ASC ? va - vb : vb - va; }); } return gs;}
// ============ 价格更新(不重建DOM) ============function updatePrices() { try { var gs = getCurrentStocks(); for (var i = 0; i < gs.length; i++) { var s = gs[i], c = STOCK_CACHE[s.idx]; var row = window._rows && window._rows[s.idx]; if (!row) continue; if (c && c.price) { var cells = row.querySelectorAll('.col-price, .col-change, .col-change-pct, .col-turnover'); var nameEl = row.querySelector('.sname'); if (nameEl) { var _nm = c.name || s.name; var _cd = s.code; nameEl.innerHTML = _nm + '<span style="color:#999;margin-left:4px;font-size:11px">' + _cd + '</span>'; nameEl.className = 'sname'; } // 市价 cells[0].textContent = c.price.toFixed(2); cells[0].className = 'col-price ' + (c.change > 0 ? 'text-red' : (c.change < 0 ? 'text-green' : 'text-gray')); // 涨跌额 var cc = c.change > 0 ? 'red' : (c.change < 0 ? 'green' : 'gray'); cells[1].innerHTML = '<span class="color-bg change-val ' + cc + '">' + (c.change >= 0 ? '+' : '') + c.change.toFixed(2) + '</span>'; // 涨跌幅 cells[2].innerHTML = '<span class="color-bg ' + cc + '">' + (c.changePct >= 0 ? '+' : '') + c.changePct.toFixed(2) + '%</span>'; // 换手率 cells[3].textContent = c.turnoverRate ? c.turnoverRate.toFixed(2) + '%' : '--'; // 展开状态刷新图表 if (EXPANDED[s.idx]) { var det = window._details && window._details[s.idx]; if (det && det.classList.contains('active')) { if (CHART_TYPE[s.idx] === 'minute') { // 鼠标悬停在图表上时不重绘,避免清除十字光标 var cv = document.getElementById('ch_' + s.idx); var cache = MINUTE_CACHE[s.idx]; if (cache && cache.points && cache.points.length) { if (cv && cv._hovering) continue; var ctx = cv.getContext('2d'); var W = 400, H = 180, dpr = window.devicePixelRatio || 1; cv.width = W * dpr; cv.height = H * dpr; cv.style.width = W + 'px'; cv.style.height = H + 'px'; ctx.scale(dpr, dpr); ctx.clearRect(0, 0, W, H); drawMinute(ctx, W, H, cache.points, cache.preClose, s.idx); } } } } } } calcProfit(); checkAlerts(); } catch(e) {} }
// ============ 排序(重建DOM) ============function sortBy(f) { if (SORT_FIELD === f) { if (!SORT_ASC) { SORT_ASC = true; } else { SORT_FIELD = null; SORT_ASC = false; } } else { SORT_FIELD = f; SORT_ASC = true; } rebuildList();}
function rebuildList() { // 保存展开状态 var savedExpanded = {}; for (var k in EXPANDED) savedExpanded[k] = EXPANDED[k]; buildList(); // 恢复展开状态 for (var k in savedExpanded) { if (savedExpanded[k]) { var det = window._details[k]; if (det) { det.classList.add('active'); CHART_TYPE[k] = CHART_TYPE[k] || 'minute'; // 激活对应tab var p = CHART_TYPE[k]; var tabs = det.querySelectorAll('.detail-tab'); for (var j = 0; j < tabs.length; j++) tabs[j].classList.toggle('active', tabs[j].dataset.p === p); setTimeout(function(idx) { return function() { renderChart(idx, CHART_TYPE[idx]); }; }(k), 100); } } } updatePrices();}
// ============ 指数渲染 ============function renderIdx() { for (var i = 0; i < INDEX_CODES.length; i++) { var el = document.querySelector('.index-item[data-idx="' + i + '"]'); if (!el) continue; var c = IDX_CACHE[INDEX_CODES[i]]; if (!c) continue; var p = el.querySelector('.index-price'); var ch = el.querySelector('.index-change'); if (p) p.textContent = c.price.toFixed(2); if (ch) { ch.textContent = (c.changePct >= 0 ? '+' : '') + c.changePct.toFixed(2) + '%'; ch.style.color = c.changePct >= 0 ? '#f56c6c' : '#67c23a'; } }}
function calcProfit() { try { var gs = getCurrentStocks() || []; var up = 0, down = 0, flat = 0; for (var i = 0; i < gs.length; i++) { var x = STOCK_CACHE[gs[i].idx]; if (x && x.price) { if (x.change > 0) up++; else if (x.change < 0) down++; else flat++; } } var e1 = document.getElementById('gsUp'); var e2 = document.getElementById('gsDown'); if (e1) e1.textContent = up; if (e2) e2.textContent = down; // 获取全A市场涨跌家数 fetchMarketBreadth(); } catch(e) {}}
// 全A市场涨跌家数缓存var MKT_BREADTH = { up: 0, flat: 0, down: 0 };function fetchMarketBreadth() { if (MKT_BREADTH._busy) return; MKT_BREADTH._busy = true; var codes = ['bkqtRank_A_sh','bkqtRank_B_sh','bkqtRank_A_sz','bkqtRank_B_sz']; var url = 'https://qt.gtimg.cn/q=' + codes.join(','); var s = document.createElement('script'); s.src = url; var loaded = 0, totalUp = 0, totalDown = 0; s.onload = s.onerror = function() { if (s.parentNode) s.parentNode.removeChild(s); for (var i = 0; i < codes.length; i++) { var raw = window['v_' + codes[i]]; if (raw && typeof raw === 'string') { var f = raw.split('~'); if (f.length >= 6) { totalUp += parseInt(f[2]) || 0; totalDown += parseInt(f[4]) || 0; } } } MKT_BREADTH.up = totalUp; MKT_BREADTH.down = totalDown; MKT_BREADTH._last = Date.now(); MKT_BREADTH._busy = false; updateMarketStats(); }; document.head.appendChild(s);}function updateMarketStats() { var e1 = document.getElementById('mkUp'); var e2 = document.getElementById('mkDown'); if (!e1 || !e2) return; var b = MKT_BREADTH; if (b.up + b.down > 0) { e1.textContent = b.up; e2.textContent = b.down; }}
// ============ 展开/折叠 ============function toggleExpand(idx) { EXPANDED[idx] = !(EXPANDED[idx] || false); var det = window._details && window._details[idx]; if (!det) return; var btn = document.querySelector('.expand-btn[data-idx="' + idx + '"]'); if (EXPANDED[idx]) { det.classList.add('active'); if (btn) btn.classList.add('expanded'); CHART_TYPE[idx] = CHART_TYPE[idx] || 'minute'; var tabs = det.querySelectorAll('.detail-tab'); for (var j = 0; j < tabs.length; j++) tabs[j].classList.toggle('active', tabs[j].dataset.p === CHART_TYPE[idx]); setTimeout(function() { renderChart(idx, CHART_TYPE[idx]); }, 100); } else { det.classList.remove('active'); if (btn) btn.classList.remove('expanded'); }}
// ============ Canvas图表 ============async function renderChart(idx, period) { var cv = document.getElementById('ch_' + idx); if (!cv) return; cv.onwheel = null; cv.onmousedown = null; cv.onmousemove = null; cv.onmouseup = null; cv.onmouseleave = null; cv.onmouseenter = null; var ctx = cv.getContext('2d'); var W = 400, H = 180, dpr = window.devicePixelRatio || 1; cv.width = W * dpr; cv.height = H * dpr; cv.style.width = W + 'px'; cv.style.height = H + 'px'; ctx.scale(dpr, dpr); ctx.clearRect(0, 0, W, H); var stock = null; for (var i = 0; i < STOCKS.length; i++) { if (STOCKS[i].idx === idx) { stock = STOCKS[i]; break; } } if (!stock) { drawNoData(ctx, W, H); return; } var tcode = (stock.code.startsWith('6') ? 'sh' : 'sz') + stock.code; var q = STOCK_CACHE[idx], preClose = q ? q.preClose : 0; if (period === 'minute') { var r = await getMinuteData(idx, tcode); if (!r || !r.points || !r.points.length) { drawNoData(ctx, W, H); return; } drawMinute(ctx, W, H, r.points, r.preClose || preClose, idx); setupCrosshair(cv, idx, r.points, r.preClose || preClose); } else { var key = idx + '_' + period; var kl = KLINE_DATA[key]; if (!kl || !kl.length) { kl = await fetchKlineData(tcode, period); if (kl && kl.length) KLINE_DATA[key] = kl; } if (!kl || !kl.length) { drawNoData(ctx, W, H); return; } var kst = KLINE_STATE[key]; if (!kst) { kst = { offset: 0, zoom: 1 }; KLINE_STATE[key] = kst; } var DEFAULT_VISIBLE = 40; var visible = Math.max(10, Math.round(DEFAULT_VISIBLE * kst.zoom)); var endIdx = kl.length - kst.offset; var startIdx = Math.max(0, endIdx - visible); var visibleKl = kl.slice(startIdx, startIdx + visible); drawKline(ctx, W, H, visibleKl, kl, startIdx, preClose, idx); setupKlineCanvas(cv, idx, period, kl.length); }}
// 仅重绘K线(不重新获取数据、不重设事件),用于拖拽/缩放function redrawKline(idx, period) { var cv = document.getElementById('ch_' + idx); if (!cv) return; var key = idx + '_' + period; var kl = KLINE_DATA[key]; if (!kl || !kl.length) return; var kst = KLINE_STATE[key]; if (!kst) return; var ctx = cv.getContext('2d'); var W = 400, H = 180, dpr = window.devicePixelRatio || 1; cv.width = W * dpr; cv.height = H * dpr; cv.style.width = W + 'px'; cv.style.height = H + 'px'; ctx.scale(dpr, dpr); ctx.clearRect(0, 0, W, H); var q = STOCK_CACHE[idx], preClose = q ? q.preClose : 0; var DEFAULT_VISIBLE = 40; var visible = Math.max(10, Math.round(DEFAULT_VISIBLE * kst.zoom)); var endIdx = kl.length - kst.offset; var startIdx = Math.max(0, endIdx - visible); var visibleKl = kl.slice(startIdx, startIdx + visible); drawKline(ctx, W, H, visibleKl, kl, startIdx, preClose, idx);}
// ============ 分时图绘制(蓝色折线+圆点,双Y轴) ============// 全局tox函数(分时时间→屏幕X)function tox(m, pl, cw) { // 09:30=570, 11:30=690, 13:00=780, 15:00=900 if (m <= 690) return pl + ((m - 570) / 240) * cw; if (m >= 780 && m <= 900) return pl + ((m - 660) / 240) * cw; return -999;}
function drawMinute(ctx, W, H, pts, pc, idx) { var pt = 18, pb = 22, pl = 45, pr = 45, cw = W - pl - pr, ch = H - pt - pb;
// 防御:如果pc无效,用第一笔的价格 if (!pc || pc <= 0) { var firstValid = 0; for (var i = 0; i < pts.length; i++) { if (pts[i].price > 0) { firstValid = pts[i].price; break; } } pc = firstValid || pts[pts.length-1].price || pts[0].price || 100; } // 计算价格范围(以昨收为中心,上下对称) var maxDev = 0; for (var i = 0; i < pts.length; i++) { var dev = Math.abs(pts[i].price - pc); if (dev > maxDev) maxDev = dev; } // 至少留4%空间 var pctRange = Math.max(maxDev / pc * 100, 4); pctRange = Math.ceil(pctRange / 2) * 2; var priceRange = pc * pctRange / 100; var minP = pc - priceRange; var maxP = pc + priceRange; var rg = maxP - minP || 1;
ctx.fillStyle = '#fff'; ctx.fillRect(0, 0, W, H);
// --- 左侧Y轴:价格刻度 --- ctx.fillStyle = '#999'; ctx.font = '9px sans-serif'; ctx.textAlign = 'right'; var pctStep = pctRange / 4; for (var i = 0; i <= 4; i++) { var p = maxP - (rg / 4) * i; var cIdx = i; // 0=top, 4=bottom var color = '#333'; if (cIdx < 2) color = '#f56c6c'; // 红色(涨) else if (cIdx > 2) color = '#67c23a'; // 绿色(跌) ctx.fillStyle = color; ctx.fillText(p.toFixed(2), pl - 4, pt + (ch / 4) * i + 3); }
// --- 右侧Y轴:涨跌幅刻度 --- ctx.textAlign = 'left'; for (var i = 0; i <= 4; i++) { var pct = pctRange - (pctRange / 4) * i; var color = '#333'; var label = ''; if (i === 2) { color = '#333'; label = '0.00%'; } else if (i < 2) { color = '#f56c6c'; label = '+' + pct.toFixed(2) + '%'; } else { color = '#67c23a'; label = '-' + pct.toFixed(2) + '%'; } ctx.fillStyle = color; ctx.fillText(label, W - pr + 4, pt + (ch / 4) * i + 3); }
// 网格虚线(每个Y轴刻度位置) ctx.strokeStyle = 'rgba(144,147,153,0.3)'; ctx.lineWidth = 0.5; ctx.setLineDash([3, 3]); for (var i = 0; i <= 4; i++) { var gy = pt + (ch / 4) * i; ctx.beginPath(); ctx.moveTo(pl, gy); ctx.lineTo(W - pr, gy); ctx.stroke(); } ctx.setLineDash([]);
var lineColor = '#1e88e5'; ctx.strokeStyle = lineColor; ctx.lineWidth = 1.5; ctx.beginPath(); var started = false; var lastMinute = -1; for (var i = 0; i < pts.length; i++) { var pm = pts[i].minutes; // 只绘制有效交易时段:上午 09:30-11:30 (570-690),下午 13:00-15:00 (780-900) if (pm < 570 || (pm > 690 && pm < 780) || pm > 900) continue; var x = tox(pm, pl, cw); if (x < 0) continue; // tox 返回 -999 表示无效时间 var y = pt + ch - ((pts[i].price - minP) / rg) * ch; if (!started) { ctx.moveTo(x, y); started = true; } else if (lastMinute >= 0 && pm - lastMinute > 10) { // 午间休市断开:结束当前路径,从新点重新开始 ctx.stroke(); ctx.beginPath(); ctx.moveTo(x, y); } else { ctx.lineTo(x, y); } lastMinute = pm; } ctx.stroke();
// 蓝色圆点标记(关键点) var dotIndices = []; var step = Math.max(1, Math.floor(pts.length / 15)); for (var i = 0; i < pts.length; i += step) dotIndices.push(i); // 始终包含最后一个点 if (dotIndices[dotIndices.length - 1] !== pts.length - 1) dotIndices.push(pts.length - 1); ctx.fillStyle = lineColor; for (var di = 0; di < dotIndices.length; di++) { var i = dotIndices[di]; var x = tox(pts[i].minutes, pl, cw), y = pt + ch - ((pts[i].price - minP) / rg) * ch; ctx.beginPath(); ctx.arc(x, y, 2.5, 0, Math.PI * 2); ctx.fill(); }
// 底部时间刻度 ctx.fillStyle = '#999'; ctx.font = '8px sans-serif'; ctx.textAlign = 'center'; ['09:30', '10:30', '11:30/13:00', '14:00', '15:00'].forEach(function(t, i) { ctx.fillText(t, pl + (cw / 4) * i, H - 4); });
// 左上标题 var nm = (STOCK_CACHE[idx] && STOCK_CACHE[idx].name) || idx; ctx.fillStyle = '#333'; ctx.font = 'bold 10px sans-serif'; ctx.textAlign = 'left'; ctx.fillText(nm + ' 分时', pl, pt - 4);
// 最新价标签(绿色圆角矩形,位于最后一笔价格旁) var lp = pts[pts.length - 1]; if (lp) { var lpx = tox(lp.minutes, pl, cw); var lpy = pt + ch - ((lp.price - minP) / rg) * ch; // 已不画贯穿横线 // 标签框:在折线右侧或左侧(取决于位置) var tagX = Math.min(Math.max(lpx + 8, pl + 10), W - pr - 55); var lc = lp.price >= pc ? '#f56c6c' : '#67c23a'; roundRect(ctx, tagX, lpy - 13, 50, 14, 3); ctx.fillStyle = lc; ctx.fill(); ctx.fillStyle = '#fff'; ctx.font = 'bold 9px sans-serif'; ctx.textAlign = 'center'; ctx.fillText(lp.price.toFixed(2), tagX + 25, lpy - 2); }}
// ============ K线图绘制 ============function drawKline(ctx, W, H, kl, allKl, startIdx, pc, idx) { var pt = 18, pb = 22, pl = 50, pr = 20, cw = W - pl - pr, ch = H - pt - pb; if (!kl || !kl.length) return; var minL = Infinity, maxH = -Infinity; for (var i = 0; i < kl.length; i++) { if (kl[i].low < minL) minL = kl[i].low; if (kl[i].high > maxH) maxH = kl[i].high; } if (minL === Infinity) { minL = 0; maxH = 100; } minL *= 0.998; maxH *= 1.002; var rg = maxH - minL || 1;
ctx.fillStyle = '#fff'; ctx.fillRect(0, 0, W, H); ctx.fillStyle = '#999'; ctx.font = '9px sans-serif'; ctx.textAlign = 'right'; for (var i = 0; i <= 4; i++) { ctx.fillText((maxH - (rg / 4) * i).toFixed(2), pl - 4, pt + (ch / 4) * i + 3); } // 网格虚线 ctx.strokeStyle = 'rgba(144,147,153,0.3)'; ctx.lineWidth = 0.5; ctx.setLineDash([3, 3]); for (var i = 0; i <= 4; i++) { var gy = pt + (ch / 4) * i; ctx.beginPath(); ctx.moveTo(pl, gy); ctx.lineTo(W - pr, gy); ctx.stroke(); } ctx.setLineDash([]);
var bw = Math.min(8, Math.floor(cw / kl.length / 1.5)); var gap = Math.max(bw * 1.5, 3); var totalW = kl.length * gap; var sx = pl + (cw - totalW) / 2 + gap / 2;
for (var i = 0; i < kl.length; i++) { var k = kl[i], x = sx + i * gap; var up = k.close >= k.open, col = up ? '#f56c6c' : '#67c23a'; var yh = pt + ch - ((k.high - minL) / rg) * ch; var yl = pt + ch - ((k.low - minL) / rg) * ch; var yo = pt + ch - ((k.open - minL) / rg) * ch; var yc = pt + ch - ((k.close - minL) / rg) * ch; ctx.strokeStyle = col; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(x, yh); ctx.lineTo(x, yl); ctx.stroke(); ctx.fillStyle = col; var bodyTop = Math.min(yo, yc), bodyH = Math.max(Math.abs(yo - yc), 1); ctx.fillRect(x - bw / 2, bodyTop, bw, bodyH); }
// 底部日期标签:基于allKl的日期,固定右侧对齐 if (allKl && allKl.length) { ctx.fillStyle = '#999'; ctx.font = '8px sans-serif'; ctx.textAlign = 'center'; var labelCount = Math.min(5, kl.length); for (var i = 0; i < labelCount; i++) { var klIdx = Math.floor((i / (labelCount - 1)) * (kl.length - 1)); var allIdx = startIdx + klIdx; if (allIdx >= 0 && allIdx < allKl.length) { var dateStr = (allKl[allIdx].date || '').substring(5); ctx.fillText(dateStr, sx + klIdx * gap, H - 4); } } }
var nm = (STOCK_CACHE[idx] && STOCK_CACHE[idx].name) || idx; var pns = { day: '日K', week: '周K', month: '月K' }; ctx.fillStyle = '#333'; ctx.font = 'bold 10px sans-serif'; ctx.textAlign = 'left'; ctx.fillText(nm + ' ' + (pns[CHART_TYPE[idx]] || '日K'), pl, pt - 4);}
// ============ K线图交互(滚轮缩放 + 鼠标拖拽) ============var KLINE_STATE = {};var KLINE_DATA = {}; // K线数据缓存 {idx_period: [kl]}function setupKlineCanvas(cv, idx, period, totalKlLen) { var key = idx + '_' + period; var state = KLINE_STATE[key]; if (!state) { state = { offset: 0, zoom: 1 }; KLINE_STATE[key] = state; } var DEFAULT_VISIBLE = 40; var isDragging = false, dragStartX = 0, dragStartOffset = 0; var dragThreshold = 3; // 拖动触发阈值(像素) var hasDragged = false;
cv.onwheel = function(e) { if (CHART_TYPE[idx] === 'minute') return; e.preventDefault(); if (e.deltaY > 0) { state.zoom = Math.max(0.3, state.zoom * 0.85); } else { state.zoom = Math.min(8, state.zoom * 1.18); } // 缩放时右侧锚定:调整offset确保右边界不变 var visible = Math.max(10, Math.round(DEFAULT_VISIBLE * state.zoom)); var maxOffset = Math.max(0, totalKlLen - visible); if (state.offset > maxOffset) state.offset = maxOffset; redrawKline(idx, period); };
cv.onmousedown = function(e) { if (CHART_TYPE[idx] === 'minute') return; isDragging = true; hasDragged = false; dragStartX = e.clientX; dragStartOffset = state.offset; cv.style.cursor = 'grabbing'; e.preventDefault(); e.stopPropagation(); };
cv.onmousemove = function(e) { if (CHART_TYPE[idx] === 'minute') return; if (isDragging) { var dx = dragStartX - e.clientX; // 向左拖>0,看更早数据 if (Math.abs(dx) < dragThreshold) return; // 未超过阈值不响应 hasDragged = true; var visible = Math.max(10, Math.round(DEFAULT_VISIBLE * state.zoom)); var maxOffset = Math.max(0, totalKlLen - visible); var barWidth = Math.max(4, 310 / visible); state.offset = Math.max(0, Math.min(maxOffset, dragStartOffset + dx / barWidth)); redrawKline(idx, period); } else { cv.style.cursor = 'grab'; } };
cv.onmouseup = function(e) { if (isDragging && !hasDragged) { // 没有实际拖动,视为点击,可以后续加点击功能 } isDragging = false; cv.style.cursor = 'grab'; }; cv.onmouseleave = function() { isDragging = false; cv.style.cursor = 'default'; };}
// ============ 分时图十字光标(仅从renderChart调用一次) ============function setupCrosshair(cv, idx, pts, pc) { var W = 400, H = 180, dpr = window.devicePixelRatio || 1; var pt = 18, pb = 22, pl = 45, pr = 45, cw = W - pl - pr, ch = H - pt - pb;
if (!pc || pc <= 0) { var fv = 0; for (var i = 0; i < pts.length; i++) { if (pts[i].price > 0) { fv = pts[i].price; break; } } pc = fv || pts[pts.length-1].price || 100; } var maxDev = 0; for (var i = 0; i < pts.length; i++) { var dev = Math.abs(pts[i].price - pc); if (dev > maxDev) maxDev = dev; } var pctRange = Math.max(maxDev / pc * 100, 4); pctRange = Math.ceil(pctRange / 2) * 2; var minP = pc - pctRange * pc / 200; var maxP = pc + pctRange * pc / 200; var rg = maxP - minP || 1;
var baseImage = null; function snapBase() { try { baseImage = cv.getContext('2d').getImageData(0, 0, W * dpr, H * dpr); } catch(e) {} } snapBase(); var lastMx = -1, lastMy = -1;
cv.onmousemove = function(e) { if (CHART_TYPE[idx] !== 'minute') return; cv._hovering = true; var rect = cv.getBoundingClientRect(); var mx = e.clientX - rect.left, my = e.clientY - rect.top; if (mx < pl || mx > W - pr || my < pt || my > pt + ch) { if (baseImage) cv.getContext('2d').putImageData(baseImage, 0, 0); cv.style.cursor = 'default'; cv._hovering = false; lastMx = -1; lastMy = -1; return; } cv.style.cursor = 'crosshair'; // 每次移动时重新拍摄底图,防止 updatePrices 重绘导致 baseImage 失效 if (!baseImage) snapBase(); var ctx = cv.getContext('2d'); ctx.putImageData(baseImage, 0, 0);
// 水平虚线 ctx.strokeStyle = 'rgba(144,147,153,0.6)'; ctx.lineWidth = 0.5; ctx.setLineDash([3, 3]); ctx.beginPath(); ctx.moveTo(pl, my); ctx.lineTo(W - pr, my); ctx.stroke(); ctx.setLineDash([]); // 竖直虚线 ctx.strokeStyle = 'rgba(144,147,153,0.6)'; ctx.lineWidth = 0.5; ctx.setLineDash([3, 3]); ctx.beginPath(); ctx.moveTo(mx, pt); ctx.lineTo(mx, pt + ch); ctx.stroke(); ctx.setLineDash([]);
// Y轴左侧:价格标签 var cursorPrice = maxP - ((my - pt) / ch) * rg; if (cursorPrice >= minP && cursorPrice <= maxP) { var pLabel = cursorPrice.toFixed(2); var pColor = cursorPrice >= pc ? '#f56c6c' : '#67c23a'; ctx.font = '9px sans-serif'; var pW = ctx.measureText(pLabel).width; roundRect(ctx, pl - pW - 10, my - 8, pW + 6, 16, 2); ctx.fillStyle = pColor; ctx.fill(); ctx.fillStyle = '#fff'; ctx.font = '9px sans-serif'; ctx.textAlign = 'right'; ctx.fillText(pLabel, pl - 6, my + 3); } // Y轴右侧:涨幅标签 var cursorPct = ((cursorPrice - pc) / pc) * 100; var pctLabel = (cursorPct >= 0 ? '+' : '') + cursorPct.toFixed(2) + '%'; var pctColor = cursorPct >= 0 ? '#f56c6c' : '#67c23a'; ctx.font = '9px sans-serif'; var pctW = ctx.measureText(pctLabel).width; roundRect(ctx, W - pr + 4, my - 8, pctW + 6, 16, 2); ctx.fillStyle = pctColor; ctx.fill(); ctx.fillStyle = '#fff'; ctx.font = '9px sans-serif'; ctx.textAlign = 'left'; ctx.fillText(pctLabel, W - pr + 7, my + 3);
// 查找最近的分时数据点 var nearPt = null, nearDist = Infinity; for (var i = 0; i < pts.length; i++) { var pm = pts[i].minutes; if (pm < 570 || (pm > 690 && pm < 780) || pm > 900) continue; var cx2 = window.tox(pm, pl, cw); if (cx2 < 0) continue; var cy2 = pt + ch - ((pts[i].price - minP) / rg) * ch; var dist = Math.abs(mx - cx2); if (dist < nearDist && dist < 30) { nearDist = dist; nearPt = pts[i]; } }
// X轴底部:时间标签 var ratio = (mx - pl) / cw; var totalMins = ratio * 240; var cursorMin = 0; if (ratio <= 0.5) { cursorMin = 570 + totalMins; } else { cursorMin = 660 + totalMins; } var curH = Math.floor(cursorMin / 60); var curM = Math.floor(cursorMin % 60); var timeLabel = (curH < 10 ? '0' : '') + curH + ':' + (curM < 10 ? '0' : '') + curM; ctx.font = '9px sans-serif'; var tW = ctx.measureText(timeLabel).width; roundRect(ctx, mx - tW / 2 - 3, pt + ch + 2, tW + 6, 14, 2); ctx.fillStyle = 'rgba(80,80,80,0.85)'; ctx.fill(); ctx.fillStyle = '#fff'; ctx.font = '9px sans-serif'; ctx.textAlign = 'center'; ctx.fillText(timeLabel, mx, pt + ch + 12);
// 悬浮信息框(基于最近数据点) if (nearPt) { var cx = window.tox(nearPt.minutes, pl, cw); var cy = pt + ch - ((nearPt.price - minP) / rg) * ch; ctx.fillStyle = '#1e88e5'; ctx.beginPath(); ctx.arc(cx, cy, 3.5, 0, Math.PI * 2); ctx.fill(); var nPct = ((nearPt.price - pc) / pc * 100); var nColor = nPct >= 0 ? '#f56c6c' : '#67c23a'; var ts = nearPt.time || ''; if (ts.indexOf(':') < 0 && ts.length >= 4) ts = ts.substring(0,2) + ':' + ts.substring(2,4); var boxW = 130, boxH = 62; var bx = cx + 10, by = cy - boxH - 5; if (bx + boxW > W - 5) bx = cx - boxW - 10; if (by < 2) by = cy + 10; ctx.fillStyle = 'rgba(0,0,0,0.78)'; roundRect(ctx, bx, by, boxW, boxH, 4); ctx.fill(); ctx.fillStyle = '#fff'; ctx.font = '10px sans-serif'; ctx.textAlign = 'left'; ctx.fillText('时间: ' + ts, bx + 6, by + 14); ctx.fillText('价格: ' + nearPt.price.toFixed(2), bx + 6, by + 28); ctx.fillStyle = nColor; ctx.fillText((nPct >= 0 ? '+' : '') + nPct.toFixed(2) + '%', bx + 6, by + 42); ctx.fillStyle = '#ccc'; var vol = nearPt.volume ? (nearPt.volume / 10000).toFixed(2) + '万' : '--'; ctx.fillText('量: ' + vol, bx + 70, by + 42); } lastMx = mx; lastMy = my; };
cv.onmouseenter = function() { cv._hovering = true; }; cv.onmouseleave = function() { if (CHART_TYPE[idx] !== 'minute') { cv.style.cursor = 'default'; return; } cv._hovering = false; if (baseImage) { cv.getContext('2d').putImageData(baseImage, 0, 0); } cv.style.cursor = 'default'; lastMx = -1; lastMy = -1; };}
function drawNoData(ctx, W, H) { ctx.fillStyle = '#fff'; ctx.fillRect(0, 0, W, H); ctx.fillStyle = '#ccc'; ctx.font = '12px sans-serif'; ctx.textAlign = 'center'; ctx.fillText('暂无数据', W / 2, H / 2);}
function roundRect(ctx, x, y, w, h, r) { ctx.beginPath(); ctx.moveTo(x + r, y); ctx.lineTo(x + w - r, y); ctx.quadraticCurveTo(x + w, y, x + w, y + r); ctx.lineTo(x + w, y + h - r); ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h); ctx.lineTo(x + r, y + h); ctx.quadraticCurveTo(x, y + h, x, y + h - r); ctx.lineTo(x, y + r); ctx.quadraticCurveTo(x, y, x + r, y); ctx.closePath();}
// ============ 刷新(行情+价格更新,不重建DOM) ============async function refreshAll() { if (REFRESHING) return; REFRESHING = true; try { var gs = getCurrentStocks(); var all = INDEX_CODES.slice(); for (var i = 0; i < gs.length; i++) { if (all.indexOf(gs[i].idx) < 0) all.push(gs[i].idx); } var q = await loadTencentQuotes(all); // 更新指数 var hi = false; for (var i = 0; i < INDEX_CODES.length; i++) { var x = q[INDEX_CODES[i]]; if (x && x.price > 0) { IDX_CACHE[INDEX_CODES[i]] = { price: x.price, change: x.change, changePct: x.changePct }; hi = true; } } if (hi) renderIdx(); // 更新个股 for (var i = 0; i < gs.length; i++) { var x = q[gs[i].idx]; if (x && x.price) STOCK_CACHE[gs[i].idx] = x; } // 更新界面数字 updatePrices(); } finally { REFRESHING = false; }}
// ============ 添加股票 ============function showAddModal() { document.getElementById('addModal').classList.add('active'); document.getElementById('searchInput').value = ''; document.getElementById('searchResults').classList.remove('active'); document.getElementById('searchResults').innerHTML = ''; document.getElementById('searchInput').style.display = ''; document.getElementById('searchResults').style.display = ''; document.getElementById('batchImportArea').style.display = 'none'; document.getElementById('batchInput').value = ''; document.getElementById('searchInput').focus(); PENDING = null;}
function showIndexChart(idx) { var box = document.getElementById('indexChartBox'); var cv = document.getElementById('indexChartCv'); var nameEl = document.getElementById('indexChartName'); var infoEl = document.getElementById('indexChartInfo'); var names = ['上证指数','深证成指','创业板指']; nameEl.textContent = names[idx] || ''; box.style.display = 'block'; infoEl.textContent = '加载中...'; var ctx = cv.getContext('2d'); var W = 800, H = 320, dpr = window.devicePixelRatio || 1; cv.width = W * dpr; cv.height = H * dpr; cv.style.width = '100%'; cv.style.height = '160px'; ctx.scale(dpr, dpr); ctx.clearRect(0, 0, W, H); ctx.fillStyle = '#999'; ctx.font = '12px sans-serif'; ctx.textAlign = 'center'; ctx.fillText('加载中...', W/2, H/2); var tcode = INDEX_CODES[idx]; fetchMinuteData(tcode).then(function(data) { if (!data || !data.points || !data.points.length) { infoEl.textContent = '暂无数据'; return; } var pc = data.preClose || 0; var cache = IDX_CACHE[tcode]; if (cache) { infoEl.textContent = cache.price.toFixed(2) + ' ' + (cache.changePct >= 0 ? '+' : '') + cache.changePct.toFixed(2) + '%'; infoEl.style.color = cache.changePct >= 0 ? '#f56c6c' : '#67c23a'; } ctx.setTransform(dpr, 0, 0, dpr, 0, 0); ctx.clearRect(0, 0, W, H); drawMinute(ctx, W, H, data.points, pc, 'index'); });}
function closeAddModal() { document.getElementById('addModal').classList.remove('active'); PENDING = null;}function showModal(type) { if (type === 'addStock') { showAddModal(); } else if (type === 'addGroup') { var m = document.getElementById('addGroupModal'); m.style.display = 'flex'; var inp = document.getElementById('newGroupNameInput'); inp.value = ''; setTimeout(function() { inp.focus(); }, 50); } else if (type === 'delGroup') { var m = document.getElementById('delGroupModal'); m.style.display = 'flex'; } else if (type === 'edit') { openEdit(); } else if (type === 'alert') { document.getElementById('alertModal').style.display = 'flex'; }}function hideModal(type) { if (type === 'addStock') { closeAddModal(); } else if (type === 'addGroup') { document.getElementById('addGroupModal').style.display = 'none'; } else if (type === 'delGroup') { document.getElementById('delGroupModal').style.display = 'none'; } else if (type === 'edit') { closeEditModal3(); } else if (type === 'alert') { document.getElementById('alertModal').style.display = 'none'; }}// 点击弹窗外部关闭document.addEventListener('click', function(e) { var modal = document.getElementById('addModal'); if (modal.classList.contains('active') && e.target === modal) { hideModal('addStock'); }});
// 点击弹窗外部关闭
var srchTimer = null;document.getElementById('searchInput').addEventListener('input', function() { clearTimeout(srchTimer); var v = this.value.trim(); if (!v.length) { document.getElementById('searchResults').classList.remove('active'); document.getElementById('searchResults').innerHTML = ''; return; } srchTimer = setTimeout(async function() { var r = await searchStock(v); var ct = document.getElementById('searchResults'); if (!r.length) { ct.innerHTML = '<div class="search-item" style="color:#999;justify-content:center">未找到匹配结果</div>'; } else { ct.innerHTML = r.map(function(i) { return '<div class="search-item" onclick="selectSearchResult(\'' + i.index + '\',\'' + i.name.replace(/'/g, "\\'") + '\',\'' + i.code + '\',\'' + i.index + '\')">' + '<span class="scode">' + i.code + '</span><span class="sname">' + i.name + '</span><span class="stag">' + i.exchange + '</span></div>'; }).join(''); } ct.classList.add('active'); }, 300);});
function selectSearchResult(sid, nm, cd, idx) { if (STOCKS.some(function(s) { return s.idx === idx; })) { showToast('该股票已在列表中'); return; } STOCKS.push({ name: nm, code: cd, idx: idx, group: CUR_GROUP }); saveCfg(); closeAddModal(); STOCK_CACHE[idx] = null; rebuildList(); refreshAll();}
function toggleBatchImport() { var area = document.getElementById('batchImportArea'); var searchResults = document.getElementById('searchResults'); var searchInput = document.getElementById('searchInput'); if (area.style.display === 'none') { area.style.display = 'block'; searchResults.style.display = 'none'; searchInput.style.display = 'none'; } else { area.style.display = 'none'; searchResults.style.display = ''; searchInput.style.display = ''; }}
function doBatchImport() { var textarea = document.getElementById('batchInput'); var raw = textarea.value.trim(); if (!raw) { showToast('请输入股票代码或名称'); return; } var lines = raw.split(/[\r\n]+/).map(function(l){ return l.trim(); }).filter(function(l){ return l.length > 0; }); if (!lines.length) { showToast('请输入股票代码或名称'); return; } var added = 0; var skipped = 0; var notFound = []; var pending = lines.length; if (pending === 0) return; function processNext(idx) { if (idx >= lines.length) { var msg = '成功导入 ' + added + ' 只股票'; if (skipped > 0) msg += ',跳过 ' + skipped + ' 只(已存在)'; if (notFound.length > 0) msg += '\n\n以下未匹配到,请检查:\n' + notFound.join('\n'); showToast(msg); if (added > 0) { saveCfg(); rebuildList(); refreshAll(); } return; } var kw = lines[idx]; searchStock(kw).then(function(results) { // 精确匹配:名称全等 或 代码全等 var found = null; var kwLower = kw.toLowerCase(); for (var i = 0; i < results.length; i++) { var r = results[i]; if (r.name === kw || r.code === kw || r.name.toLowerCase() === kwLower) { found = r; break; } } if (found) { if (STOCKS.some(function(s){ return s.idx === found.index; })) { skipped++; } else { STOCKS.push({ name: found.name, code: found.code, idx: found.index, group: CUR_GROUP }); STOCK_CACHE[found.index] = null; added++; } } else { notFound.push(kw); } processNext(idx + 1); }); } processNext(0);}
function renderGroups() { var box = document.getElementById('groupTabs'); if (!box) return; box.innerHTML = GROUPS.map(function(g) { return '<span class="group-tab' + (g === CUR_GROUP ? ' active' : '') + '" data-group="' + g + '">' + g + '</span>'; }).join('') + '<span class="group-add" id="addGroupBtn" title="新建分组">+</span>'; // 点击切换分组 var tabs = box.querySelectorAll('.group-tab'); for (var i = 0; i < tabs.length; i++) { (function(t) { t.onclick = function() { if (t.classList.contains('editing')) return; CUR_GROUP = t.dataset.group; saveCfg(); renderGroups(); rebuildList(); refreshAll(); }; t.oncontextmenu = function(e) { e.preventDefault(); window._ctxGroupTab = t; var menu = document.getElementById('groupCtxMenu'); menu.style.left = e.clientX + 'px'; menu.style.top = e.clientY + 'px'; menu.classList.add('show'); }; })(tabs[i]); } // 新建分组按钮 document.getElementById('addGroupBtn').onclick = function() { showModal('addGroup'); }; window.confirmAddGroup = function() { var input = document.getElementById('newGroupNameInput'); var name = input ? input.value.trim() : ''; if (!name) return; if (GROUPS.indexOf(name) >= 0) { showToast('分组名称已存在'); return; } GROUPS.push(name); CUR_GROUP = name; saveCfg(); renderGroups(); rebuildList(); hideModal('addGroup'); }; window.onAddGroupKey = function(e) { if (e.key === 'Enter') confirmAddGroup(); };}
// 点击页面其他区域关闭右键菜单document.addEventListener('click', function() { var menu = document.getElementById('groupCtxMenu'); if (menu) menu.classList.remove('show'); var menu2 = document.getElementById('stockCtxMenu'); if (menu2) menu2.classList.remove('show');});document.addEventListener('contextmenu', function(e) { if (!e.target.closest('.group-tab')) { var menu = document.getElementById('groupCtxMenu'); if (menu) menu.classList.remove('show'); }});
// 右键菜单 - 编辑分组window.ctxEditGroup = function() { document.getElementById('groupCtxMenu').classList.remove('show'); var t = window._ctxGroupTab; if (!t) return; t.contentEditable = true; t.classList.add('editing'); t.focus(); var range = document.createRange(); range.selectNodeContents(t); var sel = window.getSelection(); sel.removeAllRanges(); sel.addRange(range); var oldName = t.dataset.group; function finish() { t.contentEditable = false; t.classList.remove('editing'); var newName = t.textContent.trim(); if (!newName || newName === oldName) { t.textContent = oldName; return; } if (GROUPS.indexOf(newName) >= 0) { t.textContent = oldName; showToast('分组名称已存在'); return; } for (var j = 0; j < STOCKS.length; j++) { if (STOCKS[j].group === oldName) STOCKS[j].group = newName; } var gi = GROUPS.indexOf(oldName); if (gi >= 0) GROUPS[gi] = newName; if (CUR_GROUP === oldName) CUR_GROUP = newName; saveCfg(); renderGroups(); rebuildList(); } t.onblur = finish; t.onkeydown = function(e) { if (e.key === 'Enter') { e.preventDefault(); t.blur(); } };};
// ===== 右键菜单 - 关注/取消关注 =====window.ctxToggleLike = function() { document.getElementById('stockCtxMenu').classList.remove('show'); var idx = window._ctxStockIdx; if (!idx) return; for (var i = 0; i < STOCKS.length; i++) { if (STOCKS[i].idx === idx) { STOCKS[i].liked = !STOCKS[i].liked; var row = window._rows && window._rows[idx]; if (row) row.classList.toggle('liked', STOCKS[i].liked); break; } } saveCfg();};
// ===== 右键菜单 - 打开提醒面板 =====window.ctxSetAlert = function() { document.getElementById('stockCtxMenu').classList.remove('show'); var idx = window._ctxStockIdx; if (!idx) return; var stock = null; for (var i = 0; i < STOCKS.length; i++) { if (STOCKS[i].idx === idx) { stock = STOCKS[i]; break; } } if (!stock) return; window._alertStockIdx = idx; document.getElementById('alertModalTitle').textContent = '设置提醒 - ' + stock.name + ' ' + stock.code; document.getElementById('alertValue').value = ''; document.getElementById('alertType').value = 'price_up'; updateAlertUnit(); renderAlertList(); showModal('alert');};
function updateAlertUnit() { var type = document.getElementById('alertType').value; document.getElementById('alertUnit').textContent = (type === 'pct_up' || type === 'pct_down') ? '%' : '元';}
function renderAlertList() { var idx = window._alertStockIdx; var box = document.getElementById('alertExistList'); if (!box) return; var list = ALERTS.filter(function(a) { return a.idx === idx; }); if (!list.length) { box.innerHTML = '<div style="color:#999;font-size:11px;text-align:center;padding:6px 0">暂无提醒</div>'; return; } var html = ''; for (var i = 0; i < list.length; i++) { var a = list[i]; var label = ''; if (a.type === 'price_up') label = '股价高于 ' + parseFloat(a.value).toFixed(2); else if (a.type === 'price_down') label = '股价低于 ' + parseFloat(a.value).toFixed(2); else if (a.type === 'pct_up') label = '涨幅超过 ' + parseFloat(a.value).toFixed(2) + '%'; else if (a.type === 'pct_down') label = '跌幅超过 ' + parseFloat(a.value).toFixed(2) + '%'; html += '<div style="display:flex;align-items:center;justify-content:space-between;padding:4px 8px;background:#f9f9f9;border-radius:4px;margin-bottom:3px;font-size:11px">' + '<span>' + label + (a.fired ? ' <span style="color:#999">(已触发)</span>' : '') + '</span>' + '<span style="color:#f56c6c;cursor:pointer;padding:0 4px" onclick="removeAlert(\'' + a.id + '\')">删除</span></div>'; } box.innerHTML = html;}
function removeAlert(alertId) { for (var i = 0; i < ALERTS.length; i++) { if (ALERTS[i].id === alertId) { ALERTS.splice(i, 1); break; } } saveCfg(); renderAlertList();}
function doAddAlert() { var idx = window._alertStockIdx; if (!idx) return; var type = document.getElementById('alertType').value; var value = parseFloat(document.getElementById('alertValue').value); if (isNaN(value) || value <= 0) { showToast('请输入有效数值'); return; } for (var i = 0; i < ALERTS.length; i++) { if (ALERTS[i].idx === idx && ALERTS[i].type === type && parseFloat(ALERTS[i].value) === value && !ALERTS[i].fired) { showToast('该提醒已存在'); return; } } var stock = null; for (var i = 0; i < STOCKS.length; i++) { if (STOCKS[i].idx === idx) { stock = STOCKS[i]; break; } } ALERTS.push({ idx: idx, name: stock ? stock.name : '', code: stock ? stock.code : '', type: type, value: value, fired: false, id: Date.now() + '_' + Math.random().toString(36).substr(2,6) }); saveCfg(); document.getElementById('alertValue').value = ''; renderAlertList(); showToast('提醒已添加');}
// ===== 检查提醒条件 =====function checkAlerts() { for (var i = 0; i < ALERTS.length; i++) { var a = ALERTS[i]; if (a.fired) continue; var c = STOCK_CACHE[a.idx]; if (!c || !c.price) continue; var triggered = false; var msg = ''; if (a.type === 'price_up' && c.price >= parseFloat(a.value)) { triggered = true; msg = a.name + ' 股价已达到 ' + c.price.toFixed(2) + ' 元,高于设定的 ' + parseFloat(a.value).toFixed(2) + ' 元'; } else if (a.type === 'price_down' && c.price <= parseFloat(a.value)) { triggered = true; msg = a.name + ' 股价已达到 ' + c.price.toFixed(2) + ' 元,低于设定的 ' + parseFloat(a.value).toFixed(2) + ' 元'; } else if (a.type === 'pct_up' && c.changePct >= parseFloat(a.value)) { triggered = true; msg = a.name + ' 涨幅已达到 ' + c.changePct.toFixed(2) + '%,超过设定的 ' + parseFloat(a.value).toFixed(2) + '%'; } else if (a.type === 'pct_down' && c.changePct <= -parseFloat(a.value)) { triggered = true; msg = a.name + ' 跌幅已达到 ' + c.changePct.toFixed(2) + '%,超过设定的 ' + parseFloat(a.value).toFixed(2) + '%'; } if (triggered) { a.fired = true; saveCfg(); fireNotification(msg); } }}
function fireNotification(msg) { try { if (typeof $quickerSp !== 'undefined') { $quickerSp('showStockAlert', { message: msg }); return; } } catch(e) {} try { if ('Notification' in window && Notification.permission === 'granted') { new Notification(msg); return; } } catch(e) {} showToast(msg, 8000);}
// 右键菜单 - 删除分组window._delGroupName = '';window.ctxDelGroup = function() { document.getElementById('groupCtxMenu').classList.remove('show'); var t = window._ctxGroupTab; if (!t) return; var groupName = t.dataset.group; window._delGroupName = groupName; // 检查组内是否有股票 var stocksInGroup = []; for (var i = 0; i < STOCKS.length; i++) { if (STOCKS[i].group === groupName) stocksInGroup.push(STOCKS[i]); } var body = document.getElementById('delGroupBody'); var moveArea = document.getElementById('delGroupMoveArea'); var target = document.getElementById('delGroupTarget'); var moveBtn = document.getElementById('delGroupMoveBtn'); if (stocksInGroup.length === 0) { body.textContent = '确定要删除分组"' + groupName + '"吗?'; moveArea.style.display = 'none'; moveBtn.style.display = 'none'; } else { body.textContent = '分组"' + groupName + '"内有 ' + stocksInGroup.length + ' 只股票。'; moveArea.style.display = 'block'; moveBtn.style.display = ''; target.innerHTML = ''; var hasOther = false; for (var g = 0; g < GROUPS.length; g++) { if (GROUPS[g] !== groupName) { hasOther = true; var opt = document.createElement('option'); opt.value = GROUPS[g]; opt.textContent = GROUPS[g]; target.appendChild(opt); } } if (!hasOther) { moveBtn.style.display = 'none'; } } showModal('delGroup');};
window.doMoveAndDel = function() { var groupName = window._delGroupName; var target = document.getElementById('delGroupTarget'); var moveTo = target ? target.value : ''; if (!moveTo) { showToast('请选择目标分组'); return; } for (var i = 0; i < STOCKS.length; i++) { if (STOCKS[i].group === groupName) STOCKS[i].group = moveTo; } finishDelGroup(groupName);};window.doDirectDel = function() { var groupName = window._delGroupName; STOCKS = STOCKS.filter(function(s) { return s.group !== groupName; }); finishDelGroup(groupName);};window.finishDelGroup = function(groupName) { for (var i = 0; i < STOCKS.length; i++) { if (STOCKS[i].group === groupName) { delete STOCK_CACHE[STOCKS[i].idx]; delete EXPANDED[STOCKS[i].idx]; } } STOCKS = STOCKS.filter(function(s) { return s.group !== groupName; }); var gi = GROUPS.indexOf(groupName); if (gi >= 0) GROUPS.splice(gi, 1); if (CUR_GROUP === groupName) { CUR_GROUP = GROUPS.length > 0 ? GROUPS[0] : ''; } saveCfg(); renderGroups(); rebuildList(); refreshAll(); hideModal('delGroup');};;
window.hideDelGroup = function() { document.getElementById('delGroupModal').style.display = 'none';};document.getElementById('delGroupModal').addEventListener('click', function(e) { if (e.target === this) hideModal('delGroup');});
function startAutoRefresh() { refreshAll(); TIMER = setInterval(function() { refreshAll(); }, INTERVAL);}
// 编辑弹窗
function openEdit() { var gs = getCurrentStocks(); if (!gs.length) { showToast('当前分组无股票'); return; } window._editAllSelected = false; var btn = document.getElementById('editSelectAllBtn'); if (btn) btn.textContent = '全选'; document.getElementById('editGroupName3').textContent = CUR_GROUP; renderEditList3(); document.getElementById('editModal').classList.add('active');}function closeEditModal3() { document.getElementById('editModal').classList.remove('active'); }function renderEditList3() { var gs = getCurrentStocks(); var el = document.getElementById('editList3'); el.innerHTML = gs.map(function(s, i) { var c = STOCK_CACHE[s.idx]; var price = c && c.price ? c.price.toFixed(2) : '--'; return '<div class="edit-row3" data-idx="' + s.idx + '" draggable="true" style="display:flex;align-items:center;padding:6px 8px;border-bottom:1px solid #f0f0f0;font-size:10px;cursor:grab">' + '<span class="drag-h3" style="margin-right:6px;color:#ccc;font-size:14px;cursor:grab">\u2630</span>' + '<input type="checkbox" class="ecb3" value="' + s.idx + '" style="margin-right:6px">' + '<span style="flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' + s.name + '</span>' + '<span style="width:55px;text-align:right;color:#888;font-size:10px">' + price + '</span>' + '<span class="top-btn3" data-idx="' + s.idx + '" style="margin-left:6px;padding:2px 6px;font-size:9px;color:#409eff;border:1px solid #409eff;border-radius:3px;cursor:pointer;white-space:nowrap">置顶</span>' + '<span class="del-btn3" data-idx="' + s.idx + '" style="margin-left:4px;padding:2px 6px;font-size:9px;color:#f56c6c;border:1px solid #f56c6c;border-radius:3px;cursor:pointer;white-space:nowrap">删除</span>' + '</div>'; }).join(''); setupDragSort3(); // 点击行切换复选框(排除拖拽手柄和操作按钮) el.querySelectorAll('.edit-row3').forEach(function(row) { row.addEventListener('click', function(e) { if (e.target.closest('.drag-h3') || e.target.closest('.top-btn3') || e.target.closest('.del-btn3') || e.target.closest('.ecb3')) return; var cb = row.querySelector('.ecb3'); if (cb) cb.checked = !cb.checked; }); }); // 删除按钮事件 el.querySelectorAll('.del-btn3').forEach(function(b) { b.onclick = function(e) { e.stopPropagation(); delOne3(this.dataset.idx); }; }); el.querySelectorAll('.top-btn3').forEach(function(b) { b.onclick = function(e) { e.stopPropagation(); topOne3(this.dataset.idx); }; });}function setupDragSort3() { var el = document.getElementById('editList3'); var dragSrc = null; Array.from(el.querySelectorAll('.edit-row3')).forEach(function(row) { row.addEventListener('dragstart', function(e) { dragSrc = this; this.style.opacity = '0.4'; try { e.dataTransfer.effectAllowed = 'move'; } catch(ex) {} }); row.addEventListener('dragend', function() { this.style.opacity = '1'; }); row.addEventListener('dragover', function(e) { e.preventDefault(); }); row.addEventListener('drop', function(e) { e.preventDefault(); if (!dragSrc || dragSrc === this) return; var all = Array.from(el.querySelectorAll('.edit-row3')); var fromIdx = all.indexOf(dragSrc); var toIdx = all.indexOf(this); if (fromIdx < 0 || toIdx < 0) return; var gs = getCurrentStocks(); var srcGlobal = -1, tgtGlobal = -1; for (var i = 0; i < STOCKS.length; i++) { if (STOCKS[i].group === CUR_GROUP) { if (STOCKS[i].idx === dragSrc.dataset.idx) srcGlobal = i; if (STOCKS[i].idx === this.dataset.idx) tgtGlobal = i; } } if (srcGlobal >= 0 && tgtGlobal >= 0) { var item = STOCKS.splice(srcGlobal, 1)[0]; STOCKS.splice(tgtGlobal, 0, item); saveCfg(); renderEditList3(); } }); });}function delOne3(idx) { STOCKS = STOCKS.filter(function(s) { return s.idx !== idx; }); delete STOCK_CACHE[idx]; delete EXPANDED[idx]; saveCfg(); renderEditList3();}function topOne3(idx) { var gs = getCurrentStocks(); for (var i = 0; i < STOCKS.length; i++) { if (STOCKS[i].idx === idx && STOCKS[i].group === CUR_GROUP) { var item = STOCKS.splice(i, 1)[0]; STOCKS.unshift(item); saveCfg(); renderEditList3(); break; } }}function delChecked3() { var cbs = document.querySelectorAll('.ecb3:checked'); if (!cbs.length) { showToast('请先勾选要删除的股票'); return; } if (!confirm('确定删除选中的 ' + cbs.length + ' 只股?')) return; var ids = {}; cbs.forEach(function(cb) { ids[cb.value] = true; }); STOCKS = STOCKS.filter(function(s) { return !ids[s.idx]; }); for (var k in ids) { delete STOCK_CACHE[k]; delete EXPANDED[k]; } saveCfg(); renderEditList3();}
window._editAllSelected = false;window.toggleEditSelectAll = function() { var cbs = document.querySelectorAll('.ecb3'); window._editAllSelected = !window._editAllSelected; for (var i = 0; i < cbs.length; i++) cbs[i].checked = window._editAllSelected; document.getElementById('editSelectAllBtn').textContent = window._editAllSelected ? '取消全选' : '全选';};
window.toggleEditMoveMenu = function() { var menu = document.getElementById('editMoveMenu'); if (menu.style.display === 'block') { menu.style.display = 'none'; return; } var cbs = document.querySelectorAll('.ecb3:checked'); if (!cbs.length) { showToast('请先勾选要移动的股票'); return; } menu.innerHTML = ''; for (var g = 0; g < GROUPS.length; g++) { if (GROUPS[g] === CUR_GROUP) continue; var item = document.createElement('div'); item.textContent = GROUPS[g]; item.style.cssText = 'padding:6px 16px;font-size:12px;cursor:pointer;color:#333'; item.onmouseover = function() { this.style.background = '#e8f4fd'; this.style.color = '#409eff'; }; item.onmouseout = function() { this.style.background = ''; this.style.color = '#333'; }; (function(targetGroup) { item.onclick = function() { document.getElementById('editMoveMenu').style.display = 'none'; var checked = document.querySelectorAll('.ecb3:checked'); var ids = {}; checked.forEach(function(cb) { ids[cb.value] = true; }); var count = 0; for (var j = 0; j < STOCKS.length; j++) { if (ids[STOCKS[j].idx]) { STOCKS[j].group = targetGroup; count++; } } saveCfg(); renderEditList3(); window._editAllSelected = false; document.getElementById('editSelectAllBtn').textContent = '全选'; }; })(GROUPS[g]); menu.appendChild(item); } if (!menu.children.length) { var empty = document.createElement('div'); empty.textContent = '无其他分组'; empty.style.cssText = 'padding:6px 16px;font-size:11px;color:#999'; menu.appendChild(empty); } menu.style.display = 'block';};
// 点击其他区域关闭移动菜单document.addEventListener('click', function(e) { if (!e.target.closest('#editMoveToBtn') && !e.target.closest('#editMoveMenu')) { var menu = document.getElementById('editMoveMenu'); if (menu) menu.style.display = 'none'; }});function saveEdit3() { SORT_FIELD = null; SORT_ASC = false; rebuildList(); closeEditModal3();}
function init() { loadCfg();
renderGroups(); buildList(); // 一次DOM构建 updatePrices(); // 初始价格更新 startAutoRefresh(); hydrateCfgFromYanmu(); // 指数栏点击展开分时图 var idxItems = document.querySelectorAll('#indexBar .index-item'); for (var i = 0; i < idxItems.length; i++) { (function(el) { el.style.cursor = 'pointer'; el.onclick = function() { var box = document.getElementById('indexChartBox'); if (box.style.display === 'none' || INDEX_EXPANDED !== el.dataset.idx) { INDEX_EXPANDED = el.dataset.idx; var allIdx = document.querySelectorAll('#indexBar .index-item'); for (var j = 0; j < allIdx.length; j++) allIdx[j].classList.toggle('active', allIdx[j] === el); showIndexChart(parseInt(el.dataset.idx)); } else { box.style.display = 'none'; INDEX_EXPANDED = null; var allIdx2 = document.querySelectorAll('#indexBar .index-item'); for (var j = 0; j < allIdx2.length; j++) allIdx2[j].classList.remove('active'); } }; })(idxItems[i]); }}
function waitForYanmReady() { if (window.yanm && typeof window.yanm.invoke === 'function') { init(); } else { setTimeout(waitForYanmReady, 200); }}waitForYanmReady();// ============ 个股详情 ============function showStockDetail(idx) { var stock = null; for (var i = 0; i < STOCKS.length; i++) { if (STOCKS[i].idx === idx) { stock = STOCKS[i]; break; } } if (!stock) return; var tcode = (stock.code.startsWith('6') ? 'sh' : 'sz') + stock.code; var cv = document.getElementById('ch_' + idx); var det = document.getElementById('detail_' + idx); if (cv) cv.style.display = 'none'; if (!det) { det = document.createElement('div'); det.id = 'detail_' + idx; det.style.cssText = 'font-size:10px;line-height:1.8;padding:6px 8px;min-height:180px'; if (cv && cv.parentNode) cv.parentNode.insertBefore(det, cv.nextSibling); else return; } else { det.style.display = 'block'; } det.innerHTML = '<div style="text-align:left;color:#999;padding:20px">加载中...</div>'; fetchDetailData(tcode, idx);}
function fetchDetailData(tcode, idx) { var contentEl = document.getElementById('detail_' + idx); if (!contentEl) return; var varName = 'v_' + tcode; var timer = setTimeout(function() { cleanup(); contentEl.innerHTML = '<div style="text-align:left;color:#999">加载失败</div>'; }, 8000); function cleanup() { clearTimeout(timer); var sc = document.getElementById('_scr_detail'); if (sc && sc.parentNode) sc.parentNode.removeChild(sc); } var s = document.getElementById('_scr_detail'); if (s && s.parentNode) s.parentNode.removeChild(s); s = document.createElement('script'); s.id = '_scr_detail'; s.src = 'https://qt.gtimg.cn/q=' + tcode; s.onload = function() { cleanup(); var raw = window[varName]; if (!raw) { contentEl.innerHTML = '<div style="text-align:left;color:#999">暂无数据</div>'; return; } var f = raw.split('~'); if (f.length < 70) { contentEl.innerHTML = '<div style="text-align:left;color:#999">暂无数据</div>'; return; } var stock = null; for (var i = 0; i < STOCKS.length; i++) { if (STOCKS[i].idx === idx) { stock = STOCKS[i]; break; } } var preClose = parseFloat(f[4]) || 0; var open = parseFloat(f[5]) || 0; var high = parseFloat(f[33]) || 0; var low = parseFloat(f[34]) || 0; var change = parseFloat(f[31]) || 0; var changePct = parseFloat(f[32]) || 0; var vol = parseFloat(f[6]) || 0; var amount = parseFloat(f[37]) || 0; var turnover = parseFloat(f[38]) || 0; var pe = parseFloat(f[39]) || 0; var pb = parseFloat(f[43]) || 0; var amplitude = parseFloat(f[47]) || 0; var weekChange = parseFloat(f[48]) || 0; var yearChange = parseFloat(f[62]) || 0; var updateTime = f[30] || ''; if (updateTime.length >= 12) updateTime = updateTime.substring(8, 10) + ':' + updateTime.substring(10, 12); var cc = change > 0 ? '#f56c6c' : (change < 0 ? '#67c23a' : '#909399'); var cp = changePct > 0 ? '#f56c6c' : (changePct < 0 ? '#67c23a' : '#909399'); var wp = weekChange > 0 ? '#f56c6c' : (weekChange < 0 ? '#67c23a' : '#909399'); var yp = yearChange > 0 ? '#f56c6c' : (yearChange < 0 ? '#67c23a' : '#909399'); function fmtPct(v) { return (v >= 0 ? '+' : '') + v.toFixed(2) + '%'; } contentEl.innerHTML = '<div style="display:grid;grid-template-columns:auto 1fr auto 1fr auto 1fr;gap:3px 8px;padding:8px 0;align-items:center">' + '<span style="color:#999">昨收</span><span style="text-align:right;font-weight:600">' + preClose.toFixed(2) + '</span>' + '<span style="color:#999">今开</span><span style="text-align:right;font-weight:600;color:' + (open >= preClose ? '#f56c6c' : '#67c23a') + '">' + open.toFixed(2) + '</span>' + '<span style="color:#999">最高</span><span style="text-align:right;font-weight:600;color:#f56c6c">' + high.toFixed(2) + '</span>' + '<span style="color:#999">最低</span><span style="text-align:right;font-weight:600;color:#67c23a">' + low.toFixed(2) + '</span>' + '<span style="color:#999">涨跌额</span><span style="text-align:right;font-weight:600;color:' + cc + '">' + (change >= 0 ? '+' : '') + change.toFixed(2) + '</span>' + '<span style="color:#999">涨跌幅</span><span style="text-align:right;font-weight:600;color:' + cp + '">' + fmtPct(changePct) + '</span>' + '<span style="color:#999">振幅</span><span style="text-align:right">' + amplitude.toFixed(2) + '%</span>' + '<span style="color:#999">成交量</span><span style="text-align:right">' + (vol >= 10000 ? (vol/10000).toFixed(0) + '万' : vol.toFixed(0)) + '手</span>' + '<span style="color:#999">成交额</span><span style="text-align:right">' + (amount >= 10000 ? (amount/10000).toFixed(0) + '亿' : amount.toFixed(2) + '万') + '</span>' + '<span style="color:#999">换手率</span><span style="text-align:right">' + turnover.toFixed(2) + '%</span>' + '<span style="color:#999">市盈率</span><span style="text-align:right">' + (pe > 0 ? pe.toFixed(2) : '--') + '</span>' + '<span style="color:#999">市净率</span><span style="text-align:right">' + (pb > 0 ? pb.toFixed(2) : '--') + '</span>' + '<span style="color:#999">周涨跌</span><span style="text-align:right;color:' + wp + '">' + fmtPct(weekChange) + '</span>' + '<span style="color:#999">年涨跌</span><span style="text-align:right;color:' + yp + '">' + fmtPct(yearChange) + '</span>' + '</div>' + '<div style="text-align:right;color:#bbb;font-size:9px;padding-top:6px;border-top:1px solid #f0f0f0">更新时间 ' + updateTime + '</div>'; }; s.onerror = function() { cleanup(); contentEl.innerHTML = '<div style="text-align:left;color:#999">加载失败</div>'; }; document.head.appendChild(s);}
</script>
<div class="modal-overlay" id="editModal"> <div class="modal-box" style="width:380px;max-height:500px"> <div class="modal-title" style="display:flex;justify-content:space-between;align-items:center"> <span>编辑自选 - <span id="editGroupName3"></span></span> <span style="font-size:12px;color:#999;cursor:pointer" onclick="closeEditModal3()">✕</span> </div> <div style="font-size:9px;color:#999;margin:4px 0 6px">拖拽行左侧手柄可调整排序</div> <div id="editList3" style="max-height:320px;overflow-y:auto;border:1px solid #eee;border-radius:4px"></div> <div style="margin-top:8px;display:flex;gap:6px;align-items:center"> <button class="mbtn cancel" onclick="delChecked3()" style="font-size:10px;padding:5px 12px;color:#f56c6c;border:1px solid #f56c6c;flex:none;min-width:64px">删除选中</button> <button class="mbtn cancel" id="editSelectAllBtn" onclick="toggleEditSelectAll()" style="font-size:10px;padding:5px 12px;flex:none;min-width:64px">全选</button> <span style="flex:1"></span> <div style="position:relative;display:inline-block"> <button class="mbtn cancel" id="editMoveToBtn" onclick="toggleEditMoveMenu()" style="font-size:10px;padding:5px 12px;flex:none;min-width:64px">移动到 ▼</button> <div id="editMoveMenu" style="display:none;position:absolute;bottom:100%;right:0;margin-bottom:4px;background:#fff;border:1px solid #e0e0e0;border-radius:6px;box-shadow:0 4px 12px rgba(0,0,0,.15);padding:4px 0;min-width:100px;z-index:10"></div> </div> <button class="mbtn cancel" onclick="saveEdit3()" style="font-size:10px;padding:5px 12px;color:#409eff;border:1px solid #409eff;flex:none;min-width:64px">完成编辑</button> </div> </div></div>
<div id="toast" style="display:none;position:fixed;top:10px;left:50%;transform:translateX(-50%);z-index:99999; background:rgba(0,0,0,0.8);color:#fff;padding:10px 18px;border-radius:6px;font-size:12px; max-width:360px;max-height:250px;overflow-y:auto;word-break:break-all;text-align:center; box-shadow:0 2px 12px rgba(0,0,0,0.3);line-height:1.6;white-space:pre-wrap"></div>
</body></html>
为什么我复制后没你这么简洁。
你的什么样?图发上来看下 我试了下是一样的
没遇到过 不用在给豆包了 HTML复制全了嘛