fix: 恢复5个被CI覆盖的前端文件+成本数据+预警定时器

- 从.bak恢复: BudgetManagement(72%代码丢失)/KPIList/DeviationDashboard/MapReview/NodeEditDialog
- 注册4个缺失后端API模块到main.py
- 新增3个前端路由(管理报表/战略执行看板/杜邦分析)
- 修复改善行动路由指向ActionPlanLibrary(原指向AlertList)
- deploy.sh增加git pull步骤
- 运行成本种子数据: 29标准成本+31实际成本+10ABC+25分配
- 补充非财务KPI预算: 72条(客户/流程/学习维度)
- 配置预警cron: 每30分钟自动检查
This commit is contained in:
Hermes CI Fix
2026-07-13 09:52:06 +08:00
parent b26046349c
commit 957dacd248
9 changed files with 994 additions and 507 deletions
+179 -6
View File
@@ -3,7 +3,10 @@
<!-- 页头 -->
<div class="page-header">
<h3>KPI字典</h3>
<el-button type="primary" @click="showForm=true; form={}; editMode=false">+ 新建KPI</el-button>
<div class="header-actions">
<el-button @click="showTemplateSelect = true">📋 从模板创建</el-button>
<el-button type="primary" @click="showForm=true; form={}; editMode=false">+ 新建KPI</el-button>
</div>
</div>
<div class="main-layout">
@@ -99,6 +102,12 @@
<span v-else style="color:#ccc">-</span>
</template>
</el-table-column>
<el-table-column label="来源" width="70">
<template #default="{ row }">
<el-tag v-if="row.is_system" size="small" type="info">系统</el-tag>
<el-tag v-else size="small" type="success">自定义</el-tag>
</template>
</el-table-column>
<el-table-column label="预警" width="70">
<template #default="{ row }">
<el-tag v-if="row.alert_level && row.alert_level !== 'none'" :type="row.alert_level === 'red' ? 'danger' : row.alert_level === 'yellow' ? 'warning' : 'success'" size="small">
@@ -198,13 +207,22 @@
<el-form-item label="计算公式"><el-input v-model="form.formula" type="textarea" :rows="2" /></el-form-item>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="绿灯阈值"><el-input v-model="form.threshold_green" /></el-form-item>
<el-form-item label="绿灯阈值">
<el-input v-model="form.threshold_green" placeholder=" >=80" />
<div class="threshold-hint">超过此值视为正常</div>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="黄灯阈值"><el-input v-model="form.threshold_yellow" /></el-form-item>
<el-form-item label="黄灯阈值">
<el-input v-model="form.threshold_yellow" placeholder=" >=60" />
<div class="threshold-hint">超过此值预警</div>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="红灯阈值"><el-input v-model="form.threshold_red" /></el-form-item>
<el-form-item label="红灯阈值">
<el-input v-model="form.threshold_red" placeholder=" <60" />
<div class="threshold-hint">触发此值紧急</div>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="所属Epic">
@@ -218,13 +236,100 @@
<el-button type="primary" @click="saveKPI">{{ editMode ? '保存' : '创建' }}</el-button>
</template>
</MyDialog>
<!-- 从模板创建弹窗 -->
<el-dialog v-model="showTemplateSelect" title="从模板创建KPI" width="760" :close-on-click-modal="false">
<div v-loading="templateLoading">
<!-- 筛选栏 -->
<div class="search-bar" style="margin-bottom:12px;">
<el-input v-model="templateKeyword" placeholder="搜索模板名称/编码" clearable style="width:200px" />
<el-select v-model="templateDimFilter" placeholder="维度" clearable style="width:110px">
<el-option label="财务" value="finance" />
<el-option label="客户" value="customer" />
<el-option label="内部流程" value="process" />
<el-option label="学习成长" value="learning" />
</el-select>
<el-button @click="loadTemplates">查询</el-button>
</div>
<!-- 模板列表 -->
<el-table :data="templates" style="width:100%" border stripe size="small"
:highlight-current-row="true"
@current-change="onTemplateSelect"
>
<el-table-column type="index" width="40" />
<el-table-column prop="kpi_code" label="编码" width="120">
<template #default="{ row }"><span class="kpi-code">{{ row.kpi_code }}</span></template>
</el-table-column>
<el-table-column prop="kpi_name" label="名称" min-width="140" />
<el-table-column prop="dimension" label="维度" width="70">
<template #default="{ row }"><el-tag :type="dimTagType(row.dimension)" size="small">{{ dimLabel(row.dimension) }}</el-tag></template>
</el-table-column>
<el-table-column label="类别" width="80">
<template #default="{ row }">{{ catLabel(row.category) }}</template>
</el-table-column>
<el-table-column prop="unit" label="单位" width="60" />
<el-table-column prop="target_value" label="建议目标" width="80">
<template #default="{ row }">{{ row.target_value ?? '-' }}</template>
</el-table-column>
<el-table-column prop="description" label="说明" min-width="120" show-overflow-tooltip />
<el-table-column label="来源" width="60">
<template #default="{ row }">
<el-tag v-if="row.is_system" size="small" type="info">系统</el-tag>
<el-tag v-else size="small" type="success">自定义</el-tag>
</template>
</el-table-column>
</el-table>
<div v-if="!selectedTemplate" style="color:#999;font-size:13px;margin-top:8px;">请选择一个模板然后填写实例化信息</div>
<!-- 实例化表单 -->
<div v-if="selectedTemplate" class="instantiate-form" style="margin-top:12px;border-top:1px solid #eee;padding-top:12px;">
<div style="font-size:14px;font-weight:500;margin-bottom:8px;">{{ selectedTemplate.kpi_name }}创建KPI实例</div>
<el-form :model="instantiateForm" label-width="90px" size="small">
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="KPI编码"><el-input v-model="instantiateForm.kpi_code" :placeholder="selectedTemplate.kpi_code" /></el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="KPI名称"><el-input v-model="instantiateForm.kpi_name" :placeholder="selectedTemplate.kpi_name" /></el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="目标值"><el-input-number v-model="instantiateForm.target_value" :min="0" style="width:100%" :placeholder="String(selectedTemplate.target_value ?? '')" /></el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="数据源">
<el-select v-model="instantiateForm.data_source_type" style="width:100%">
<el-option label="ERP自动" value="erp" />
<el-option label="Excel导入" value="excel" />
<el-option label="手工填报" value="manual" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="负责部门"><el-input v-model="instantiateForm.responsible_dept" /></el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="负责人"><el-input v-model="instantiateForm.responsible_user" /></el-form-item>
</el-col>
</el-row>
</el-form>
<div style="color:#999;font-size:12px;margin-top:4px;">* 编码和名称不填则使用模板默认值后续可在KPI字典中修改</div>
</div>
</div>
<template #footer>
<el-button @click="showTemplateSelect=false">取消</el-button>
<el-button type="primary" :disabled="!selectedTemplate" @click="doInstantiate">从模板创建</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, computed } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { kpiApi, dashboardApi } from '../api/index'
import { kpiApi, templateApi, dashboardApi } from '../api/index'
import MyDialog from '../components/MyDialog.vue'
// ── 数据 ──
@@ -448,10 +553,68 @@ async function saveKPI() {
function doDelete(id: number) {
ElMessageBox.confirm('确定删除此KPI', '提示').then(async () => {
try { await kpiApi.delete(id); ElMessage.success('已删除'); loadKpis(); loadCategories() }
catch (e) { ElMessage.error('删除失败') }
catch (e: any) { ElMessage.error(e?.response?.data?.detail || '删除失败') }
}).catch(() => {})
}
// ── 从模板创建 ──
const showTemplateSelect = ref(false)
const templateLoading = ref(false)
const templates = ref<any[]>([])
const templateKeyword = ref('')
const templateDimFilter = ref('')
const selectedTemplate = ref<any>(null)
const instantiateForm = ref<any>({})
async function loadTemplates() {
templateLoading.value = true
try {
const params: any = {}
if (templateKeyword.value) params.keyword = templateKeyword.value
if (templateDimFilter.value) params.dimension = templateDimFilter.value
const r: any = await templateApi.list(params)
templates.value = r.data || []
} catch (e) {
console.error('加载模板失败', e)
}
templateLoading.value = false
}
function onTemplateSelect(tpl: any) {
selectedTemplate.value = tpl
instantiateForm.value = {
kpi_code: '',
kpi_name: '',
target_value: undefined,
data_source_type: 'manual',
responsible_dept: '',
responsible_user: '',
}
}
async function doInstantiate() {
if (!selectedTemplate.value) return
try {
const data: any = {}
if (instantiateForm.value.kpi_code) data.kpi_code = instantiateForm.value.kpi_code
if (instantiateForm.value.kpi_name) data.kpi_name = instantiateForm.value.kpi_name
if (instantiateForm.value.target_value !== undefined && instantiateForm.value.target_value !== null)
data.target_value = instantiateForm.value.target_value
if (instantiateForm.value.data_source_type) data.data_source_type = instantiateForm.value.data_source_type
if (instantiateForm.value.responsible_dept) data.responsible_dept = instantiateForm.value.responsible_dept
if (instantiateForm.value.responsible_user) data.responsible_user = instantiateForm.value.responsible_user
await templateApi.instantiate(selectedTemplate.value.id, data)
ElMessage.success(`已从模板「${selectedTemplate.value.kpi_name}」创建KPI`)
showTemplateSelect.value = false
selectedTemplate.value = null
instantiateForm.value = {}
loadKpis()
loadCategories()
} catch (e: any) {
ElMessage.error(e?.response?.data?.detail || '创建失败')
}
}
onMounted(() => {
loadCategories()
loadKpis()
@@ -475,6 +638,10 @@ onMounted(() => {
font-size: 18px;
font-weight: 600;
}
.header-actions {
display: flex;
gap: 8px;
}
.main-layout {
display: flex;
gap: 12px;
@@ -545,6 +712,12 @@ onMounted(() => {
font-size: 12px;
color: #606266;
}
.threshold-hint {
font-size: 11px;
color: #909399;
line-height: 1.4;
margin-top: 2px;
}
.status-red { color: #f56c6c; font-weight: 500; }
.status-yellow { color: #e6a23c; font-weight: 500; }
.status-green { color: #67c23a; font-weight: 500; }