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:
@@ -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; }
|
||||
|
||||
Reference in New Issue
Block a user