feat: KPI多粒度目标智能派生 — 基准值+按类型派生+手动覆盖
- 后端: target_calc_type字段(accumulate累计/ratio比率) + infer_calc_type名称/单位推断 - 派生规则: 累计型 月×3=季×12=年(季×4=年); 比率型 季/年沿用基准不可乘 - 虚拟派生不落库: kpi_to_dict返回derived_targets+derived_flags(自动标记) - DB: 302个KPI回填类型(226累计/76比率) - 前端KPIList: 指标类型选择 + 季/年自动派生预览(↳自动=N) - 前端KPIDetail: 元数据卡片自动标记 + 编辑表单指标类型 - 回归: pytest 451 passed
This commit is contained in:
@@ -35,9 +35,21 @@
|
||||
<el-tag v-else type="warning" size="small">{{ kpi.data_owner || '缺失' }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="单位">{{ kpi.unit || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="目标值(月)">{{ kpi.target_monthly ?? '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="目标值(季)">{{ kpi.target_quarterly ?? '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="目标值(年)">{{ kpi.target_yearly ?? '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="目标值(月)">
|
||||
{{ fmtDerived(kpi, 'monthly') }}
|
||||
<el-tag v-if="isDerived(kpi, 'monthly')" size="small" type="info" effect="plain">自动</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="目标值(季)">
|
||||
{{ fmtDerived(kpi, 'quarterly') }}
|
||||
<el-tag v-if="isDerived(kpi, 'quarterly')" size="small" type="info" effect="plain">自动</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="目标值(年)">
|
||||
{{ fmtDerived(kpi, 'yearly') }}
|
||||
<el-tag v-if="isDerived(kpi, 'yearly')" size="small" type="info" effect="plain">自动</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="指标类型">
|
||||
{{ kpi.target_calc_type === 'accumulate' ? '累计型' : kpi.target_calc_type === 'ratio' ? '比率型' : (kpi.target_calc_type || '-') }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="数据源类型">{{ kpi.data_source_type || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="维度">{{ dimLabel(kpi.dimension) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="频率">{{ kpi.frequency || '-' }}</el-descriptions-item>
|
||||
@@ -68,6 +80,12 @@
|
||||
<el-form-item label="目标值(月度)"><el-input-number v-model="kpi.target_monthly" :min="0" style="width:100%" /></el-form-item>
|
||||
<el-form-item label="目标值(季度)"><el-input-number v-model="kpi.target_quarterly" :min="0" style="width:100%" /></el-form-item>
|
||||
<el-form-item label="目标值(年度)"><el-input-number v-model="kpi.target_yearly" :min="0" style="width:100%" /></el-form-item>
|
||||
<el-form-item label="指标类型">
|
||||
<el-select v-model="kpi.target_calc_type" style="width:100%">
|
||||
<el-option label="累计型(月×3=季,可乘)" value="accumulate" />
|
||||
<el-option label="比率型(季沿用月,不可乘)" value="ratio" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="单位"><el-input v-model="kpi.unit" /></el-form-item>
|
||||
<el-form-item label="频率">
|
||||
<el-select v-model="kpi.frequency" style="width:100%">
|
||||
@@ -385,6 +403,42 @@ const userOptions = computed(() => {
|
||||
|
||||
function dimLabel(d: string) { return ({ finance: '财务', customer: '客户', process: '流程', learning: '学习' } as any)[d] || d }
|
||||
function catLabel(c: string) { return CAT_MAP[c] || c }
|
||||
|
||||
// ── 多粒度目标派生显示(与后端 derive_cycle_targets 一致)──
|
||||
function derivedFor(k: any): { monthly: number | null; quarterly: number | null; yearly: number | null; flags: any } {
|
||||
if (k?.derived_targets) return { ...k.derived_targets, flags: k.derived_flags || {} }
|
||||
// 本地兜底(旧数据无 derived_targets 时)
|
||||
const f = k || {}
|
||||
const type = f.target_calc_type || 'ratio'
|
||||
const freq = f.frequency || 'monthly'
|
||||
let base: number | null = null
|
||||
if (freq === 'yearly') base = f.target_yearly
|
||||
else if (freq === 'quarterly' || freq === 'half_year') base = f.target_quarterly
|
||||
else base = f.target_monthly
|
||||
if (base == null) base = f.target_value
|
||||
const flags = { monthly: false, quarterly: false, yearly: false }
|
||||
let q: number | null = f.target_quarterly
|
||||
let y: number | null = f.target_yearly
|
||||
if (base != null) {
|
||||
if (type === 'accumulate') {
|
||||
const m: number = f.target_monthly ?? base
|
||||
if (q == null) { q = m * 3; flags.quarterly = true }
|
||||
if (y == null) { y = m * 12; flags.yearly = true }
|
||||
} else {
|
||||
if (q == null) { q = base; flags.quarterly = true }
|
||||
if (y == null) { y = base; flags.yearly = true }
|
||||
}
|
||||
}
|
||||
return { monthly: f.target_monthly, quarterly: q, yearly: y, flags }
|
||||
}
|
||||
function fmtDerived(k: any, cycle: 'monthly' | 'quarterly' | 'yearly') {
|
||||
const d = derivedFor(k)
|
||||
const v = d[cycle]
|
||||
return v == null ? '-' : String(v)
|
||||
}
|
||||
function isDerived(k: any, cycle: 'monthly' | 'quarterly' | 'yearly') {
|
||||
return !!derivedFor(k).flags?.[cycle]
|
||||
}
|
||||
function dimTagType(d: string) { return ({ finance: '', customer: 'success', process: 'warning', learning: 'info' } as any)[d] || '' }
|
||||
|
||||
const metadataComplete = computed(() => {
|
||||
|
||||
@@ -231,12 +231,18 @@
|
||||
<el-form-item label="目标值(月)"><el-input-number v-model="form.target_monthly" :min="0" style="width:100%" /></el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="目标值(季)"><el-input-number v-model="form.target_quarterly" :min="0" style="width:100%" /></el-form-item>
|
||||
<el-form-item label="目标值(季)">
|
||||
<el-input-number v-model="form.target_quarterly" :min="0" style="width:100%" />
|
||||
<div v-if="derivedPreview.quarterly !== null" class="derive-hint">↳ 自动={{ derivedPreview.quarterly }}</div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="目标值(年)"><el-input-number v-model="form.target_yearly" :min="0" style="width:100%" /></el-form-item>
|
||||
<el-form-item label="目标值(年)">
|
||||
<el-input-number v-model="form.target_yearly" :min="0" style="width:100%" />
|
||||
<div v-if="derivedPreview.yearly !== null" class="derive-hint">↳ 自动={{ derivedPreview.yearly }}</div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="单位"><el-input v-model="form.unit" placeholder="%, 元, 次, 个..." /></el-form-item>
|
||||
@@ -254,6 +260,16 @@
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="指标类型">
|
||||
<el-select v-model="form.target_calc_type" style="width:100%">
|
||||
<el-option label="累计型(月×3=季,可乘)" value="accumulate" />
|
||||
<el-option label="比率型(季沿用月,不可乘)" value="ratio" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="数据源">
|
||||
<el-select v-model="form.data_source_type" style="width:100%">
|
||||
@@ -495,6 +511,30 @@ const designChecks = computed(() => {
|
||||
]
|
||||
})
|
||||
|
||||
// ── 多粒度目标派生预览(填基准值后季/年自动提示,与后端 derive_cycle_targets 一致)──
|
||||
const derivedPreview = computed(() => {
|
||||
const f = form.value || {}
|
||||
const type = f.target_calc_type || 'ratio'
|
||||
const freq = f.frequency || 'monthly'
|
||||
let base: number | null = null
|
||||
if (freq === 'yearly') base = f.target_yearly
|
||||
else if (freq === 'quarterly' || freq === 'half_year') base = f.target_quarterly
|
||||
else base = f.target_monthly
|
||||
if (base == null) base = f.target_value
|
||||
if (base == null) return { quarterly: null, yearly: null }
|
||||
let q: number | null = f.target_quarterly
|
||||
let y: number | null = f.target_yearly
|
||||
if (type === 'accumulate') {
|
||||
const m: number = f.target_monthly ?? base
|
||||
if (q == null) q = m * 3
|
||||
if (y == null) y = m * 12
|
||||
} else {
|
||||
if (q == null) q = base
|
||||
if (y == null) y = base
|
||||
}
|
||||
return { quarterly: f.target_quarterly == null ? q : null, yearly: f.target_yearly == null ? y : null }
|
||||
})
|
||||
|
||||
// ── 编辑入口:列表行 → 弹窗编辑(与新建一致)──
|
||||
function openEdit(row: any) {
|
||||
editMode.value = true
|
||||
@@ -518,6 +558,7 @@ function openEdit(row: any) {
|
||||
threshold_yellow: row.threshold_yellow,
|
||||
threshold_red: row.threshold_red,
|
||||
epic: row.epic,
|
||||
target_calc_type: row.target_calc_type,
|
||||
}
|
||||
showForm.value = true
|
||||
}
|
||||
@@ -952,6 +993,12 @@ onMounted(() => {
|
||||
color: #E6A23C;
|
||||
font-size: 12px;
|
||||
}
|
||||
.derive-hint {
|
||||
font-size: 12px;
|
||||
color: #409EFF;
|
||||
margin-top: 2px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
Reference in New Issue
Block a user