修改页面布局样式

This commit is contained in:
zlt 2026-08-12 14:51:22 +08:00
parent 0fb958ce51
commit 0ff719ad0b
3 changed files with 265 additions and 86 deletions

View File

@ -209,6 +209,58 @@ def _scan_open(view, params, stock_params, skip, mkt, out) -> list:
return res["candidates"] return res["candidates"]
def disposition_snapshot(now=None) -> dict:
"""候选池处置快照 (只读, 无副作用): 候选池里每只票今天为什么下单 / 没下单。
页面系统在盯的候选原来只铺上游榜单, 看不出每只票的去向这里补上去向与原因:
名额满 / 可投金额不足 / 超上限 / 不足一手 / 在途 / 已持有 / 今日已拒 这些原因每分钟的
scan_and_route 都算了, 但算完即弃 (评审账本只留规则闸/研判闸的驳回, 这一层查不到)
实现上**直接复用** `_scan_open`: 把它写 out / mkt 的两个收集器换成丢弃用的临时容器,
一个字都不改扫描逻辑, 口径与实盘扫描逐字一致 (不另写一份, 免得日后分叉)_scan_open 内部
只读库与行情不落任何表, 所以这样调用是安全的
已下单 / 已生成提议交给前端用它已在手的 instructions / proposals 按股票对齐, 这里不重复查
"""
now = now or datetime.now()
res = {"ok": True, "ready": True, "at": now.isoformat(timespec="seconds"),
"by_code": {}, "notes": [],
"autonomy": param_store.get("PMS_AUTONOMY", AUTONOMY_PROPOSE),
"open_autonomy": param_store.get("PMS_OPEN_AUTONOMY", AUTONOMY_FULL)}
if res["autonomy"] == AUTONOMY_OFF:
res["notes"].append("自主档位 off —— 本轮不扫描候选池 (off 是总闸)")
return res
if param_store.get_bool("PMS_GLOBAL_EXEC_HALT", False):
res["notes"].append("全局暂停执行 (休假模式) —— 本轮不扫描候选池")
return res
if res["open_autonomy"] == AUTONOMY_OFF:
res["notes"].append("新建仓档位 off —— 不扫描候选池")
return res
try:
view = portfolio.positions_view()
params = _scan_params(view)
stock_params = command_service.effective_stock_params()
# 与实盘扫描同一份 skip: 在途 / 今日已被规则闸拒 / 今日已被研判闸驳回的新建仓
skip = {**_judge_rejected_open_keys(), **_rejected_today_keys(), **_inflight_keys()}
sink = {"skipped": []} # 丢弃用: _scan_open 只往里 append/extend, 不读它
would = _scan_open(view, params, stock_params, skip, {}, sink)
except Exception as e:
logger.exception("候选处置快照失败")
return {"ok": False, "ready": False, "error": f"{type(e).__name__}: {e}",
"by_code": {}, "notes": []}
for c in would:
res["by_code"][c["ts_code"]] = {
"disp": "would", "why": "名额与资金都够, 本轮将建底仓 (下一跳落单)"}
for s in sink["skipped"]:
code = s.get("ts_code")
if not code or code == "*": # 池级原因 (名额满/资金为零/候选池空…) 挂到 notes
if s.get("why"):
res["notes"].append(s["why"])
else: # 单票原因; 不覆盖已判 would 的
res["by_code"].setdefault(
code, {"disp": "deny", "why": s.get("why") or "未产出候选"})
return res
def _route_one(c, view, params, stock_params, brake_active, now, dry_run, out, def _route_one(c, view, params, stock_params, brake_active, now, dry_run, out,
*, deadline=None): *, deadline=None):
code, action, side = c["ts_code"], c["action"], c["side"] code, action, side = c["ts_code"], c["action"], c["side"]

View File

@ -486,6 +486,18 @@ def api_clear_resync():
# ================================================================ 上游选股计划 # ================================================================ 上游选股计划
@app.get("/api/open-scan")
def api_open_scan():
"""候选池处置快照 (只读): 每只候选今天为什么下单 / 没下单。
复用每分钟那条自主扫描的 scan_open 口径, 但止步于规则闸之前不送研判不落任何表
补上名额/资金/上限/不足一手/在途/已持有这些算完即弃评审账本里查不到的原因
已下单/已生成提议由前端拿它已在手的 instructions/proposals 按股票对齐, 这里不重复查
"""
from app.services import proposal_service
return ok(proposal_service.disposition_snapshot)
@app.get("/api/upstream/plan") @app.get("/api/upstream/plan")
def api_upstream_plan(limit: int = Query(30), date: str = Query(None), def api_upstream_plan(limit: int = Query(30), date: str = Query(None),
bucket: str = Query("main")): bucket: str = Query("main")):

View File

@ -1,79 +1,128 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="zh-CN" class="dark"> <html lang="zh-CN" data-theme="light">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>PMS 持仓管理系统</title> <title>PMS 持仓管理系统</title>
<link rel="stylesheet" href="https://unpkg.com/element-plus/dist/index.css"/> <link rel="stylesheet" href="https://unpkg.com/element-plus/dist/index.css"/>
<link rel="stylesheet" href="https://unpkg.com/element-plus/theme-chalk/dark/css-vars.css"/>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script> <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script src="https://unpkg.com/element-plus"></script> <script src="https://unpkg.com/element-plus"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script> <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<style> <style>
body { margin:0; background:#0f0f0f; color:#ccc; font-family:'Inter',system-ui,sans-serif; } :root, html[data-theme=light]{
.app { padding:12px 18px 32px; } --plane:#f4f5f2; --surface:#ffffff; --surface-2:#fafaf8; --ink:#141412; --ink-2:#54534f; --muted:#8a8882;
.hd { display:flex; align-items:center; gap:14px; flex-wrap:wrap; --hair:#e7e6df; --ring:rgba(17,17,16,.10); --accent:#2a78d6; --accent-weak:#ecf3fd;
padding:10px 14px; background:#1a1a1a; border:1px solid #333; border-radius:8px; } --up:#d5342f; --up-weak:#fbeceb; --down:#158a4c; --down-weak:#e7f4ec;
.hd h1 { font-size:17px; margin:0; color:#eee; font-weight:600; letter-spacing:.5px; } --warn:#9a6a00; --warn-weak:#fbf1d6; --stop:#b4552b; --stop-weak:#fbeadf;
.hd .ver { color:#666; font-size:12px; } --shadow:0 1px 2px rgba(17,17,16,.04),0 2px 8px rgba(17,17,16,.045);
.spacer { flex:1; } }
.metrics { display:flex; gap:10px; flex-wrap:wrap; margin:12px 0; } [v-cloak]{display:none;}
.m { background:#1a1a1a; border:1px solid #333; border-radius:8px; padding:10px 14px; min-width:118px; } body{margin:0;background:var(--plane);color:var(--ink);
.m .k { color:#888; font-size:12px; } font-family:system-ui,-apple-system,"Segoe UI","PingFang SC","Microsoft YaHei",sans-serif;font-size:14px;line-height:1.5;-webkit-font-smoothing:antialiased;}
.m .v { color:#eee; font-size:18px; font-weight:600; margin-top:3px; } .app{max-width:1280px;margin:0 auto;padding:0 20px 44px;}
.m .v.up { color:#f56c6c; } .m .v.down { color:#67c23a; } .mono{font-family:ui-monospace,Menlo,Consolas,monospace;font-variant-numeric:tabular-nums;}
.panel { background:#1a1a1a; border:1px solid #333; border-radius:8px; padding:14px; margin-bottom:14px; } .muted{color:var(--muted);font-size:12.5px;}
.panel h3 { margin:0 0 10px; font-size:14px; color:#409EFF; font-weight:600; } .nowrap{white-space:nowrap;}
.muted { color:#777; font-size:12px; } .spacer{flex:1;}
.row { display:flex; gap:10px; align-items:center; flex-wrap:wrap; } .row{display:flex;gap:10px;align-items:center;flex-wrap:wrap;}
.banner { margin:10px 0; } .clr-up{color:var(--up);} .clr-down{color:var(--down);}
.el-table { background:transparent !important; font-size:13px; } /* 顶栏 */
.tier { font-size:12px; color:#909399; } .hd{position:sticky;top:0;z-index:30;display:flex;align-items:center;gap:9px;flex-wrap:wrap;
.mono { font-family:ui-monospace,Menlo,Consolas,monospace; } margin:0 -20px 6px;padding:10px 20px;background:rgba(244,245,242,.92);backdrop-filter:blur(8px);border-bottom:1px solid var(--hair);}
.nowrap { white-space:nowrap; } .hd h1{font-size:15px;margin:0;font-weight:680;letter-spacing:.2px;}
pre.json { background:#111; border:1px solid #333; border-radius:6px; padding:10px; .hd .ver{color:var(--muted);font-size:12px;font-weight:400;margin-left:6px;}
max-height:420px; overflow:auto; font-size:12px; color:#bbb; } .seg{display:inline-flex;border:1px solid var(--hair);border-radius:8px;overflow:hidden;background:var(--surface);}
.tcard{background:#161616;border:1px solid #333;border-radius:8px;padding:10px 12px;margin-bottom:10px;} .seg button{border:0;background:transparent;color:var(--ink-2);padding:5px 12px;font-size:13px;cursor:pointer;}
.clr-up{color:#f56c6c;} .clr-down{color:#67c23a;} .seg button.on{background:var(--accent);color:#fff;}
.attn{color:#e6a23c;padding:3px 0;} .tbtn{border:1px solid var(--hair);background:var(--surface);color:var(--ink);border-radius:8px;padding:6px 12px;font-size:13px;cursor:pointer;font-weight:520;}
.tblk h3{margin:0 0 10px;font-size:14px;color:#409EFF;font-weight:600;} .tbtn:hover{border-color:var(--accent);color:var(--accent);}
.op-row{display:flex;align-items:flex-start;gap:12px;padding:6px 0;border-bottom:1px solid #262626;} .tbtn.pri{background:var(--accent);border-color:var(--accent);color:#fff;}
.op-row:last-child{border-bottom:none;} .tbtn.pri:hover{color:#fff;}
.op-btn{flex:none;width:150px;} .tbtn.sm{padding:3px 10px;font-size:12.5px;}
.op-btn .el-button{width:100%;} /* 状态胶囊(系统状态:不用红绿) */
.op-row .d{color:#999;font-size:12px;line-height:1.5;padding-top:3px;} .chip{display:inline-flex;align-items:center;gap:5px;font-size:12px;font-weight:560;padding:3px 9px;border-radius:999px;white-space:nowrap;}
.set-card{border:1px solid #333;border-radius:6px;padding:10px 12px;margin-bottom:10px;} .chip .dot{width:6px;height:6px;border-radius:50%;flex:none;}
.set-sum{color:#ddd;font-size:13px;margin-bottom:8px;line-height:1.6;} .chip.ok{background:var(--accent-weak);color:var(--accent);} .chip.ok .dot{background:var(--accent);}
.set-fields{display:flex;flex-wrap:wrap;gap:10px 16px;align-items:center;color:#999;font-size:12px;} .chip.warn{background:var(--warn-weak);color:var(--warn);} .chip.warn .dot{background:var(--warn);}
.chip.idle{background:rgba(138,136,130,.15);color:var(--ink-2);} .chip.idle .dot{background:var(--muted);}
.chip.stop{background:var(--stop-weak);color:var(--stop);} .chip.stop .dot{background:var(--stop);}
.help{display:inline-flex;align-items:center;justify-content:center;width:15px;height:15px;border-radius:50%;
border:1px solid var(--muted);color:var(--muted);font-size:10px;cursor:help;position:relative;font-weight:600;margin-left:3px;}
.help:hover::after{content:attr(data-tip);position:absolute;left:0;top:150%;width:250px;background:var(--ink);color:#fff;
font-size:12px;font-weight:400;line-height:1.5;padding:8px 10px;border-radius:8px;z-index:50;box-shadow:0 6px 20px rgba(0,0,0,.22);}
/* KPI 焦点行 */
.metrics{display:grid;grid-template-columns:repeat(auto-fit,minmax(148px,1fr));gap:12px;margin:16px 0;}
.m{background:var(--surface);border:1px solid var(--hair);border-radius:12px;padding:12px 15px;box-shadow:var(--shadow);}
.m .k{color:var(--muted);font-size:12px;}
.m .v{color:var(--ink);font-size:21px;font-weight:680;margin-top:5px;letter-spacing:-.2px;font-variant-numeric:tabular-nums;}
.m .v .muted,.m .v span{font-weight:500;}
.m .v.up{color:var(--up);} .m .v.down{color:var(--down);}
/* 颜色约定图例 */
.legend{display:flex;gap:15px;flex-wrap:wrap;align-items:center;color:var(--muted);font-size:12px;margin:0 2px 16px;}
.legend .sw{display:inline-block;width:9px;height:9px;border-radius:2px;margin-right:4px;vertical-align:middle;}
/* 卡片/面板 */
.panel{background:var(--surface);border:1px solid var(--hair);border-radius:14px;padding:16px 18px;margin-bottom:16px;box-shadow:var(--shadow);}
.panel h3,.tblk h3{margin:0 0 10px;font-size:15px;color:var(--ink);font-weight:660;}
.sub2{color:var(--muted);font-size:12.5px;margin:-4px 0 12px;}
.banner{margin:10px 0;}
.attn{color:var(--warn);padding:4px 0;}
.tcard{background:var(--surface-2);border:1px solid var(--hair);border-radius:11px;padding:12px 14px;margin-bottom:10px;}
.feed-row{padding:5px 0;border-bottom:1px solid var(--hair);font-size:13px;}
.feed-row:last-child{border-bottom:none;}
.tag{display:inline-block;font-size:11.5px;font-weight:560;padding:2px 8px;border-radius:6px;background:rgba(138,136,130,.14);color:var(--ink-2);}
.tier{font-size:12px;color:var(--muted);}
/* 处置胶囊(候选表) */
.disp{display:inline-flex;align-items:center;gap:3px;font-size:12px;font-weight:600;padding:3px 9px;border-radius:7px;}
.disp.done{background:var(--accent-weak);color:var(--accent);}
.disp.queue{background:var(--warn-weak);color:var(--warn);}
.disp.deny{background:var(--stop-weak);color:var(--stop);}
.disp.would{background:var(--surface);color:var(--accent);border:1px solid var(--accent);}
.disp.wait{background:rgba(138,136,130,.14);color:var(--ink-2);}
.why{color:var(--ink-2);font-size:12.5px;margin-top:3px;line-height:1.45;}
.pool-note{margin:0 0 12px;padding:9px 12px;background:var(--surface-2);border:1px solid var(--hair);border-radius:9px;color:var(--ink-2);font-size:12.5px;}
.tabn{color:var(--muted);font-weight:600;font-size:12px;margin-left:3px;}
/* 组合操作 / 设置 */
.op-row{display:flex;align-items:flex-start;gap:12px;padding:9px 0;border-bottom:1px solid var(--hair);}
.op-row:last-child{border-bottom:none;}
.op-btn{flex:none;width:150px;} .op-btn .el-button{width:100%;}
.op-row .d{color:var(--ink-2);font-size:12.5px;line-height:1.55;padding-top:3px;}
.set-card{border:1px solid var(--hair);border-radius:10px;padding:12px 14px;margin-bottom:10px;background:var(--surface-2);}
.set-sum{color:var(--ink);font-size:13px;margin-bottom:8px;line-height:1.6;}
.set-fields{display:flex;flex-wrap:wrap;gap:10px 16px;align-items:center;color:var(--ink-2);font-size:12px;}
pre.json{background:var(--surface-2);border:1px solid var(--hair);border-radius:8px;padding:10px;max-height:420px;overflow:auto;font-size:12px;color:var(--ink-2);}
/* Element Plus 浅色微调 */
.el-table{background:transparent!important;font-size:13px;
--el-table-border-color:var(--hair);--el-table-header-bg-color:var(--surface-2);
--el-table-row-hover-bg-color:var(--surface-2);--el-table-bg-color:transparent;--el-table-tr-bg-color:transparent;}
.el-table th.el-table__cell{background:var(--surface-2)!important;color:var(--muted)!important;font-weight:560;}
.el-table td.el-table__cell,.el-table th.el-table__cell{padding:10px 0;border-color:var(--hair)!important;}
.el-table .cell{color:var(--ink);line-height:1.5;}
.el-tabs__item{font-size:14px;color:var(--muted);font-weight:560;}
.el-tabs__item.is-active{color:var(--ink);}
.el-tabs__active-bar{background:var(--accent);}
.el-tabs__nav-wrap::after{background:var(--hair);height:1px;}
</style> </style>
</head> </head>
<body> <body>
<div id="app" class="app" v-cloak> <div id="app" class="app" v-cloak>
<div class="hd"> <div class="hd">
<h1>PMS 持仓管理系统</h1> <h1>PMS 持仓管理<span class="ver">v{{ health.version || '-' }} · {{ health.now || '' }}</span></h1>
<span class="ver">v{{ health.version || '-' }} · {{ health.now || '' }} <span class="chip" :class="health.trade_day?'ok':'idle'"><span class="dot"></span>{{ health.trade_day ? '交易日':'非交易日' }}</span>
<el-tag v-if="health.trade_day" size="small" type="success" effect="dark">交易日</el-tag> <span class="chip" :class="ov.buy_halt?'warn':'ok'"><span class="dot"></span>买入 {{ ov.buy_halt ? '已暂停':'正常' }}</span>
<el-tag v-else size="small" type="info" effect="dark">非交易日</el-tag> <span class="chip" :class="ov.exec_halt?'warn':'ok'"><span class="dot"></span>执行 {{ ov.exec_halt ? '已暂停':'正常' }}</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>
<el-button-group style="margin-left:8px"> <span class="chip" :class="planStatus.ok===false?'warn':'ok'"><span class="dot"></span>计划 {{ planStatus.ok===false ? '不可用' : (planStatus.date || '-') }}</span>
<el-button size="small" :type="mode==='trader'?'primary':''" @click="mode='trader'">交易员</el-button> <span class="chip" :class="dm.shadow?'idle':'ok'" v-show="mode==='ops'"><span class="dot"></span>下发 {{ dm.mode || '-' }}<span class="help" data-tip="实盘=真发到 QMT 下单;影子=只记录不真下单,用来观察。">?</span></span>
<el-button size="small" :type="mode==='ops'?'primary':''" @click="mode='ops'">运维</el-button>
</el-button-group>
<div class="spacer"></div> <div class="spacer"></div>
<el-tag :type="ov.buy_halt ? 'danger':'info'" effect="dark" size="small"> <div class="seg">
买入 {{ ov.buy_halt ? '已暂停':'正常' }}</el-tag> <button :class="{on:mode==='trader'}" @click="mode='trader'">交易员</button>
<el-tag :type="ov.exec_halt ? 'danger':'info'" effect="dark" size="small"> <button :class="{on:mode==='ops'}" @click="mode='ops'">运维</button>
执行 {{ ov.exec_halt ? '已暂停':'正常' }}</el-tag> </div>
<el-tag type="warning" effect="dark" size="small">自主档位 {{ tx('autonomy', ov.autonomy) }}</el-tag> <button class="tbtn" @click="openPlan">上游计划</button>
<el-tag :type="dm.shadow ? 'info':'success'" effect="dark" size="small" v-show="mode==='ops'"> <button class="tbtn" @click="openReport">日报</button>
下发 {{ dm.mode || '-' }}</el-tag> <button class="tbtn" v-show="mode==='ops'" @click="opsDrawer=true">运维</button>
<el-tag :type="planStatus.ok===false ? 'danger':'success'" effect="dark" size="small"> <button class="tbtn pri" :disabled="loading" @click="loadAll">{{ loading ? '刷新中…' : '刷新' }}</button>
计划 {{ planStatus.ok===false ? '不可用' : (planStatus.date || '-') }}</el-tag>
<el-button size="small" @click="openPlan">上游计划</el-button>
<el-button size="small" @click="openReport">日报</el-button>
<el-button size="small" v-show="mode==='ops'" @click="opsDrawer=true">运维</el-button>
<el-button size="small" type="primary" :loading="loading" @click="loadAll">刷新</el-button>
</div> </div>
<el-alert v-if="err" class="banner" :title="err" type="error" effect="dark" show-icon <el-alert v-if="err" class="banner" :title="err" type="error" effect="dark" show-icon
@ -113,6 +162,16 @@
<div class="v" :class="proposals.length?'up':''">{{ proposals.length }} 条</div></div> <div class="v" :class="proposals.length?'up':''">{{ proposals.length }} 条</div></div>
<div class="m"><div class="k">自主档位</div><div class="v" style="font-size:15px">{{ tx('autonomy', ov.autonomy) }}</div></div> <div class="m"><div class="k">自主档位</div><div class="v" style="font-size:15px">{{ tx('autonomy', ov.autonomy) }}</div></div>
</div> </div>
<div class="legend">
<span>颜色约定:</span>
<span><i class="sw" style="background:var(--up)"></i><b class="clr-up">红=涨/盈</b></span>
<span><i class="sw" style="background:var(--down)"></i><b class="clr-down">绿=跌/亏</b></span>
<span><i class="sw" style="background:var(--warn)"></i><b style="color:var(--warn)">琥珀=需注意</b></span>
<span><i class="sw" style="background:var(--stop)"></i><b style="color:var(--stop)">橙=被拦下</b></span>
<span><i class="sw" style="background:var(--accent)"></i><b style="color:var(--accent)">蓝=可操作/正常</b></span>
<span class="help" data-tip="红绿只用于价格涨跌和盈亏;系统状态(正常/暂停/过期/否决)改用蓝/琥珀/橙/灰,避免看混。">?</span>
</div>
<div class="panel" v-if="attention.length" style="border-color:#7a5b1e"> <div class="panel" v-if="attention.length" style="border-color:#7a5b1e">
<h3 style="color:#e6a23c">需要注意</h3> <h3 style="color:#e6a23c">需要注意</h3>
<div v-for="(a,i) in attention" :key="i" class="attn">· {{ a }}</div> <div v-for="(a,i) in attention" :key="i" class="attn">· {{ a }}</div>
@ -216,19 +275,42 @@
<!-- 4) 今日在办与动向 --> <!-- 4) 今日在办与动向 -->
<div class="panel tblk"> <div class="panel tblk">
<h3>今日在办</h3> <h3>今日在办</h3>
<el-table :data="instructions" size="small" border max-height="240" empty-text="今天没有在办的买卖"> <div class="sub2">今天下出去的买卖单,按状态分三档:还在跑的、已成交的、已经结束的。</div>
<el-table-column label="股票" width="150"><template #default="s"><b>{{ nm(s.row.ts_code) }}</b> <span class="muted mono">{{ s.row.ts_code }}</span></template></el-table-column> <el-tabs v-model="insTab">
<el-table-column label="动作" width="150"><template #default="s">{{ tx('action', s.row.action) }} · {{ tx('side', s.row.side) }}</template></el-table-column> <el-tab-pane name="live">
<el-table-column label="数量 / 已成" width="110"><template #default="s">{{ s.row.qty }} / {{ s.row.exec_qty||0 }}</template></el-table-column> <template #label>在办中 <span class="tabn">{{ insLive.length }}</span></template>
<el-table-column prop="limit_price" label="限价" width="90"></el-table-column> <el-table :data="insLive" size="small" max-height="260" empty-text="没有在办的买卖">
<el-table-column label="状态" width="130"><template #default="s">{{ tx('insStatus', s.row.status) }}</template></el-table-column> <el-table-column label="股票" min-width="150"><template #default="s"><b>{{ nm(s.row.ts_code) }}</b> <span class="muted mono">{{ s.row.ts_code }}</span></template></el-table-column>
<el-table-column label="操作" width="90"><template #default="s"><el-button size="small" @click="cancelIns(s.row.instruction_id)">撤单</el-button></template></el-table-column> <el-table-column label="动作" width="150"><template #default="s">{{ tx('action', s.row.action) }} · {{ tx('side', s.row.side) }}</template></el-table-column>
</el-table> <el-table-column label="数量 / 已成" width="120" align="right"><template #default="s"><span class="mono">{{ s.row.qty }} / {{ s.row.exec_qty||0 }}</span></template></el-table-column>
<el-table-column label="限价" width="82" align="right"><template #default="s"><span class="mono">{{ s.row.limit_price||'—' }}</span></template></el-table-column>
<el-table-column label="状态" width="118"><template #default="s"><span class="chip idle"><span class="dot"></span>{{ tx('insStatus', s.row.status) }}</span></template></el-table-column>
<el-table-column label="操作" width="76"><template #default="s"><button class="tbtn sm" @click="cancelIns(s.row.instruction_id)">撤单</button></template></el-table-column>
</el-table>
</el-tab-pane>
<el-tab-pane name="done">
<template #label>今日成交 <span class="tabn">{{ insDone.length }}</span></template>
<el-table :data="insDone" size="small" max-height="260" empty-text="今天还没有成交">
<el-table-column label="股票" min-width="150"><template #default="s"><b>{{ nm(s.row.ts_code) }}</b> <span class="muted mono">{{ s.row.ts_code }}</span></template></el-table-column>
<el-table-column label="动作" width="150"><template #default="s">{{ tx('action', s.row.action) }} · {{ tx('side', s.row.side) }}</template></el-table-column>
<el-table-column label="数量 / 已成" width="120" align="right"><template #default="s"><span class="mono">{{ s.row.qty }} / {{ s.row.exec_qty||0 }}</span></template></el-table-column>
<el-table-column label="状态" width="120"><template #default="s"><span class="chip ok"><span class="dot"></span>{{ tx('insStatus', s.row.status) }}</span></template></el-table-column>
</el-table>
</el-tab-pane>
<el-tab-pane name="end">
<template #label>已了结 <span class="tabn">{{ insEnd.length }}</span></template>
<el-table :data="insEnd" size="small" max-height="260" empty-text="今天没有了结的单">
<el-table-column label="股票" min-width="150"><template #default="s"><b>{{ nm(s.row.ts_code) }}</b> <span class="muted mono">{{ s.row.ts_code }}</span></template></el-table-column>
<el-table-column label="动作" width="150"><template #default="s">{{ tx('action', s.row.action) }} · {{ tx('side', s.row.side) }}</template></el-table-column>
<el-table-column label="结束方式" min-width="150"><template #default="s"><span class="chip warn"><span class="dot"></span>{{ tx('insStatus', s.row.status) }}</span></template></el-table-column>
</el-table>
</el-tab-pane>
</el-tabs>
<h3 style="margin-top:14px;cursor:pointer" @click="showLedger=!showLedger">今天发生了什么 · {{ ledger.length }} 条 <span class="muted" style="font-size:12px;font-weight:normal">{{ showLedger?'▲ 收起':'▼ 展开' }}</span></h3> <h3 style="margin-top:14px;cursor:pointer" @click="showLedger=!showLedger">今天发生了什么 · {{ ledger.length }} 条 <span class="muted" style="font-size:12px;font-weight:normal">{{ showLedger?'▲ 收起':'▼ 展开' }}</span></h3>
<template v-if="showLedger"> <template v-if="showLedger">
<div class="muted" v-if="!ledger.length">今天还没有记录。</div> <div class="muted" v-if="!ledger.length">今天还没有记录。</div>
<div v-for="(l,i) in ledger" :key="i" style="padding:3px 0;border-bottom:1px solid #262626"> <div v-for="(l,i) in ledger" :key="i" class="feed-row">
<span class="muted">{{ (l.decided_at||'').slice(11,16) }}</span> <span class="muted mono">{{ (l.decided_at||'').slice(11,16) }}</span>
<b> {{ nm(l.ts_code) }}</b>{{ tx('arbiter', l.arbiter) }}{{ tx('verdict', l.verdict) }} {{ tx('action', l.action) }}<span v-if="l.reason" class="muted"> —— {{ l.reason }}</span> <b> {{ nm(l.ts_code) }}</b>{{ tx('arbiter', l.arbiter) }}{{ tx('verdict', l.verdict) }} {{ tx('action', l.action) }}<span v-if="l.reason" class="muted"> —— {{ l.reason }}</span>
</div> </div>
</template> </template>
@ -237,7 +319,7 @@
<!-- 4.5) 我的策略 + 操作日志 --> <!-- 4.5) 我的策略 + 操作日志 -->
<div class="panel tblk"> <div class="panel tblk">
<h3>我的策略 · {{ strategies.length }} 个 <h3>我的策略 · {{ strategies.length }} 个
<el-tag size="small" :type="stratEnabled?'success':'info'" effect="dark" style="margin-left:8px"> <el-tag size="small" :type="stratEnabled?'primary':'info'" effect="dark" style="margin-left:8px">
{{ stratEnabled ? '策略层已启用' : '策略层总开关未开PMS_STRATEGY_ENABLED' }}</el-tag> {{ stratEnabled ? '策略层已启用' : '策略层总开关未开PMS_STRATEGY_ENABLED' }}</el-tag>
</h3> </h3>
<el-alert v-if="!stratEnabled" type="info" effect="dark" :closable="false" show-icon style="margin-bottom:8px" <el-alert v-if="!stratEnabled" type="info" effect="dark" :closable="false" show-icon style="margin-bottom:8px"
@ -248,10 +330,10 @@
<el-table-column label="类型" width="86"><template #default="s"><el-tag size="small" effect="dark">{{ tx('stype', s.row.type) }}</el-tag></template></el-table-column> <el-table-column label="类型" width="86"><template #default="s"><el-tag size="small" effect="dark">{{ tx('stype', s.row.type) }}</el-tag></template></el-table-column>
<el-table-column label="自主档" width="86"><template #default="s">{{ s.row.autonomy==='auto'?'自动执行':'待我拍板' }}</template></el-table-column> <el-table-column label="自主档" width="86"><template #default="s">{{ s.row.autonomy==='auto'?'自动执行':'待我拍板' }}</template></el-table-column>
<el-table-column label="实时状态" min-width="230"><template #default="s"><span class="muted">{{ stratStateText(s.row) }}</span> <el-table-column label="实时状态" min-width="230"><template #default="s"><span class="muted">{{ stratStateText(s.row) }}</span>
<el-tag v-if="s.row.buy_paused" size="small" type="danger" effect="dark" style="margin-left:6px">买入已暂停</el-tag> <el-tag v-if="s.row.buy_paused" size="small" type="warning" effect="dark" style="margin-left:6px">买入已暂停</el-tag>
<div v-if="s.row.buy_paused" style="color:#f56c6c;font-size:11px">决策系统风控:{{ s.row.buy_paused.reason || '资金异动' }}(卖出/平回不受影响;确认无碍点右侧「恢复买入」)</div> <div v-if="s.row.buy_paused" style="color:#9a6a00;font-size:11px">决策系统风控:{{ s.row.buy_paused.reason || '资金异动' }}(卖出/平回不受影响;确认无碍点右侧「恢复买入」)</div>
</template></el-table-column> </template></el-table-column>
<el-table-column label="状态" width="84"><template #default="s"><el-tag size="small" :type="s.row.status==='ACTIVE'?'success':'info'">{{ tx('stratStatus', s.row.status) }}</el-tag></template></el-table-column> <el-table-column label="状态" width="84"><template #default="s"><el-tag size="small" :type="s.row.status==='ACTIVE'?'primary':'info'">{{ tx('stratStatus', s.row.status) }}</el-tag></template></el-table-column>
<el-table-column label="操作" width="260"><template #default="s"> <el-table-column label="操作" width="260"><template #default="s">
<el-button v-if="s.row.status==='ACTIVE'" size="small" @click="setStrategyStatus(s.row.strategy_id,'PAUSED')">暂停</el-button> <el-button v-if="s.row.status==='ACTIVE'" size="small" @click="setStrategyStatus(s.row.strategy_id,'PAUSED')">暂停</el-button>
<el-button v-else-if="s.row.status==='PAUSED'" size="small" type="success" @click="setStrategyStatus(s.row.strategy_id,'ACTIVE')">恢复</el-button> <el-button v-else-if="s.row.status==='PAUSED'" size="small" type="success" @click="setStrategyStatus(s.row.strategy_id,'ACTIVE')">恢复</el-button>
@ -265,7 +347,7 @@
<div class="muted" v-if="!opLog.length">还没有操作记录。</div> <div class="muted" v-if="!opLog.length">还没有操作记录。</div>
<div v-for="(o,i) in opLog" :key="i" style="padding:3px 0;border-bottom:1px solid #262626"> <div v-for="(o,i) in opLog" :key="i" style="padding:3px 0;border-bottom:1px solid #262626">
<span class="muted">{{ (o.at||'').slice(11,16) }}</span> <span class="muted">{{ (o.at||'').slice(11,16) }}</span>
<el-tag size="small" effect="dark" :type="o.result==='OK'?'success':(o.result==='BLOCKED'?'danger':'warning')" style="margin:0 6px">{{ o.result }}</el-tag> <el-tag size="small" :type="o.result==='OK'?'primary':(o.result==='BLOCKED'?'warning':'info')" style="margin:0 6px">{{ o.result }}</el-tag>
<b>{{ o.op }}</b><span v-if="o.ts_code" class="muted"> · {{ nm(o.ts_code) }}</span> <b>{{ o.op }}</b><span v-if="o.ts_code" class="muted"> · {{ nm(o.ts_code) }}</span>
<span v-if="o.reason" style="color:#f56c6c"> — {{ o.reason }}</span> <span v-if="o.reason" style="color:#f56c6c"> — {{ o.reason }}</span>
</div> </div>
@ -441,16 +523,20 @@
<!-- 6) 系统在盯的候选 --> <!-- 6) 系统在盯的候选 -->
<div class="panel tblk"> <div class="panel tblk">
<h3>系统在盯的候选(今日选股计划)<span class="muted" style="font-weight:400"> · 计划日 {{ planStatus.date || '—' }}</span></h3> <h3>系统在盯的候选 <span class="muted" style="font-weight:400">· 今日选股计划 · 计划日 {{ planStatus.date || '—' }}</span></h3>
<div class="sub2">决策系统盯着、可能冒出建仓提议的票。<b style="color:var(--ink-2)">每只都标了今天为什么下单、或为什么没下单。</b></div>
<div class="pool-note" v-if="(openScan.notes||[]).length"><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>
<el-table v-else :data="planRows.slice(0,20)" size="small" border max-height="300" empty-text="暂无"> <el-table v-else :data="planRows.slice(0,20)" size="small" max-height="360" empty-text="暂无">
<el-table-column prop="rank" label="名次" width="60"></el-table-column> <el-table-column label="#" width="52" align="right"><template #default="s"><span class="mono muted">{{ s.row.rank }}</span></template></el-table-column>
<el-table-column label="股票" width="160"><template #default="s"><b>{{ s.row.name || nm(s.row.ts_code) }}</b> <span class="muted mono">{{ s.row.ts_code }}</span></template></el-table-column> <el-table-column label="股票" min-width="150"><template #default="s"><b>{{ s.row.name || nm(s.row.ts_code) }}</b> <span class="muted mono">{{ s.row.ts_code }}</span></template></el-table-column>
<el-table-column prop="theme" label="主题" width="130"></el-table-column> <el-table-column label="主题" width="130"><template #default="s"><span class="tag" v-if="s.row.theme">{{ s.row.theme }}</span><span v-else class="muted"></span></template></el-table-column>
<el-table-column prop="tier" label="传导档" width="90"></el-table-column> <el-table-column label="传导档" width="88"><template #default="s"><span class="muted">{{ s.row.tier || '—' }}</span></template></el-table-column>
<el-table-column label="预期空间" width="100"><template #default="s">{{ s.row.upside==null ? '—' : ((s.row.upside>0?'+':'')+(s.row.upside*100).toFixed(0)+'%') }}</template></el-table-column> <el-table-column label="处置 · 为什么" min-width="300"><template #default="s">
<span class="disp" :class="dispOf(s.row.ts_code).cls">● {{ dispOf(s.row.ts_code).label }}</span>
<div class="why" v-if="dispOf(s.row.ts_code).why">{{ dispOf(s.row.ts_code).why }}</div>
</template></el-table-column>
</el-table> </el-table>
<div class="muted" style="margin-top:6px">这是系统当前在盯、可能冒出提议的池子;仅供参考,不自动下单。</div>
</div> </div>
</div> </div>
@ -1144,6 +1230,8 @@ createApp({
const instructions = ref([]), ledger = ref([]), proposals = ref([]); const instructions = ref([]), ledger = ref([]), proposals = ref([]);
const strategies = ref([]), stratEnabled = ref(false), opLog = ref([]); const strategies = ref([]), stratEnabled = ref(false), opLog = ref([]);
const showLedger = ref(false); // 「今天发生了什么」默认折叠 const showLedger = ref(false); // 「今天发生了什么」默认折叠
const openScan = ref({ by_code: {}, notes: [] });
const insTab = ref('live');
const stratDlg = reactive({ visible:false, busy:false, reasons:[], ts_code:'', price:0, const stratDlg = reactive({ visible:false, busy:false, reasons:[], ts_code:'', price:0,
total_qty:0, avail_qty:0, cushion_pct:null, type:'T0', autonomy:'auto', total_qty:0, avail_qty:0, cushion_pct:null, type:'T0', autonomy:'auto',
p:{ t_ratio:0.2, lower:0, upper:0, center:0, step_pct:0.02, per_lot:100, max_capital:0, p:{ t_ratio:0.2, lower:0, upper:0, center:0, step_pct:0.02, per_lot:100, max_capital:0,
@ -1244,7 +1332,7 @@ createApp({
}[n] || n); }[n] || n);
const stTag = s => ({ EXECUTING: 'primary', DONE: 'success', PARTIAL: 'warning', const stTag = s => ({ EXECUTING: 'primary', DONE: 'success', PARTIAL: 'warning',
CANCELLED: 'info', EFFECTIVE: 'success', SUPERSEDED: 'info' }[s] || 'warning'); CANCELLED: 'info', EFFECTIVE: 'success', SUPERSEDED: 'info' }[s] || 'warning');
const cuTag = s => ({ SOLID: 'success', THIN: 'warning', NONE: 'danger' }[s] || 'info'); const cuTag = s => ({ SOLID: 'info', THIN: 'warning', NONE: 'warning' }[s] || 'info');
const canCancel = r => r.cmd_class === 'task' && const canCancel = r => r.cmd_class === 'task' &&
['PENDING', 'PLANNING', 'EXECUTING', 'PARTIAL'].includes(r.status); ['PENDING', 'PLANNING', 'EXECUTING', 'PARTIAL'].includes(r.status);
const progPct = p => { const progPct = p => {
@ -1296,6 +1384,28 @@ createApp({
} }
const heldPositions = computed(() => (positions.value || []).filter(p => (p.total_qty || 0) > 0)); const heldPositions = computed(() => (positions.value || []).filter(p => (p.total_qty || 0) > 0));
// ---- 今日在办分档 + 候选处置 (2026-08-12 重构新增) ----
const todayYmd = computed(() => (String(health.value.now || '').slice(0, 10).replace(/-/g, '')) || '');
const _LIVE_INS = ['PROPOSED', 'RULE_PASSED', 'JUDGE_PASSED', 'DISPATCHED'];
const _sameDayIns = r => { const y = todayYmd.value;
return !y || String(r.instruction_id || '').indexOf('INS_' + y + '_') === 0; };
const insLive = computed(() => (instructions.value || []).filter(r => _LIVE_INS.includes(r.status)));
const insDone = computed(() => (instructions.value || []).filter(r => r.status === 'CONFIRMED' && _sameDayIns(r)));
const insEnd = computed(() => (instructions.value || []).filter(r => ['EXPIRED', 'CANCELLED', 'REJECTED'].includes(r.status) && _sameDayIns(r)));
// 每只候选今天为什么下单/没下单: 已落地的事实(指令/提议)优先, 其余看 /api/open-scan 的只读处置。
function dispOf(code) {
const inst = (instructions.value || []).find(i => i.ts_code === code && i.action === 'OPEN');
if (inst) return { cls: 'done', label: '已下单', why: '在『今日在办』(' + tx('insStatus', inst.status) + '' };
const prop = (proposals.value || []).find(p => p.ts_code === code && p.action === 'OPEN');
if (prop) return { cls: 'queue', label: '待你确认', why: '已生成提议,在『等我拍板』等你点头' };
const d = (openScan.value.by_code || {})[code];
if (d) {
if (d.disp === 'would') return { cls: 'would', label: '将建仓', why: d.why };
return { cls: 'deny', label: '被否决', why: d.why };
}
return { cls: 'wait', label: '在盯', why: '' };
}
const settingKeys = ['PMS_TOTAL_SCALE', 'PMS_PORTFOLIO_CAP', 'PMS_STOCK_CAP', const settingKeys = ['PMS_TOTAL_SCALE', 'PMS_PORTFOLIO_CAP', 'PMS_STOCK_CAP',
'PMS_MAX_NAMES', 'PMS_CASH_RESERVE', 'PMS_MAX_NAMES', 'PMS_CASH_RESERVE',
'PMS_DCA_MAX_RATIO', 'PMS_DCA_TRIGGERS', 'PMS_DCA_DEEP_CONFIRM', 'PMS_DCA_MAX_RATIO', 'PMS_DCA_TRIGGERS', 'PMS_DCA_DEEP_CONFIRM',
@ -1516,7 +1626,7 @@ createApp({
} }
async function loadInstructions() { async function loadInstructions() {
const d = await call('get', const d = await call('get',
'/api/instructions?status=PROPOSED,RULE_PASSED,JUDGE_PASSED,DISPATCHED&limit=100'); '/api/instructions?status=PROPOSED,RULE_PASSED,JUDGE_PASSED,DISPATCHED,CONFIRMED,EXPIRED,CANCELLED,REJECTED&limit=200');
instructions.value = d.data || []; instructions.value = d.data || [];
} }
async function loadLedger() { async function loadLedger() {
@ -1525,6 +1635,10 @@ createApp({
async function loadProposals() { async function loadProposals() {
const d = await call('get', '/api/proposals'); proposals.value = d.data || []; const d = await call('get', '/api/proposals'); proposals.value = d.data || [];
} }
async function loadOpenScan() {
const d = await call('get', '/api/open-scan');
openScan.value = (d && d.by_code) ? d : { by_code: {}, notes: [] };
}
async function loadPlans(cid) { async function loadPlans(cid) {
plansOf.value = cid; plansOf.value = cid;
const d = await call('get', '/api/plans?command_id=' + cid); plans.value = d.data || []; const d = await call('get', '/api/plans?command_id=' + cid); plans.value = d.data || [];
@ -1559,7 +1673,7 @@ createApp({
loading.value = true; err.value = ''; loading.value = true; err.value = '';
await Promise.all([loadOverview(), loadParams(), loadCatalog(), loadCommands(), await Promise.all([loadOverview(), loadParams(), loadCatalog(), loadCommands(),
loadPositions(), loadInstructions(), loadLedger(), loadProposals(), loadPositions(), loadInstructions(), loadLedger(), loadProposals(),
loadDispatchMode(), loadWs(), loadStrategies(), loadOpLog()]); loadDispatchMode(), loadWs(), loadStrategies(), loadOpLog(), loadOpenScan()]);
await loadNames(); await loadNames();
loading.value = false; loading.value = false;
wsPollSync(); wsPollSync();
@ -1774,7 +1888,8 @@ createApp({
pmap, pval, dcaOn, sumPosition, sumDca, sumAutonomy, pmap, pval, dcaOn, sumPosition, sumDca, sumAutonomy,
strategies, stratEnabled, opLog, stratDlg, stratQty, loadStrategies, loadOpLog, strategies, stratEnabled, opLog, stratDlg, stratQty, loadStrategies, loadOpLog,
openStrategy, validateStrategy, attachStrategy, setStrategyStatus, stratStateText, resumeBuy, openStrategy, validateStrategy, attachStrategy, setStrategyStatus, stratStateText, resumeBuy,
showLedger, onPosExpand }; showLedger, onPosExpand,
openScan, insTab, todayYmd, insLive, insDone, insEnd, dispOf, loadOpenScan };
} }
}).use(ElementPlus).mount('#app'); }).use(ElementPlus).mount('#app');
</script> </script>