feat(pipeline): 评审页落选原因行+过滤逻辑抽共享模块——可判却未达 t 门(无数据/t 不显著)的因子在下拉下方列明原因,3 可判 vs 1 可评的鸿沟可见;isReviewCandidate/exclusionReason 供下拉与落选行共用单一口径,修三矛盾① [vps] [no-doc]
This commit is contained in:
@@ -26,4 +26,19 @@ describe('PromotionReview.vue', () => {
|
||||
await flushPromises()
|
||||
expect(w.text()).toContain('暂无待评审候选')
|
||||
})
|
||||
it('落选可见:可判但未达 t 门的因子显示原因行', async () => {
|
||||
// factorsMock 已有 D17_ps(assessable,t=1.9)——落选行=2 项:未达门+无数据两型原因
|
||||
const mock = [
|
||||
...factorsMock,
|
||||
{ id: 'fa_nodata', category: 'fundamental', version: '1',
|
||||
status: 'assessable', icRecentT: null, icFullT: null,
|
||||
promotedAtT: null, decayMonths: 0, lastEvalDate: '' } as never,
|
||||
]
|
||||
vi.mocked(api.getFactors).mockResolvedValue(mock)
|
||||
const w = mount(PromotionReview, { global: { plugins: [ElementPlus] } })
|
||||
await flushPromises()
|
||||
expect(w.text()).toContain('另有 2 个可判因子未达评审门')
|
||||
expect(w.text()).toContain('fa_nodata(无近窗 IC 数据)')
|
||||
expect(w.text()).toContain('D17_ps(近窗 t=1.90 未达 +2)')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -6,11 +6,15 @@ import { postFactorVerdict } from '@/api/pipeline'
|
||||
import type { PipelineFactor } from '@/api/pipeline'
|
||||
import type { FactorVerdict } from '@/api/pipeline'
|
||||
import PanelCard from './PanelCard.vue'
|
||||
import { isReviewCandidate, exclusionReason } from './promotionFilter'
|
||||
|
||||
const factors = ref<PipelineFactor[]>([])
|
||||
const all = ref<PipelineFactor[]>([])
|
||||
const factors = computed(() => all.value.filter(isReviewCandidate))
|
||||
const excluded = computed(() =>
|
||||
all.value.filter((f) => f.status === 'assessable' && !isReviewCandidate(f)))
|
||||
const picked = ref('')
|
||||
onMounted(async () => {
|
||||
factors.value = (await getFactors()).filter((f) => f.status === 'assessable' && (f.icRecentT ?? 0) >= 2)
|
||||
all.value = await getFactors().catch(() => [])
|
||||
picked.value = factors.value[0]?.id ?? ''
|
||||
})
|
||||
|
||||
@@ -28,7 +32,7 @@ const answers = ref<Record<number, 'y' | 'n' | ''>>({})
|
||||
const note = ref('')
|
||||
const busy = ref(false)
|
||||
const reload = async () => {
|
||||
factors.value = (await getFactors()).filter((f) => f.status === 'assessable' && (f.icRecentT ?? 0) >= 2)
|
||||
all.value = await getFactors().catch(() => [])
|
||||
picked.value = factors.value[0]?.id ?? ''
|
||||
answers.value = {}
|
||||
}
|
||||
@@ -64,6 +68,11 @@ async function submit(verdict: FactorVerdict): Promise<void> {
|
||||
<option v-for="f in factors" :key="f.id" :value="f.id">{{ f.id }}@{{ f.version }}</option>
|
||||
</select>
|
||||
</template>
|
||||
<div v-if="excluded.length" class="excluded">
|
||||
另有 {{ excluded.length }} 个可判因子未达评审门:{{
|
||||
excluded.map((f) => `${f.id}(${exclusionReason(f)})`).join('、')
|
||||
}}
|
||||
</div>
|
||||
<div v-if="!factors.length" class="empty">暂无待评审候选 —— 月度批评后会自动补位</div>
|
||||
<template v-else-if="current">
|
||||
<div class="ic-row">
|
||||
@@ -99,6 +108,7 @@ async function submit(verdict: FactorVerdict): Promise<void> {
|
||||
<style scoped>
|
||||
.pl-page { max-width: 860px; margin: 0 auto; }
|
||||
.empty { color: var(--text-3); font-size: 12.5px; padding: 24px 0; text-align: center; }
|
||||
.excluded { color: var(--text-3); font-size: 11px; margin-bottom: 8px; font-family: var(--mono); }
|
||||
.pick { background: var(--bg); border: 1px solid var(--border); border-radius: var(--r-sm); color: var(--text); font-family: var(--mono); font-size: 12px; padding: 3px 8px; }
|
||||
.ic-row { display: flex; gap: 20px; font-size: 12.5px; color: var(--text-2); margin-bottom: 12px; font-family: var(--mono); }
|
||||
.ic-row b { color: var(--brand); font-weight: 600; }
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { isReviewCandidate, exclusionReason, REVIEW_T_GATE } from './promotionFilter'
|
||||
import type { PipelineFactor } from '@/api/pipeline'
|
||||
|
||||
const f = (over: Partial<PipelineFactor>): PipelineFactor => ({
|
||||
id: 'x', category: 'fundamental', version: '1', status: 'assessable',
|
||||
icRecentT: null, icFullT: null, promotedAtT: null, decayMonths: 0,
|
||||
lastEvalDate: '', ...over,
|
||||
})
|
||||
|
||||
describe('promotionFilter', () => {
|
||||
it('候选门=assessable 且近窗 t≥2', () => {
|
||||
expect(REVIEW_T_GATE).toBe(2)
|
||||
expect(isReviewCandidate(f({ icRecentT: 10.2 }))).toBe(true)
|
||||
expect(isReviewCandidate(f({ icRecentT: 2.0 }))).toBe(true)
|
||||
})
|
||||
it('无数据/不显著/非可判都不是候选', () => {
|
||||
expect(isReviewCandidate(f({ icRecentT: null }))).toBe(false)
|
||||
expect(isReviewCandidate(f({ icRecentT: -0.21 }))).toBe(false)
|
||||
expect(isReviewCandidate(f({ status: 'promoted', icRecentT: 9 }))).toBe(false)
|
||||
})
|
||||
it('落选原因可读:无数据 vs 未达门', () => {
|
||||
expect(exclusionReason(f({ icRecentT: null }))).toContain('无近窗 IC 数据')
|
||||
expect(exclusionReason(f({ icRecentT: -0.21 }))).toContain('未达 +2')
|
||||
expect(exclusionReason(f({ status: 'incubating' }))).toBe('')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,15 @@
|
||||
import type { PipelineFactor } from '@/api/pipeline'
|
||||
|
||||
// 评审门:与后端 routes_pipeline.REVIEW_T_GATE 同值镜像,改门须两处同改+spec 注记
|
||||
export const REVIEW_T_GATE = 2
|
||||
|
||||
export function isReviewCandidate(f: PipelineFactor): boolean {
|
||||
return f.status === 'assessable' && (f.icRecentT ?? 0) >= REVIEW_T_GATE
|
||||
}
|
||||
|
||||
/** 可判却进不了评审页的原因(三矛盾①:鸿沟可见);非可判返回空串 */
|
||||
export function exclusionReason(f: PipelineFactor): string {
|
||||
if (f.status !== 'assessable') return ''
|
||||
if (f.icRecentT == null) return '无近窗 IC 数据'
|
||||
return `近窗 t=${f.icRecentT.toFixed(2)} 未达 +${REVIEW_T_GATE}`
|
||||
}
|
||||
Reference in New Issue
Block a user