fix: 战略地图P0修复 — KPI code统一+放开同层连线+数据迁移

This commit is contained in:
Hermes CI Fix
2026-07-14 16:02:33 +08:00
parent c7d436ec7a
commit 6218a7d2db
8775 changed files with 4125 additions and 1758555 deletions
+308 -18
View File
@@ -1,17 +1,52 @@
<template>
<div>
<h3>预警中心</h3>
<div style="margin:12px 0;display:flex;gap:12px;">
<div class="page-header">
<h3>预警中心</h3>
<div class="header-actions">
<el-button size="small" @click="batchCheckAlerts">🔄 执行预警检查</el-button>
<el-button size="small" @click="generateRules" :loading="generating">⚙️ 生成默认规则</el-button>
<el-button size="small" @click="calcDynamic" :loading="calcLoading">📊 计算动态阈值</el-button>
</div>
</div>
<!-- 顶部选项卡 -->
<div style="margin:12px 0;display:flex;gap:12px;align-items:center;">
<el-radio-group v-model="activeTab" size="small">
<el-radio-button value="alerts">待处理预警</el-radio-button>
<el-radio-button value="alerts">预警列表</el-radio-button>
<el-radio-button value="rules">预警规则配置</el-radio-button>
<el-radio-button value="plans">改善行动</el-radio-button>
</el-radio-group>
<span v-if="totalRules > 0" style="color:#909399;font-size:12px;">共 {{ totalRules }} 条规则</span>
</div>
<!-- ===== 预警列表 ===== -->
<template v-if="activeTab === 'alerts'">
<el-table :data="alerts" v-loading="loading" style="width:100%" border stripe>
<el-table-column prop="alert_message" label="预警信息" min-width="300" />
<!-- 预警类型/状态过滤 -->
<div style="margin-bottom:12px;display:flex;gap:8px;flex-wrap:wrap;">
<el-select v-model="alertTypeFilter" placeholder="预警类型" clearable size="small" style="width:120px" @change="loadAlerts">
<el-option label="静态阈值" value="static" />
<el-option label="动态趋势" value="trend" />
</el-select>
<el-select v-model="alertStatusFilter" placeholder="状态" clearable size="small" style="width:100px" @change="loadAlerts">
<el-option label="待处理" value="pending" />
<el-option label="处理中" value="processing" />
<el-option label="已处理" value="resolved" />
</el-select>
<el-tag v-if="alertTypeFilter" closable @close="alertTypeFilter='';loadAlerts()">
{{ alertTypeFilter === 'static' ? '静态阈值' : '动态趋势' }}
</el-tag>
</div>
<el-table :data="alerts" v-loading="loading" style="width:100%" border stripe size="small">
<el-table-column prop="alert_message" label="预警信息" min-width="320">
<template #default="{ row }">
<div style="display:flex;align-items:center;gap:4px;">
<el-tag v-if="isTrendAlert(row.alert_message)" size="small" type="info" effect="plain">趋势</el-tag>
<el-tag v-else size="small" type="primary" effect="plain">静态</el-tag>
<span>{{ row.alert_message }}</span>
</div>
</template>
</el-table-column>
<el-table-column prop="alert_level" label="级别" width="70">
<template #default="{ row }">
<el-tag :type="row.alert_level==='red'?'danger':'warning'" size="small">
@@ -22,6 +57,9 @@
<el-table-column prop="status" label="状态" width="90">
<template #default="{ row }">{{ {pending:'待处理',processing:'处理中',resolved:'已处理'}[row.status] || row.status }}</template>
</el-table-column>
<el-table-column prop="created_at" label="时间" width="150">
<template #default="{ row }">{{ formatTime(row.created_at) }}</template>
</el-table-column>
<el-table-column label="操作" width="200" fixed="right">
<template #default="{ row }">
<el-button v-if="row.status==='pending'" size="small" type="primary" @click="openResolveDialog(row)">处理</el-button>
@@ -31,12 +69,62 @@
</el-table>
</template>
<!-- ===== 预警规则配置 ===== -->
<template v-if="activeTab === 'rules'">
<div style="margin-bottom:12px;display:flex;gap:8px;">
<el-input v-model="ruleSearch" placeholder="搜索KPI名称/编码" size="small" style="width:200px" clearable />
<el-select v-model="ruleTypeFilter" placeholder="规则类型" clearable size="small" style="width:130px">
<el-option label="静态阈值" value="static" />
<el-option label="动态阈值" value="dynamic" />
<el-option label="上升趋势" value="trend_up" />
<el-option label="下降趋势" value="trend_down" />
</el-select>
<el-select v-model="ruleEnabledFilter" placeholder="状态" clearable size="small" style="width:100px">
<el-option label="已启用" :value="1" />
<el-option label="已禁用" :value="0" />
</el-select>
<el-button type="primary" size="small" @click="loadRules">查询</el-button>
</div>
<el-table :data="rules" v-loading="rulesLoading" style="width:100%" border stripe size="small">
<el-table-column label="KPI" min-width="180">
<template #default="{ row }">
<strong>{{ row.kpi_name || '?' }}</strong>
<span style="color:#909399;margin-left:4px;font-size:12px;">{{ row.kpi_code }}</span>
</template>
</el-table-column>
<el-table-column prop="rule_type" label="规则类型" width="100">
<template #default="{ row }">
<el-tag :type="ruleTypeTag(row.rule_type)" size="small">
{{ ruleTypeLabel(row.rule_type) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="参数" min-width="200">
<template #default="{ row }">
<span style="font-size:12px;color:#606266;">{{ formatRuleParams(row.params, row.rule_type) }}</span>
</template>
</el-table-column>
<el-table-column label="启用" width="70">
<template #default="{ row }">
<el-switch v-model="row.enabled" :active-value="1" :inactive-value="0" size="small" @change="(v) => toggleRule(row.id, v)" />
</template>
</el-table-column>
<el-table-column label="操作" width="120" fixed="right">
<template #default="{ row }">
<el-button size="small" text @click="editRule(row)">编辑</el-button>
<el-button size="small" text type="danger" @click="deleteRule(row.id)">删除</el-button>
</template>
</el-table-column>
</el-table>
</template>
<!-- ===== 改善行动计划 ===== -->
<template v-if="activeTab === 'plans'">
<div style="margin-bottom:12px;">
<el-button type="primary" size="small" @click="showPlanForm = true; planForm = { title: '', kpi_id: null, assignee: '', priority: 'medium', description: '' }">新建改善计划</el-button>
</div>
<el-table :data="plans" v-loading="loadingPlans" style="width:100%" border stripe>
<el-table :data="plans" v-loading="loadingPlans" style="width:100%" border stripe size="small">
<el-table-column prop="title" label="计划名称" min-width="200" />
<el-table-column prop="kpi_name" label="关联KPI" width="140" />
<el-table-column prop="assignee" label="负责人" width="100" />
@@ -71,6 +159,11 @@
<!-- 处理预警弹窗 -->
<el-dialog v-model="showDialog" title="处理预警" width="500px">
<el-form :model="resolveForm" label-width="100px">
<el-form-item label="预警类型">
<el-tag :type="isTrendAlert(currentAlert?.alert_message) ? 'info' : 'primary'" size="small">
{{ isTrendAlert(currentAlert?.alert_message) ? '动态趋势' : '静态阈值' }}
</el-tag>
</el-form-item>
<el-form-item label="处理人">
<el-select v-model="resolveForm.assignee" filterable style="width:100%;">
<el-option v-for="u in users" :key="u.id" :label="u.name" :value="u.name" />
@@ -97,6 +190,44 @@
</template>
</el-dialog>
<!-- 编辑预警规则弹窗 -->
<el-dialog v-model="showRuleForm" title="编辑预警规则" width="500px">
<el-form :model="ruleForm" label-width="110px" size="small">
<el-form-item label="规则类型">
<el-select v-model="ruleForm.rule_type" style="width:100%">
<el-option label="静态阈值" value="static" />
<el-option label="动态阈值" value="dynamic" />
<el-option label="上升趋势预警" value="trend_up" />
<el-option label="下降趋势预警" value="trend_down" />
</el-select>
</el-form-item>
<el-form-item label="是否启用">
<el-switch v-model="ruleForm.enabled" :active-value="1" :inactive-value="0" />
</el-form-item>
<template v-if="ruleForm.rule_type === 'static'">
<el-form-item label="绿灯阈值"><el-input v-model="ruleForm.params.green" placeholder="如 >=90" /></el-form-item>
<el-form-item label="黄灯阈值"><el-input v-model="ruleForm.params.yellow" placeholder="如 >=80" /></el-form-item>
<el-form-item label="红灯阈值"><el-input v-model="ruleForm.params.red" placeholder="如 <80" /></el-form-item>
</template>
<template v-if="ruleForm.rule_type === 'dynamic'">
<el-form-item label="敏感度">
<el-input-number v-model="ruleForm.params.sensitivity" :min="0.5" :max="3" :step="0.1" />
<span style="color:#909399;font-size:12px;margin-left:8px;">值越大越不敏感</span>
</el-form-item>
</template>
<template v-if="ruleForm.rule_type === 'trend_up' || ruleForm.rule_type === 'trend_down'">
<el-form-item label="变化阈值(%)">
<el-input-number v-model="ruleForm.params.threshold_pct" :min="1" :max="100" />
<span style="color:#909399;font-size:12px;margin-left:8px;">超过此百分比触发预警</span>
</el-form-item>
</template>
</el-form>
<template #footer>
<el-button @click="showRuleForm=false">取消</el-button>
<el-button type="primary" @click="saveRule" :loading="savingRule">保存</el-button>
</template>
</el-dialog>
<!-- 新建/编辑改善计划弹窗 -->
<el-dialog v-model="showPlanForm" :title="editingPlan ? '编辑改善计划' : '新建改善计划'" width="550px">
<el-form :model="planForm" label-width="100px">
@@ -137,24 +268,45 @@
import { ref, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { alertApi, userApi, actionPlanApi, kpiApi } from '../api/index'
import { alertApi, userApi, actionPlanApi, kpiApi, alertRulesApi } from '../api/index'
const route = useRoute()
const activeTab = ref((route.meta?.tab as string) || 'alerts')
// 预警
// ── 预警 ──
const alerts = ref<any[]>([])
const loading = ref(false)
const showDialog = ref(false)
const currentAlert = ref<any>(null)
const resolving = ref(false)
const alertTypeFilter = ref('')
const alertStatusFilter = ref('')
const resolveForm = ref({
assignee: '', resolution: '',
createPlan: false, planTitle: '', planDueDate: null,
})
const users = ref<any[]>([])
// 改善计划
// ── 预警规则 ──
const rules = ref<any[]>([])
const rulesLoading = ref(false)
const totalRules = ref(0)
const ruleSearch = ref('')
const ruleTypeFilter = ref('')
const ruleEnabledFilter = ref<number | null>(null)
const showRuleForm = ref(false)
const savingRule = ref(false)
const editingRuleId = ref<number | null>(null)
const generating = ref(false)
const calcLoading = ref(false)
const ruleForm = ref({
kpi_id: null as number | null,
rule_type: 'static',
enabled: 1,
params: { green: '', yellow: '', red: '', sensitivity: 1.0, threshold_pct: 10 },
})
// ── 改善计划 ──
const plans = ref<any[]>([])
const loadingPlans = ref(false)
const showPlanForm = ref(false)
@@ -166,6 +318,33 @@ const planForm = ref<any>({
description: '', due_date: null,
})
// ── 工具函数 ──
function isTrendAlert(msg: string): boolean {
return msg?.startsWith('[趋势') || msg?.startsWith('[动态')
}
function formatTime(t: string): string {
if (!t) return '-'
try { return new Date(t).toLocaleString('zh-CN', { hour12: false }) } catch { return t }
}
function ruleTypeLabel(type: string): string {
return { static: '静态阈值', dynamic: '动态阈值', trend_up: '上升趋势', trend_down: '下降趋势' }[type] || type
}
function ruleTypeTag(type: string): string {
return { static: 'primary', dynamic: 'success', trend_up: 'warning', trend_down: 'danger' }[type] || 'info'
}
function formatRuleParams(params: any, type: string): string {
if (!params) return '-'
if (typeof params === 'string') { try { params = JSON.parse(params) } catch { return params } }
if (type === 'static') return `绿${params.green||'-'} / 黄${params.yellow||'-'} / 红${params.red||'-'}`
if (type === 'dynamic') return `敏感度=${params.sensitivity||1.0}`
if (type === 'trend_up' || type === 'trend_down') return `阈值>${params.threshold_pct||10}%`
return JSON.stringify(params)
}
// ── 预警操作 ──
function openResolveDialog(row: any) {
currentAlert.value = row
resolveForm.value = {
@@ -177,7 +356,6 @@ function openResolveDialog(row: any) {
}
function openPlanDialog(row: any) {
// 从预警创建改善计划
const kpiId = row.kpi_id || null
showPlanForm.value = true
editingPlan.value = null
@@ -199,8 +377,6 @@ async function doResolve() {
assignee: resolveForm.value.assignee,
resolution: resolveForm.value.resolution,
})
// 如果勾选了创建改善计划
if (resolveForm.value.createPlan && resolveForm.value.planTitle) {
await actionPlanApi.create({
title: resolveForm.value.planTitle,
@@ -212,14 +388,87 @@ async function doResolve() {
due_date: resolveForm.value.planDueDate || null,
})
}
ElMessage.success('已处理')
showDialog.value = false
load()
loadAlerts()
} catch (e) { ElMessage.error('操作失败') }
resolving.value = false
}
// ── 预警规则操作 ──
function editRule(row: any) {
editingRuleId.value = row.id
let params = row.params
if (typeof params === 'string') { try { params = JSON.parse(params) } catch { params = {} } }
ruleForm.value = {
kpi_id: row.kpi_id,
rule_type: row.rule_type,
enabled: row.enabled,
params: { green: '', yellow: '', red: '', sensitivity: 1.0, threshold_pct: 10, ...(params || {}) },
}
showRuleForm.value = true
}
async function saveRule() {
savingRule.value = true
try {
const data = {
rule_type: ruleForm.value.rule_type,
enabled: ruleForm.value.enabled,
params: ruleForm.value.params,
}
if (editingRuleId.value) {
await alertRulesApi.update(editingRuleId.value, data)
}
ElMessage.success('已保存')
showRuleForm.value = false
loadRules()
} catch (e) { ElMessage.error('保存失败') }
savingRule.value = false
}
async function toggleRule(id: number, enabled: number) {
try { await alertRulesApi.update(id, { enabled }) } catch { ElMessage.error('操作失败') }
}
async function deleteRule(id: number) {
try {
await ElMessageBox.confirm('确定删除此预警规则?')
await alertRulesApi.delete(id)
ElMessage.success('已删除')
loadRules()
} catch (e) { if (e !== 'cancel') ElMessage.error('删除失败') }
}
async function generateRules() {
generating.value = true
try {
const r: any = await alertRulesApi.generateDefaults()
ElMessage.success(r.message || '规则已生成')
loadRules()
} catch (e) { ElMessage.error('生成失败') }
generating.value = false
}
async function calcDynamic() {
calcLoading.value = true
try {
const r: any = await alertRulesApi.calculateDynamic()
ElMessage.success(r.message || '动态阈值已计算')
} catch (e) { ElMessage.error('计算失败') }
calcLoading.value = false
}
async function batchCheckAlerts() {
ElMessage.info('正在执行预警检查...')
try {
const r: any = await alertRulesApi.checkAll()
ElMessage.success(r.message || '检查完成')
loadAlerts()
} catch (e) { ElMessage.error('检查失败') }
}
// ── 改善计划操作 ──
async function savePlan() {
if (!planForm.value.title || !planForm.value.kpi_id) {
ElMessage.warning('请填写计划名称并选择关联KPI')
@@ -229,7 +478,6 @@ async function savePlan() {
try {
const data = { ...planForm.value }
if (data.due_date) data.due_date = new Date(data.due_date).toISOString()
if (editingPlan.value) {
await actionPlanApi.update(editingPlan.value.id, data)
ElMessage.success('已更新')
@@ -267,13 +515,48 @@ async function deletePlan(id: number) {
}
}
async function load() {
// ── 加载数据 ──
async function loadAlerts() {
loading.value = true
try { const r: any = await alertApi.list(); alerts.value = r.data || [] } catch (e) {}
try {
const params: any = {}
if (alertStatusFilter.value) params.status = alertStatusFilter.value
const r: any = await alertApi.list(params)
let items = r.data || []
// 客户端过滤预警类型
if (alertTypeFilter.value === 'static') {
items = items.filter((a: any) => !isTrendAlert(a.alert_message))
} else if (alertTypeFilter.value === 'trend') {
items = items.filter((a: any) => isTrendAlert(a.alert_message))
}
alerts.value = items
} catch (e) {}
try { const u: any = await userApi.list(); users.value = u.data || [] } catch (e) {}
loading.value = false
}
async function loadRules() {
rulesLoading.value = true
try {
const params: any = {}
if (ruleTypeFilter.value) params.rule_type = ruleTypeFilter.value
if (ruleEnabledFilter.value !== null) params.enabled = ruleEnabledFilter.value
const r: any = await alertRulesApi.list(params)
let items = r.data || []
// 客户端搜索
if (ruleSearch.value) {
const kw = ruleSearch.value.toLowerCase()
items = items.filter((i: any) =>
(i.kpi_name || '').toLowerCase().includes(kw) ||
(i.kpi_code || '').toLowerCase().includes(kw)
)
}
rules.value = items
totalRules.value = r.total || items.length
} catch (e) { console.error(e) }
rulesLoading.value = false
}
async function loadPlans() {
loadingPlans.value = true
try { const r: any = await actionPlanApi.list(); plans.value = r.data || [] } catch (e) {}
@@ -285,8 +568,15 @@ async function loadKpis() {
}
onMounted(() => {
load()
loadAlerts()
loadRules()
loadPlans()
loadKpis()
})
</script>
<style scoped>
.page-header { display:flex; justify-content:space-between; align-items:center; margin-bottom:12px; }
.page-header h3 { margin:0; }
.header-actions { display:flex; gap:8px; }
</style>