- KPI详情: 负责部门+负责人 el-input→el-select filterable - KPI字典: 新建/编辑/模板实例化 三处同步修改 - 改善行动: 负责人筛选+表单 从userApi.list加载 - 预警中心: 指派人+负责人 从userApi.list加载 - api/index.ts新增orgApi封装
723 lines
31 KiB
Vue
723 lines
31 KiB
Vue
<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>
|