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渲染/校验联动全通
This commit is contained in:
@@ -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);
|
||||
|
||||
<!-- ================= ① 提交页 ================= -->
|
||||
<section id="tab-new" class="tab on">
|
||||
<div class="dcap"><span class="no">N1</span><span class="txt"><b>提交页</b>:自选因子 × 自选标的池 × 自选区间的自由实验台。终端风替换 Element 表单——因子改为「搜索 + 分组点选」面板(240 个因子下拉翻页不便)。</span></div>
|
||||
<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">
|
||||
@@ -214,18 +238,34 @@ input:focus-visible,textarea:focus-visible{outline:1px solid rgba(0,229,255,.5);
|
||||
<div class="formrow">
|
||||
<div class="flab">标的池</div>
|
||||
<div class="fbody">
|
||||
<textarea class="sym-area" id="symarea" spellcheck="false" placeholder="每行一个标的代码(≥2,IC 横截面需多标的);支持空格 / 逗号 / 换行混合分隔">600519
|
||||
000858
|
||||
600036
|
||||
601318
|
||||
000001
|
||||
600000
|
||||
300750
|
||||
002594</textarea>
|
||||
<!-- ① 预设池一键选 -->
|
||||
<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">8 只</span>
|
||||
<span class="hint-inline">IC 横截面统计需要 ≥2 只;截面排名类因子(cs_rank 族)建议 300+ 只才不失真</span>
|
||||
<button class="preset" id="sympool">载入:跨行业 30 只样本</button>
|
||||
<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>
|
||||
@@ -304,14 +344,18 @@ input:focus-visible,textarea:focus-visible{outline:1px solid rgba(0,229,255,.5);
|
||||
<h3 class="nh">范围</h3>
|
||||
<ul class="ask">
|
||||
<li><b>New.vue(提交页)</b> + <b>Result.vue 外壳(指标卡 + IC 明细表)</b> 两处终端风重构;<b>TearsPanel 不动</b>(已是新版终端风)。</li>
|
||||
<li>要素零变化:因子多选按类分组 / 标的池 textarea 混合分隔 / 起止日期禁选未来 / 提交校验(≥1 因子 + ≥2 标的)/ query.factor 预填。</li>
|
||||
<li>纯前端改造,零后端改动;沿用 tokens.css,与排行榜 / 详情 / 对比 / tears 同一套视觉语言。</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>标的池:mono 字体 textarea + 实时计数徽标 + 快捷预设(跨行业 30 只,与详情页一键直达同一样本,方便复现)。</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>
|
||||
@@ -321,7 +365,7 @@ input:focus-visible,textarea:focus-visible{outline:1px solid rgba(0,229,255,.5);
|
||||
</ul>
|
||||
<h3 class="nh">实现拆分(预估)</h3>
|
||||
<ul class="ask">
|
||||
<li><b>New.vue</b>:因子面板组件 FactorPicker.vue(搜索 + 分组网格 + 已选托盘)+ 表单区终端化,约半天。</li>
|
||||
<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>
|
||||
@@ -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='<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=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();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user