PMS信号区重排: 告警+信号源合并为固定分类网格(常驻不跳)+级别前端筛选+移到顶部; 名称解析补全(含mtf前缀码)"

This commit is contained in:
zlt 2026-08-14 11:13:37 +08:00
parent ae022f960d
commit ae2c7bd5b7
1 changed files with 159 additions and 109 deletions

View File

@ -139,6 +139,17 @@ pre.json{background:var(--surface-2);border:1px solid var(--hair);border-radius:
.alert-win .el-table__row.alert-stale{opacity:.5;} .alert-win .el-table__row.alert-stale{opacity:.5;}
.alert-win .el-table__row.alert-stale:hover{opacity:.72;} .alert-win .el-table__row.alert-stale:hover{opacity:.72;}
.alert-age{color:var(--muted);font-size:10.5px;margin-left:5px;} .alert-age{color:var(--muted);font-size:10.5px;margin-left:5px;}
/* 上游信号中心: 最新告警细提示 + 级别筛选 chip + 分节标题 */
.sig-ticker{display:flex;align-items:center;gap:8px;flex-wrap:wrap;background:var(--surface);border:1px solid var(--hair);border-left:3px solid var(--hair);border-radius:12px;padding:8px 14px;margin:16px 0 8px;box-shadow:var(--shadow);font-size:13px;}
.sig-ticker.lv-danger{border-left-color:var(--stop);} .sig-ticker.lv-warn{border-left-color:var(--warn);}
.sig-ticker .sig-tk-label{font-weight:640;color:var(--ink-2);white-space:nowrap;}
.sig-filter{display:flex;align-items:center;gap:7px;flex-wrap:wrap;margin:2px 0 12px;}
.lv-chip{font-size:12px;font-weight:560;padding:2px 11px;border-radius:999px;cursor:pointer;border:1px solid var(--hair);user-select:none;color:var(--ink-2);background:var(--surface-2);}
.lv-chip.off{color:var(--muted);text-decoration:line-through;opacity:.65;}
.lv-chip.on.lv-danger{color:var(--stop);background:var(--stop-weak);border-color:var(--stop-weak);}
.lv-chip.on.lv-warn{color:var(--warn);background:var(--warn-weak);border-color:var(--warn-weak);}
.lv-chip.on.lv-info{color:var(--ink-2);background:rgba(138,136,130,.14);}
.sig-sep{font-size:12px;color:var(--muted);font-weight:600;margin:16px 0 8px;padding-top:9px;border-top:1px dashed var(--hair);}
</style> </style>
</head> </head>
<body> <body>
@ -157,6 +168,7 @@ pre.json{background:var(--surface-2);border:1px solid var(--hair);border-radius:
<button :class="{on:mode==='trader'}" @click="mode='trader'">交易员</button> <button :class="{on:mode==='trader'}" @click="mode='trader'">交易员</button>
<button :class="{on:mode==='ops'}" @click="mode='ops'">运维</button> <button :class="{on:mode==='ops'}" @click="mode='ops'">运维</button>
</div> </div>
<button class="tbtn" @click="openPlan">上游计划</button> <button class="tbtn" @click="openPlan">上游计划</button>
<button class="tbtn" @click="openReport">日报</button> <button class="tbtn" @click="openReport">日报</button>
<button class="tbtn" v-show="mode==='ops'" @click="opsDrawer=true">运维</button> <button class="tbtn" v-show="mode==='ops'" @click="opsDrawer=true">运维</button>
@ -191,45 +203,104 @@ pre.json{background:var(--surface-2);border:1px solid var(--hair);border-radius:
<!-- ==================== 交易员视图 ==================== --> <!-- ==================== 交易员视图 ==================== -->
<div v-show="mode==='trader'"> <div v-show="mode==='trader'">
<!-- 0) 顶部风控告警条 (折叠态常驻最新一条; 展开后按类型分窗, 每窗可切「折叠条显示」) --> <!-- 0) 最新告警细提示 (常驻一行; 完整信号中心=级别筛选+固定分类告警+信号源, 在下方「上游信号」区) -->
<div class="alert-bar" :class="'lv-'+alertLevelClass"> <div class="sig-ticker" :class="'lv-'+alertLevelClass">
<span class="ab-title"><span class="alert-dot" :class="'lv-'+alertLevelClass"></span>风控告警</span> <span class="alert-dot" :class="'lv-'+alertLevelClass"></span><span class="sig-tk-label">最新告警</span>
<template v-if="alertItems.length"> <template v-if="alertLatest.ts_code">
<span class="ab-badge">{{ alertStripCount }} 条<span v-if="alertTotalCount>alertStripCount" class="muted" style="font-weight:400"> / 全部 {{ alertTotalCount }}</span></span> <b>{{ nm(alertLatest.ts_code) }}</b> <span class="muted mono">{{ alertLatest.ts_code }}</span>
<span class="ab-latest" v-if="alertLatest.ts_code">最新 · <span v-if="alertLatest.direction && alertLatest.direction!=='—'" :class="['看涨','涨'].includes(alertLatest.direction)?'clr-up':'clr-down'">{{ alertLatest.direction }}</span>
<b>{{ nm(alertLatest.ts_code) }}</b> <span class="muted mono">{{ alertLatest.ts_code }}</span> <span class="muted">{{ alertLatest.cat_label }}</span>
<span v-if="alertLatest.direction && alertLatest.direction!=='—'" :class="['看涨','涨'].includes(alertLatest.direction)?'clr-up':'clr-down'">{{ alertLatest.direction }}</span> <span class="disp" :class="lvClass(alertLatest.level)" style="padding:1px 7px">{{ lvLabel(alertLatest.level) }}</span>
<span class="muted">{{ alertLatest.cat_label }}</span> <span class="muted">{{ alertLatest.time }}<template v-if="ageText(alertLatest)"> · {{ ageText(alertLatest) }}</template></span>
<span class="disp" :class="alertLatest.level==='DANGER'?'deny':(alertLatest.level==='WARNING'?'queue':'wait')" style="padding:1px 7px">{{ alertLatest.level }}</span> <span v-if="alertLatest.stale" class="ab-stale" title="超过告警时间窗, 已置灰标注为陈旧">陈旧</span>
<span class="muted">{{ alertLatest.time }}<template v-if="ageText(alertLatest)"> · {{ ageText(alertLatest) }}</template></span>
<span v-if="alertLatest.stale" class="ab-stale" title="超过告警时间窗, 已置灰标注为陈旧">陈旧</span>
</span>
<span class="ab-latest muted" v-else>· 折叠条已隐藏当前类型(展开后可切回)</span>
<button class="ab-toggle" @click="showAlerts=!showAlerts">{{ showAlerts?'收起 ▲':'展开全部 ▼' }}</button>
</template> </template>
<template v-else-if="usrc('alerts') && usrc('alerts').ok===false"> <span class="muted" v-else-if="usrc('alerts') && usrc('alerts').ok===false">· 读取失败:{{ usrc('alerts').error }}</span>
<span class="ab-empty">· 读取失败:{{ usrc('alerts').error }}</span> <span class="muted" v-else>· 暂无告警</span>
</template> <div class="spacer"></div>
<template v-else> <span class="muted" style="font-size:11.5px;cursor:pointer" @click="showUpstream=true">下方查看全部 ▾</span>
<span class="ab-empty">· 暂无风控告警</span> </div>
<!-- 7) 上游信号 (只读参考) -->
<div class="panel tblk">
<h3 style="cursor:pointer" @click="showUpstream=!showUpstream">上游信号 <span class="muted" style="font-weight:400">· 只读参考,不进下单逻辑</span> <span class="muted" style="font-size:12px;font-weight:normal">{{ showUpstream?'▲ 收起':'▼ 展开' }}</span></h3>
<template v-if="showUpstream">
<div class="muted" style="margin:-4px 0 12px" v-if="upstream.at">最近取数 {{ (upstream.at||'').slice(11,19) }}(每次刷新拉一次;源自 mtf / 盘中择时层 / 决策系统)</div>
<div class="sig-filter">
<span class="muted" style="font-size:12px">级别筛选</span>
<span v-for="L in ALERT_LEVELS" :key="L.key" class="lv-chip" :class="levelShown(L.key)?('on lv-'+L.cls):'off'" @click="toggleLevel(L.key)">{{ L.label }} <span style="opacity:.7">{{ levelCount(L.key) }}</span></span>
<span class="muted" style="font-size:11px;margin-left:4px">点一下切换显隐,只前端过滤、不查后端</span>
</div>
<div class="up-grid">
<div class="up-cell" v-for="g in alertGridFixed" :key="g.key">
<div class="up-h">{{ g.label }} <span class="muted" style="font-weight:400">· {{ g.items.length }}</span></div>
<el-table :data="g.items.slice(0,20)" size="small" max-height="200" empty-text="暂无" :row-class-name="alertRowClass">
<el-table-column label="股票" min-width="140"><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="56"><template #default="s"><span v-if="s.row.direction && s.row.direction!=='—'" :class="['看涨','涨'].includes(s.row.direction)?'clr-up':'clr-down'">{{ s.row.direction }}</span><span v-else class="muted"></span></template></el-table-column>
<el-table-column label="级别" width="66"><template #default="s"><span class="disp" :class="lvClass(s.row.level)">{{ lvLabel(s.row.level) }}</span></template></el-table-column>
<el-table-column label="时间" width="116" align="right"><template #default="s"><span class="mono muted">{{ s.row.time }}</span><span v-if="ageText(s.row)" class="alert-age">{{ ageText(s.row) }}</span></template></el-table-column>
</el-table>
</div>
<div class="up-cell">
<div class="up-h">决策系统 买/卖<span class="src-err" v-if="usrc('intraday') && usrc('intraday').ok===false"> · 读取失败:{{ usrc('intraday').error }}</span></div>
<el-table :data="((usrc('intraday')||{}).decision||[]).slice(0,8)" size="small" max-height="200" empty-text="暂无">
<el-table-column label="股票" min-width="130"><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="128"><template #default="s"><span class="disp" :class="s.row.action==='SELL'?'deny':'done'">{{ s.row.action }} · {{ s.row.signal_type }}</span></template></el-table-column>
<el-table-column label="时间" width="66" align="right"><template #default="s"><span class="mono muted">{{ s.row.time }}</span></template></el-table-column>
</el-table>
</div>
<div class="up-cell">
<div class="up-h">盘中择时层 · BUY 入场<span class="src-err" v-if="usrc('intraday') && usrc('intraday').ok===false"> · 读取失败</span></div>
<el-table :data="((usrc('intraday')||{}).timing||[]).slice(0,8)" size="small" max-height="200" empty-text="暂无">
<el-table-column label="股票" min-width="130"><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="82" align="right"><template #default="s"><span class="mono">{{ s.row.price==null?'—':s.row.price }}</span></template></el-table-column>
<el-table-column label="置信" width="66" align="right"><template #default="s"><span class="mono">{{ s.row.confidence==null?'—':s.row.confidence }}</span></template></el-table-column>
<el-table-column label="时间" width="60" align="right"><template #default="s"><span class="mono muted">{{ s.row.time }}</span></template></el-table-column>
</el-table>
</div>
<div class="up-cell">
<div class="up-h">mtf 资金异动<span class="src-err" v-if="usrc('metrics') && usrc('metrics').ok===false"> · 读取失败</span></div>
<el-table :data="((usrc('metrics')||{}).items||[]).filter(x=>passLevel(x.level)).slice(0,8)" size="small" max-height="200" empty-text="暂无">
<el-table-column label="股票" min-width="120"><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="78"><template #default="s"><span class="muted">{{ s.row.direction || '—' }}</span></template></el-table-column>
<el-table-column label="z_dd" width="66" align="right"><template #default="s"><span class="mono">{{ s.row.z_dd==null?'—':s.row.z_dd }}</span></template></el-table-column>
<el-table-column label="级别" width="86"><template #default="s"><span class="disp" :class="lvClass(s.row.level)">{{ lvLabel(s.row.level) }}</span></template></el-table-column>
</el-table>
</div>
<div class="up-cell">
<div class="up-h">决策系统 · 风控卖出<span class="src-err" v-if="usrc('sell_actions') && usrc('sell_actions').ok===false"> · 读取失败</span></div>
<el-table :data="((usrc('sell_actions')||{}).items||[]).slice(0,8)" size="small" max-height="200" empty-text="暂无">
<el-table-column label="股票" min-width="118"><template #default="s"><b>{{ nm(s.row.ts_code) }}</b></template></el-table-column>
<el-table-column label="置信" width="60" align="right"><template #default="s"><span class="mono">{{ s.row.confidence==null?'—':s.row.confidence }}</span></template></el-table-column>
<el-table-column label="理由" min-width="180"><template #default="s"><span class="muted" style="font-size:12px">{{ s.row.reason || '—' }}</span></template></el-table-column>
<el-table-column label="时间" width="56" align="right"><template #default="s"><span class="mono muted">{{ s.row.time }}</span></template></el-table-column>
</el-table>
</div>
<div class="up-cell">
<div class="up-h">mtf 实况分 · 关注 / 回避榜<span class="src-err" v-if="usrc('mr') && usrc('mr').ok===false"> · 读取失败</span></div>
<div class="mr-two">
<div>
<div class="muted" style="margin-bottom:5px">关注(高分)</div>
<div v-for="(r,i) in ((usrc('mr')||{}).watch||[]).slice(0,8)" :key="i" class="mr-row"><b>{{ nm(r.ts_code) }}</b> <span class="muted mono">{{ r.ts_code }}</span><span class="mono" style="float:right;color:var(--accent)">{{ r.score }}</span></div>
<div class="muted" v-if="!((usrc('mr')||{}).watch||[]).length">暂无</div>
</div>
<div>
<div class="muted" style="margin-bottom:5px">回避(低分)</div>
<div v-for="(r,i) in ((usrc('mr')||{}).avoid||[]).slice(0,8)" :key="i" class="mr-row"><b>{{ nm(r.ts_code) }}</b> <span class="muted mono">{{ r.ts_code }}</span><span class="mono" style="float:right;color:var(--warn)">{{ r.score }}</span></div>
<div class="muted" v-if="!((usrc('mr')||{}).avoid||[]).length">暂无</div>
</div>
</div>
</div>
</div>
</template> </template>
</div> </div>
<div class="alert-wins" v-if="showAlerts && alertItems.length">
<div class="alert-win" v-for="g in alertGroups" :key="g.key">
<div class="aw-h">
<span class="aw-title">{{ g.label }} <span class="muted" style="font-weight:400">· {{ g.items.length }}</span></span>
<span class="aw-toggle" :class="{off:!catShown(g.key)}" @click="toggleCat(g.key)" :title="catShown(g.key)?'点此不在顶部折叠条里显示这类':'点此让这类回到顶部折叠条'">{{ catShown(g.key) ? '折叠条显示 ✓' : '折叠条隐藏' }}</span>
</div>
<el-table :data="g.items.slice(0,20)" size="small" max-height="240" empty-text="暂无" :row-class-name="alertRowClass">
<el-table-column label="股票" min-width="140"><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="58"><template #default="s"><span v-if="s.row.direction && s.row.direction!=='—'" :class="['看涨','涨'].includes(s.row.direction)?'clr-up':'clr-down'">{{ s.row.direction }}</span><span v-else class="muted"></span></template></el-table-column>
<el-table-column label="级别" width="86"><template #default="s"><span class="disp" :class="s.row.level==='DANGER'?'deny':(s.row.level==='WARNING'?'queue':'wait')">{{ s.row.level }}</span></template></el-table-column>
<el-table-column label="来源" min-width="150"><template #default="s"><span class="muted" style="font-size:11px">{{ s.row.source }}</span></template></el-table-column>
<el-table-column label="时间" width="120" align="right"><template #default="s"><span class="mono muted">{{ s.row.time }}</span><span v-if="ageText(s.row)" class="alert-age">{{ ageText(s.row) }}</span></template></el-table-column>
</el-table>
</div>
</div>
<!-- 1) 今日一览 + 需要注意 --> <!-- 1) 今日一览 + 需要注意 -->
<div class="metrics"> <div class="metrics">
@ -637,72 +708,6 @@ pre.json{background:var(--surface-2);border:1px solid var(--hair);border-radius:
</el-table> </el-table>
</div> </div>
<!-- 7) 上游信号 (只读参考) -->
<div class="panel tblk">
<h3 style="cursor:pointer" @click="showUpstream=!showUpstream">上游信号 <span class="muted" style="font-weight:400">· 只读参考,不进下单逻辑</span> <span class="muted" style="font-size:12px;font-weight:normal">{{ showUpstream?'▲ 收起':'▼ 展开' }}</span></h3>
<template v-if="showUpstream">
<div class="muted" style="margin:-4px 0 12px" v-if="upstream.at">最近取数 {{ (upstream.at||'').slice(11,19) }}(每次刷新拉一次;源自 mtf / 盘中择时层 / 决策系统)</div>
<div class="up-grid">
<div class="up-cell">
<div class="up-h">决策系统 买/卖<span class="src-err" v-if="usrc('intraday') && usrc('intraday').ok===false"> · 读取失败:{{ usrc('intraday').error }}</span></div>
<el-table :data="((usrc('intraday')||{}).decision||[]).slice(0,8)" size="small" max-height="200" empty-text="暂无">
<el-table-column label="股票" min-width="130"><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="128"><template #default="s"><span class="disp" :class="s.row.action==='SELL'?'deny':'done'">{{ s.row.action }} · {{ s.row.signal_type }}</span></template></el-table-column>
<el-table-column label="时间" width="66" align="right"><template #default="s"><span class="mono muted">{{ s.row.time }}</span></template></el-table-column>
</el-table>
</div>
<div class="up-cell">
<div class="up-h">盘中择时层 · BUY 入场<span class="src-err" v-if="usrc('intraday') && usrc('intraday').ok===false"> · 读取失败</span></div>
<el-table :data="((usrc('intraday')||{}).timing||[]).slice(0,8)" size="small" max-height="200" empty-text="暂无">
<el-table-column label="股票" min-width="130"><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="82" align="right"><template #default="s"><span class="mono">{{ s.row.price==null?'—':s.row.price }}</span></template></el-table-column>
<el-table-column label="置信" width="66" align="right"><template #default="s"><span class="mono">{{ s.row.confidence==null?'—':s.row.confidence }}</span></template></el-table-column>
<el-table-column label="时间" width="60" align="right"><template #default="s"><span class="mono muted">{{ s.row.time }}</span></template></el-table-column>
</el-table>
</div>
<div class="up-cell">
<div class="up-h">mtf 资金异动<span class="src-err" v-if="usrc('metrics') && usrc('metrics').ok===false"> · 读取失败</span></div>
<el-table :data="((usrc('metrics')||{}).items||[]).slice(0,8)" size="small" max-height="200" empty-text="暂无">
<el-table-column label="股票" min-width="120"><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="78"><template #default="s"><span class="muted">{{ s.row.direction || '—' }}</span></template></el-table-column>
<el-table-column label="z_dd" width="66" align="right"><template #default="s"><span class="mono">{{ s.row.z_dd==null?'—':s.row.z_dd }}</span></template></el-table-column>
<el-table-column label="级别" width="86"><template #default="s"><span class="muted">{{ s.row.level || '—' }}</span></template></el-table-column>
</el-table>
</div>
<div class="up-cell">
<div class="up-h">决策系统 · 风控卖出<span class="src-err" v-if="usrc('sell_actions') && usrc('sell_actions').ok===false"> · 读取失败</span></div>
<el-table :data="((usrc('sell_actions')||{}).items||[]).slice(0,8)" size="small" max-height="200" empty-text="暂无">
<el-table-column label="股票" min-width="118"><template #default="s"><b>{{ nm(s.row.ts_code) }}</b></template></el-table-column>
<el-table-column label="置信" width="60" align="right"><template #default="s"><span class="mono">{{ s.row.confidence==null?'—':s.row.confidence }}</span></template></el-table-column>
<el-table-column label="理由" min-width="180"><template #default="s"><span class="muted" style="font-size:12px">{{ s.row.reason || '—' }}</span></template></el-table-column>
<el-table-column label="时间" width="56" align="right"><template #default="s"><span class="mono muted">{{ s.row.time }}</span></template></el-table-column>
</el-table>
</div>
<div class="up-cell">
<div class="up-h">mtf 实况分 · 关注 / 回避榜<span class="src-err" v-if="usrc('mr') && usrc('mr').ok===false"> · 读取失败</span></div>
<div class="mr-two">
<div>
<div class="muted" style="margin-bottom:5px">关注(高分)</div>
<div v-for="(r,i) in ((usrc('mr')||{}).watch||[]).slice(0,8)" :key="i" class="mr-row"><b>{{ nm(r.ts_code) }}</b> <span class="muted mono">{{ r.ts_code }}</span><span class="mono" style="float:right;color:var(--accent)">{{ r.score }}</span></div>
<div class="muted" v-if="!((usrc('mr')||{}).watch||[]).length">暂无</div>
</div>
<div>
<div class="muted" style="margin-bottom:5px">回避(低分)</div>
<div v-for="(r,i) in ((usrc('mr')||{}).avoid||[]).slice(0,8)" :key="i" class="mr-row"><b>{{ nm(r.ts_code) }}</b> <span class="muted mono">{{ r.ts_code }}</span><span class="mono" style="float:right;color:var(--warn)">{{ r.score }}</span></div>
<div class="muted" v-if="!((usrc('mr')||{}).avoid||[]).length">暂无</div>
</div>
</div>
</div>
</div>
</template>
</div>
</div> </div>
<div v-show="mode==='ops'"> <div v-show="mode==='ops'">
@ -1402,6 +1407,7 @@ createApp({
const showUpstream = ref(true); const showUpstream = ref(true);
const showAlerts = ref(false); const showAlerts = ref(false);
const alertShowMap = reactive({ money_flow:true, capital:true, qrs:true, volume:true, multi:true, price_notice:false, buy_emitted:false, other:true }); const alertShowMap = reactive({ money_flow:true, capital:true, qrs:true, volume:true, multi:true, price_notice:false, buy_emitted:false, other:true });
const levelShow = reactive({ DANGER:true, WARNING:true, IMPORTANT:true, INFO:false, NORMAL:false });
const openScan = ref({ by_code: {}, notes: [] }); const openScan = ref({ by_code: {}, notes: [] });
const insTab = ref('live'); 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,
@ -1540,14 +1546,29 @@ createApp({
}; };
const nameMap = ref({}); const nameMap = ref({});
const nm = c => (nameMap.value[c] && nameMap.value[c].name) || c; const normCode = c => {
const fullName = c => (nameMap.value[c] && nameMap.value[c].full) || ''; // mtf 用 SH688662/SZ300404 前缀格式, 名称库是 688662.SH 点分格式; 归一后才查得到名字。
const m = String(c || '').match(/^(SH|SZ|BJ)(\d{6})$/i);
return m ? (m[2] + '.' + m[1].toUpperCase()) : String(c || '');
};
const nm = c => { const k = normCode(c); return (nameMap.value[k] && nameMap.value[k].name) || c; };
const fullName = c => { const k = normCode(c); return (nameMap.value[k] && nameMap.value[k].full) || ''; };
async function loadNames() { async function loadNames() {
const set = new Set(); const set = new Set();
(positions.value || []).forEach(p => p.ts_code && set.add(p.ts_code)); const push = c => { if (c) set.add(normCode(c)); };
(proposals.value || []).forEach(p => p.ts_code && set.add(p.ts_code)); (positions.value || []).forEach(p => push(p.ts_code));
(instructions.value || []).forEach(p => p.ts_code && set.add(p.ts_code)); (proposals.value || []).forEach(p => push(p.ts_code));
(ledger.value || []).forEach(p => p.ts_code && set.add(p.ts_code)); (instructions.value || []).forEach(p => push(p.ts_code));
(ledger.value || []).forEach(p => push(p.ts_code));
// 上游信号面板的代码原来没送去查名 —— 只有恰好在持仓里的才带名。这里一并收进来 (含 mtf 前缀码归一)。
const up = (upstream.value && upstream.value.sources) || {};
((up.alerts && up.alerts.items) || []).forEach(x => push(x.ts_code));
((up.intraday && up.intraday.decision) || []).forEach(x => push(x.ts_code));
((up.intraday && up.intraday.timing) || []).forEach(x => push(x.ts_code));
((up.metrics && up.metrics.items) || []).forEach(x => push(x.ts_code));
((up.sell_actions && up.sell_actions.items) || []).forEach(x => push(x.ts_code));
((up.mr && up.mr.watch) || []).forEach(x => push(x.ts_code));
((up.mr && up.mr.avoid) || []).forEach(x => push(x.ts_code));
const codes = [...set].filter(Boolean); const codes = [...set].filter(Boolean);
if (!codes.length) return; if (!codes.length) return;
const d = await call('get', '/api/names?codes=' + encodeURIComponent(codes.join(','))); const d = await call('get', '/api/names?codes=' + encodeURIComponent(codes.join(',')));
@ -1829,6 +1850,34 @@ createApp({
} }
const usrc = k => (upstream.value.sources || {})[k]; const usrc = k => (upstream.value.sources || {})[k];
const alertItems = computed(() => { const a = usrc('alerts'); return (a && Array.isArray(a.items)) ? a.items : []; }); const alertItems = computed(() => { const a = usrc('alerts'); return (a && Array.isArray(a.items)) ? a.items : []; });
// 级别筛选 (纯前端): 已知级别按开关过滤; 未知级别一律放行 (风控从宽, 不误杀)。
const KNOWN_LV = new Set(['DANGER','WARNING','IMPORTANT','INFO','NORMAL']);
const levelShown = k => levelShow[k] !== false;
function toggleLevel(k){ levelShow[k] = !levelShown(k); }
const passLevel = lv => { const x = String(lv == null ? '' : lv); return !KNOWN_LV.has(x) || levelShow[x] !== false; };
const alertItemsF = computed(() => alertItems.value.filter(a => passLevel(a.level)));
const levelCount = k => alertItems.value.reduce((n, a) => n + (String(a.level) === k ? 1 : 0), 0);
const ALERT_LEVELS = [
{ key:'DANGER', label:'危险', cls:'danger' }, { key:'WARNING', label:'警告', cls:'warn' },
{ key:'IMPORTANT', label:'重要', cls:'warn' }, { key:'INFO', label:'提示', cls:'info' },
{ key:'NORMAL', label:'普通', cls:'info' }];
const LV_LABEL = { DANGER:'危险', WARNING:'警告', IMPORTANT:'重要', INFO:'提示', NORMAL:'普通' };
const lvLabel = v => LV_LABEL[v] || v || '—';
const lvClass = v => v === 'DANGER' ? 'deny' : ((v === 'WARNING' || v === 'IMPORTANT') ? 'queue' : 'wait');
// 告警固定分类格子: 6 类常驻 + 其他(catch-all), 永远这几个框、空的显示暂无 —— 不再随数据冒出冒消。
const ALERT_CATS_FIXED = [
{ key:'money_flow', label:'资金流强度' }, { key:'capital', label:'资金分布' }, { key:'qrs', label:'日QRS对称' },
{ key:'volume', label:'放量异动' }, { key:'multi', label:'多源升级' }, { key:'price_notice', label:'股价异动' }];
const alertGridFixed = computed(() => {
const by = {};
for (const a of alertItemsF.value) { (by[a.cat] || (by[a.cat] = [])).push(a); }
const fixed = new Set(ALERT_CATS_FIXED.map(c => c.key));
const boxes = ALERT_CATS_FIXED.map(c => ({ key:c.key, label:c.label, items: by[c.key] || [] }));
const other = [];
Object.keys(by).forEach(k => { if (!fixed.has(k)) other.push(...(by[k] || [])); });
boxes.push({ key:'other', label:'其他', items: other });
return boxes;
});
const ALERT_CAT_ORDER = ['money_flow','capital','qrs','volume','multi','price_notice','buy_emitted','other']; const ALERT_CAT_ORDER = ['money_flow','capital','qrs','volume','multi','price_notice','buy_emitted','other'];
const ALERT_CAT_DIRECTIONAL = { money_flow:true, capital:true, qrs:true, volume:true, multi:true, price_notice:true, buy_emitted:false, other:false }; const ALERT_CAT_DIRECTIONAL = { money_flow:true, capital:true, qrs:true, volume:true, multi:true, price_notice:true, buy_emitted:false, other:false };
const catShown = k => alertShowMap[k] !== false; // 未登记类默认进折叠条 const catShown = k => alertShowMap[k] !== false; // 未登记类默认进折叠条
@ -1841,7 +1890,7 @@ createApp({
return keys.map(k => ({ key:k, label:(by[k][0].cat_label) || k, items:by[k], return keys.map(k => ({ key:k, label:(by[k][0].cat_label) || k, items:by[k],
directional: ALERT_CAT_DIRECTIONAL[k] !== false })); directional: ALERT_CAT_DIRECTIONAL[k] !== false }));
}); });
const alertStripItems = computed(() => alertItems.value.filter(a => catShown(a.cat))); const alertStripItems = computed(() => alertItemsF.value.filter(a => catShown(a.cat)));
const alertLatest = computed(() => alertStripItems.value[0] || {}); const alertLatest = computed(() => alertStripItems.value[0] || {});
const alertStripCount = computed(() => alertStripItems.value.length); const alertStripCount = computed(() => alertStripItems.value.length);
const alertTotalCount = computed(() => alertItems.value.length); const alertTotalCount = computed(() => alertItems.value.length);
@ -2138,7 +2187,8 @@ createApp({
showOpLog, showSettings, watchCandidates, marketOpen, marketState, lastRefresh, autoRefresh, showOpLog, showSettings, watchCandidates, marketOpen, marketState, lastRefresh, autoRefresh,
upstream, showUpstream, usrc, loadUpstream, upstream, showUpstream, usrc, loadUpstream,
showAlerts, alertItems, alertLatest, alertLevelClass, alertGroups, alertStripItems, showAlerts, alertItems, alertLatest, alertLevelClass, alertGroups, alertStripItems,
alertStripCount, alertTotalCount, catShown, toggleCat, ageText, alertRowClass }; alertStripCount, alertTotalCount, catShown, toggleCat, ageText, alertRowClass,
alertItemsF, levelShown, toggleLevel, levelCount, ALERT_LEVELS, lvLabel, lvClass, alertGridFixed, passLevel };
} }
}).use(ElementPlus).mount('#app'); }).use(ElementPlus).mount('#app');
</script> </script>