页面专业化包·抽屉重构与裁决弹窗、候选可点击、来源芯片(台账 015 / §4 §5.3)
单票抽屉重构成三段固定结构加顶部吸顶锚点条(研究面·三源合议 / 今日信号 / 系统动作), 三条黄色警告条移到今日信号段标题下,新增计算属性 csSnapMetrics/csSnapMr/csSignalCount/csDisp。 裁决改成 el-dialog 选预设理由码(取不到用内置副本),选其他补充必填,submitDecide 校验提交。 建仓候选/提议卡/未建仓表票名可点开个股详情。顶栏芯片显示计划来源与赛道闸状态(plan_feed 收三字段)。 浏览器冒烟:全页挂载不白屏、抽屉三段与锚点、裁决弹窗七个预设理由均正常。全量 897 例 ALL SUITES PASS。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
c94787acd1
commit
77db6b830f
|
|
@ -1131,5 +1131,9 @@ def status() -> dict:
|
||||||
"theme_cap": plan.get("theme_cap"), "encoding": plan.get("encoding"),
|
"theme_cap": plan.get("theme_cap"), "encoding": plan.get("encoding"),
|
||||||
"theme_sync": plan.get("theme_sync"), "snapshot": plan.get("snapshot"),
|
"theme_sync": plan.get("theme_sync"), "snapshot": plan.get("snapshot"),
|
||||||
"fetched_at": plan.get("fetched_at"), "url": plan.get("url"),
|
"fetched_at": plan.get("fetched_at"), "url": plan.get("url"),
|
||||||
|
# 来源与赛道闸 (2026-09-14 §5.3): 页面顶栏芯片显示; 现算 (live) 盘中给一条提示。
|
||||||
|
"plan_source": plan.get("plan_source"),
|
||||||
|
"snapshot_generated_at": plan.get("snapshot_generated_at"),
|
||||||
|
"gate_on": plan.get("gate_on"),
|
||||||
"hint": f"计划 {plan['date']} 已就绪 (日龄 {plan.get('age_tdays')} 交易日)"})
|
"hint": f"计划 {plan['date']} 已就绪 (日龄 {plan.get('age_tdays')} 交易日)"})
|
||||||
return st
|
return st
|
||||||
|
|
|
||||||
|
|
@ -348,6 +348,10 @@ body.dock-r:not(.r-fold) .side-r .strip{display:none;}
|
||||||
.sigrow2 .cat{color:var(--muted);font-size:11.5px;white-space:nowrap;}
|
.sigrow2 .cat{color:var(--muted);font-size:11.5px;white-space:nowrap;}
|
||||||
.sigrow2 .sp{flex:1;}
|
.sigrow2 .sp{flex:1;}
|
||||||
.sigrow2.old{opacity:.5;}
|
.sigrow2.old{opacity:.5;}
|
||||||
|
.sigrow2.clk{cursor:pointer;border-radius:4px;}
|
||||||
|
.sigrow2.clk:hover{background:rgba(64,120,220,.08);}
|
||||||
|
.lk{cursor:pointer;text-decoration:underline dotted;}
|
||||||
|
.lk:hover{color:var(--accent,#3a6ea5);}
|
||||||
.pin{font-size:10px;font-weight:700;color:var(--stop);background:var(--stop-weak);border-radius:4px;padding:0 4px;flex:none;}
|
.pin{font-size:10px;font-weight:700;color:var(--stop);background:var(--stop-weak);border-radius:4px;padding:0 4px;flex:none;}
|
||||||
.pin.plan{color:var(--accent);background:var(--accent-weak);}
|
.pin.plan{color:var(--accent);background:var(--accent-weak);}
|
||||||
.srcgroup{border:1px solid var(--hair);border-radius:10px;margin-bottom:7px;overflow:hidden;background:var(--surface-2);}
|
.srcgroup{border:1px solid var(--hair);border-radius:10px;margin-bottom:7px;overflow:hidden;background:var(--surface-2);}
|
||||||
|
|
@ -384,6 +388,13 @@ body.dock-r:not(.r-fold) .side-r .strip{display:none;}
|
||||||
.cs-sect{margin:0 0 12px;}
|
.cs-sect{margin:0 0 12px;}
|
||||||
.cs-sect>.h{font-size:12px;font-weight:660;color:var(--muted);margin:0 0 5px;
|
.cs-sect>.h{font-size:12px;font-weight:660;color:var(--muted);margin:0 0 5px;
|
||||||
border-bottom:1px dashed var(--hair);padding-bottom:4px;}
|
border-bottom:1px dashed var(--hair);padding-bottom:4px;}
|
||||||
|
/* 抽屉三段重构 (2026-09-14 §4.4): 顶部吸顶锚点条 + 三个固定段。 */
|
||||||
|
.cs-anchors{position:sticky;top:0;z-index:2;display:flex;gap:6px;padding:6px 0;margin:-2px 0 8px;
|
||||||
|
background:var(--panel,#fff);border-bottom:1px solid var(--hair);}
|
||||||
|
.cs-anchors a{font-size:12px;color:var(--muted);text-decoration:none;padding:2px 8px;border-radius:12px;cursor:pointer;}
|
||||||
|
.cs-anchors a:hover{background:rgba(64,120,220,.10);color:var(--fg,#222);}
|
||||||
|
.cs-part{margin:0 0 16px;scroll-margin-top:44px;}
|
||||||
|
.cs-part>.h{font-size:13px;font-weight:700;margin:0 0 8px;padding-bottom:5px;border-bottom:1px solid var(--hair);}
|
||||||
.cs-row{display:flex;align-items:baseline;gap:7px;padding:4px 0;font-size:12.5px;
|
.cs-row{display:flex;align-items:baseline;gap:7px;padding:4px 0;font-size:12.5px;
|
||||||
border-bottom:1px solid rgba(0,0,0,.04);flex-wrap:wrap;}
|
border-bottom:1px solid rgba(0,0,0,.04);flex-wrap:wrap;}
|
||||||
.cs-row .t{font-size:11.5px;color:var(--muted);width:40px;flex:none;}
|
.cs-row .t{font-size:11.5px;color:var(--muted);width:40px;flex:none;}
|
||||||
|
|
@ -451,7 +462,7 @@ body.dock-r:not(.r-fold) .side-r .strip{display:none;}
|
||||||
<span class="chip hd-buy" :class="ov.buy_halt?'warn':'ok'"><span class="dot"></span>买入 {{ ov.buy_halt ? '已暂停':'正常' }}</span>
|
<span class="chip hd-buy" :class="ov.buy_halt?'warn':'ok'"><span class="dot"></span>买入 {{ ov.buy_halt ? '已暂停':'正常' }}</span>
|
||||||
<span class="chip hd-exec" :class="ov.exec_halt?'warn':'ok'"><span class="dot"></span>执行 {{ ov.exec_halt ? '已暂停':'正常' }}</span>
|
<span class="chip hd-exec" :class="ov.exec_halt?'warn':'ok'"><span class="dot"></span>执行 {{ ov.exec_halt ? '已暂停':'正常' }}</span>
|
||||||
<span class="chip" :class="ov.autonomy==='off'?'warn':'ok'"><span class="dot"></span>自主 {{ tx('autonomy', ov.autonomy) }}<span class="help" data-tip="系统能自主到什么程度。全自动=各项检查通过即直接下单;仅提议=先在「待确认提议」等人工确认;暂停执行=系统不自动动作。">?</span></span>
|
<span class="chip" :class="ov.autonomy==='off'?'warn':'ok'"><span class="dot"></span>自主 {{ tx('autonomy', ov.autonomy) }}<span class="help" data-tip="系统能自主到什么程度。全自动=各项检查通过即直接下单;仅提议=先在「待确认提议」等人工确认;暂停执行=系统不自动动作。">?</span></span>
|
||||||
<span class="chip hd-plan" :class="planStatus.ok===false?'warn':'ok'"><span class="dot"></span>选股计划<span class="hd-plan-d"> {{ planStatus.ok===false ? '读不到' : (planStatus.date || '-') }}</span></span>
|
<span class="chip hd-plan" :class="planStatus.ok===false?'warn':'ok'"><span class="dot"></span>选股计划<span class="hd-plan-d"> {{ planStatus.ok===false ? '读不到' : (planStatus.date || '-') }}</span><span class="hd-plan-d" v-if="planStatus.ok!==false && planStatus.plan_source"> · {{ planStatus.plan_source==='snapshot'?'快照':(planStatus.plan_source==='live'?'现算':planStatus.plan_source) }}<span v-if="planStatus.gate_on===true"> · 赛道闸开</span><span v-else-if="planStatus.gate_on===false"> · 赛道闸关</span><span class="help" data-tip="计划来源:快照=盘前落盘那份,毫秒返回、可复现;现算=盘中实时装配,较慢,多半是快照过旧回落。赛道闸=上游是否只在十条赛道内选票。">?</span></span></span>
|
||||||
<span class="chip hd-tech" :class="techChipCls"><span class="dot"></span>技术面<span class="hd-plan-d"> {{ techChipText }}</span><span class="help" data-tip="每晚算的技术面读数(布林线、多空布林线、SAR)是哪天的。绿=今天的映射新鲜;黄=读不到或已过期;灰=技术面这一层没开。">?</span></span>
|
<span class="chip hd-tech" :class="techChipCls"><span class="dot"></span>技术面<span class="hd-plan-d"> {{ techChipText }}</span><span class="help" data-tip="每晚算的技术面读数(布林线、多空布林线、SAR)是哪天的。绿=今天的映射新鲜;黄=读不到或已过期;灰=技术面这一层没开。">?</span></span>
|
||||||
<el-tooltip placement="bottom-start" effect="light" :show-after="120">
|
<el-tooltip placement="bottom-start" effect="light" :show-after="120">
|
||||||
<template #content>
|
<template #content>
|
||||||
|
|
@ -1041,8 +1052,8 @@ body.dock-r:not(.r-fold) .side-r .strip{display:none;}
|
||||||
<el-tab-pane name="deny">
|
<el-tab-pane name="deny">
|
||||||
<template #label>未建仓候选 <span class="tabn">{{ denyToday.length }}</span></template>
|
<template #label>未建仓候选 <span class="tabn">{{ denyToday.length }}</span></template>
|
||||||
<div class="sub2" style="margin:8px 2px 4px">未建仓候选</div>
|
<div class="sub2" style="margin:8px 2px 4px">未建仓候选</div>
|
||||||
<el-table :data="denyToday" size="small" max-height="320" empty-text="今天没有被否决的候选">
|
<el-table :data="denyToday" size="small" max-height="320" empty-text="当前没有未建仓的候选">
|
||||||
<el-table-column label="股票" width="180"><template #default="s"><b>{{ s.row.name }}</b> <span class="muted mono">{{ s.row.ts_code }}</span></template></el-table-column>
|
<el-table-column label="股票" width="180"><template #default="s"><b class="lk" title="查看个股详情" @click="openCodeSignals(s.row.ts_code)">{{ s.row.name }}</b> <span class="muted mono">{{ s.row.ts_code }}</span></template></el-table-column>
|
||||||
<el-table-column label="为什么没建仓" min-width="380"><template #default="s"><div class="why" style="margin:0">{{ s.row.why || '—' }}</div></template></el-table-column>
|
<el-table-column label="为什么没建仓" min-width="380"><template #default="s"><div class="why" style="margin:0">{{ s.row.why || '—' }}</div></template></el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
|
|
@ -1103,8 +1114,8 @@ body.dock-r:not(.r-fold) .side-r .strip{display:none;}
|
||||||
<div class="sectb">
|
<div class="sectb">
|
||||||
<div class="muted" v-if="!proposals.length">当前没有需人工确认的事。</div>
|
<div class="muted" v-if="!proposals.length">当前没有需人工确认的事。</div>
|
||||||
<div v-for="p in proposals" :key="p.proposal_id" class="mini">
|
<div v-for="p in proposals" :key="p.proposal_id" class="mini">
|
||||||
<div class="top"><b>{{ nm(p.ts_code) }}</b>
|
<div class="top"><b class="lk" title="查看个股详情" @click="openCodeSignals(p.ts_code)">{{ nm(p.ts_code) }}</b>
|
||||||
<span class="muted mono" style="font-size:11px">{{ p.ts_code }}</span>
|
<span class="muted mono lk" style="font-size:11px" title="查看个股详情" @click="openCodeSignals(p.ts_code)">{{ p.ts_code }}</span>
|
||||||
<el-tag size="small" effect="dark" type="primary">{{ tx('action', p.action) }}</el-tag>
|
<el-tag size="small" effect="dark" type="primary">{{ tx('action', p.action) }}</el-tag>
|
||||||
<span class="spacer"></span><span class="muted" style="font-size:11px">{{ p.expire_at }} 前有效</span></div>
|
<span class="spacer"></span><span class="muted" style="font-size:11px">{{ p.expire_at }} 前有效</span></div>
|
||||||
<!-- 2026-09-10 重排。原先这里一口气铺十六行、实测一张卡二十七八个可视行,
|
<!-- 2026-09-10 重排。原先这里一口气铺十六行、实测一张卡二十七八个可视行,
|
||||||
|
|
@ -1255,9 +1266,10 @@ body.dock-r:not(.r-fold) .side-r .strip{display:none;}
|
||||||
<span v-for="(n,i) in openScan.notes" :key="i">{{ n }}<span v-if="i < openScan.notes.length-1">;</span></span></div>
|
<span v-for="(n,i) in openScan.notes" :key="i">{{ n }}<span v-if="i < openScan.notes.length-1">;</span></span></div>
|
||||||
<div class="muted" v-if="planStatus.ok===false">上游的选股计划现在读不到。</div>
|
<div class="muted" v-if="planStatus.ok===false">上游的选股计划现在读不到。</div>
|
||||||
<div class="muted" v-else-if="!watchCandidates.length">当前没有待建仓的候选,都已处理。</div>
|
<div class="muted" v-else-if="!watchCandidates.length">当前没有待建仓的候选,都已处理。</div>
|
||||||
<div v-else v-for="r in watchCandidates" :key="r.ts_code" class="sigrow2">
|
<div v-else v-for="r in watchCandidates" :key="r.ts_code" class="sigrow2 clk"
|
||||||
|
title="查看个股详情" @click="openCodeSignals(r.ts_code)">
|
||||||
<span class="muted mono t">第{{ r.rank }}</span>
|
<span class="muted mono t">第{{ r.rank }}</span>
|
||||||
<span class="nm">{{ r.name || nm(r.ts_code) }}</span>
|
<span class="nm" style="text-decoration:underline dotted">{{ r.name || nm(r.ts_code) }}</span>
|
||||||
<span class="cat" v-if="r.theme">{{ r.theme }}</span>
|
<span class="cat" v-if="r.theme">{{ r.theme }}</span>
|
||||||
<span class="sp"></span>
|
<span class="sp"></span>
|
||||||
<!-- 三源合议两枚芯片 (2026-09-14 页面收尾包): 质地档 + 技术面立场·相位。数据从候选处置的合议摘要来。 -->
|
<!-- 三源合议两枚芯片 (2026-09-14 页面收尾包): 质地档 + 技术面立场·相位。数据从候选处置的合议摘要来。 -->
|
||||||
|
|
@ -1364,6 +1376,22 @@ body.dock-r:not(.r-fold) .side-r .strip{display:none;}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 挂交易方案 对话框 (非盲填: 现价 / 持仓 / 可卖 / 浮盈都摆出来) -->
|
<!-- 挂交易方案 对话框 (非盲填: 现价 / 持仓 / 可卖 / 浮盈都摆出来) -->
|
||||||
|
<!-- 裁决弹窗 (2026-09-14 台账 015): 采纳/驳回从预设理由里选, 选其他再补一句。 -->
|
||||||
|
<el-dialog v-model="decideDlg.visible" width="460px"
|
||||||
|
:title="(decideDlg.decision==='ACCEPTED'?'采纳':'驳回') + ' · ' + (decideDlg.row ? nm(decideDlg.row.ts_code) : '') + (decideDlg.row ? ' ' + tx('action', decideDlg.row.action) : '')">
|
||||||
|
<div class="muted" style="margin-bottom:8px;font-size:12px">选一个理由记进评审账本。选「其他」时补充必填。</div>
|
||||||
|
<el-radio-group v-model="decideDlg.code" style="display:flex;flex-direction:column;gap:6px;align-items:flex-start">
|
||||||
|
<el-radio v-for="(label,code) in decideCodes" :key="code" :label="code">{{ label }}</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
<el-input v-model="decideDlg.note" type="textarea" :rows="2" style="margin-top:10px"
|
||||||
|
:placeholder="decideDlg.code && decideDlg.code.endsWith('_OTHER') ? '补充说明(必填)' : '补充说明(选填)'"></el-input>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="decideDlg.visible=false">取消</el-button>
|
||||||
|
<el-button :type="decideDlg.decision==='ACCEPTED'?'success':'danger'" :loading="decideDlg.busy" @click="submitDecide">
|
||||||
|
{{ decideDlg.decision==='ACCEPTED'?'确认采纳':'确认驳回' }}</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
<el-dialog v-model="stratDlg.visible" :title="'挂交易方案 · ' + nm(stratDlg.ts_code)" width="580px">
|
<el-dialog v-model="stratDlg.visible" :title="'挂交易方案 · ' + nm(stratDlg.ts_code)" width="580px">
|
||||||
<div class="muted" style="margin-bottom:10px">
|
<div class="muted" style="margin-bottom:10px">
|
||||||
现价 <b>{{ stratDlg.price }}</b> · 持仓 <b>{{ stratDlg.total_qty }}</b> 股(明天才能卖的不算,现在能卖 <b>{{ stratDlg.avail_qty }}</b>)· 浮盈 <b>{{ pct(stratDlg.cushion_pct) }}</b>。
|
现价 <b>{{ stratDlg.price }}</b> · 持仓 <b>{{ stratDlg.total_qty }}</b> 股(明天才能卖的不算,现在能卖 <b>{{ stratDlg.avail_qty }}</b>)· 浮盈 <b>{{ pct(stratDlg.cushion_pct) }}</b>。
|
||||||
|
|
@ -2076,24 +2104,18 @@ body.dock-r:not(.r-fold) .side-r .strip{display:none;}
|
||||||
<button class="iconbtn" @click="closeCodeSignals" title="关闭">✕</button>
|
<button class="iconbtn" @click="closeCodeSignals" title="关闭">✕</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="cs-b">
|
<div class="cs-b">
|
||||||
<div class="cs-warn" v-if="csErr">
|
<div class="cs-anchors">
|
||||||
完整信号没取到({{ csErr }})。下面是页面手上已有的那份快照,它只留了每类最新的若干条,
|
<a @click="scrollTo('csp-research')">研究面</a>
|
||||||
早盘的信号可能已被覆盖。
|
<a @click="scrollTo('csp-signals')">今日信号</a>
|
||||||
<el-button size="small" style="margin-left:6px" @click="csLoadFull">重试</el-button>
|
<a @click="scrollTo('csp-actions')">系统动作</a>
|
||||||
</div>
|
|
||||||
<div class="cs-warn" v-if="csTruncated.length">
|
|
||||||
回扫到了上限还没见底({{ csTruncated.map(k=>csSrcLabel[k]||k).join('、') }}),更早的信号没列出来。
|
|
||||||
要看全,把设置里的「单票信号回扫上限」调大。
|
|
||||||
</div>
|
|
||||||
<div class="cs-warn" v-if="csSrcFails.length">
|
|
||||||
这几路读不到:{{ csSrcFails.map(k=>csSrcLabel[k]||k).join('、') }}。
|
|
||||||
下面列出的不是这只票的全部,是能读到的那部分。
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="cs-sect" v-if="techFeed || techFeedErr">
|
<!-- 一、研究面 · 三源合议 (2026-09-14 §4.4) -->
|
||||||
|
<div class="cs-part" id="csp-research">
|
||||||
<div class="h">研究面 · 三源合议<span class="muted" style="font-weight:400" v-if="techFeed && techFeed.tech && techFeed.tech.data_date"> · 技术面读数日 {{ techFeed.tech.data_date }}</span></div>
|
<div class="h">研究面 · 三源合议<span class="muted" style="font-weight:400" v-if="techFeed && techFeed.tech && techFeed.tech.data_date"> · 技术面读数日 {{ techFeed.tech.data_date }}</span></div>
|
||||||
<div class="muted" v-if="techFeedErr">读不到研究面:{{ techFeedErr }}</div>
|
<div class="cs-warn" v-if="techFeedErr">研究面读取失败:{{ techFeedErr }}<el-button size="small" style="margin-left:6px" @click="csLoadResearch">重试</el-button></div>
|
||||||
<template v-else-if="techFeed">
|
<div class="muted" v-else-if="!techFeed">该股票暂无研究面读数。</div>
|
||||||
|
<template v-else>
|
||||||
<!-- 四枚芯片: 基本面 / 技术面 / 择时 / 合议。按立场红涨绿跌, 无读数灰; 悬停看一句理由。 -->
|
<!-- 四枚芯片: 基本面 / 技术面 / 择时 / 合议。按立场红涨绿跌, 无读数灰; 悬停看一句理由。 -->
|
||||||
<div class="cs-row" style="gap:10px;flex-wrap:wrap">
|
<div class="cs-row" style="gap:10px;flex-wrap:wrap">
|
||||||
<b :class="techStanceCls((techFeed.fund||{}).stance)" :title="(techFeed.fund||{}).fact || (techFeed.fund||{}).why || ('来源:' + ((techFeed.sources||{}).fund||'—'))">基本面 {{ (techFeed.fund||{}).stance || '无读数' }}<span class="muted" v-if="(techFeed.fund||{}).mark"> · {{ techFeed.fund.mark }}</span></b>
|
<b :class="techStanceCls((techFeed.fund||{}).stance)" :title="(techFeed.fund||{}).fact || (techFeed.fund||{}).why || ('来源:' + ((techFeed.sources||{}).fund||'—'))">基本面 {{ (techFeed.fund||{}).stance || '无读数' }}<span class="muted" v-if="(techFeed.fund||{}).mark"> · {{ techFeed.fund.mark }}</span></b>
|
||||||
|
|
@ -2135,10 +2157,20 @@ body.dock-r:not(.r-fold) .side-r .strip{display:none;}
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="cs-sect">
|
<!-- 二、今日信号 (上游信号时间线 + 当前快照; 三条黄色警告条移到本段标题之下) -->
|
||||||
<div class="h">时间线 · {{ csTimeline.length }} 条</div>
|
<div class="cs-part" id="csp-signals">
|
||||||
<div class="muted" v-if="!csTimeline.length && !csLoading">
|
<div class="h">今日信号 · {{ csSignalCount }} 条</div>
|
||||||
这只票今天没有任何上游信号。</div>
|
<div class="cs-warn" v-if="csErr">
|
||||||
|
完整信号没取到({{ csErr }})。下面是页面手上已有的那份快照,只留每类最新若干条,早盘的信号可能已被覆盖。
|
||||||
|
<el-button size="small" style="margin-left:6px" @click="csLoadFull">重试</el-button>
|
||||||
|
</div>
|
||||||
|
<div class="cs-warn" v-if="csTruncated.length">
|
||||||
|
回扫到了上限还没见底({{ csTruncated.map(k=>csSrcLabel[k]||k).join('、') }}),更早的信号没列出来。要看全,把设置里的「单票信号回扫上限」调大。
|
||||||
|
</div>
|
||||||
|
<div class="cs-warn" v-if="csSrcFails.length">
|
||||||
|
这几路读不到:{{ csSrcFails.map(k=>csSrcLabel[k]||k).join('、') }}。下面列出的不是全部,是能读到的那部分。
|
||||||
|
</div>
|
||||||
|
<div class="muted" v-if="!csTimeline.length && !csLoading">当日暂无上游信号。</div>
|
||||||
<div v-for="(r,i) in csTimeline" :key="i" class="cs-row" :class="{old:r.stale}">
|
<div v-for="(r,i) in csTimeline" :key="i" class="cs-row" :class="{old:r.stale}">
|
||||||
<span class="t mono">{{ r.time || '—' }}</span>
|
<span class="t mono">{{ r.time || '—' }}</span>
|
||||||
<span class="s">{{ r.src_label }}</span>
|
<span class="s">{{ r.src_label }}</span>
|
||||||
|
|
@ -2155,33 +2187,30 @@ body.dock-r:not(.r-fold) .side-r .strip{display:none;}
|
||||||
<span class="ab-stale" v-if="r.stale">已过时</span>
|
<span class="ab-stale" v-if="r.stale">已过时</span>
|
||||||
<div style="flex-basis:100%;font-size:12px;line-height:1.45" class="muted" v-if="r.reason">{{ r.reason }}</div>
|
<div style="flex-basis:100%;font-size:12px;line-height:1.45" class="muted" v-if="r.reason">{{ r.reason }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="cs-row" style="margin-top:8px" v-if="csSnapMetrics.length || csSnapMr.watch || csSnapMr.avoid">
|
||||||
|
<span class="s muted">当前快照</span><span class="muted" style="font-size:11.5px">这两路上游只给当前值,不带时间</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="cs-row" v-for="(r,i) in csSnapMetrics" :key="'m'+i">
|
||||||
<div class="cs-sect">
|
|
||||||
<div class="h">当前快照 · 这两路上游只给当前值,不带时间</div>
|
|
||||||
<div class="cs-row" v-for="(r,i) in ((csData&&csData.snapshot&&csData.snapshot.metrics)||[])" :key="'m'+i">
|
|
||||||
<span class="s">资金异动</span>
|
<span class="s">资金异动</span>
|
||||||
<span v-if="r.direction && r.direction!=='—'" :class="dirCls(r.direction)">{{ r.direction }}</span>
|
<span v-if="r.direction && r.direction!=='—'" :class="dirCls(r.direction)">{{ r.direction }}</span>
|
||||||
<span class="mono" v-if="r.window_net!=null" :class="(r.window_net||0)>=0?'clr-up':'clr-down'">{{ moneyCn(r.window_net) }}</span>
|
<span class="mono" v-if="r.window_net!=null" :class="(r.window_net||0)>=0?'clr-up':'clr-down'">{{ moneyCn(r.window_net) }}</span>
|
||||||
<span class="muted" v-if="r.z_dd!=null">偏离 <span class="mono">{{ r.z_dd }}</span></span>
|
<span class="muted" v-if="r.z_dd!=null">偏离 <span class="mono">{{ r.z_dd }}</span></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="cs-row" v-if="csData&&csData.snapshot&&csData.snapshot.mr&&csData.snapshot.mr.watch">
|
<div class="cs-row" v-if="csSnapMr.watch">
|
||||||
<span class="s">实况评分</span><span>在关注名单</span>
|
<span class="s">实况评分</span><span>在关注名单</span>
|
||||||
<span class="mono" style="color:var(--accent)">{{ csData.snapshot.mr.watch.score }}</span>
|
<span class="mono" style="color:var(--accent)">{{ csSnapMr.watch.score }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="cs-row" v-if="csData&&csData.snapshot&&csData.snapshot.mr&&csData.snapshot.mr.avoid">
|
<div class="cs-row" v-if="csSnapMr.avoid">
|
||||||
<span class="s">实况评分</span><span>在回避名单</span>
|
<span class="s">实况评分</span><span>在回避名单</span>
|
||||||
<span class="mono" style="color:var(--warn)">{{ csData.snapshot.mr.avoid.score }}</span>
|
<span class="mono" style="color:var(--warn)">{{ csSnapMr.avoid.score }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="muted" v-if="!((csData&&csData.snapshot&&csData.snapshot.metrics)||[]).length
|
|
||||||
&& !(csData&&csData.snapshot&&csData.snapshot.mr&&(csData.snapshot.mr.watch||csData.snapshot.mr.avoid))">
|
|
||||||
这只票现在不在资金异动和实况评分里。</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="cs-sect">
|
<!-- 三、系统动作 (当日评审账本 + 处置) -->
|
||||||
<div class="h">今天的账本记录 · {{ csLedger.length }} 条</div>
|
<div class="cs-part" id="csp-actions">
|
||||||
<div class="muted" v-if="csLedgerErr">读不到账本:{{ csLedgerErr }}</div>
|
<div class="h">系统动作 · {{ csLedger.length }} 条</div>
|
||||||
<div class="muted" v-else-if="!csLedger.length">今天这只票没有账本记录。</div>
|
<div class="cs-warn" v-if="csLedgerErr">评审账本读取失败:{{ csLedgerErr }}<el-button size="small" style="margin-left:6px" @click="csLoadLedger">重试</el-button></div>
|
||||||
|
<div class="muted" v-else-if="!csLedger.length">当日暂无评审记录。</div>
|
||||||
<div v-for="(r,i) in csLedger" :key="'l'+i" class="cs-row">
|
<div v-for="(r,i) in csLedger" :key="'l'+i" class="cs-row">
|
||||||
<span class="t mono">{{ String(r.created_at||r.decided_at||'').slice(11,16) }}</span>
|
<span class="t mono">{{ String(r.created_at||r.decided_at||'').slice(11,16) }}</span>
|
||||||
<span class="s">{{ tx('action', r.action) || r.action }}</span>
|
<span class="s">{{ tx('action', r.action) || r.action }}</span>
|
||||||
|
|
@ -2189,13 +2218,10 @@ body.dock-r:not(.r-fold) .side-r .strip{display:none;}
|
||||||
<span class="muted" v-if="r.arbiter">{{ tx('arbiter', r.arbiter) || r.arbiter }}</span>
|
<span class="muted" v-if="r.arbiter">{{ tx('arbiter', r.arbiter) || r.arbiter }}</span>
|
||||||
<div style="flex-basis:100%;font-size:12px;line-height:1.45" class="muted" v-if="r.reason">{{ r.reason }}</div>
|
<div style="flex-basis:100%;font-size:12px;line-height:1.45" class="muted" v-if="r.reason">{{ r.reason }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="cs-row" style="margin-top:8px"><span class="s">处置</span>
|
||||||
|
<span>{{ csDisp.held ? '在持仓里' : '没有持仓' }}</span>
|
||||||
<div class="cs-sect">
|
|
||||||
<div class="h">处置</div>
|
|
||||||
<div class="cs-row"><span>{{ csHeld(csCode) ? '在持仓里' : '没有持仓' }}</span>
|
|
||||||
<span class="muted">·</span>
|
<span class="muted">·</span>
|
||||||
<span>{{ csInPlan(csCode) ? '在今天的选股计划里' : '不在今天的选股计划里' }}</span></div>
|
<span>{{ csDisp.inPlan ? '在今天的选股计划里' : '不在今天的选股计划里' }}</span></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
@ -3662,6 +3688,11 @@ createApp({
|
||||||
metrics: '资金异动', mr: '实况评分' };
|
metrics: '资金异动', mr: '实况评分' };
|
||||||
function csHeld(code) { return positions.value.some(p => code6(p.ts_code) === code6(code)); }
|
function csHeld(code) { return positions.value.some(p => code6(p.ts_code) === code6(code)); }
|
||||||
function csInPlan(code) { return planCodeSet.value.has(code6(code)); }
|
function csInPlan(code) { return planCodeSet.value.has(code6(code)); }
|
||||||
|
// 抽屉三段重构 (2026-09-14 §4.4): 今日信号段与系统动作段的几个取数, 抽成计算属性让模板干净。
|
||||||
|
const csSnapMetrics = computed(() => (csData.value && csData.value.snapshot && csData.value.snapshot.metrics) || []);
|
||||||
|
const csSnapMr = computed(() => (csData.value && csData.value.snapshot && csData.value.snapshot.mr) || {});
|
||||||
|
const csSignalCount = computed(() => (csTimeline.value || []).length);
|
||||||
|
const csDisp = computed(() => ({ held: csHeld(csCode.value), inPlan: csInPlan(csCode.value) }));
|
||||||
|
|
||||||
// 「今天的信号」列与持仓角标都只数今天的 (2026-08-28): 昨天的风控卖出不再冒充今天
|
// 「今天的信号」列与持仓角标都只数今天的 (2026-08-28): 昨天的风控卖出不再冒充今天
|
||||||
const sigBriefHeldCount = computed(() => sigBrief.value.filter(x => x.held && x.today !== false).length);
|
const sigBriefHeldCount = computed(() => sigBrief.value.filter(x => x.held && x.today !== false).length);
|
||||||
|
|
@ -3878,29 +3909,45 @@ createApp({
|
||||||
(d.errors || [d.error]).join('; '));
|
(d.errors || [d.error]).join('; '));
|
||||||
await loadCommands(); if (plansOf.value === cid) await loadPlans(cid);
|
await loadCommands(); if (plansOf.value === cid) await loadPlans(cid);
|
||||||
}
|
}
|
||||||
async function decide(row, decision) {
|
// 裁决理由预设码表 (2026-09-14 台账 015): 从后端取一次并缓存, 取不到用内置副本 (与 decide_reasons.py 同)。
|
||||||
// 采纳与驳回都必须写理由 (2026-09-03): 理由随请求体进评审账本, 复盘时「人为什么这么判」
|
const DECIDE_REASONS_FALLBACK = {
|
||||||
// 才查得到。空理由不提交; 点取消就什么都不发。后端 /decide 早已接受 reason, 不必改。
|
accept: { ACCEPT_CONSENSUS:'合议放行,按系统建议执行', ACCEPT_MANUAL_JUDGE:'人工判断机会成立',
|
||||||
const accepted = decision === 'ACCEPTED';
|
ACCEPT_PER_COMMAND:'按既有命令执行', ACCEPT_RISK_CONTROL:'风控需要', ACCEPT_OTHER:'其他' },
|
||||||
let reason = '';
|
decline: { DECLINE_CONSENSUS_MISMATCH:'与研判结论不符', DECLINE_TECH_WEAK:'技术面已转弱',
|
||||||
try {
|
DECLINE_POSITION_FULL:'仓位已达上限', DECLINE_SECTOR_CONCENTRATED:'行业集中度过高',
|
||||||
const r = await ElementPlus.ElMessageBox.prompt(
|
DECLINE_WAIT_PULLBACK:'等待回调再议', DECLINE_MARKET_RISK:'大盘风险偏高', DECLINE_OTHER:'其他' },
|
||||||
(accepted ? '采纳' : '驳回') + ' ' + nm(row.ts_code) + ' 的' + tx('action', row.action) + '提议。请写下理由(必填,会记进评审账本)',
|
};
|
||||||
accepted ? '采纳理由' : '驳回理由',
|
const decideReasons = reactive({ accept:{}, decline:{} });
|
||||||
{ inputType: 'textarea', inputPlaceholder: '例如:候选卡理由仍成立、今天量能配合 / 上游确认线缺失、先观望',
|
async function loadDecideReasons() {
|
||||||
inputValidator: v => (v && v.trim()) ? true : '理由不能为空',
|
const d = await call('get', '/api/proposals/decide-reasons');
|
||||||
confirmButtonText: accepted ? '确认采纳' : '确认驳回', cancelButtonText: '取消' });
|
const src = (d && d.ok !== false) ? d : DECIDE_REASONS_FALLBACK;
|
||||||
reason = ((r && r.value) || '').trim();
|
decideReasons.accept = src.accept || DECIDE_REASONS_FALLBACK.accept;
|
||||||
} catch (e) { return; }
|
decideReasons.decline = src.decline || DECIDE_REASONS_FALLBACK.decline;
|
||||||
if (!reason) { ElementPlus.ElMessage.warning('理由不能为空,未提交'); return; }
|
|
||||||
const d = await call('post', '/api/proposals/' + row.proposal_id + '/decide', { decision, reason });
|
|
||||||
if (d.ok) {
|
|
||||||
ElementPlus.ElMessage.success(decision === 'ACCEPTED'
|
|
||||||
? ('已采纳, 指令 ' + d.instruction_id) : '已驳回');
|
|
||||||
if (d.warning) ElementPlus.ElMessage.warning(d.warning); // 一次性纪律失效等提示 (2026-08-28)
|
|
||||||
}
|
}
|
||||||
else ElementPlus.ElMessage.error(d.error);
|
const decideDlg = reactive({ visible:false, busy:false, row:null, decision:'', code:'', note:'' });
|
||||||
|
// 采纳/驳回都从预设理由里选一条 (选「其他」再补一句), 不再手写整段 (三源合议自决后剩的都是真交人的)。
|
||||||
|
function decide(row, decision) {
|
||||||
|
if (!Object.keys(decideReasons.accept).length) loadDecideReasons();
|
||||||
|
Object.assign(decideDlg, { visible:true, busy:false, row, decision, code:'', note:'' });
|
||||||
|
}
|
||||||
|
const decideCodes = computed(() =>
|
||||||
|
decideDlg.decision === 'ACCEPTED' ? decideReasons.accept : decideReasons.decline);
|
||||||
|
async function submitDecide() {
|
||||||
|
const dd = decideDlg;
|
||||||
|
if (!dd.code && !dd.note.trim()) { ElementPlus.ElMessage.warning('选一个理由,或补一句说明'); return; }
|
||||||
|
if (dd.code && dd.code.endsWith('_OTHER') && !dd.note.trim()) {
|
||||||
|
ElementPlus.ElMessage.warning('选「其他」时必须填补充说明'); return; }
|
||||||
|
dd.busy = true;
|
||||||
|
const d = await call('post', '/api/proposals/' + dd.row.proposal_id + '/decide',
|
||||||
|
{ decision: dd.decision, reason_code: dd.code || null, reason_note: dd.note.trim() || null });
|
||||||
|
dd.busy = false;
|
||||||
|
if (d.ok) {
|
||||||
|
ElementPlus.ElMessage.success(dd.decision === 'ACCEPTED'
|
||||||
|
? ('已采纳, 指令 ' + d.instruction_id) : '已驳回');
|
||||||
|
if (d.warning) ElementPlus.ElMessage.warning(d.warning);
|
||||||
|
dd.visible = false;
|
||||||
await Promise.all([loadProposals(), loadInstructions(), loadLedger()]);
|
await Promise.all([loadProposals(), loadInstructions(), loadLedger()]);
|
||||||
|
} else { ElementPlus.ElMessage.error(d.error); }
|
||||||
}
|
}
|
||||||
async function loadStrategies() {
|
async function loadStrategies() {
|
||||||
const q = '/api/strategies' + (showAllStrategies.value ? '?include_archived=1' : '');
|
const q = '/api/strategies' + (showAllStrategies.value ? '?include_archived=1' : '');
|
||||||
|
|
@ -4301,7 +4348,7 @@ createApp({
|
||||||
scaleGap,
|
scaleGap,
|
||||||
wsRaw, wsAuto, wsHidePong, wsc, wst, wsMode, wsOrders, wsInbox, wsWarnings, loadWs,
|
wsRaw, wsAuto, wsHidePong, wsc, wst, wsMode, wsOrders, wsInbox, wsWarnings, loadWs,
|
||||||
loadAll, loadParams, saveParams, loadPlans, loadLots, onCmdChange, issue,
|
loadAll, loadParams, saveParams, loadPlans, loadLots, onCmdChange, issue,
|
||||||
cancelCmd, replan, decide, ops, loadSchema, openReport, cancelIns,
|
cancelCmd, replan, decide, decideDlg, decideCodes, submitDecide, ops, loadSchema, openReport, cancelIns,
|
||||||
planDrawer, planLoading, planStatus, planRows, planCand, planResult,
|
planDrawer, planLoading, planStatus, planRows, planCand, planResult,
|
||||||
loadPlan, openPlan, refreshPlan,
|
loadPlan, openPlan, refreshPlan,
|
||||||
techStatus, techChipText, techChipCls,
|
techStatus, techChipText, techChipCls,
|
||||||
|
|
@ -4337,6 +4384,7 @@ createApp({
|
||||||
upstream, showUpstream, usrc, loadUpstream, srcFail,
|
upstream, showUpstream, usrc, loadUpstream, srcFail,
|
||||||
csOpen, csCode, csLoading, csErr, csData, csFromSnap, csTimeline, csSrcFails,
|
csOpen, csCode, csLoading, csErr, csData, csFromSnap, csTimeline, csSrcFails,
|
||||||
csTruncated, csSrcLabel, csLedger, csLedgerErr, csHeld, csInPlan,
|
csTruncated, csSrcLabel, csLedger, csLedgerErr, csHeld, csInPlan,
|
||||||
|
csSnapMetrics, csSnapMr, csSignalCount, csDisp, csLoadResearch, csLoadLedger,
|
||||||
openCodeSignals, closeCodeSignals, csLoadFull,
|
openCodeSignals, closeCodeSignals, csLoadFull,
|
||||||
techFeed, techFeedErr, techStanceCls,
|
techFeed, techFeedErr, techStanceCls,
|
||||||
macro, macroGateOn, macroUnavailable, mzLabel, mzColor, mzBarStyle,
|
macro, macroGateOn, macroUnavailable, mzLabel, mzColor, mzBarStyle,
|
||||||
|
|
|
||||||
|
|
@ -568,11 +568,13 @@ def _():
|
||||||
assert f"'{v}'" in vseg, f"判决 {v} 要有对应的说法"
|
assert f"'{v}'" in vseg, f"判决 {v} 要有对应的说法"
|
||||||
assert "h.missing" in vseg or "missing" in vseg, \
|
assert "h.missing" in vseg or "missing" in vseg, \
|
||||||
"「关注」要说清缺什么 —— missing 一路带到前端却从没被渲染过"
|
"「关注」要说清缺什么 —— missing 一路带到前端却从没被渲染过"
|
||||||
j = html.index("async function decide(")
|
# 2026-09-14 台账 015: 裁决改成弹窗选预设理由码 (decide 只负责打开, submitDecide 校验与提交)。
|
||||||
|
j = html.index("function decide(row, decision)")
|
||||||
dseg = html[j:html.index("async function loadStrategies(")]
|
dseg = html[j:html.index("async function loadStrategies(")]
|
||||||
assert "ElMessageBox.prompt" in dseg and "inputValidator" in dseg, "采纳与驳回都要弹必填理由"
|
assert "decideDlg" in dseg and "async function submitDecide(" in dseg, "裁决要走弹窗 decideDlg + submitDecide"
|
||||||
assert "{ decision, reason }" in dseg, "理由要随请求体发 (后端 /decide 已读 payload.reason)"
|
assert "reason_code" in dseg and "reason_note" in dseg, "裁决理由码与补充要随请求体发"
|
||||||
assert "if (!reason)" in dseg, "空理由不许提交"
|
assert "_OTHER" in dseg and "补充" in dseg, "选「其他」时补充必填"
|
||||||
|
assert "submitDecide," in html and "decideDlg," in html, "弹窗名字要注册到模板 (return 清单)"
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue