diff --git a/docs/factor_research/factor-new-result-mockup.html b/docs/factor_research/factor-new-result-mockup.html index 0a1fcfa..0bc6e9f 100644 --- a/docs/factor_research/factor-new-result-mockup.html +++ b/docs/factor_research/factor-new-result-mockup.html @@ -105,7 +105,31 @@ input:focus-visible,textarea:focus-visible{outline:1px solid rgba(0,229,255,.5); .picked .clear:hover{color:var(--danger)} /* ===== 标的池 / 日期 ===== */ -.sym-area{width:100%;min-height:128px;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} +.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} @@ -179,7 +203,7 @@ input:focus-visible,textarea:focus-visible{outline:1px solid rgba(0,229,255,.5);
-
N1提交页:自选因子 × 自选标的池 × 自选区间的自由实验台。终端风替换 Element 表单——因子改为「搜索 + 分组点选」面板(240 个因子下拉翻页不便)。
+
N1提交页:自选因子 × 自选标的池 × 自选区间的自由实验台。终端风替换 Element 表单——因子改为「搜索 + 分组点选」面板(240 个因子下拉翻页不便);标的池改三层输入:预设池一键选 + 搜索加自选 + chips 已选池(批量粘贴降级为折叠兜底)。
← 投研 / 因子分析
@@ -214,18 +238,34 @@ input:focus-visible,textarea:focus-visible{outline:1px solid rgba(0,229,255,.5);
标的池
- + +
+ 预设池 + + + + + + +
+ +
+ + +
+ +
用上方预设池或搜索加入标的(≥2 只)
- 8 只 - IC 横截面统计需要 ≥2 只;截面排名类因子(cs_rank 族)建议 300+ 只才不失真 - + 0 只 + IC 横截面需 ≥2 只;截面排名类因子(cs_rank 族)建议 300+ 只才不失真 + + + 清空 +
+ +
@@ -304,14 +344,18 @@ input:focus-visible,textarea:focus-visible{outline:1px solid rgba(0,229,255,.5);

范围

  • New.vue(提交页) + Result.vue 外壳(指标卡 + IC 明细表) 两处终端风重构;TearsPanel 不动(已是新版终端风)。
  • -
  • 要素零变化:因子多选按类分组 / 标的池 textarea 混合分隔 / 起止日期禁选未来 / 提交校验(≥1 因子 + ≥2 标的)/ query.factor 预填。
  • -
  • 纯前端改造,零后端改动;沿用 tokens.css,与排行榜 / 详情 / 对比 / tears 同一套视觉语言。
  • +
  • 要素零变化:因子多选按类分组 / 标的池支持混合分隔批量粘贴 / 起止日期禁选未来 / 提交校验(≥1 因子 + ≥2 标的)/ query.factor 预填。
  • +
  • 前端为主,沿用 tokens.css 与全站视觉语言;标的池三层输入需 3 个后端小端点(见下)。

提交页交互变化

  • 因子选择从「多选下拉」改为「搜索 + 分组点选」面板:240 个因子在下拉里翻页困难;点选即加入已选托盘,托盘 chips 可单删 / 清空,与排行榜「加入对比」托盘交互一致。
  • 保留类别三色:alpha101 青 / alpha158 紫 / builtin 琥珀(同排行榜类别 chip)。
  • -
  • 标的池:mono 字体 textarea + 实时计数徽标 + 快捷预设(跨行业 30 只,与详情页一键直达同一样本,方便复现)。
  • +
  • 标的池从 textarea 改为三层输入(参考聚宽/万得选股器 + 收件人式 tags input):
    + ① 预设池一键选——跨行业30 / 沪深300 / 中证500 / 中证1000 / 中证2000 / 全A主板+创业板,大样本一键载入(cs_rank 族因子要 300+ 只才不失真,手贴不现实);
    + ② 搜索加自选——输代码或名称,候选下拉带「代码 · 名称 · 行业」,点选加入;
    + ③ chips 已选池——可删标签 + 计数 + 复制清单 + 清空;批量粘贴收进折叠兜底入口(能力保留,不再是主入口)。
  • +
  • 后端配套 3 个小端点:池清单(指数名+数量)/ 池成分(代码+名称,读 constituent_unified)/ 标的搜索(代码前缀+名称子串);跨行业 30 只与详情页一键直达同一样本。
  • 提交按钮在未满足校验时禁用并给出原因(替代提交后弹 warning)。

结果页外壳变化

@@ -321,7 +365,7 @@ input:focus-visible,textarea:focus-visible{outline:1px solid rgba(0,229,255,.5);

实现拆分(预估)

    -
  • New.vue:因子面板组件 FactorPicker.vue(搜索 + 分组网格 + 已选托盘)+ 表单区终端化,约半天。
  • +
  • New.vue:因子面板组件 FactorPicker.vue + 标的池组件 UniversePicker.vue(预设池 + 搜索 combobox + chips)+ 后端 3 小端点,约 1 天。
  • Result.vue:指标条 + 表格模板替换(数据逻辑不动),约 1–2 小时。
  • npm run build + push NAS 验收。
@@ -401,21 +445,118 @@ function refreshCounts(){ el.classList.toggle('sel-builtin',sel&&c==='builtin'); }); } -const symArea=document.getElementById('symarea'); -function symCount(){return symArea.value.split(/[\s,,]+/).map(s=>s.trim()).filter(Boolean).length;} + +// —— 标的池三层:预设池 + 搜索 + 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='用上方预设池或搜索加入标的(≥2 只)';} + else{ + box.innerHTML=''; + for(const [c,n] of symPool){ + const s=document.createElement('span');s.className='schip'; + s.innerHTML=`${c}${n}`; + 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='
无匹配标的
';return;} + cand.innerHTML=''; + hits.forEach(s=>{ + const d=document.createElement('div');d.className='ci'+(symPool.has(s[0])?' sel':''); + d.innerHTML=`${s[0]}${s[1]}${s[2]}${symPool.has(s[0])?'已选':''}`; + 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=symCount(); - document.getElementById('symbadge').textContent=`${sc} 只`; + 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='';} } -symArea.addEventListener('input',validate); -document.getElementById('sympool').onclick=()=>{ - symArea.value=['600519','000858','600036','601318','000001','600000','300750','002594','600276','000538','600887','000651','000333','600690','000725','002415','600030','601166','600028','601857','600585','000625','601633','002352','600104','601398','600941','300059','002230','601888'].join('\n'); - validate(); -}; // 区间跨度 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'); @@ -454,7 +595,8 @@ document.querySelectorAll('.tears-tabs .tt').forEach(b=>{ }); // init -renderPicked();refreshCounts();validate();span(); +STOCKS.slice(0,8).forEach(s=>symPool.set(s[0],s[1])); +renderPicked();refreshCounts();renderPool();span();