feat: KPI多粒度目标值补提交 — dashboard API + 前端3视图(目标值月/季/年)
已上线未提交的历史功能(2026-08-17): kpi_target_by_frequency + target_monthly/quarterly/yearly 字段透传
This commit is contained in:
@@ -52,6 +52,24 @@ def period_prefix(period_type: str):
|
|||||||
return str(datetime.now().year)
|
return str(datetime.now().year)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def kpi_target_by_frequency(k):
|
||||||
|
"""按考核频率返回对应周期的目标值(多粒度改造)
|
||||||
|
monthly/weekly -> target_monthly; quarterly/half_year -> target_quarterly; yearly -> target_yearly
|
||||||
|
兼容: 对应列无值时回退 target_value
|
||||||
|
"""
|
||||||
|
freq = (k.frequency or "monthly").lower()
|
||||||
|
if freq in ("monthly", "weekly"):
|
||||||
|
val = getattr(k, "target_monthly", None)
|
||||||
|
elif freq in ("quarterly", "half_year"):
|
||||||
|
val = getattr(k, "target_quarterly", None)
|
||||||
|
elif freq == "yearly":
|
||||||
|
val = getattr(k, "target_yearly", None)
|
||||||
|
else:
|
||||||
|
val = None
|
||||||
|
if val is None:
|
||||||
|
val = k.target_value
|
||||||
|
return val
|
||||||
|
|
||||||
@router.get("/summary")
|
@router.get("/summary")
|
||||||
def get_dashboard_summary(role: str = Query("ceo"), period: str = Query("month"),
|
def get_dashboard_summary(role: str = Query("ceo"), period: str = Query("month"),
|
||||||
db: Session = Depends(get_db), entity_id: int = Depends(get_entity_id)):
|
db: Session = Depends(get_db), entity_id: int = Depends(get_entity_id)):
|
||||||
@@ -143,6 +161,7 @@ def get_dashboard_kpis(role: str = Query("ceo"), period: str = Query("month"),
|
|||||||
result.append({
|
result.append({
|
||||||
"id": k.id, "kpi_code": k.kpi_code, "kpi_name": k.kpi_name,
|
"id": k.id, "kpi_code": k.kpi_code, "kpi_name": k.kpi_name,
|
||||||
"dimension": k.dimension, "unit": k.unit, "target_value": k.target_value,
|
"dimension": k.dimension, "unit": k.unit, "target_value": k.target_value,
|
||||||
|
"target_monthly": k.target_monthly, "target_quarterly": k.target_quarterly, "target_yearly": k.target_yearly,
|
||||||
"actual_value": latest.actual_value if latest else None,
|
"actual_value": latest.actual_value if latest else None,
|
||||||
"period": latest.period if latest else None,
|
"period": latest.period if latest else None,
|
||||||
"alert_level": alert.alert_level if alert else "none",
|
"alert_level": alert.alert_level if alert else "none",
|
||||||
@@ -198,6 +217,7 @@ def get_my_kpis(
|
|||||||
"id": k.id, "kpi_code": k.kpi_code, "kpi_name": k.kpi_name,
|
"id": k.id, "kpi_code": k.kpi_code, "kpi_name": k.kpi_name,
|
||||||
"dimension": k.dimension, "unit": k.unit,
|
"dimension": k.dimension, "unit": k.unit,
|
||||||
"target_value": k.target_value,
|
"target_value": k.target_value,
|
||||||
|
"target_monthly": k.target_monthly, "target_quarterly": k.target_quarterly, "target_yearly": k.target_yearly,
|
||||||
"actual_value": latest.actual_value if latest else None,
|
"actual_value": latest.actual_value if latest else None,
|
||||||
"period": latest.period if latest else period_str,
|
"period": latest.period if latest else period_str,
|
||||||
"alert_level": alert.alert_level if alert else "none",
|
"alert_level": alert.alert_level if alert else "none",
|
||||||
@@ -251,6 +271,7 @@ def get_finance_analysis(
|
|||||||
kpi_data.append({
|
kpi_data.append({
|
||||||
"id": k.id, "kpi_code": k.kpi_code, "kpi_name": k.kpi_name,
|
"id": k.id, "kpi_code": k.kpi_code, "kpi_name": k.kpi_name,
|
||||||
"unit": k.unit, "target_value": k.target_value,
|
"unit": k.unit, "target_value": k.target_value,
|
||||||
|
"target_monthly": k.target_monthly, "target_quarterly": k.target_quarterly, "target_yearly": k.target_yearly,
|
||||||
"actual_value": latest.actual_value if latest else None,
|
"actual_value": latest.actual_value if latest else None,
|
||||||
"threshold_green": k.threshold_green,
|
"threshold_green": k.threshold_green,
|
||||||
"threshold_yellow": k.threshold_yellow,
|
"threshold_yellow": k.threshold_yellow,
|
||||||
@@ -414,7 +435,7 @@ def my_dashboard(
|
|||||||
).order_by(KPIValue.calculated_at.desc()).first()
|
).order_by(KPIValue.calculated_at.desc()).first()
|
||||||
|
|
||||||
actual = latest_v.actual_value if latest_v else None
|
actual = latest_v.actual_value if latest_v else None
|
||||||
target = k.target_value
|
target = kpi_target_by_frequency(k)
|
||||||
level = "gray"
|
level = "gray"
|
||||||
if actual is not None and target:
|
if actual is not None and target:
|
||||||
# 反向指标(越低越好):费用率/渠补率/应收天数/返利率/成本率
|
# 反向指标(越低越好):费用率/渠补率/应收天数/返利率/成本率
|
||||||
@@ -437,6 +458,10 @@ def my_dashboard(
|
|||||||
"dimension": k.dimension,
|
"dimension": k.dimension,
|
||||||
"category": k.category,
|
"category": k.category,
|
||||||
"target_value": target,
|
"target_value": target,
|
||||||
|
"target_monthly": k.target_monthly,
|
||||||
|
"target_quarterly": k.target_quarterly,
|
||||||
|
"target_yearly": k.target_yearly,
|
||||||
|
"frequency": k.frequency,
|
||||||
"actual_value": actual,
|
"actual_value": actual,
|
||||||
"unit": k.unit,
|
"unit": k.unit,
|
||||||
"level": level,
|
"level": level,
|
||||||
|
|||||||
@@ -0,0 +1,155 @@
|
|||||||
|
# KPI目标值多粒度改造(月/季/年分离)
|
||||||
|
|
||||||
|
> 需求提出:2026-08-17 | 提出人:任总(工作台使用中发现混淆)
|
||||||
|
> 文档版本:v1.0 | 状态:待评审
|
||||||
|
> 类型:后端改表 + 前端改造 + 数据清洗
|
||||||
|
|
||||||
|
## 一、问题描述
|
||||||
|
|
||||||
|
### 用户故事
|
||||||
|
|
||||||
|
作为CMA系统的操作人员,我在工作台看到"目标 1200"时,**无法判断这个目标是月度、季度还是年度**;当我点击进入KPI详情调整目标值时,**不知道该改的是月目标、季目标还是年目标**——系统只有一个单值 target_value,三个周期的目标混在一起,必然造成操作混淆。
|
||||||
|
|
||||||
|
### 现状痛点
|
||||||
|
|
||||||
|
```
|
||||||
|
① KPI定义的frequency字段:monthly 63个 / quarterly 11个 / weekly 2个 / half_year 10个
|
||||||
|
→ 定义了"考核频率",但target_value是单值,没说清对应哪个周期
|
||||||
|
|
||||||
|
② target_value单值无法表达多周期目标:
|
||||||
|
同一KPI(如F_REVENUE营收):
|
||||||
|
frequency=quarterly(季度考核)
|
||||||
|
target_value=1200(单值)
|
||||||
|
kpi_values.period=2026.06(月度数据!)
|
||||||
|
→ 1200到底是月目标?季目标?年度目标?三个信息互相矛盾
|
||||||
|
|
||||||
|
③ period格式混乱:
|
||||||
|
2026.06 / 2025.1(缺零)/ 2026H1 / 2026-06 / 2021(年)
|
||||||
|
→ 前端排序、环比计算、图表展示不可靠
|
||||||
|
```
|
||||||
|
|
||||||
|
### 核心矛盾
|
||||||
|
|
||||||
|
```
|
||||||
|
考核频率(月/季/年)× 目标值(单值)× 实际数据周期(混合格式)
|
||||||
|
↓
|
||||||
|
三者不对齐 = 数据粒度不匹配 = 使用者必然混淆
|
||||||
|
```
|
||||||
|
|
||||||
|
## 二、解决方案
|
||||||
|
|
||||||
|
### 方案总览:目标值分级 + period格式化
|
||||||
|
|
||||||
|
```
|
||||||
|
① 数据库:kpi_definitions 增加三列
|
||||||
|
target_monthly / target_quarterly / target_yearly
|
||||||
|
|
||||||
|
② 数据迁移:现有target_value按frequency归入对应列
|
||||||
|
|
||||||
|
③ 前端显示:按frequency显示对应目标值 + 周期标签
|
||||||
|
|
||||||
|
④ period格式统一:kpi_values.period 标准化为 YYYY-MM / YYYY-Qn / YYYY
|
||||||
|
```
|
||||||
|
|
||||||
|
## 三、功能列表
|
||||||
|
|
||||||
|
### 后端(backend)
|
||||||
|
|
||||||
|
- [ ] 1. `kpi_definitions` 表加3列(target_monthly / target_quarterly / target_yearly,decimal(15,2) 可空)
|
||||||
|
- [ ] 2. 数据迁移脚本:`UPDATE ... SET target_monthly=target_value WHERE frequency='monthly'`(quarterly/yearly同理);weekly/half_year归入最接近周期(weekly→monthly,half_year→quarterly)并记录
|
||||||
|
- [ ] 3. API改造:KPI详情/列表接口返回 `target_monthly`、`target_quarterly`、`target_yearly` + `frequency`
|
||||||
|
- [ ] 4. 前端保存KPI时,按frequency写入对应目标列(如frequency=monthly则写target_monthly)
|
||||||
|
- [ ] 5. 新增period标准化工具脚本(清洗历史数据)
|
||||||
|
|
||||||
|
### 前端(frontend)
|
||||||
|
|
||||||
|
- [ ] 6. 工作台KPI卡:目标值旁显示周期标签"目标(月)"/"目标(季)"/"目标(年)"
|
||||||
|
- [ ] 7. KPI详情页"目标值"编辑:显示当前frequency对应的目标字段,明确标注周期
|
||||||
|
- [ ] 8. 历史数据tab:period统一展示格式(YYYY-MM)
|
||||||
|
|
||||||
|
### 数据(data)
|
||||||
|
|
||||||
|
- [ ] 9. 清洗kpi_values.period历史数据(2025.1→2025-01,2026H1→2026-H1,等)
|
||||||
|
|
||||||
|
## 四、数据流
|
||||||
|
|
||||||
|
```
|
||||||
|
展示流:
|
||||||
|
工作台/详情页 GET /kpis/:id
|
||||||
|
→ 返回 kpi + target_monthly/quarterly/yearly + frequency
|
||||||
|
→ 前端按frequency取对应目标值
|
||||||
|
→ 显示"目标(月): 18%" 或 "目标(季): 1200"
|
||||||
|
|
||||||
|
编辑流:
|
||||||
|
用户在详情页改目标值
|
||||||
|
→ 前端按frequency路由到对应字段(monthly→target_monthly)
|
||||||
|
→ PUT /kpis/:id { target_monthly: 20 }
|
||||||
|
→ 后端只更新该列,其他周期目标不受影响
|
||||||
|
```
|
||||||
|
|
||||||
|
## 五、界面设计
|
||||||
|
|
||||||
|
### 工作台KPI卡(改动)
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────┐
|
||||||
|
│ 毛利率 [达标] │
|
||||||
|
│ 18.5% 目标(月): 18% │ ← 加频率标签
|
||||||
|
│ ████████████░░░░░ │
|
||||||
|
└─────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### KPI详情页-基本信息(改动)
|
||||||
|
|
||||||
|
```
|
||||||
|
目标值(月度): [ 18 ] % ← frequency=monthly时显示这个
|
||||||
|
目标值(季度): [ ] ← 季度目标可另行填写
|
||||||
|
目标值(年度): [ ] ← 年度目标可另行填写
|
||||||
|
```
|
||||||
|
|
||||||
|
## 六、验收标准
|
||||||
|
|
||||||
|
1. 工作台KPI卡显示带周期标签的目标值(如"目标(月): 18%")
|
||||||
|
2. 月度KPI改目标只改target_monthly,季度/年度目标不受影响
|
||||||
|
3. frequency=quarterly的KPI(如F_REVENUE)工作台显示季度目标
|
||||||
|
4. kpi_values.period全部为标准格式(无2025.1、2026H1等异常格式)
|
||||||
|
5. 数据迁移后:原monthly的target_value已迁入target_monthly,无丢失
|
||||||
|
6. 一致性检查通过(consistency-check不报新错误)
|
||||||
|
|
||||||
|
## 七、工作量评估
|
||||||
|
|
||||||
|
| 项 | 内容 | 人天 |
|
||||||
|
|:--|:----|:---:|
|
||||||
|
| 后端 | 加列+迁移脚本+API改造 | 0.5 |
|
||||||
|
| 前端 | 工作台标签+详情页多周期目标 | 0.5 |
|
||||||
|
| 数据 | period清洗脚本+执行 | 0.3 |
|
||||||
|
| 测试 | 验收用例(多frequency验证) | 0.2 |
|
||||||
|
| **合计** | | **1.5天** |
|
||||||
|
|
||||||
|
## 八、关联模块
|
||||||
|
|
||||||
|
- 工作台(MyDashboard.vue)—— KPI卡显示
|
||||||
|
- KPI详情页(KPIDetail.vue)—— 目标值编辑
|
||||||
|
- KPI字典列表(KPIList.vue)—— 列表目标值列
|
||||||
|
- kpis.py API —— 目标值读写
|
||||||
|
- dashboard.py API —— my-dashboard返回
|
||||||
|
- 与"科目≠KPI"规范(kpi-account-governance-rule.md)配套:本需求解决"时间粒度",前者解决"类型混用"
|
||||||
|
|
||||||
|
## 九、风险与注意
|
||||||
|
|
||||||
|
```
|
||||||
|
⚠️ 迁移不可逆:先备份kpi_definitions表(参考2026-08-17备份流程)
|
||||||
|
⚠️ weekly/half_year归并需人工确认映射规则
|
||||||
|
⚠️ period清洗会影响历史报表展示,需回归测试
|
||||||
|
⚠️ 前端所有"目标值"引用点需全局搜索(不止工作台和详情页)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 十、执行分工建议
|
||||||
|
|
||||||
|
```
|
||||||
|
方案评审:研学(已分析)
|
||||||
|
需求文档:研学(本文档)
|
||||||
|
开发实施:项目Bot或全栈Bot(按此文档)
|
||||||
|
数据迁移:开发时一起做(备份先行)
|
||||||
|
验收:研学(按验收标准逐条验证,铁律七)
|
||||||
|
```
|
||||||
@@ -35,7 +35,9 @@
|
|||||||
<el-tag v-else type="warning" size="small">{{ kpi.data_owner || '缺失' }}</el-tag>
|
<el-tag v-else type="warning" size="small">{{ kpi.data_owner || '缺失' }}</el-tag>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="单位">{{ kpi.unit || '-' }}</el-descriptions-item>
|
<el-descriptions-item label="单位">{{ kpi.unit || '-' }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="目标值">{{ kpi.target_value ?? '-' }}</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="数据源类型">{{ kpi.data_source_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="维度">{{ dimLabel(kpi.dimension) }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="频率">{{ kpi.frequency || '-' }}</el-descriptions-item>
|
<el-descriptions-item label="频率">{{ kpi.frequency || '-' }}</el-descriptions-item>
|
||||||
@@ -63,12 +65,16 @@
|
|||||||
<el-option v-for="c in categoryOptions" :key="c.value" :label="c.label" :value="c.value" />
|
<el-option v-for="c in categoryOptions" :key="c.value" :label="c.label" :value="c.value" />
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="目标值"><el-input-number v-model="kpi.target_value" :min="0" style="width:100%" /></el-form-item>
|
<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-input v-model="kpi.unit" /></el-form-item>
|
<el-form-item label="单位"><el-input v-model="kpi.unit" /></el-form-item>
|
||||||
<el-form-item label="频率">
|
<el-form-item label="频率">
|
||||||
<el-select v-model="kpi.frequency" style="width:100%">
|
<el-select v-model="kpi.frequency" style="width:100%">
|
||||||
<el-option label="月度" value="monthly" />
|
<el-option label="月度" value="monthly" />
|
||||||
|
<el-option label="周度" value="weekly" />
|
||||||
<el-option label="季度" value="quarterly" />
|
<el-option label="季度" value="quarterly" />
|
||||||
|
<el-option label="半年" value="half_year" />
|
||||||
<el-option label="年度" value="yearly" />
|
<el-option label="年度" value="yearly" />
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@@ -543,6 +549,13 @@ async function save() {
|
|||||||
if (saving.value) return
|
if (saving.value) return
|
||||||
saving.value = true
|
saving.value = true
|
||||||
try {
|
try {
|
||||||
|
// 多粒度目标同步:按frequency将对应周期目标写入target_value(兼容评分/阈值等旧逻辑)
|
||||||
|
const freq = kpi.value.frequency || 'monthly'
|
||||||
|
let tv: any = null
|
||||||
|
if (freq === 'quarterly' || freq === 'half_year') tv = kpi.value.target_quarterly
|
||||||
|
else if (freq === 'yearly') tv = kpi.value.target_yearly
|
||||||
|
else tv = kpi.value.target_monthly
|
||||||
|
if (tv != null) kpi.value.target_value = tv
|
||||||
await kpiApi.update(kpi.value.id, kpi.value)
|
await kpiApi.update(kpi.value.id, kpi.value)
|
||||||
ElMessage.success('保存成功')
|
ElMessage.success('保存成功')
|
||||||
isDirty.value = false
|
isDirty.value = false
|
||||||
|
|||||||
@@ -103,8 +103,10 @@
|
|||||||
<el-table-column label="二级类别" width="90">
|
<el-table-column label="二级类别" width="90">
|
||||||
<template #default="{ row }">{{ catLabel(row.category) }}</template>
|
<template #default="{ row }">{{ catLabel(row.category) }}</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="target_value" label="目标值" width="90">
|
<el-table-column prop="target_value" label="目标值" width="110">
|
||||||
<template #default="{ row }">{{ formatTarget(row.target_value, row.unit) }}</template>
|
<template #default="{ row }">
|
||||||
|
<span class="target-cell">{{ targetLabel(row) }} <b>{{ formatTarget(targetForRow(row), row.unit) }}</b></span>
|
||||||
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="frequency" label="频率" width="70">
|
<el-table-column prop="frequency" label="频率" width="70">
|
||||||
<template #default="{ row }">{{ freqLabel(row.frequency) }}</template>
|
<template #default="{ row }">{{ freqLabel(row.frequency) }}</template>
|
||||||
@@ -198,7 +200,15 @@
|
|||||||
</el-row>
|
</el-row>
|
||||||
<el-row :gutter="20">
|
<el-row :gutter="20">
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-form-item label="目标值"><el-input-number v-model="form.target_value" :min="0" style="width:100%" /></el-form-item>
|
<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-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-col>
|
</el-col>
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-form-item label="单位"><el-input v-model="form.unit" placeholder="%, 元, 次, 个..." /></el-form-item>
|
<el-form-item label="单位"><el-input v-model="form.unit" placeholder="%, 元, 次, 个..." /></el-form-item>
|
||||||
@@ -209,7 +219,9 @@
|
|||||||
<el-form-item label="频率">
|
<el-form-item label="频率">
|
||||||
<el-select v-model="form.frequency" style="width:100%">
|
<el-select v-model="form.frequency" style="width:100%">
|
||||||
<el-option label="月度" value="monthly" />
|
<el-option label="月度" value="monthly" />
|
||||||
|
<el-option label="周度" value="weekly" />
|
||||||
<el-option label="季度" value="quarterly" />
|
<el-option label="季度" value="quarterly" />
|
||||||
|
<el-option label="半年" value="half_year" />
|
||||||
<el-option label="年度" value="yearly" />
|
<el-option label="年度" value="yearly" />
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@@ -430,6 +442,9 @@ function openEdit(row: any) {
|
|||||||
dimension: row.dimension,
|
dimension: row.dimension,
|
||||||
category: row.category,
|
category: row.category,
|
||||||
target_value: row.target_value,
|
target_value: row.target_value,
|
||||||
|
target_monthly: row.target_monthly,
|
||||||
|
target_quarterly: row.target_quarterly,
|
||||||
|
target_yearly: row.target_yearly,
|
||||||
unit: row.unit,
|
unit: row.unit,
|
||||||
frequency: row.frequency,
|
frequency: row.frequency,
|
||||||
data_source_type: row.data_source_type,
|
data_source_type: row.data_source_type,
|
||||||
@@ -505,6 +520,21 @@ function formatTarget(val: number, unit: string) {
|
|||||||
return val + (unit || '')
|
return val + (unit || '')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 多粒度目标:按frequency取对应周期目标值
|
||||||
|
function targetForRow(row: any) {
|
||||||
|
const freq = row.frequency || 'monthly'
|
||||||
|
if (freq === 'quarterly' || freq === 'half_year') return row.target_quarterly ?? row.target_value
|
||||||
|
if (freq === 'yearly') return row.target_yearly ?? row.target_value
|
||||||
|
return row.target_monthly ?? row.target_value
|
||||||
|
}
|
||||||
|
|
||||||
|
function targetLabel(row: any) {
|
||||||
|
const freq = row.frequency || 'monthly'
|
||||||
|
if (freq === 'quarterly' || freq === 'half_year') return '季'
|
||||||
|
if (freq === 'yearly') return '年'
|
||||||
|
return '月'
|
||||||
|
}
|
||||||
|
|
||||||
// ── 加载分类树 ──
|
// ── 加载分类树 ──
|
||||||
async function loadCategories() {
|
async function loadCategories() {
|
||||||
try {
|
try {
|
||||||
@@ -639,6 +669,13 @@ async function saveKPI() {
|
|||||||
if (saving.value) return
|
if (saving.value) return
|
||||||
saving.value = true
|
saving.value = true
|
||||||
try {
|
try {
|
||||||
|
// 多粒度目标同步:按frequency将对应周期目标写入target_value(兼容评分/阈值等旧逻辑)
|
||||||
|
const freq = form.value.frequency || 'monthly'
|
||||||
|
let tv: any = null
|
||||||
|
if (freq === 'quarterly' || freq === 'half_year') tv = form.value.target_quarterly
|
||||||
|
else if (freq === 'yearly') tv = form.value.target_yearly
|
||||||
|
else tv = form.value.target_monthly
|
||||||
|
if (tv != null) form.value.target_value = tv
|
||||||
if (editMode.value) {
|
if (editMode.value) {
|
||||||
await kpiApi.update(form.value.id, form.value)
|
await kpiApi.update(form.value.id, form.value)
|
||||||
ElMessage.success('保存成功')
|
ElMessage.success('保存成功')
|
||||||
|
|||||||
@@ -70,7 +70,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="kpi-value-row">
|
<div class="kpi-value-row">
|
||||||
<div class="kpi-actual">{{ fmtValue(k.actual_value, k.kpi_code) }}</div>
|
<div class="kpi-actual">{{ fmtValue(k.actual_value, k.kpi_code) }}</div>
|
||||||
<div class="kpi-target">目标 {{ k.target_value ?? '-' }}</div>
|
<div class="kpi-target">{{ targetLabel(k) }} {{ k.target_value ?? '-' }}</div>
|
||||||
</div>
|
</div>
|
||||||
<el-progress
|
<el-progress
|
||||||
:percentage="k.target_value ? Math.min(100, Math.round((k.actual_value || 0) / k.target_value * 100)) : 0"
|
:percentage="k.target_value ? Math.min(100, Math.round((k.actual_value || 0) / k.target_value * 100)) : 0"
|
||||||
@@ -140,6 +140,13 @@ function fmtValue(val: any, code: string) {
|
|||||||
return val
|
return val
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function targetLabel(k: any) {
|
||||||
|
const freq = k.frequency || 'monthly'
|
||||||
|
if (freq === 'quarterly' || freq === 'half_year') return '目标(季)'
|
||||||
|
if (freq === 'yearly') return '目标(年)'
|
||||||
|
return '目标(月)'
|
||||||
|
}
|
||||||
|
|
||||||
function planIcon(p: any) {
|
function planIcon(p: any) {
|
||||||
if (p.status === 'completed') return '✅'
|
if (p.status === 'completed') return '✅'
|
||||||
if (p.overdue) return '🔴'
|
if (p.overdue) return '🔴'
|
||||||
|
|||||||
Reference in New Issue
Block a user