feat(frontend): 因子详情全期弹药行+疑似换皮徽标(工厂列表/详情两触点) [vps]
This commit is contained in:
@@ -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); }
|
||||
|
||||
Reference in New Issue
Block a user