271adedbb5
参考聚宽/万得选股器+收件人式tags input,把'手打代码'变'点选': ①预设池一键选:跨行业30/沪深300/中证500/1000/2000/全A主板+创业板(cs_rank族 因子300+只才不失真,手贴不现实) ②搜索加自选:代码/名称模糊→候选下拉(代码·名称·行业·已选标记)点选加入 ③chips已选池:600519贵州茅台✕可删标签+计数徽标+复制清单+清空; 批量粘贴收进折叠兜底入口(能力保留不再是主入口) 配套说明:后端3小端点(池清单/池成分读constituent_unified/标的搜索), New.vue拆UniversePicker.vue组件,预估1天 Chrome实测:池载入54只+按钮点亮/搜茅台候选带已选标/chips渲染/校验联动全通
603 lines
36 KiB
HTML
603 lines
36 KiB
HTML
<!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 & 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-nums,IC / 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>
|