Files
cma-management/frontend/src/views/AlertList.vue
T
Hermes CI Fix 189f442da4 feat: 所有表单的部门/负责人改为下拉框(自动读取组织+用户数据)
- KPI详情: 负责部门+负责人 el-input→el-select filterable
- KPI字典: 新建/编辑/模板实例化 三处同步修改
- 改善行动: 负责人筛选+表单 从userApi.list加载
- 预警中心: 指派人+负责人 从userApi.list加载
- api/index.ts新增orgApi封装
2026-07-29 18:25:08 +08:00

723 lines
31 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div>
<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="riskMatrix">风险矩阵</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'">
<!-- 预警类型/状态过滤 -->
<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="300">
<template #default="{ row }">
<div style="display:flex;align-items:center;gap:4px;">
<el-tag v-if="row.alert_type === 'forecast'" size="small" type="warning" 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">
{{ row.alert_level==='red'?'🔴紧急':'🟡警告' }}
</el-tag>
</template>
</el-table-column>
<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>
<el-button v-if="row.status==='pending'" size="small" @click="openPlanDialog(row)">创建改善计划</el-button>
</template>
</el-table-column>
</el-table>
</template>
<!-- ===== 风险矩阵热力图 ===== -->
<template v-if="activeTab === 'riskMatrix'">
<div style="margin-bottom:12px;display:flex;gap:8px;align-items:center;">
<span style="font-weight:500;">风险矩阵 CMA P2 ERM框架</span>
<el-select v-model="riskEntity" size="small" style="width:160px;" @change="loadRiskMatrix">
<el-option label="陕西酣客(白酒经销)" value="hanke" />
<el-option label="陕西博海科技(IT服务)" value="bohai" />
</el-select>
<el-button size="small" @click="loadRiskMatrix" :loading="riskLoading">刷新</el-button>
</div>
<div v-if="riskData" style="display:flex;gap:20px;flex-wrap:wrap;">
<!-- 四象限矩阵图 -->
<div style="flex:1;min-width:500px;">
<div style="font-size:12px;color:#909399;margin-bottom:4px;text-align:right;">概率 </div>
<div style="display:grid;grid-template-columns:50px 1fr 1fr 1fr;gap:4px;">
<div style="font-size:11px;color:#909399;display:flex;align-items:center;justify-content:center;">影响 </div>
<!-- Probability labels -->
<div style="font-size:11px;color:#909399;text-align:center;"></div>
<div style="font-size:11px;color:#909399;text-align:center;"></div>
<div style="font-size:11px;color:#909399;text-align:center;"></div>
<!-- 高影响 row -->
<div style="font-size:11px;color:#909399;display:flex;align-items:center;justify-content:center;"></div>
<risk-cell v-for="q in highImpactQuads" :key="q.label" :quadrant="q" @click="selectRisk(q)" />
<!-- 中影响 row -->
<div style="font-size:11px;color:#909399;display:flex;align-items:center;justify-content:center;"></div>
<risk-cell v-for="q in medImpactQuads" :key="q.label" :quadrant="q" @click="selectRisk(q)" />
<!-- 低影响 row -->
<div style="font-size:11px;color:#909399;display:flex;align-items:center;justify-content:center;"></div>
<risk-cell v-for="q in lowImpactQuads" :key="q.label" :quadrant="q" @click="selectRisk(q)" />
</div>
</div>
<!-- 风险详情面板 -->
<div style="width:360px;" v-if="selectedRisk">
<div style="font-weight:600;margin-bottom:8px;font-size:15px;">
<span :style="{color: riskTypeColor(selectedRisk.type)}"></span>
{{ selectedRisk.name }}
</div>
<el-card shadow="never" style="margin-bottom:8px;">
<div style="margin-bottom:6px;"><span style="color:#909399;font-size:12px;">影响</span>{{ selectedRisk.impact_label }}{{ selectedRisk.impact_value }}</div>
<div style="margin-bottom:6px;"><span style="color:#909399;font-size:12px;">概率</span>{{ selectedRisk.probability_label }}{{ selectedRisk.probability_value }}</div>
<div style="margin-bottom:6px;"><span style="color:#909399;font-size:12px;">详情</span>{{ selectedRisk.detail }}</div>
<div style="margin-bottom:6px;"><span style="color:#909399;font-size:12px;">应对措施</span>
<ul style="margin:4px 0 0 16px;padding:0;">
<li v-for="m in selectedRisk.measures" :key="m" style="font-size:13px;">{{ m }}</li>
</ul>
</div>
<div style="margin-bottom:6px;"><span style="color:#909399;font-size:12px;">责任人</span>{{ selectedRisk.responsible }}</div>
<div><span style="color:#909399;font-size:12px;">截止日</span>{{ selectedRisk.deadline }}</div>
</el-card>
</div>
<div v-else style="width:360px;display:flex;align-items:center;justify-content:center;color:#ccc;font-size:14px;">
点击风险卡片查看详情
</div>
</div>
<div v-else-if="!riskLoading" style="padding:40px 0;text-align:center;color:#999;">暂无风险数据</div>
</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>
<el-button size="small" @click="checkForecastAlerts" :loading="fcLoading">🔮 检查预测值</el-button>
<el-button size="small" @click="generateAlertSuggestions" :loading="sgLoading">💡 生成情景建议</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 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" />
<el-table-column prop="priority" label="优先级" width="80">
<template #default="{ row }">
<el-tag :type="row.priority==='high'?'danger':row.priority==='medium'?'warning':'info'" size="small">
{{ {high:'高',medium:'中',low:'低'}[row.priority] || row.priority }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<el-tag :type="row.status==='completed'?'success':row.status==='in_progress'?'warning':'info'" size="small">
{{ {pending:'待开始',in_progress:'进行中',completed:'已完成',cancelled:'已取消'}[row.status] || row.status }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="progress" label="进度" width="120">
<template #default="{ row }">
<el-progress :percentage="row.progress || 0" :status="row.progress >= 100 ? 'success' : ''" :stroke-width="12" />
</template>
</el-table-column>
<el-table-column label="操作" width="150" fixed="right">
<template #default="{ row }">
<el-button size="small" @click="editPlan(row)">编辑</el-button>
<el-button size="small" type="danger" @click="deletePlan(row.id)">删除</el-button>
</template>
</el-table-column>
</el-table>
</template>
<!-- 处理预警弹窗 -->
<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" />
</el-select>
</el-form-item>
<el-form-item label="处理结果">
<el-input v-model="resolveForm.resolution" type="textarea" :rows="3" placeholder="描述处理结果" />
</el-form-item>
<el-form-item label="创建改善计划">
<el-checkbox v-model="resolveForm.createPlan">同时创建改善行动计划</el-checkbox>
</el-form-item>
<template v-if="resolveForm.createPlan">
<el-form-item label="计划名称">
<el-input v-model="resolveForm.planTitle" placeholder="如:制定客户拓展方案" />
</el-form-item>
<el-form-item label="截止日期">
<el-date-picker v-model="resolveForm.planDueDate" type="date" placeholder="选择截止日期" style="width:100%;" />
</el-form-item>
</template>
</el-form>
<template #footer>
<el-button @click="showDialog=false">取消</el-button>
<el-button type="primary" @click="doResolve" :loading="resolving">确认处理</el-button>
</template>
</el-dialog>
<!-- 编辑预警规则弹窗 -->
<el-dialog v-model="showRuleForm" title="编辑预警规则" width="520px">
<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-select v-model="ruleForm.trigger_on" style="width:100%">
<el-option label="实际值超限" value="actual" />
<el-option label="预测值超限" value="forecast" />
<el-option label="两者都触发" value="both" />
</el-select>
<span style="color:#909399;font-size:12px;margin-top:2px;display:block;">预测值超限时系统会检查未来7天现金流预测是否触及阈值</span>
</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">
<el-form-item label="计划名称"><el-input v-model="planForm.title" /></el-form-item>
<el-form-item label="关联KPI">
<el-select v-model="planForm.kpi_id" filterable style="width:100%;">
<el-option v-for="k in kpiOptions" :key="k.id" :label="k.kpi_name" :value="k.id" />
</el-select>
</el-form-item>
<el-form-item label="负责人">
<el-select v-model="planForm.assignee" filterable style="width:100%;">
<el-option v-for="u in users" :key="u.id" :label="u.name" :value="u.name" />
</el-select>
</el-form-item>
<el-form-item label="优先级">
<el-select v-model="planForm.priority">
<el-option label="高" value="high" />
<el-option label="中" value="medium" />
<el-option label="低" value="low" />
</el-select>
</el-form-item>
<el-form-item label="描述">
<el-input v-model="planForm.description" type="textarea" :rows="3" />
</el-form-item>
<el-form-item label="截止日期">
<el-date-picker v-model="planForm.due_date" type="date" placeholder="选择截止日期" style="width:100%;" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="showPlanForm=false">取消</el-button>
<el-button type="primary" @click="savePlan" :loading="savingPlan">保存</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { alertApi, userApi, actionPlanApi, kpiApi, alertRulesApi } from '../api/index'
import RiskCell from '../components/charts/RiskCell.vue'
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',
trigger_on: 'actual',
enabled: 1,
params: { green: '', yellow: '', red: '', sensitivity: 1.0, threshold_pct: 10 },
})
// ── 改善计划 ──
const plans = ref<any[]>([])
const loadingPlans = ref(false)
const showPlanForm = ref(false)
const editingPlan = ref<any>(null)
const savingPlan = ref(false)
const kpiOptions = ref<any[]>([])
const planForm = ref<any>({
title: '', kpi_id: null, assignee: '', priority: 'medium',
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 = {
assignee: '', resolution: '',
createPlan: false, planTitle: `${row.alert_message?.slice(0, 20) || '改善'}方案`,
planDueDate: null,
}
showDialog.value = true
}
function openPlanDialog(row: any) {
const kpiId = row.kpi_id || null
showPlanForm.value = true
editingPlan.value = null
planForm.value = {
title: `${row.alert_message?.slice(0, 30) || '改善'}方案`,
kpi_id: kpiId,
assignee: '',
priority: 'high',
description: row.alert_message || '',
due_date: null,
}
}
async function doResolve() {
if (!resolveForm.value.assignee) { ElMessage.warning('请选择处理人'); return }
resolving.value = true
try {
await alertApi.resolve(currentAlert.value.id, {
assignee: resolveForm.value.assignee,
resolution: resolveForm.value.resolution,
})
if (resolveForm.value.createPlan && resolveForm.value.planTitle) {
await actionPlanApi.create({
title: resolveForm.value.planTitle,
kpi_id: currentAlert.value.kpi_id || 1,
alert_id: currentAlert.value.id,
description: resolveForm.value.resolution || '',
assignee: resolveForm.value.assignee,
priority: 'high',
due_date: resolveForm.value.planDueDate || null,
})
}
ElMessage.success('已处理')
showDialog.value = false
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,
trigger_on: row.trigger_on || 'actual',
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,
trigger_on: ruleForm.value.trigger_on,
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 checkForecastAlerts() {
fcLoading.value = true
try {
const r: any = await alertRulesApi.checkForecast()
ElMessage.success(r.message || '预测值检查完成')
loadAlerts()
} catch (e) { ElMessage.error('检查失败') }
fcLoading.value = false
}
async function generateAlertSuggestions() {
sgLoading.value = true
try {
const r: any = await alertRulesApi.generateSuggestions()
ElMessage.success(r.message || '情景建议已生成')
loadAlerts()
} catch (e) { ElMessage.error('生成失败') }
sgLoading.value = false
}
const fcLoading = ref(false)
const sgLoading = ref(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')
return
}
savingPlan.value = true
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('已更新')
} else {
await actionPlanApi.create(data)
ElMessage.success('已创建')
}
showPlanForm.value = false
loadPlans()
} catch (e) { ElMessage.error('保存失败') }
savingPlan.value = false
}
function editPlan(row: any) {
editingPlan.value = row
planForm.value = {
title: row.title,
kpi_id: row.kpi_id,
assignee: row.assignee,
priority: row.priority,
description: row.description,
due_date: row.due_date || null,
}
showPlanForm.value = true
}
async function deletePlan(id: number) {
try {
await ElMessageBox.confirm('确定删除此改善计划?')
await actionPlanApi.delete(id)
ElMessage.success('已删除')
loadPlans()
} catch (e) {
if (e !== 'cancel') ElMessage.error('删除失败')
}
}
// ── 加载数据 ──
async function loadAlerts() {
loading.value = true
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) {}
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) {}
loadingPlans.value = false
}
async function loadKpis() {
try { const r: any = await kpiApi.list(); kpiOptions.value = r.data || [] } catch (e) {}
}
async function loadUsers() {
try { const r: any = await userApi.list(); users.value = r.data || [] } catch (e) {}
}
// ── 风险矩阵 ──
const riskEntity = ref('hanke')
const riskData = ref<any>(null)
const riskLoading = ref(false)
const selectedRisk = ref<any>(null)
const highImpactQuads = computed(() => riskData.value?.quadrants?.filter((q: any) => q.impact === 'high') || [])
const medImpactQuads = computed(() => riskData.value?.quadrants?.filter((q: any) => q.impact === 'medium') || [])
const lowImpactQuads = computed(() => riskData.value?.quadrants?.filter((q: any) => q.impact === 'low') || [])
function riskTypeColor(type: string): string {
return { red: '#f56c6c', orange: '#e6a23c', yellow: '#409eff', green: '#67c23a' }[type] || '#999'
}
function selectRisk(q: any) {
// If quad has only one risk, select it directly; otherwise pick first
selectedRisk.value = q.risks?.[0] || null
}
async function loadRiskMatrix() {
riskLoading.value = true
selectedRisk.value = null
try {
const r: any = await alertApi.riskMatrix({ entity: riskEntity.value })
riskData.value = r
} catch { ElMessage.error('加载风险矩阵失败') }
riskLoading.value = false
}
onMounted(() => {
loadAlerts()
loadRules()
loadPlans()
loadKpis()
loadUsers()
})
</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; }
.risk-cell { min-height:80px; border-radius:6px; padding:6px; background:#fafafa; border:1px solid #e8e8e8; }
.risk-item { padding:4px 6px; margin-bottom:3px; border-radius:4px; font-size:12px; line-height:1.4; }
.risk-item:last-child { margin-bottom:0; }
.risk-dot { display:inline-block; width:8px; height:8px; border-radius:50%; margin-right:4px; }
.risk-name { font-weight:500; }
.risk-value { display:block; font-size:11px; color:#666; margin-top:1px; padding-left:12px; }
.risk-empty { color:#ddd; text-align:center; line-height:80px; font-size:13px; }
</style>