feat(frontend): 因子详情全期弹药行+疑似换皮徽标(工厂列表/详情两触点) [vps]

This commit is contained in:
2026-10-10 10:37:28 +08:00
parent f45e14234e
commit 031348c9a3
5 changed files with 84 additions and 0 deletions
+4
View File
@@ -60,6 +60,7 @@ export interface PipelineFactor {
promotedAtT: number | null
decayMonths: number
lastEvalDate: string
similarity?: Array<{ name: string; ratio: number }> | null // 疑似换皮提示(注册时落)
}
export interface MonthlyReview {
@@ -201,6 +202,9 @@ export interface FactorDetail {
createdAt: string | null
version: string
icRecentT: number | null
icStats?: { icAll: number | null; tAll: number | null;
positiveRatio: number | null; byYear: Record<string, number> } | null
similarity?: Array<{ name: string; ratio: number }> | null
lastEvalDate: string
}
birth: { jobId: string; startedAt: string; rounds: number | null; hypId: string } | null
@@ -131,3 +131,28 @@ describe('FactorDetail.vue 危险区送墓园(2026-10-09 补丁)', () => {
'insider_buy_decay60', 'graveyard', [], '衰减不止')
})
})
describe('FactorDetail.vue 全期弹药+疑似换皮(spec §4.2 2026-10-10)', () => {
beforeEach(() => vi.clearAllMocks())
it('渲染弹药行(全期IC/合成t/正IC占比/逐年)与换皮 chips', async () => {
const w = await mountDetail({
factor: { ...DETAIL.factor,
icStats: { icAll: 0.04, tAll: 1.73, positiveRatio: 1.0,
byYear: { 2025: 0.05, 2026: 0.02 } },
similarity: [{ name: 'fa_old', ratio: 1.0 }] },
birth: null,
})
expect(w.text()).toContain('全期IC 0.0400')
expect(w.text()).toContain('合成t 1.73')
expect(w.text()).toContain('正IC占比 100%')
expect(w.text()).toContain('2025:0.0500')
expect(w.text()).toContain('2026:0.0200')
expect(w.findAll('.sim-chip').length).toBe(1)
expect(w.text()).toContain('fa_old')
})
it('无 icStats(月度点不足或首班未跑)显示 — 占位,不造数', async () => {
const w = await mountDetail()
expect(w.text()).toContain('月度点不足或首班未跑')
expect(w.findAll('.sim-chip').length).toBe(0)
})
})
@@ -67,6 +67,11 @@ async function sendToGraveyard(): Promise<void> {
ElMessage.error(`判死失败: ${e instanceof Error ? e.message : e}`)
} finally { burying.value = false }
}
// 疑似换皮 chip 点击跳对方详情(比对靠人,一键可达)
function goFactor(n: string): void {
router.push(`/pipeline/factors/${n}`)
}
</script>
<template>
@@ -119,6 +124,22 @@ async function sendToGraveyard(): Promise<void> {
<div v-if="isLegacy" class="brow"><span class="bk">档案说明</span>
<span class="dim">此因子出生于档案制度(2026-10-09)之前,表达式/自述/承载逻辑未随出生落盘——存量形态,非数据丢失</span>
</div>
<div class="brow"><span class="bk">全期弹药</span>
<span class="val">
<template v-if="detail.factor.icStats && detail.factor.icStats.tAll !== null">
全期IC {{ detail.factor.icStats.icAll?.toFixed(4) }} ·
合成t {{ detail.factor.icStats.tAll?.toFixed(2) }} ·
正IC占比 {{ ((detail.factor.icStats.positiveRatio ?? 0) * 100).toFixed(0) }}% ·
逐年 {{ Object.entries(detail.factor.icStats.byYear).map(([y, v]) => `${y}:${v.toFixed(4)}`).join(' ') || '—' }}
</template>
<template v-else>—(月度点不足或首班未跑)</template>
</span></div>
<div class="brow" v-if="detail.factor.similarity?.length"><span class="bk">疑似换皮</span>
<span class="val warn">
<span v-for="s in detail.factor.similarity" :key="s.name"
class="sim-chip" @click="goFactor(s.name)">
{{ s.name }} · {{ (s.ratio * 100).toFixed(0) }}%</span>
</span></div>
</PanelCard>
<div class="stats-row">
@@ -169,4 +190,8 @@ async function sendToGraveyard(): Promise<void> {
.empty-box { color: var(--text-3); font-size: 12px; padding: 14px 4px; }
/* 危险区:送墓园 */
.grave-row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
/* 疑似换皮 chips */
.val.warn { color: var(--warn, #e6a23c); }
.sim-chip { cursor: pointer; border: 1px solid rgba(230, 162, 60, 0.45); background: rgba(230, 162, 60, 0.08); border-radius: 5px; padding: 0 6px; margin-right: 6px; line-height: 18px; display: inline-block; font-size: 11px; }
.sim-chip:hover { border-color: var(--warn, #e6a23c); }
</style>
@@ -69,3 +69,24 @@ describe('FactorFactory.vue', () => {
expect(w.findAll('.src-none').length).toBe(factorsMock.filter((f) => !f.hypothesis).length)
})
})
describe('FactorFactory.vue 疑似换皮徽标(spec §4.2 2026-10-10)', () => {
beforeEach(() => {
vi.clearAllMocks()
vi.mocked(api.getFactors).mockResolvedValue(factorsMock)
vi.mocked(api.getMonthlyReview).mockResolvedValue(monthlyReviewMock)
})
it('similarity 落款的行出 ⚠ 徽标,无落款不出', async () => {
const rows = factorsMock.map((f, i) => i === 0
? { ...f, similarity: [{ name: 'fa_old', ratio: 0.9 }] } : f)
vi.mocked(api.getFactors).mockResolvedValueOnce(rows)
const w = mount(FactorFactory, { global: { plugins: [ElementPlus] } })
await flushPromises()
expect(w.findAll('.dup-badge').length).toBe(1)
})
it('全部无 similarity 时零徽标', async () => {
const w = mount(FactorFactory, { global: { plugins: [ElementPlus] } })
await flushPromises()
expect(w.findAll('.dup-badge').length).toBe(0)
})
})
@@ -69,6 +69,13 @@ const fmt = (v: number | null): string => (v === null ? '—' : v.toFixed(2))
<a class="src-link" @click.stop="router.push('/pipeline/hypotheses')">🔗</a>
</el-tooltip>
<span v-else class="src-none">—</span>
<!-- AST 防换皮徽标(spec §4.2 2026-10-10):注册时相似度≥0.6 落
similarity,列表侧只提示——点行进详情看比对 chips,判定人做 -->
<el-tooltip v-if="row.similarity?.length"
:content="`疑似换皮: ${row.similarity.map((s: { name: string }) => s.name).join(', ')}`"
placement="top">
<span class="dup-badge" @click.stop="router.push(`/pipeline/factors/${row.id}`)">⚠</span>
</el-tooltip>
</div>
</template>
</el-table-column>
@@ -128,6 +135,8 @@ const fmt = (v: number | null): string => (v === null ? '—' : v.toFixed(2))
.src-link { font-size: 10.5px; color: var(--cyan-dim); cursor: pointer; border-bottom: 1px dashed rgba(0, 184, 204, 0.4); }
.src-link:hover { color: var(--brand); }
.src-none { color: var(--text-3); font-size: 10.5px; }
/* 疑似换皮徽标:提示非硬拒,点行进详情看比对 */
.dup-badge { cursor: pointer; font-size: 11px; color: var(--warn, #e6a23c); }
.el-table { cursor: pointer; }
.alert-txt { color: #ff5f6d; font-family: var(--mono); font-size: 11.5px; }
.dim { color: var(--text-3); }