小组件:股票盯盘助手

经验创意 · 250 次浏览
我的梦想捐钱修路建学校 创建于 2026-05-19 17:33

作者:湘喑

原动作:https://getquicker.net/Sharedaction?code=bdbfb418-5858-4162-2850-08deb3e75025

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()">&#9733; &#x5173;&#x6ce8;</div>
  <div class="ctx-menu-item" onclick="ctxSetAlert()">&#x1f514; &#x63d0;&#x9192;</div>
</div>
<div class="modal-overlay" id="alertModal">
  <div class="modal-box">
    <div class="modal-title" id="alertModalTitle">&#x8bbe;&#x7f6e;&#x63d0;&#x9192; - </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">&#x80a1;&#x4ef7;&#x9ad8;&#x4e8e;</option>
        <option value="price_down">&#x80a1;&#x4ef7;&#x4f4e;&#x4e8e;</option>
        <option value="pct_up">&#x6da8;&#x5e45;&#x8d85;&#x8fc7;</option>
        <option value="pct_down">&#x8dcc;&#x5e45;&#x8d85;&#x8fc7;</option>
      </select>
      <input class="modal-input" id="alertValue" type="number" step="0.01" placeholder="&#x6570;&#x503c;" style="flex:0.7;padding:5px 8px;font-size:11px">
      <span style="font-size:11px;color:#999" id="alertUnit">&#x5143;</span>
    </div>
    <div class="modal-btns">
      <button class="mbtn cancel" onclick="hideModal('alert')">&#x53d6;&#x6d88;</button>
      <button class="mbtn confirm" onclick="doAddAlert()">&#x6dfb;&#x52a0;</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="每行一个股票代码或名称,例如:&#10;000001&#10;平安银行&#10;600519&#10;贵州茅台"></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 ? '&#9733; &#x53d6;&#x6d88;&#x5173;&#x6ce8;' : '&#9734; &#x5173;&#x6ce8;';
    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>

我的梦想捐钱修路建学校 最后更新于 2026/5/19

2026-05-20 00:11 :

为什么我复制后没你这么简洁。


你的什么样?图发上来看下 我试了下是一样的

好像要给豆包一下在复制吗

没遇到过 不用在给豆包了 HTML复制全了嘛

全了。但就是这样。放到豆包就可以。但是不能添加代码。奇怪了。创意很好,就是不方便添加。
回复内容
暂无回复
回复主贴