+
实况评分在关注名单
- {{ csData.snapshot.mr.watch.score }}
+ {{ csSnapMr.watch.score }}
-
+
实况评分在回避名单
- {{ csData.snapshot.mr.avoid.score }}
+ {{ csSnapMr.avoid.score }}
-
- 这只票现在不在资金异动和实况评分里。
-
-
今天的账本记录 · {{ csLedger.length }} 条
-
读不到账本:{{ csLedgerErr }}
-
今天这只票没有账本记录。
+
+
+
系统动作 · {{ csLedger.length }} 条
+
评审账本读取失败:{{ csLedgerErr }}重试
+
当日暂无评审记录。
{{ String(r.created_at||r.decided_at||'').slice(11,16) }}
{{ tx('action', r.action) || r.action }}
@@ -2189,13 +2218,10 @@ body.dock-r:not(.r-fold) .side-r .strip{display:none;}
{{ tx('arbiter', r.arbiter) || r.arbiter }}
{{ r.reason }}
-
-
-
-
处置
-
{{ csHeld(csCode) ? '在持仓里' : '没有持仓' }}
+
处置
+ {{ csDisp.held ? '在持仓里' : '没有持仓' }}
·
- {{ csInPlan(csCode) ? '在今天的选股计划里' : '不在今天的选股计划里' }}
+
{{ csDisp.inPlan ? '在今天的选股计划里' : '不在今天的选股计划里' }}
@@ -3662,6 +3688,11 @@ createApp({
metrics: '资金异动', mr: '实况评分' };
function csHeld(code) { return positions.value.some(p => code6(p.ts_code) === 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): 昨天的风控卖出不再冒充今天
const sigBriefHeldCount = computed(() => sigBrief.value.filter(x => x.held && x.today !== false).length);
@@ -3878,29 +3909,45 @@ createApp({
(d.errors || [d.error]).join('; '));
await loadCommands(); if (plansOf.value === cid) await loadPlans(cid);
}
- async function decide(row, decision) {
- // 采纳与驳回都必须写理由 (2026-09-03): 理由随请求体进评审账本, 复盘时「人为什么这么判」
- // 才查得到。空理由不提交; 点取消就什么都不发。后端 /decide 早已接受 reason, 不必改。
- const accepted = decision === 'ACCEPTED';
- let reason = '';
- try {
- const r = await ElementPlus.ElMessageBox.prompt(
- (accepted ? '采纳' : '驳回') + ' ' + nm(row.ts_code) + ' 的' + tx('action', row.action) + '提议。请写下理由(必填,会记进评审账本)',
- accepted ? '采纳理由' : '驳回理由',
- { inputType: 'textarea', inputPlaceholder: '例如:候选卡理由仍成立、今天量能配合 / 上游确认线缺失、先观望',
- inputValidator: v => (v && v.trim()) ? true : '理由不能为空',
- confirmButtonText: accepted ? '确认采纳' : '确认驳回', cancelButtonText: '取消' });
- reason = ((r && r.value) || '').trim();
- } catch (e) { return; }
- if (!reason) { ElementPlus.ElMessage.warning('理由不能为空,未提交'); return; }
- const d = await call('post', '/api/proposals/' + row.proposal_id + '/decide', { decision, reason });
+ // 裁决理由预设码表 (2026-09-14 台账 015): 从后端取一次并缓存, 取不到用内置副本 (与 decide_reasons.py 同)。
+ const DECIDE_REASONS_FALLBACK = {
+ accept: { ACCEPT_CONSENSUS:'合议放行,按系统建议执行', ACCEPT_MANUAL_JUDGE:'人工判断机会成立',
+ ACCEPT_PER_COMMAND:'按既有命令执行', ACCEPT_RISK_CONTROL:'风控需要', ACCEPT_OTHER:'其他' },
+ decline: { DECLINE_CONSENSUS_MISMATCH:'与研判结论不符', DECLINE_TECH_WEAK:'技术面已转弱',
+ DECLINE_POSITION_FULL:'仓位已达上限', DECLINE_SECTOR_CONCENTRATED:'行业集中度过高',
+ DECLINE_WAIT_PULLBACK:'等待回调再议', DECLINE_MARKET_RISK:'大盘风险偏高', DECLINE_OTHER:'其他' },
+ };
+ const decideReasons = reactive({ accept:{}, decline:{} });
+ async function loadDecideReasons() {
+ const d = await call('get', '/api/proposals/decide-reasons');
+ const src = (d && d.ok !== false) ? d : DECIDE_REASONS_FALLBACK;
+ decideReasons.accept = src.accept || DECIDE_REASONS_FALLBACK.accept;
+ decideReasons.decline = src.decline || DECIDE_REASONS_FALLBACK.decline;
+ }
+ 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(decision === 'ACCEPTED'
+ ElementPlus.ElMessage.success(dd.decision === 'ACCEPTED'
? ('已采纳, 指令 ' + d.instruction_id) : '已驳回');
- if (d.warning) ElementPlus.ElMessage.warning(d.warning); // 一次性纪律失效等提示 (2026-08-28)
- }
- else ElementPlus.ElMessage.error(d.error);
- await Promise.all([loadProposals(), loadInstructions(), loadLedger()]);
+ if (d.warning) ElementPlus.ElMessage.warning(d.warning);
+ dd.visible = false;
+ await Promise.all([loadProposals(), loadInstructions(), loadLedger()]);
+ } else { ElementPlus.ElMessage.error(d.error); }
}
async function loadStrategies() {
const q = '/api/strategies' + (showAllStrategies.value ? '?include_archived=1' : '');
@@ -4301,7 +4348,7 @@ createApp({
scaleGap,
wsRaw, wsAuto, wsHidePong, wsc, wst, wsMode, wsOrders, wsInbox, wsWarnings, loadWs,
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,
loadPlan, openPlan, refreshPlan,
techStatus, techChipText, techChipCls,
@@ -4337,6 +4384,7 @@ createApp({
upstream, showUpstream, usrc, loadUpstream, srcFail,
csOpen, csCode, csLoading, csErr, csData, csFromSnap, csTimeline, csSrcFails,
csTruncated, csSrcLabel, csLedger, csLedgerErr, csHeld, csInPlan,
+ csSnapMetrics, csSnapMr, csSignalCount, csDisp, csLoadResearch, csLoadLedger,
openCodeSignals, closeCodeSignals, csLoadFull,
techFeed, techFeedErr, techStanceCls,
macro, macroGateOn, macroUnavailable, mzLabel, mzColor, mzBarStyle,
diff --git a/scripts/test_batch20_units.py b/scripts/test_batch20_units.py
index 7ea709d..4cdcf6a 100644
--- a/scripts/test_batch20_units.py
+++ b/scripts/test_batch20_units.py
@@ -568,11 +568,13 @@ def _():
assert f"'{v}'" in vseg, f"判决 {v} 要有对应的说法"
assert "h.missing" in vseg or "missing" in vseg, \
"「关注」要说清缺什么 —— 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(")]
- assert "ElMessageBox.prompt" in dseg and "inputValidator" in dseg, "采纳与驳回都要弹必填理由"
- assert "{ decision, reason }" in dseg, "理由要随请求体发 (后端 /decide 已读 payload.reason)"
- assert "if (!reason)" in dseg, "空理由不许提交"
+ assert "decideDlg" in dseg and "async function submitDecide(" in dseg, "裁决要走弹窗 decideDlg + submitDecide"
+ assert "reason_code" in dseg and "reason_note" in dseg, "裁决理由码与补充要随请求体发"
+ assert "_OTHER" in dseg and "补充" in dseg, "选「其他」时补充必填"
+ assert "submitDecide," in html and "decideDlg," in html, "弹窗名字要注册到模板 (return 清单)"
def main():