Files
sanguo_vnpy_v2/docs/factor_research/factor-new-result-mockup.html
T
claude_dev 271adedbb5
CI/CD / test (push) Successful in 1s
CI/CD / nas-deploy (push) Successful in 6s
CI/CD / nas-verify (push) Successful in 8s
docs(factor): 设计稿v2——标的池textarea改三层输入(用户反馈'太原始') [nas]
参考聚宽/万得选股器+收件人式tags input,把'手打代码'变'点选':
①预设池一键选:跨行业30/沪深300/中证500/1000/2000/全A主板+创业板(cs_rank族
因子300+只才不失真,手贴不现实)
②搜索加自选:代码/名称模糊→候选下拉(代码·名称·行业·已选标记)点选加入
③chips已选池:600519贵州茅台✕可删标签+计数徽标+复制清单+清空;
批量粘贴收进折叠兜底入口(能力保留不再是主入口)
配套说明:后端3小端点(池清单/池成分读constituent_unified/标的搜索),
New.vue拆UniversePicker.vue组件,预估1天
Chrome实测:池载入54只+按钮点亮/搜茅台候选带已选标/chips渲染/校验联动全通
2026-08-30 09:48:09 +08:00

603 lines
36 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>因子分析两页终端风设计稿</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&display=swap">
<style>
:root{
--bg:#05080d;--bg-card:#0a0f17;--bg-hover:#0e1622;--bg-overlay:#0d1320;--panel:#070b12;
--text:#d4e4f0;--text-2:#7a8a9a;--text-3:#4a5868;
--brand:#00e5ff;--cyan-dim:#00b8cc;--cyan-soft:rgba(0,229,255,0.14);
--purple-dim:#b48cff;--amber:#ffb000;--amber-soft:rgba(255,176,0,0.14);
--up:#ff4d5e;--down:#2ee68a;--danger:#ff4d5e;
--lamp-ok:#2ee68a;--lamp-warn:#ffb000;--lamp-idle:#4a5868;
--border:#16202e;--border-2:#0f1822;
--grid-line:rgba(0,229,255,0.035);--grid-size:32px;
--r-sm:2px;--r-md:3px;--r-lg:4px;
--mono:'JetBrains Mono',ui-monospace,'SFMono-Regular',Consolas,'Liberation Mono',Menlo,monospace;
--sans:-apple-system,BlinkMacSystemFont,'Segoe UI','PingFang SC','Hiragino Sans GB','Microsoft YaHei',Roboto,sans-serif;
--ease:cubic-bezier(0.4,0,0.2,1);
}
*{box-sizing:border-box}
html,body{margin:0;padding:0}
@media (prefers-reduced-motion:no-preference){html{scroll-behavior:smooth}}
body{background:var(--bg);color:var(--text);font-family:var(--sans);font-size:13px;line-height:1.55;-webkit-font-smoothing:antialiased}
body::before{content:'';position:fixed;inset:0;pointer-events:none;z-index:0;
background-image:linear-gradient(var(--grid-line) 1px,transparent 1px),linear-gradient(90deg,var(--grid-line) 1px,transparent 1px);
background-size:var(--grid-size) var(--grid-size)}
.wrap{position:relative;z-index:1;max-width:1240px;margin:0 auto;padding:18px 22px 90px}
.mono{font-family:var(--mono)}
button{font-family:inherit}
button:focus-visible{outline:1px solid var(--brand);outline-offset:2px}
input,textarea{font-family:inherit}
input:focus-visible,textarea:focus-visible{outline:1px solid rgba(0,229,255,.5);outline-offset:0}
/* ===== 顶栏 ===== */
.topbar{display:flex;align-items:center;justify-content:space-between;gap:14px;border:1px solid var(--border);background:var(--bg-card);border-radius:var(--r-lg);padding:10px 14px;margin-bottom:14px;flex-wrap:wrap}
.topbar .t{font-family:var(--mono);font-size:12.5px;letter-spacing:.05em;font-weight:700}
.topbar .t .dim{color:var(--text-3);font-weight:400}
.tabs{display:flex;gap:6px;flex-wrap:wrap}
.tabs button{font-family:var(--mono);font-size:11.5px;padding:5px 14px;border-radius:var(--r-sm);border:1px solid var(--border);background:transparent;color:var(--text-2);cursor:pointer;transition:all .15s var(--ease)}
.tabs button:hover{color:var(--text);border-color:var(--text-3)}
.tabs button.on{color:var(--brand);background:var(--cyan-soft);border-color:rgba(0,229,255,.35)}
.tab{display:none}
.tab.on{display:block}
/* ===== 设计标注 ===== */
.dcap{display:flex;align-items:center;gap:10px;margin:16px 2px 10px;flex-wrap:wrap}
.dcap .no{font-family:var(--mono);font-size:11px;color:var(--amber);border:1px solid rgba(255,176,0,.4);background:var(--amber-soft);border-radius:var(--r-sm);padding:2px 8px;letter-spacing:.08em;white-space:nowrap}
.dcap .txt{color:var(--text-2);font-size:12px}
.dcap .txt b{color:var(--text)}
.mock{border:1px solid var(--border);background:var(--bg);border-radius:var(--r-lg);padding:20px 22px 26px;box-shadow:0 8px 32px rgba(0,0,0,.6)}
.mock.flash{box-shadow:0 0 0 1px rgba(0,229,255,.55),0 0 24px rgba(0,229,255,.15)}
.new-tag{font-family:var(--mono);font-size:9px;color:var(--amber);border:1px dashed rgba(255,176,0,.45);border-radius:var(--r-sm);padding:1px 6px;letter-spacing:.06em;margin-left:8px;vertical-align:1px}
/* ===== 页面骨架 ===== */
.backlink{color:var(--brand);font-size:12px;margin-bottom:12px;display:inline-block;font-family:var(--mono);letter-spacing:.03em}
.page-head{display:flex;justify-content:space-between;align-items:flex-end;gap:14px;margin-bottom:16px;flex-wrap:wrap}
.page-title{font-size:18px;font-weight:700;letter-spacing:.04em;margin:0}
.page-sub{color:var(--text-2);font-size:12px;margin:2px 0 0;font-family:var(--mono);letter-spacing:.02em}
/* ===== 面板 ===== */
.pane{background:var(--bg-card);border:1px solid var(--border);border-radius:var(--r-lg);margin-bottom:12px;min-width:0}
.pane>.ph{display:flex;align-items:center;gap:10px;padding:10px 14px;border-bottom:1px solid var(--border-2);flex-wrap:wrap}
.pane>.ph .pt{font-size:12.5px;font-weight:700;letter-spacing:.06em}
.pane>.ph .tag{font-family:var(--mono);font-size:9.5px;color:var(--text-3);letter-spacing:.1em}
.pane>.pb{padding:14px}
.formrow{display:flex;align-items:flex-start;gap:14px}
.flab{width:88px;flex-shrink:0;padding-top:7px;text-align:right;color:var(--text-2);font-size:12.5px}
.fbody{flex:1;min-width:0}
/* ===== 因子选择器 ===== */
.fx-search{display:flex;gap:8px;margin-bottom:10px;flex-wrap:wrap;align-items:center}
.fx-search input{flex:1;min-width:200px;background:var(--panel);border:1px solid var(--border);border-radius:var(--r-sm);color:var(--text);font-family:var(--mono);font-size:12px;padding:6px 10px}
.fx-search input::placeholder{color:var(--text-3)}
.fx-count{font-family:var(--mono);font-size:10.5px;color:var(--text-3);white-space:nowrap}
.fx-groups{display:grid;grid-template-columns:1fr 1fr;gap:12px}
@media(max-width:980px){.fx-groups{grid-template-columns:1fr}}
.fx-group>.gh{display:flex;align-items:center;gap:8px;margin-bottom:8px}
.fx-group>.gh .gt{font-family:var(--mono);font-size:11px;letter-spacing:.08em;font-weight:600}
.fx-group>.gh .gc{font-family:var(--mono);font-size:10px;color:var(--text-3)}
.cat-chip{display:inline-block;padding:1px 8px;border-radius:var(--r-sm);font-family:var(--mono);font-size:10.5px;font-weight:600;line-height:17px;border:1px solid transparent}
.cat-alpha101{color:var(--brand);background:rgba(0,229,255,.1);border-color:rgba(0,229,255,.3)}
.cat-alpha158{color:var(--purple-dim);background:rgba(180,140,255,.1);border-color:rgba(180,140,255,.3)}
.cat-builtin{color:var(--amber);background:rgba(255,176,0,.1);border-color:rgba(255,176,0,.3)}
.fx-grid{display:flex;flex-wrap:wrap;gap:5px}
.fxp{font-family:var(--mono);font-size:11px;padding:2px 9px;border-radius:var(--r-sm);border:1px solid var(--border);background:var(--panel);color:var(--text-2);cursor:pointer;transition:all .12s var(--ease);user-select:none}
.fxp:hover{color:var(--text);border-color:var(--text-3)}
.fxp.sel-alpha101{color:var(--brand);border-color:rgba(0,229,255,.45);background:var(--cyan-soft)}
.fxp.sel-alpha158{color:var(--purple-dim);border-color:rgba(180,140,255,.45);background:rgba(180,140,255,.12)}
.fxp.sel-builtin{color:var(--amber);border-color:rgba(255,176,0,.45);background:var(--amber-soft)}
.fxp.hidden{display:none}
.preset{font-family:var(--mono);font-size:10.5px;padding:3px 10px;border-radius:var(--r-sm);border:1px dashed var(--border);background:transparent;color:var(--text-2);cursor:pointer}
.preset:hover{color:var(--brand);border-color:rgba(0,229,255,.4)}
/* ===== 已选托盘 ===== */
.picked{display:flex;align-items:center;gap:8px;margin-top:12px;border:1px dashed rgba(0,229,255,.3);background:rgba(0,229,255,.04);border-radius:var(--r-md);padding:8px 12px;flex-wrap:wrap;min-height:38px}
.picked .pk{font-family:var(--mono);font-size:10.5px;color:var(--brand);letter-spacing:.1em;white-space:nowrap}
.picked .chips{display:flex;gap:5px;flex-wrap:wrap;flex:1}
.pchip{display:inline-flex;align-items:center;gap:6px;font-family:var(--mono);font-size:11px;border:1px solid var(--border);border-radius:var(--r-sm);padding:2px 8px;background:var(--panel)}
.pchip .x{cursor:pointer;color:var(--text-3)}
.pchip .x:hover{color:var(--danger)}
.picked .clear{font-family:var(--mono);font-size:10.5px;color:var(--text-3);cursor:pointer;white-space:nowrap}
.picked .clear:hover{color:var(--danger)}
/* ===== 标的池 / 日期 ===== */
.poolrow{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-bottom:10px}
.poolrow .plab{font-family:var(--mono);font-size:10.5px;color:var(--text-3);letter-spacing:.1em}
.pool{font-family:var(--mono);font-size:11px;padding:3px 12px;border-radius:var(--r-sm);border:1px solid var(--border);background:var(--panel);color:var(--text-2);cursor:pointer;transition:all .12s var(--ease)}
.pool:hover{color:var(--brand);border-color:rgba(0,229,255,.4)}
.pool.on{color:var(--brand);border-color:rgba(0,229,255,.45);background:var(--cyan-soft)}
.symsearch{position:relative;margin-bottom:10px}
.symsearch input{width:100%;background:var(--panel);border:1px solid var(--border);border-radius:var(--r-sm);color:var(--text);font-family:var(--mono);font-size:12px;padding:6px 10px}
.symsearch input::placeholder{color:var(--text-3)}
.cand{position:absolute;top:calc(100% + 3px);left:0;right:0;z-index:30;background:var(--bg-overlay);border:1px solid var(--border);border-radius:var(--r-sm);box-shadow:0 8px 32px rgba(0,0,0,.6);max-height:264px;overflow-y:auto}
.cand .ci{display:flex;align-items:center;gap:10px;padding:6px 10px;cursor:pointer;border-bottom:1px solid var(--border-2)}
.cand .ci:last-child{border-bottom:none}
.cand .ci:hover{background:var(--bg-hover)}
.cand .ci .cc{font-family:var(--mono);font-size:12px;color:var(--brand);width:64px}
.cand .ci .cn{font-size:12px;color:var(--text)}
.cand .ci .in{font-size:10.5px;color:var(--text-3);margin-left:auto}
.cand .ci.sel{opacity:.45;cursor:default}
.cand .none{padding:8px 10px;color:var(--text-3);font-size:11.5px}
.poolbox{display:flex;flex-wrap:wrap;gap:5px;border:1px dashed var(--border);border-radius:var(--r-md);background:var(--panel);padding:9px 10px;min-height:44px;max-height:180px;overflow-y:auto}
.schip{display:inline-flex;align-items:center;gap:7px;font-family:var(--mono);font-size:11px;border:1px solid var(--border);border-radius:var(--r-sm);padding:2px 8px;background:var(--bg-card)}
.schip .code{color:var(--brand)}
.schip .nm{color:var(--text-2);font-family:var(--sans);font-size:11px}
.schip .x{cursor:pointer;color:var(--text-3)}
.schip .x:hover{color:var(--danger)}
.bulkwrap{margin-top:10px}
.sym-area{width:100%;min-height:96px;background:var(--panel);border:1px solid var(--border);border-radius:var(--r-sm);color:var(--text);font-family:var(--mono);font-size:12px;line-height:1.8;padding:8px 10px;resize:vertical}
.sym-area::placeholder{color:var(--text-3)}
.sym-meta{display:flex;align-items:center;gap:10px;margin-top:8px;flex-wrap:wrap}
.sym-badge{font-family:var(--mono);font-size:10.5px;color:var(--brand);border:1px solid rgba(0,229,255,.3);background:var(--cyan-soft);border-radius:var(--r-sm);padding:2px 8px}
.hint-inline{font-family:var(--mono);font-size:10.5px;color:var(--text-3)}
.dates{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
.dates input[type=date]{background:var(--panel);border:1px solid var(--border);border-radius:var(--r-sm);color:var(--text);font-family:var(--mono);font-size:12px;padding:6px 10px;color-scheme:dark}
.dates .sep{color:var(--text-3);font-family:var(--mono);font-size:11px}
.span-note{font-family:var(--mono);font-size:10.5px;color:var(--text-3)}
/* ===== 提交条 ===== */
.submit-bar{display:flex;align-items:center;gap:14px;margin-top:4px;flex-wrap:wrap}
.btn{font-size:12.5px;font-weight:600;border-radius:var(--r-md);padding:8px 18px;border:1px solid var(--border);color:var(--text-2);background:var(--bg-card);transition:all .15s var(--ease);cursor:pointer}
.btn:hover{color:var(--text);border-color:var(--text-3)}
.btn.primary{color:#032027;background:var(--brand);border-color:var(--brand);box-shadow:0 0 0 1px rgba(0,229,255,.22),0 0 18px rgba(0,229,255,.08);font-weight:700}
.btn.primary:hover{background:#33ecff}
.btn:disabled{opacity:.45;cursor:not-allowed;box-shadow:none}
.btn.primary:disabled:hover{background:var(--brand)}
.valid-note{font-family:var(--mono);font-size:11px;color:var(--amber)}
/* ===== 结果页指标条 ===== */
.metric-strip{display:grid;grid-template-columns:repeat(5,1fr);gap:1px;background:var(--border);border:1px solid var(--border);border-radius:var(--r-lg);overflow:hidden;margin-bottom:12px}
.metric{background:var(--bg-card);padding:10px 12px;min-width:0}
.metric .k{color:var(--text-3);font-family:var(--mono);font-size:9.5px;letter-spacing:.12em;text-transform:uppercase;white-space:nowrap}
.metric .v{font-family:var(--mono);font-variant-numeric:tabular-nums;font-size:17px;font-weight:600;margin-top:2px;white-space:nowrap}
@media(max-width:1080px){.metric-strip{grid-template-columns:repeat(3,1fr)}}
/* ===== 表格 ===== */
.tblwrap{overflow-x:auto}
.tbl{width:100%;border-collapse:collapse;font-size:12px}
.tbl th{font-family:var(--mono);font-size:9.5px;letter-spacing:.12em;text-transform:uppercase;color:var(--text-3);text-align:right;padding:8px 10px;border-bottom:1px solid var(--border);background:var(--bg-hover);white-space:nowrap}
.tbl th:first-child{text-align:left}
.tbl td{padding:8px 10px;border-bottom:1px solid var(--border-2);white-space:nowrap;text-align:right;font-variant-numeric:tabular-nums}
.tbl td:first-child{text-align:left}
.tbl tr:hover td{background:var(--bg-hover)}
.dim{color:var(--text-3)}
.cy{color:var(--brand)}
.pos{color:var(--up)}
.neg{color:var(--down)}
.fname-s{color:var(--brand);font-family:var(--mono)}
.sig{font-weight:700}
.best-row td{background:rgba(0,229,255,.05)}
.best-row td:first-child{box-shadow:inset 2px 0 0 var(--brand)}
/* ===== tears 区块示意 ===== */
.tears-tabs{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:10px}
.tears-tabs .tt{font-family:var(--mono);font-size:11px;padding:3px 12px;border-radius:var(--r-sm);border:1px solid var(--border);color:var(--text-2);cursor:pointer}
.tears-tabs .tt.on{color:var(--brand);background:var(--cyan-soft);border-color:rgba(0,229,255,.35)}
.tears-ph{border:1px dashed var(--border);border-radius:var(--r-md);background:var(--panel);padding:26px 18px;text-align:center}
.tears-ph .big{font-family:var(--mono);font-size:13px;color:var(--brand);letter-spacing:.04em}
.tears-ph .sm{color:var(--text-3);font-size:11.5px;margin-top:6px}
/* ===== 说明页 ===== */
.nh{font-size:13.5px;font-weight:700;letter-spacing:.03em;margin:20px 0 8px;color:var(--text)}
.nh:first-child{margin-top:0}
.ask{margin:6px 0 0;padding-left:20px;color:var(--text-2)}
.ask li{margin:6px 0}
.ask b{color:var(--text)}
.foot{color:var(--text-3);font-family:var(--mono);font-size:10.5px;text-align:center;margin-top:26px}
</style>
</head>
<body>
<div class="wrap">
<div class="topbar">
<div class="t">因子分析 · 两页终端风重构 <span class="dim">/ factor-new-result-mockup · 2026-08-30</span></div>
<nav class="tabs">
<button class="on" data-tab="new">① 提交页 New</button>
<button data-tab="result">② 结果页 Result 外壳</button>
<button data-tab="notes">③ 实现说明</button>
</nav>
</div>
<!-- ================= ① 提交页 ================= -->
<section id="tab-new" class="tab on">
<div class="dcap"><span class="no">N1</span><span class="txt"><b>提交页</b>:自选因子 × 自选标的池 × 自选区间的自由实验台。终端风替换 Element 表单——因子改为「搜索 + 分组点选」面板(240 个因子下拉翻页不便);<b>标的池改三层输入:预设池一键选 + 搜索加自选 + chips 已选池</b>(批量粘贴降级为折叠兜底)。</span></div>
<div class="mock flash">
<span class="backlink">← 投研 / 因子分析</span>
<div class="page-head">
<div>
<h2 class="page-title">因子分析 <span class="new-tag">终端风重构</span></h2>
<p class="page-sub">IC 横截面 · 分层 tears 报告 · 自由实验台(排行榜答不了的换池 / 分年 / 多因子并排归这里)</p>
</div>
</div>
<!-- 因子选择 -->
<div class="pane">
<div class="ph"><span class="pt">因子选择</span><span class="tag">FACTOR PICKER · 支持多选</span></div>
<div class="pb">
<div class="fx-search">
<input id="fxq" type="text" placeholder="搜索因子名 / 类别,如 alpha16、KMID、alpha158…">
<span class="fx-count" id="fxcount"></span>
<button class="preset" id="fxtop">预选:榜单 top 因子</button>
</div>
<div class="fx-groups" id="fxgroups"><!-- JS 渲染 --></div>
<div class="picked" id="picked">
<span class="pk">已选 <span id="pkn">0</span></span>
<div class="chips" id="pkchips"><span class="hint-inline">点击上方因子加入,≥1 个可提交</span></div>
<span class="clear" id="pkclear">清空</span>
</div>
</div>
</div>
<!-- 标的池 + 区间 -->
<div class="pane">
<div class="ph"><span class="pt">标的池与区间</span><span class="tag">UNIVERSE &amp; SPAN</span></div>
<div class="pb">
<div class="formrow">
<div class="flab">标的池</div>
<div class="fbody">
<!-- ① 预设池一键选 -->
<div class="poolrow">
<span class="plab">预设池</span>
<button class="pool" data-pool="cross30">跨行业 30 只</button>
<button class="pool" data-pool="hs300">沪深300</button>
<button class="pool" data-pool="zz500">中证500</button>
<button class="pool" data-pool="zz1000">中证1000</button>
<button class="pool" data-pool="zz2000">中证2000</button>
<button class="pool" data-pool="allmain">全A主板+创业板</button>
</div>
<!-- ② 搜索加自选 -->
<div class="symsearch">
<input id="symq" type="text" spellcheck="false" autocomplete="off" placeholder="搜索代码 / 名称加入自选,如 600519 或 茅台 …">
<div class="cand" id="cand" hidden></div>
</div>
<!-- ③ 已选池 chips -->
<div class="poolbox" id="poolbox"><span class="hint-inline" id="poolempty">用上方预设池或搜索加入标的(≥2 只)</span></div>
<div class="sym-meta">
<span class="sym-badge" id="symbadge">0 只</span>
<span class="hint-inline">IC 横截面需 ≥2 只;截面排名类因子(cs_rank 族)建议 300+ 只才不失真</span>
<button class="preset" id="bulktoggle">批量粘贴</button>
<button class="preset" id="copybtn">复制清单</button>
<span class="preset" id="poolclear" style="cursor:pointer">清空</span>
</div>
<!-- 批量粘贴(折叠兜底入口) -->
<div class="bulkwrap" id="bulkwrap" hidden>
<textarea class="sym-area" id="symarea" spellcheck="false" placeholder="粘贴一列股票代码(空格 / 逗号 / 换行混合均可),点「解析加入」合并进已选池"></textarea>
<div style="margin-top:8px"><button class="preset" id="bulkadd">解析加入</button></div>
</div>
</div>
</div>
<div class="formrow" style="margin-top:14px">
<div class="flab">分析区间</div>
<div class="fbody">
<div class="dates">
<input type="date" id="dstart" value="2024-01-02">
<span class="sep"></span>
<input type="date" id="dend" value="2026-06-30">
<span class="span-note" id="spannote"></span>
</div>
<p class="hint-inline" style="margin:8px 0 0">区间前半作因子计算预热,后半(test 段)进入 IC 与分层统计</p>
</div>
</div>
</div>
</div>
<!-- 提交 -->
<div class="submit-bar">
<button class="btn primary" id="submitbtn">提交分析 →</button>
<span class="valid-note" id="validnote"></span>
<span class="hint-inline">提交后进入进度页,完成自动跳转结果页</span>
</div>
</div>
</section>
<!-- ================= ② 结果页 ================= -->
<section id="tab-result" class="tab">
<div class="dcap"><span class="no">R1</span><span class="txt"><b>结果页外壳</b>:指标条 + IC 统计明细终端风替换;分层 tears 区块(TearsPanel)已是新版终端风,<b>本次不动</b>,仅示意位置。</span></div>
<div class="mock">
<span class="backlink">← 投研 / 因子分析</span>
<div class="page-head">
<div>
<h2 class="page-title">因子分析结果 <span class="dim mono" style="font-size:13px">factor_e3a91c07</span></h2>
<p class="page-sub">IC 横截面统计 · 分层 tears 报告 · 3 因子 × 8 标的 × 2024-01-02 → 2026-06-30</p>
</div>
</div>
<div class="metric-strip">
<div class="metric"><div class="k">因子数</div><div class="v">3</div></div>
<div class="metric"><div class="k">最优 ICIR</div><div class="v pos">0.412</div></div>
<div class="metric"><div class="k">最优因子</div><div class="v cy" style="font-size:15px">alpha54</div></div>
<div class="metric"><div class="k">平均 |IC|</div><div class="v">0.0431</div></div>
<div class="metric"><div class="k">显著数 |t|≥2</div><div class="v">4 / 9</div></div>
</div>
<div class="pane">
<div class="ph"><span class="pt">IC 统计明细</span><span class="tag">按因子分组 · 最优 ICIR 行青标 · IC/ICIR/t 正红负绿</span></div>
<div class="pb tblwrap">
<table class="tbl">
<thead><tr><th>因子</th><th>周期</th><th>IC 均值</th><th>IC 标准差</th><th>ICIR</th><th>t 统计</th><th>样本数</th></tr></thead>
<tbody id="icrows"><!-- JS 渲染 --></tbody>
</table>
</div>
</div>
<div class="pane">
<div class="ph"><span class="pt">分层 tears 报告</span><span class="tag">TEARS · 已上线新版,本次重构不动</span></div>
<div class="pb">
<div class="tears-tabs">
<span class="tt on">alpha54</span><span class="tt">alpha16</span><span class="tt">KMID</span>
</div>
<div class="tears-ph">
<div class="big">TearsPanel(新版终端风 · 已上线)</div>
<div class="sm">指标条 + 月度 IC 柱 / 累计线 + 热力图 + 分组净值 + 多空净值 + IC 衰减,1 / 5 / 10D 联动 —— 此区域保持现状,不在本次两页重构范围内</div>
</div>
</div>
</div>
</div>
</section>
<!-- ================= ③ 实现说明 ================= -->
<section id="tab-notes" class="tab">
<div class="mock">
<h3 class="nh">范围</h3>
<ul class="ask">
<li><b>New.vue(提交页)</b> + <b>Result.vue 外壳(指标卡 + IC 明细表)</b> 两处终端风重构;<b>TearsPanel 不动</b>(已是新版终端风)。</li>
<li>要素零变化:因子多选按类分组 / 标的池支持混合分隔批量粘贴 / 起止日期禁选未来 / 提交校验(≥1 因子 + ≥2 标的)/ query.factor 预填。</li>
<li>前端为主,沿用 tokens.css 与全站视觉语言;标的池三层输入需 3 个后端小端点(见下)。</li>
</ul>
<h3 class="nh">提交页交互变化</h3>
<ul class="ask">
<li><b>因子选择从「多选下拉」改为「搜索 + 分组点选」面板</b>:240 个因子在下拉里翻页困难;点选即加入已选托盘,托盘 chips 可单删 / 清空,与排行榜「加入对比」托盘交互一致。</li>
<li>保留类别三色:alpha101 青 / alpha158 紫 / builtin 琥珀(同排行榜类别 chip)。</li>
<li><b>标的池从 textarea 改为三层输入</b>(参考聚宽/万得选股器 + 收件人式 tags input):<br>
<b>预设池一键选</b>——跨行业30 / 沪深300 / 中证500 / 中证1000 / 中证2000 / 全A主板+创业板,大样本一键载入(cs_rank 族因子要 300+ 只才不失真,手贴不现实);<br>
<b>搜索加自选</b>——输代码或名称,候选下拉带「代码 · 名称 · 行业」,点选加入;<br>
<b>chips 已选池</b>——可删标签 + 计数 + 复制清单 + 清空;批量粘贴收进折叠兜底入口(能力保留,不再是主入口)。</li>
<li>后端配套 3 个小端点:池清单(指数名+数量)/ 池成分(代码+名称,读 constituent_unified/ 标的搜索(代码前缀+名称子串);跨行业 30 只与详情页一键直达同一样本。</li>
<li>提交按钮在未满足校验时禁用并给出原因(替代提交后弹 warning)。</li>
</ul>
<h3 class="nh">结果页外壳变化</h3>
<ul class="ask">
<li>五格指标条换 metric-strip 形态(同 tears 页指标条),最优 ICIR 红涨色、最优因子青色。</li>
<li>IC 明细表换终端表格:数值右对齐 tabular-numsIC / ICIR / t 正红负绿,|t|≥2 加粗,全局最优 ICIR 行青色左标。</li>
</ul>
<h3 class="nh">实现拆分(预估)</h3>
<ul class="ask">
<li><b>New.vue</b>:因子面板组件 FactorPicker.vue + 标的池组件 UniversePicker.vue(预设池 + 搜索 combobox + chips+ 后端 3 小端点,约 1 天。</li>
<li><b>Result.vue</b>:指标条 + 表格模板替换(数据逻辑不动),约 1–2 小时。</li>
<li>npm run build + push NAS 验收。</li>
</ul>
<p class="foot">设计稿数据为种子合成(mulberry32),仅供形态确认 · docs/factor_research/factor-new-result-mockup.html</p>
</div>
</section>
</div>
<script>
// —— 种子随机(形态稳定) ——
function mulberry32(a){return function(){a|=0;a=a+0x6D2B79F5|0;let t=Math.imul(a^a>>>15,1|a);t=t+Math.imul(t^t>>>7,61|t)^t;return((t^t>>>14)>>>0)/4294967296}}
const rnd=mulberry32(20260830);
// —— 因子种子数据(真实名抽样) ——
const CATS=[
{key:'alpha101',label:'alpha101',color:'cat-alpha101',names:['alpha1','alpha2','alpha3','alpha6','alpha9','alpha12','alpha16','alpha18','alpha32','alpha42','alpha44','alpha54','alpha65','alpha83','alpha101']},
{key:'alpha158',label:'alpha158',color:'cat-alpha158',names:['KMID','KLEN','KUP','KLOW','KMID2','ROC','ROC20','MA','MA20','STD','STD20','BETA','RSQR','RESI','RESI5','MAX','MIN','QTLU','QTLD','RANK','CORR','CNT','VMA','VSTD','WVMA']},
{key:'builtin',label:'builtin',color:'cat-builtin',names:['bb','cc','dontchg','environment','newhigh','sigma','tq','tq20','vol','year_high']}
];
let picked=new Set(['alpha16']);
const DEFAULT_TOP=['alpha16','alpha54','RESI5','alpha42','alpha32'];
// —— 渲染因子分组 ——
const groupsEl=document.getElementById('fxgroups');
CATS.forEach(cat=>{
const g=document.createElement('div');g.className='fx-group';
g.innerHTML=`<div class="gh"><span class="cat-chip ${cat.color}">${cat.label}</span><span class="gc">${cat.names.length} 个</span></div>`;
const grid=document.createElement('div');grid.className='fx-grid';
cat.names.forEach(n=>{
const b=document.createElement('span');b.className='fxp';b.textContent=n;b.dataset.cat=cat.key;b.dataset.name=n;
if(picked.has(n))b.classList.add('sel-'+cat.key);
b.onclick=()=>{picked.has(n)?picked.delete(n):picked.add(n);renderPicked();refreshCounts();};
grid.appendChild(b);
});
g.appendChild(grid);groupsEl.appendChild(g);
});
// —— 搜索过滤 ——
const q=document.getElementById('fxq');
q.addEventListener('input',()=>{
const v=q.value.trim().toLowerCase();
document.querySelectorAll('.fxp').forEach(el=>{
el.classList.toggle('hidden',v&&!el.dataset.name.toLowerCase().includes(v)&&!el.dataset.cat.includes(v));
});
refreshCounts();
});
document.getElementById('fxtop').onclick=()=>{picked=new Set(DEFAULT_TOP);renderPicked();refreshCounts();};
// —— 已选托盘 ——
function catOf(n){for(const c of CATS)if(c.names.includes(n))return c;return CATS[2];}
function renderPicked(){
const chips=document.getElementById('pkchips');
document.getElementById('pkn').textContent=picked.size;
if(!picked.size){chips.innerHTML='<span class="hint-inline">点击上方因子加入,≥1 个可提交</span>';}
else{
chips.innerHTML='';
[...picked].forEach(n=>{
const c=catOf(n);
const s=document.createElement('span');s.className='pchip';
s.innerHTML=`<span class="cat-chip ${c.color}">${c.label}</span>${n}<span class="x" title="移除">✕</span>`;
s.querySelector('.x').onclick=()=>{picked.delete(n);renderPicked();refreshCounts();};
chips.appendChild(s);
});
}
validate();
}
document.getElementById('pkclear').onclick=()=>{picked.clear();renderPicked();refreshCounts();};
// —— 计数 / 校验 ——
function visibleCount(){return [...document.querySelectorAll('.fxp')].filter(e=>!e.classList.contains('hidden')).length;}
function refreshCounts(){
document.getElementById('fxcount').textContent=`显示 ${visibleCount()} / 51`;
document.querySelectorAll('.fxp').forEach(el=>{
const c=el.dataset.cat,sel=picked.has(el.dataset.name);
el.classList.toggle('sel-alpha101',sel&&c==='alpha101');
el.classList.toggle('sel-alpha158',sel&&c==='alpha158');
el.classList.toggle('sel-builtin',sel&&c==='builtin');
});
}
// —— 标的池三层:预设池 + 搜索 + chips ——
const STOCKS=[
['600519','贵州茅台','白酒'],['000858','五粮液','白酒'],['600036','招商银行','银行'],
['601318','中国平安','保险'],['000001','平安银行','银行'],['600000','浦发银行','银行'],
['300750','宁德时代','电池'],['002594','比亚迪','汽车'],['600276','恒瑞医药','医药'],
['000538','云南白药','医药'],['600887','伊利股份','食品'],['000651','格力电器','家电'],
['000333','美的集团','家电'],['600690','海尔智家','家电'],['000725','京东方A','面板'],
['002415','海康威视','安防'],['600030','中信证券','券商'],['601166','兴业银行','银行'],
['600028','中国石化','石化'],['601857','中国石油','石化'],['600585','海螺水泥','建材'],
['000625','长安汽车','汽车'],['601633','长城汽车','汽车'],['002352','顺丰控股','物流'],
['600104','上汽集团','汽车'],['601398','工商银行','银行'],['600941','中国移动','通信'],
['300059','东方财富','券商'],['002230','科大讯飞','AI'],['601888','中国中免','免税']
];
// 种子合成扩充候选池(形态示意)
(function(){
const pre=['华','中','新','东方','金','天','大','恒','瑞','联'];
const mid=['创','科','安','能','信','光','泰','海','正','宏'];
const suf=['电子','机械','能源','软件','生物','材料','环保','通信','电气','化工'];
const seen=new Set(STOCKS.map(s=>s[0]));
for(let i=0;STOCKS.length<96;i++){
let code=String(100000+Math.floor(rnd()*899999));
if(seen.has(code))continue;seen.add(code);
STOCKS.push([code,pre[Math.floor(rnd()*10)]+mid[Math.floor(rnd()*10)]+suf[Math.floor(rnd()*10)],suf[Math.floor(rnd()*10)]]);
}
})();
const POOLS={
cross30:{label:'跨行业 30 只',list:STOCKS.slice(0,30)},
hs300:{label:'沪深300',list:STOCKS.slice(4,54)},
zz500:{label:'中证500',list:STOCKS.slice(20,80)},
zz1000:{label:'中证1000',list:STOCKS.slice(30,96)},
zz2000:{label:'中证2000',list:STOCKS.slice(40,96)},
allmain:{label:'全A主板+创业板',list:STOCKS.slice()}
};
const symPool=new Map(); // code -> name
function renderPool(){
const box=document.getElementById('poolbox');
document.getElementById('symbadge').textContent=`${symPool.size}`;
if(!symPool.size){box.innerHTML='<span class="hint-inline" id="poolempty">用上方预设池或搜索加入标的(≥2 只)</span>';}
else{
box.innerHTML='';
for(const [c,n] of symPool){
const s=document.createElement('span');s.className='schip';
s.innerHTML=`<span class="code">${c}</span><span class="nm">${n}</span><span class="x" title="移除">✕</span>`;
s.querySelector('.x').onclick=()=>{symPool.delete(c);clearPoolOn();renderPool();};
box.appendChild(s);
}
}
validate();
}
function clearPoolOn(){document.querySelectorAll('.pool').forEach(b=>b.classList.remove('on'));}
document.querySelectorAll('.pool').forEach(b=>{
b.onclick=()=>{
const p=POOLS[b.dataset.pool];
p.list.forEach(s=>symPool.set(s[0],s[1]));
clearPoolOn();b.classList.add('on');renderPool();
};
});
document.getElementById('poolclear').onclick=()=>{symPool.clear();clearPoolOn();renderPool();};
// 搜索 combobox
const symq=document.getElementById('symq'),cand=document.getElementById('cand');
symq.addEventListener('input',()=>{
const v=symq.value.trim();
if(!v){cand.hidden=true;return;}
const hits=STOCKS.filter(s=>s[0].includes(v)||s[1].includes(v)).slice(0,10);
cand.hidden=false;
if(!hits.length){cand.innerHTML='<div class="none">无匹配标的</div>';return;}
cand.innerHTML='';
hits.forEach(s=>{
const d=document.createElement('div');d.className='ci'+(symPool.has(s[0])?' sel':'');
d.innerHTML=`<span class="cc">${s[0]}</span><span class="cn">${s[1]}</span><span class="in">${s[2]}</span>${symPool.has(s[0])?'<span class="in">已选</span>':''}`;
d.onclick=()=>{
if(symPool.has(s[0]))return;
symPool.set(s[0],s[1]);clearPoolOn();renderPool();
symq.value='';cand.hidden=true;symq.focus();
};
cand.appendChild(d);
});
});
symq.addEventListener('keydown',e=>{if(e.key==='Escape'){cand.hidden=true;}});
document.addEventListener('click',e=>{if(!e.target.closest('.symsearch'))cand.hidden=true;});
// 批量粘贴兜底 + 复制
document.getElementById('bulktoggle').onclick=()=>{
const w=document.getElementById('bulkwrap');w.hidden=!w.hidden;
document.getElementById('bulktoggle').textContent=w.hidden?'批量粘贴':'收起粘贴';
};
document.getElementById('bulkadd').onclick=()=>{
const codes=document.getElementById('symarea').value.split(/[\s,]+/).map(s=>s.trim()).filter(Boolean);
let added=0;
codes.forEach(c=>{
if(symPool.has(c))return;
const hit=STOCKS.find(s=>s[0]===c);
symPool.set(c,hit?hit[1]:c);added++;
});
clearPoolOn();renderPool();
};
document.getElementById('copybtn').onclick=()=>{
const txt=[...symPool.keys()].join('\n');
if(navigator.clipboard)navigator.clipboard.writeText(txt).then(()=>alert('已复制 '+symPool.size+' 个代码到剪贴板'));
else alert(txt);
};
function validate(){
const sc=symPool.size;
const btn=document.getElementById('submitbtn'),note=document.getElementById('validnote');
if(!picked.size){btn.disabled=true;note.textContent='待办:至少选 1 个因子';}
else if(sc<2){btn.disabled=true;note.textContent='待办:标的池 ≥2 只';}
else{btn.disabled=false;note.textContent='';}
}
// 区间跨度
function span(){try{const a=new Date(dstart.value),b=new Date(dend.value);const d=Math.round((b-a)/864e5);document.getElementById('spannote').textContent=isFinite(d)?`跨度 ${d} 天 ≈ ${(d/365).toFixed(1)}`:'';}catch(e){}}
const dstart=document.getElementById('dstart'),dend=document.getElementById('dend');
dstart.oninput=span;dend.oninput=span;
// 提交(示意)
document.getElementById('submitbtn').onclick=()=>{alert('示意:提交 → 跳转进度页(/factor/progress/{task_id}),完成自动进结果页。真实实现走现有 submitFactor API。');};
// —— 结果页 IC 表(种子合成) ——
const factors=['alpha54','alpha16','KMID'];
const rows=[];
factors.forEach(f=>{
[1,5,10].forEach(p=>{
const m=(rnd()-0.42)*0.12, sd=0.06+rnd()*0.05;
const n=280+Math.floor(rnd()*24), icir=m/sd, t=icir*Math.sqrt(n);
rows.push({f,p,m,sd,icir,t,n});
});
});
const best=Math.max(...rows.map(r=>r.icir));
document.getElementById('icrows').innerHTML=rows.map(r=>{
const cls=v=>v>=0?'pos':'neg';
const sig=Math.abs(r.t)>=2?' sig':'';
const bestRow=r.icir===best?' class="best-row"':'';
return `<tr${bestRow}><td><span class="fname-s">${r.f}</span>${r.icir===best?' <span class="cy dim" style="font-size:10px">◈ 最优</span>':''}</td><td class="dim mono">${r.p}D</td><td class="${cls(r.m)}">${r.m.toFixed(4)}</td><td>${r.sd.toFixed(4)}</td><td class="${cls(r.icir)}${sig}">${r.icir.toFixed(4)}</td><td class="${cls(r.t)}${sig}">${r.t.toFixed(2)}</td><td class="dim">${r.n}</td></tr>`;
}).join('');
// —— tab 切换 ——
document.querySelectorAll('.tabs button').forEach(b=>{
b.onclick=()=>{
document.querySelectorAll('.tabs button').forEach(x=>x.classList.remove('on'));
document.querySelectorAll('.tab').forEach(x=>x.classList.remove('on'));
b.classList.add('on');document.getElementById('tab-'+b.dataset.tab).classList.add('on');
};
});
document.querySelectorAll('.tears-tabs .tt').forEach(b=>{
b.onclick=()=>{document.querySelectorAll('.tears-tabs .tt').forEach(x=>x.classList.remove('on'));b.classList.add('on');};
});
// init
STOCKS.slice(0,8).forEach(s=>symPool.set(s[0],s[1]));
renderPicked();refreshCounts();renderPool();span();
</script>
</body>
</html>