fix: 战略地图P0修复 — KPI code统一+放开同层连线+数据迁移
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user