|
|
@@ -1,6 +1,6 @@
|
|
|
<script setup lang="ts" name="aidopS8WatchRuleWizardDialog">
|
|
|
-// CONFIG-RULE-WIZARD-MVP-1:配置中心添加规则向导(4 步)。
|
|
|
-// 1) 报警机制 2) 业务维度 3) 异常类型 4) 基础配置 + 保存
|
|
|
+// CONFIG-RULE-WIZARD-BIZ-CONDITION-MVP-1:业务条件向导(5 步)
|
|
|
+// 1) 报警机制 2) 业务维度 3) 监控对象/指标 4) 判定标准 5) 基础配置 + 保存
|
|
|
// MANUAL_REPORT 不创建 watch_rule,跳转主动提报页。
|
|
|
import { computed, reactive, ref, watch } from 'vue';
|
|
|
import { useRouter } from 'vue-router';
|
|
|
@@ -56,12 +56,95 @@ const SEVERITY_OPTIONS = [
|
|
|
{ value: 'SERIOUS', label: '严重' },
|
|
|
];
|
|
|
|
|
|
+// CONFIG-RULE-WIZARD-BIZ-CONDITION-MVP-1:业务对象 + 监控指标常量字典(前端 MVP,不依赖后端字典)
|
|
|
+interface BizMetric {
|
|
|
+ metricCode: string;
|
|
|
+ metricLabel: string;
|
|
|
+ mechanism: 'DATE' | 'VALUE_RANGE' | 'RATIO';
|
|
|
+ dueAtField?: string;
|
|
|
+ statusField?: string;
|
|
|
+ measuredValueField?: string;
|
|
|
+ unit: string;
|
|
|
+}
|
|
|
+interface BizObject {
|
|
|
+ objectType: string;
|
|
|
+ objectLabel: string;
|
|
|
+ mechanisms: Array<'DATE' | 'VALUE_RANGE' | 'RATIO'>;
|
|
|
+ metrics: BizMetric[];
|
|
|
+}
|
|
|
+const BUSINESS_MONITOR_OPTIONS: BizObject[] = [
|
|
|
+ {
|
|
|
+ objectType: 'ORDER',
|
|
|
+ objectLabel: '订单交付',
|
|
|
+ mechanisms: ['DATE', 'RATIO'],
|
|
|
+ metrics: [
|
|
|
+ { metricCode: 'ORDER_DUE_AT', metricLabel: '计划交付时间', mechanism: 'DATE', dueAtField: 'due_at', statusField: 'status', unit: '分钟' },
|
|
|
+ { metricCode: 'ORDER_DELIVERY_RATE', metricLabel: '订单交付满足率', mechanism: 'RATIO', measuredValueField: 'measured_value', unit: '%' },
|
|
|
+ ],
|
|
|
+ },
|
|
|
+ {
|
|
|
+ objectType: 'ORDER',
|
|
|
+ objectLabel: '订单变更',
|
|
|
+ mechanisms: ['VALUE_RANGE'],
|
|
|
+ metrics: [
|
|
|
+ { metricCode: 'ORDER_CHANGE_COUNT', metricLabel: '订单变更次数', mechanism: 'VALUE_RANGE', measuredValueField: 'measured_value', unit: '次' },
|
|
|
+ ],
|
|
|
+ },
|
|
|
+ {
|
|
|
+ objectType: 'PURCHASE_ORDER',
|
|
|
+ objectLabel: '供应商交付',
|
|
|
+ mechanisms: ['DATE', 'RATIO'],
|
|
|
+ metrics: [
|
|
|
+ { metricCode: 'PO_DUE_AT', metricLabel: '计划到货时间', mechanism: 'DATE', dueAtField: 'due_at', statusField: 'status', unit: '分钟' },
|
|
|
+ { metricCode: 'PO_DELIVERY_RATE', metricLabel: '到货达成率', mechanism: 'RATIO', measuredValueField: 'measured_value', unit: '%' },
|
|
|
+ ],
|
|
|
+ },
|
|
|
+ {
|
|
|
+ objectType: 'IQC',
|
|
|
+ objectLabel: 'IQC 检验',
|
|
|
+ mechanisms: ['VALUE_RANGE', 'RATIO'],
|
|
|
+ metrics: [
|
|
|
+ { metricCode: 'IQC_VALUE', metricLabel: '检验值', mechanism: 'VALUE_RANGE', measuredValueField: 'measured_value', unit: '' },
|
|
|
+ { metricCode: 'IQC_PASS_RATE', metricLabel: '检验合格率', mechanism: 'RATIO', measuredValueField: 'measured_value', unit: '%' },
|
|
|
+ ],
|
|
|
+ },
|
|
|
+ {
|
|
|
+ objectType: 'WORK_ORDER',
|
|
|
+ objectLabel: '生产工单',
|
|
|
+ mechanisms: ['DATE', 'RATIO'],
|
|
|
+ metrics: [
|
|
|
+ { metricCode: 'WO_DUE_AT', metricLabel: '计划完工时间', mechanism: 'DATE', dueAtField: 'due_at', statusField: 'status', unit: '分钟' },
|
|
|
+ { metricCode: 'WO_COMPLETION_RATE', metricLabel: '工单完工率', mechanism: 'RATIO', measuredValueField: 'measured_value', unit: '%' },
|
|
|
+ ],
|
|
|
+ },
|
|
|
+ {
|
|
|
+ objectType: 'INVENTORY',
|
|
|
+ objectLabel: '库存',
|
|
|
+ mechanisms: ['VALUE_RANGE'],
|
|
|
+ metrics: [
|
|
|
+ { metricCode: 'INV_QTY', metricLabel: '当前库存量', mechanism: 'VALUE_RANGE', measuredValueField: 'measured_value', unit: '' },
|
|
|
+ ],
|
|
|
+ },
|
|
|
+];
|
|
|
+
|
|
|
const form = reactive({
|
|
|
mechanism: '' as '' | 'DATE' | 'VALUE_RANGE' | 'RATIO' | 'MANUAL_REPORT',
|
|
|
stageCode: '',
|
|
|
orderFlowCode: '',
|
|
|
+ objectIndex: -1, // BUSINESS_MONITOR_OPTIONS index
|
|
|
+ metricCode: '',
|
|
|
exceptionTypeCode: '',
|
|
|
severity: 'FOLLOW',
|
|
|
+ // 判定标准
|
|
|
+ graceMinutes: 0,
|
|
|
+ completedStates: 'CLOSED,COMPLETED,DONE',
|
|
|
+ lowerBound: null as number | null,
|
|
|
+ upperBound: null as number | null,
|
|
|
+ unit: '',
|
|
|
+ toleranceAbs: 0,
|
|
|
+ toleranceRatioPct: 0,
|
|
|
+ targetRatio: 95,
|
|
|
+ // 基础
|
|
|
ruleCode: '',
|
|
|
pollIntervalSeconds: 300,
|
|
|
triggerCountRequired: 1,
|
|
|
@@ -74,20 +157,61 @@ function resetForm() {
|
|
|
form.mechanism = '';
|
|
|
form.stageCode = '';
|
|
|
form.orderFlowCode = '';
|
|
|
+ form.objectIndex = -1;
|
|
|
+ form.metricCode = '';
|
|
|
form.exceptionTypeCode = '';
|
|
|
form.severity = 'FOLLOW';
|
|
|
+ form.graceMinutes = 0;
|
|
|
+ form.completedStates = 'CLOSED,COMPLETED,DONE';
|
|
|
+ form.lowerBound = null;
|
|
|
+ form.upperBound = null;
|
|
|
+ form.unit = '';
|
|
|
+ form.toleranceAbs = 0;
|
|
|
+ form.toleranceRatioPct = 0;
|
|
|
+ form.targetRatio = 95;
|
|
|
form.ruleCode = '';
|
|
|
form.pollIntervalSeconds = 300;
|
|
|
form.triggerCountRequired = 1;
|
|
|
form.recoverCountRequired = 1;
|
|
|
}
|
|
|
|
|
|
-watch(visible, (v) => {
|
|
|
- if (v) resetForm();
|
|
|
-});
|
|
|
+watch(visible, (v) => { if (v) resetForm(); });
|
|
|
|
|
|
const isManualReport = computed(() => form.mechanism === 'MANUAL_REPORT');
|
|
|
|
|
|
+// 当前机制下可选业务对象
|
|
|
+const filteredObjects = computed(() => {
|
|
|
+ if (!form.mechanism || isManualReport.value) return [];
|
|
|
+ return BUSINESS_MONITOR_OPTIONS.filter((o) => o.mechanisms.includes(form.mechanism as any));
|
|
|
+});
|
|
|
+
|
|
|
+const selectedObject = computed<BizObject | null>(() => {
|
|
|
+ if (form.objectIndex < 0) return null;
|
|
|
+ return filteredObjects.value[form.objectIndex] ?? null;
|
|
|
+});
|
|
|
+
|
|
|
+const filteredMetrics = computed<BizMetric[]>(() => {
|
|
|
+ if (!selectedObject.value) return [];
|
|
|
+ return selectedObject.value.metrics.filter((m) => m.mechanism === form.mechanism);
|
|
|
+});
|
|
|
+
|
|
|
+const selectedMetric = computed<BizMetric | null>(() => {
|
|
|
+ return filteredMetrics.value.find((m) => m.metricCode === form.metricCode) ?? null;
|
|
|
+});
|
|
|
+
|
|
|
+watch(selectedMetric, (m) => {
|
|
|
+ if (m) form.unit = m.unit;
|
|
|
+});
|
|
|
+
|
|
|
+// 当机制切换时,重置后续选择
|
|
|
+watch(() => form.mechanism, () => {
|
|
|
+ form.objectIndex = -1;
|
|
|
+ form.metricCode = '';
|
|
|
+});
|
|
|
+watch(() => form.objectIndex, () => {
|
|
|
+ form.metricCode = '';
|
|
|
+});
|
|
|
+
|
|
|
const filteredExceptionTypes = computed(() => {
|
|
|
const all = props.exceptionTypes.filter((t) => t.enabled);
|
|
|
if (!form.stageCode) return all;
|
|
|
@@ -95,24 +219,43 @@ const filteredExceptionTypes = computed(() => {
|
|
|
return matched.length > 0 ? matched : all;
|
|
|
});
|
|
|
|
|
|
-const selectedExceptionType = computed(() =>
|
|
|
+const exceptionTypeRow = computed(() =>
|
|
|
props.exceptionTypes.find((t) => t.typeCode === form.exceptionTypeCode) ?? null,
|
|
|
);
|
|
|
|
|
|
+// thresholdDisplay 推导
|
|
|
+const thresholdDisplay = computed(() => {
|
|
|
+ const u = form.unit || '';
|
|
|
+ if (form.mechanism === 'DATE') {
|
|
|
+ return form.graceMinutes > 0 ? `超期 ${form.graceMinutes} 分钟仍未完成触发` : '超期即触发';
|
|
|
+ }
|
|
|
+ if (form.mechanism === 'RATIO') {
|
|
|
+ return `低于 ${form.targetRatio}% 触发`;
|
|
|
+ }
|
|
|
+ if (form.mechanism === 'VALUE_RANGE') {
|
|
|
+ const segs: string[] = [];
|
|
|
+ if (form.lowerBound != null) segs.push(`低于 ${form.lowerBound}${u}`);
|
|
|
+ if (form.upperBound != null) segs.push(`高于 ${form.upperBound}${u}`);
|
|
|
+ return segs.length > 0 ? segs.join(' 或 ') + ' 触发' : '未配置上下限';
|
|
|
+ }
|
|
|
+ return '';
|
|
|
+});
|
|
|
+
|
|
|
+// 显示 label
|
|
|
const mechanismLabel = computed(() => MECHANISM_OPTIONS.find((o) => o.value === form.mechanism)?.label ?? '—');
|
|
|
const stageNodeLabel = computed(() => props.stageNodes.find((n) => n.nodeCode === form.stageCode)?.nodeName ?? '—');
|
|
|
const orderFlowNodeLabel = computed(() => {
|
|
|
if (!form.orderFlowCode) return '不指定';
|
|
|
return props.orderFlowNodes.find((n) => n.nodeCode === form.orderFlowCode)?.nodeName ?? form.orderFlowCode;
|
|
|
});
|
|
|
-const exceptionTypeLabel = computed(() => selectedExceptionType.value?.typeName ?? '—');
|
|
|
+const exceptionTypeLabel = computed(() => exceptionTypeRow.value?.typeName ?? '—');
|
|
|
const severityLabel = computed(() => SEVERITY_OPTIONS.find((o) => o.value === form.severity)?.label ?? form.severity);
|
|
|
|
|
|
const ruleCodePlaceholder = computed(() => {
|
|
|
switch (form.mechanism) {
|
|
|
- case 'DATE': return '如 RULE_S7_DELIVERY_DELAY_DATE_DELAY';
|
|
|
+ case 'DATE': return '如 RULE_S7_ORDER_DUE_AT_DATE_DELAY';
|
|
|
case 'VALUE_RANGE': return '如 RULE_S1_ORDER_CHANGE_BOUNDS';
|
|
|
- case 'RATIO': return '如 RULE_S7_ORDER_DELIVERY_RATE_RATIO_BELOW';
|
|
|
+ case 'RATIO': return '如 RULE_S7_DELIVERY_RATE_RATIO_BELOW';
|
|
|
default: return '请输入规则编码';
|
|
|
}
|
|
|
});
|
|
|
@@ -124,58 +267,98 @@ function pickMechanism(value: string) {
|
|
|
function nextStep() {
|
|
|
if (step.value === 0) {
|
|
|
if (!form.mechanism) { ElMessage.warning('请先选择报警机制'); return; }
|
|
|
- if (isManualReport.value) return; // 第 1 步切换到说明视图,不再走自动规则路径
|
|
|
+ if (isManualReport.value) return;
|
|
|
step.value = 1;
|
|
|
} else if (step.value === 1) {
|
|
|
if (!form.stageCode) { ElMessage.warning('请选择阶段维度'); return; }
|
|
|
step.value = 2;
|
|
|
} else if (step.value === 2) {
|
|
|
+ if (form.objectIndex < 0) { ElMessage.warning('请选择监控对象'); return; }
|
|
|
+ if (!form.metricCode) { ElMessage.warning('请选择监控指标'); return; }
|
|
|
+ step.value = 3;
|
|
|
+ } else if (step.value === 3) {
|
|
|
+ // 判定标准校验
|
|
|
+ if (form.mechanism === 'DATE') {
|
|
|
+ const states = form.completedStates.split(/[,\s]+/).map((s) => s.trim()).filter(Boolean);
|
|
|
+ if (states.length === 0) { ElMessage.warning('完成状态至少填一个'); return; }
|
|
|
+ }
|
|
|
+ if (form.mechanism === 'VALUE_RANGE') {
|
|
|
+ if (form.lowerBound == null && form.upperBound == null) { ElMessage.warning('上下限至少填一个'); return; }
|
|
|
+ }
|
|
|
+ if (form.mechanism === 'RATIO') {
|
|
|
+ if (!(form.targetRatio > 0 && form.targetRatio <= 100)) { ElMessage.warning('目标比例须在 0–100 之间'); return; }
|
|
|
+ }
|
|
|
if (!form.exceptionTypeCode) { ElMessage.warning('请选择异常类型'); return; }
|
|
|
- // 自动带出 severity(若异常类型有默认值)
|
|
|
- const def = selectedExceptionType.value?.severityDefault;
|
|
|
+ const def = exceptionTypeRow.value?.severityDefault;
|
|
|
if (def) form.severity = def;
|
|
|
- step.value = 3;
|
|
|
+ step.value = 4;
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
-function prevStep() {
|
|
|
- if (step.value > 0) step.value -= 1;
|
|
|
-}
|
|
|
-
|
|
|
-function onExceptionTypeChange(typeCode: string) {
|
|
|
- form.exceptionTypeCode = typeCode;
|
|
|
- const def = props.exceptionTypes.find((t) => t.typeCode === typeCode)?.severityDefault;
|
|
|
- if (def) form.severity = def;
|
|
|
-}
|
|
|
+function prevStep() { if (step.value > 0) step.value -= 1; }
|
|
|
|
|
|
function ruleTypeFromMechanism(mech: string): string {
|
|
|
if (mech === 'DATE') return 'TIMEOUT';
|
|
|
- return 'OUT_OF_RANGE'; // VALUE_RANGE 和 RATIO 共用 OOR evaluator
|
|
|
+ return 'OUT_OF_RANGE'; // VALUE_RANGE / RATIO 共用 OOR evaluator
|
|
|
}
|
|
|
|
|
|
-function buildParamsJson(mech: string, exceptionTypeCode: string): string {
|
|
|
- if (mech === 'DATE') {
|
|
|
- return JSON.stringify({
|
|
|
- dueAtField: 'due_at',
|
|
|
- statusField: 'status',
|
|
|
- graceMinutes: 0,
|
|
|
- completedStates: ['CLOSED', 'COMPLETED', 'DONE'],
|
|
|
- objectIdField: 'source_object_id',
|
|
|
- objectCodeField: 'related_object_code',
|
|
|
- objectNameField: 'related_object_name',
|
|
|
- exceptionTypeCode,
|
|
|
- });
|
|
|
- }
|
|
|
- // VALUE_RANGE / RATIO 共用 OUT_OF_RANGE 模板
|
|
|
- return JSON.stringify({
|
|
|
- measuredValueField: 'measured_value',
|
|
|
- lowerBoundField: 'lower_bound',
|
|
|
- upperBoundField: 'upper_bound',
|
|
|
+function buildParamsJson(): string {
|
|
|
+ const obj = selectedObject.value;
|
|
|
+ const m = selectedMetric.value;
|
|
|
+ const objectLabel = obj?.objectLabel ?? '';
|
|
|
+ const metricLabel = m?.metricLabel ?? '';
|
|
|
+ const exceptionTypeCode = form.exceptionTypeCode;
|
|
|
+ const common = {
|
|
|
objectIdField: 'source_object_id',
|
|
|
objectCodeField: 'related_object_code',
|
|
|
objectNameField: 'related_object_name',
|
|
|
exceptionTypeCode,
|
|
|
- });
|
|
|
+ objectLabel,
|
|
|
+ metricLabel,
|
|
|
+ };
|
|
|
+ if (form.mechanism === 'DATE') {
|
|
|
+ const states = form.completedStates.split(/[,\s]+/).map((s) => s.trim()).filter(Boolean);
|
|
|
+ return JSON.stringify({
|
|
|
+ ...common,
|
|
|
+ dueAtField: m?.dueAtField ?? 'due_at',
|
|
|
+ statusField: m?.statusField ?? 'status',
|
|
|
+ completedStates: states,
|
|
|
+ graceMinutes: Number(form.graceMinutes) || 0,
|
|
|
+ unit: form.unit || '分钟',
|
|
|
+ thresholdDisplay: thresholdDisplay.value,
|
|
|
+ });
|
|
|
+ }
|
|
|
+ if (form.mechanism === 'VALUE_RANGE') {
|
|
|
+ const params: Record<string, unknown> = {
|
|
|
+ ...common,
|
|
|
+ measuredValueField: m?.measuredValueField ?? 'measured_value',
|
|
|
+ unit: form.unit,
|
|
|
+ thresholdDisplay: thresholdDisplay.value,
|
|
|
+ };
|
|
|
+ if (form.lowerBound != null) params.lowerBound = form.lowerBound;
|
|
|
+ if (form.upperBound != null) params.upperBound = form.upperBound;
|
|
|
+ if (form.toleranceAbs > 0) params.toleranceAbs = form.toleranceAbs;
|
|
|
+ if (form.toleranceRatioPct > 0) params.toleranceRatio = form.toleranceRatioPct / 100;
|
|
|
+ return JSON.stringify(params);
|
|
|
+ }
|
|
|
+ if (form.mechanism === 'RATIO') {
|
|
|
+ // RATIO 只写固定 lowerBound(目标比例),禁止写 lowerBoundField/upperBoundField
|
|
|
+ return JSON.stringify({
|
|
|
+ ...common,
|
|
|
+ measuredValueField: m?.measuredValueField ?? 'measured_value',
|
|
|
+ lowerBound: Number(form.targetRatio),
|
|
|
+ unit: '%',
|
|
|
+ thresholdDisplay: thresholdDisplay.value,
|
|
|
+ });
|
|
|
+ }
|
|
|
+ return JSON.stringify(common);
|
|
|
+}
|
|
|
+
|
|
|
+function buildExpression(): string {
|
|
|
+ if (form.mechanism === 'DATE') {
|
|
|
+ return "SELECT 'DEMO-OBJECT-001' AS related_object_code, 'DEMO-OBJECT-001' AS source_object_id, 'DEMO-OBJECT-001' AS related_object_name, DATE_SUB(NOW(), INTERVAL 1 HOUR) AS due_at, 'PENDING' AS status WHERE 1=0";
|
|
|
+ }
|
|
|
+ // VALUE_RANGE / RATIO
|
|
|
+ return "SELECT 'DEMO-OBJECT-001' AS related_object_code, 'DEMO-OBJECT-001' AS source_object_id, 'DEMO-OBJECT-001' AS related_object_name, 0 AS measured_value WHERE 1=0";
|
|
|
}
|
|
|
|
|
|
function pickDataSourceId(): number | null {
|
|
|
@@ -188,21 +371,22 @@ async function save() {
|
|
|
if (!form.ruleCode.trim()) { ElMessage.warning('规则编码不能为空'); return; }
|
|
|
if (!form.stageCode) { ElMessage.warning('阶段维度未选择'); return; }
|
|
|
if (!form.exceptionTypeCode) { ElMessage.warning('异常类型未选择'); return; }
|
|
|
+ if (form.objectIndex < 0 || !form.metricCode) { ElMessage.warning('监控对象或监控指标未选择'); return; }
|
|
|
if (form.pollIntervalSeconds <= 0) { ElMessage.warning('轮询间隔必须大于 0'); return; }
|
|
|
|
|
|
const dataSourceId = pickDataSourceId();
|
|
|
- if (dataSourceId == null) {
|
|
|
- ElMessage.error('缺少数据源,无法创建规则');
|
|
|
- return;
|
|
|
- }
|
|
|
+ if (dataSourceId == null) { ElMessage.error('缺少数据源,无法创建规则'); return; }
|
|
|
+
|
|
|
+ const obj = selectedObject.value;
|
|
|
+ if (!obj) { ElMessage.warning('监控对象异常'); return; }
|
|
|
|
|
|
const payload: S8WatchRuleCreatePayload = {
|
|
|
tenantId: props.tenantId ?? 1,
|
|
|
factoryId: props.factoryId ?? 1,
|
|
|
ruleCode: form.ruleCode.trim(),
|
|
|
- sceneCode: form.stageCode, // scene_code 与 stage_code 同口径(S1-S7)
|
|
|
+ sceneCode: form.stageCode,
|
|
|
dataSourceId,
|
|
|
- watchObjectType: 'ORDER',
|
|
|
+ watchObjectType: obj.objectType,
|
|
|
ruleType: ruleTypeFromMechanism(form.mechanism),
|
|
|
ruleMechanism: form.mechanism,
|
|
|
stageCode: form.stageCode,
|
|
|
@@ -214,7 +398,8 @@ async function save() {
|
|
|
recoverCountRequired: form.recoverCountRequired,
|
|
|
consecutiveFailureCount: 0,
|
|
|
enabled: false,
|
|
|
- paramsJson: buildParamsJson(form.mechanism, form.exceptionTypeCode),
|
|
|
+ paramsJson: buildParamsJson(),
|
|
|
+ expression: buildExpression(),
|
|
|
};
|
|
|
|
|
|
saving.value = true;
|
|
|
@@ -238,15 +423,16 @@ function goManualReport() {
|
|
|
</script>
|
|
|
|
|
|
<template>
|
|
|
- <el-dialog v-model="visible" title="添加规则" width="640px" :close-on-click-modal="false" destroy-on-close>
|
|
|
+ <el-dialog v-model="visible" title="新建监控配置" width="680px" :close-on-click-modal="false" destroy-on-close>
|
|
|
<el-steps :active="isManualReport ? 0 : step" finish-status="success" simple style="margin-bottom: 16px">
|
|
|
<el-step title="报警机制" />
|
|
|
<el-step title="业务维度" />
|
|
|
- <el-step title="异常类型" />
|
|
|
+ <el-step title="监控对象/指标" />
|
|
|
+ <el-step title="判定标准" />
|
|
|
<el-step title="基础配置" />
|
|
|
</el-steps>
|
|
|
|
|
|
- <!-- Step 0: 选择报警机制 -->
|
|
|
+ <!-- Step 0: 报警机制 -->
|
|
|
<div v-if="step === 0">
|
|
|
<div class="wizard-mech-grid">
|
|
|
<div
|
|
|
@@ -260,7 +446,6 @@ function goManualReport() {
|
|
|
<div class="wizard-mech-card__desc">{{ o.desc }}</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
-
|
|
|
<el-alert
|
|
|
v-if="isManualReport"
|
|
|
type="info"
|
|
|
@@ -288,44 +473,108 @@ function goManualReport() {
|
|
|
</el-form>
|
|
|
</div>
|
|
|
|
|
|
- <!-- Step 2: 异常类型 -->
|
|
|
+ <!-- Step 2: 监控对象/指标 -->
|
|
|
<div v-else-if="step === 2">
|
|
|
<el-form label-position="top" size="small">
|
|
|
+ <el-form-item label="监控对象" required>
|
|
|
+ <el-select v-model="form.objectIndex" placeholder="选择监控对象" style="width: 100%">
|
|
|
+ <el-option
|
|
|
+ v-for="(o, idx) in filteredObjects"
|
|
|
+ :key="`${o.objectType}-${idx}`"
|
|
|
+ :label="o.objectLabel"
|
|
|
+ :value="idx"
|
|
|
+ />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="监控指标" required>
|
|
|
+ <el-select v-model="form.metricCode" placeholder="选择监控指标" style="width: 100%" :disabled="form.objectIndex < 0">
|
|
|
+ <el-option
|
|
|
+ v-for="m in filteredMetrics"
|
|
|
+ :key="m.metricCode"
|
|
|
+ :label="m.metricLabel"
|
|
|
+ :value="m.metricCode"
|
|
|
+ />
|
|
|
+ </el-select>
|
|
|
+ <span v-if="selectedMetric" class="wizard-hint">单位:{{ selectedMetric.unit || '—' }}</span>
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- Step 3: 判定标准 -->
|
|
|
+ <div v-else-if="step === 3">
|
|
|
+ <el-form label-position="top" size="small">
|
|
|
+ <!-- DATE -->
|
|
|
+ <template v-if="form.mechanism === 'DATE'">
|
|
|
+ <el-alert type="info" :closable="false" show-icon title="判定逻辑">
|
|
|
+ <template #default>到期后仍未到达「完成状态」即触发。</template>
|
|
|
+ </el-alert>
|
|
|
+ <el-form-item label="完成状态值(逗号分隔)" required>
|
|
|
+ <el-input v-model="form.completedStates" placeholder="如 DONE,CLOSED,COMPLETED" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="宽限分钟(≥0)">
|
|
|
+ <el-input-number v-model="form.graceMinutes" :min="0" />
|
|
|
+ <span class="wizard-hint">超过到期时间后再等待 N 分钟才触发,0 表示立即触发</span>
|
|
|
+ </el-form-item>
|
|
|
+ </template>
|
|
|
+ <!-- VALUE_RANGE -->
|
|
|
+ <template v-else-if="form.mechanism === 'VALUE_RANGE'">
|
|
|
+ <el-alert type="info" :closable="false" show-icon title="判定逻辑">
|
|
|
+ <template #default>低于下限或高于上限即触发;至少填写下限或上限之一。</template>
|
|
|
+ </el-alert>
|
|
|
+ <el-form-item label="下限(可空)">
|
|
|
+ <el-input-number v-model="form.lowerBound" :step="1" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="上限(可空)">
|
|
|
+ <el-input-number v-model="form.upperBound" :step="1" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="单位">
|
|
|
+ <el-input v-model="form.unit" placeholder="如 次 / mm / kg" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="绝对容差(≥0)">
|
|
|
+ <el-input-number v-model="form.toleranceAbs" :min="0" :step="0.1" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="比例容差(0–100%)">
|
|
|
+ <el-input-number v-model="form.toleranceRatioPct" :min="0" :max="100" :step="1" />
|
|
|
+ </el-form-item>
|
|
|
+ </template>
|
|
|
+ <!-- RATIO -->
|
|
|
+ <template v-else-if="form.mechanism === 'RATIO'">
|
|
|
+ <el-alert type="info" :closable="false" show-icon title="判定逻辑">
|
|
|
+ <template #default>实际比例低于目标比例即触发。</template>
|
|
|
+ </el-alert>
|
|
|
+ <el-form-item label="目标比例(%)" required>
|
|
|
+ <el-input-number v-model="form.targetRatio" :min="0" :max="100" :step="1" />
|
|
|
+ <span class="wizard-hint">如交付满足率目标 95%</span>
|
|
|
+ </el-form-item>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <el-divider />
|
|
|
<el-form-item label="异常类型" required>
|
|
|
- <el-select
|
|
|
- v-model="form.exceptionTypeCode"
|
|
|
- filterable
|
|
|
- clearable
|
|
|
- placeholder="选择异常类型"
|
|
|
- style="width: 100%"
|
|
|
- @change="onExceptionTypeChange"
|
|
|
- >
|
|
|
+ <el-select v-model="form.exceptionTypeCode" filterable clearable placeholder="选择异常类型" style="width: 100%">
|
|
|
<el-option
|
|
|
v-for="t in filteredExceptionTypes"
|
|
|
:key="t.typeCode"
|
|
|
:label="t.typeName"
|
|
|
:value="t.typeCode"
|
|
|
- >
|
|
|
- <span>{{ t.typeName }}</span>
|
|
|
- <span class="wizard-type-code-hint">{{ t.typeCode }}</span>
|
|
|
- </el-option>
|
|
|
- </el-select>
|
|
|
- </el-form-item>
|
|
|
- <el-form-item label="严重度">
|
|
|
- <el-select v-model="form.severity" style="width: 200px">
|
|
|
- <el-option v-for="o in SEVERITY_OPTIONS" :key="o.value" :label="o.label" :value="o.value" />
|
|
|
+ />
|
|
|
</el-select>
|
|
|
- <span class="wizard-hint">默认从异常类型带出,可手工调整</span>
|
|
|
</el-form-item>
|
|
|
+
|
|
|
+ <el-alert type="success" :closable="false" style="margin-top: 8px">
|
|
|
+ <template #default>判定标准预览:<strong>{{ thresholdDisplay || '—' }}</strong></template>
|
|
|
+ </el-alert>
|
|
|
</el-form>
|
|
|
</div>
|
|
|
|
|
|
- <!-- Step 3: 基础配置 + 摘要 -->
|
|
|
- <div v-else-if="step === 3">
|
|
|
+ <!-- Step 4: 基础配置 + 摘要 -->
|
|
|
+ <div v-else-if="step === 4">
|
|
|
<el-descriptions :column="1" size="small" border style="margin-bottom: 12px">
|
|
|
<el-descriptions-item label="报警机制">{{ mechanismLabel }}</el-descriptions-item>
|
|
|
- <el-descriptions-item label="阶段维度">{{ stageNodeLabel }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="所属阶段">{{ stageNodeLabel }}</el-descriptions-item>
|
|
|
<el-descriptions-item label="订单流程">{{ orderFlowNodeLabel }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="监控对象">{{ selectedObject?.objectLabel || '—' }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="监控指标">{{ selectedMetric?.metricLabel || '—' }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="判定标准">{{ thresholdDisplay }}</el-descriptions-item>
|
|
|
<el-descriptions-item label="异常类型">{{ exceptionTypeLabel }}</el-descriptions-item>
|
|
|
<el-descriptions-item label="严重度">{{ severityLabel }}</el-descriptions-item>
|
|
|
<el-descriptions-item label="保存后状态">
|
|
|
@@ -338,23 +587,20 @@ function goManualReport() {
|
|
|
<el-form-item label="规则编码" required>
|
|
|
<el-input v-model="form.ruleCode" :placeholder="ruleCodePlaceholder" maxlength="64" show-word-limit />
|
|
|
</el-form-item>
|
|
|
- <el-form-item label="轮询间隔(秒)">
|
|
|
+ <el-form-item label="检测频率(秒)">
|
|
|
<el-input-number v-model="form.pollIntervalSeconds" :min="60" :max="86400" :step="60" />
|
|
|
<span class="wizard-hint">默认 300 秒(5 分钟),范围 60–86400</span>
|
|
|
</el-form-item>
|
|
|
<el-form-item label="持续异常次数">
|
|
|
<el-input-number v-model="form.triggerCountRequired" :min="1" :max="10" />
|
|
|
- <span class="wizard-hint">连续命中 N 次才触发,默认 1</span>
|
|
|
</el-form-item>
|
|
|
<el-form-item label="持续正常次数">
|
|
|
<el-input-number v-model="form.recoverCountRequired" :min="1" :max="10" />
|
|
|
- <span class="wizard-hint">连续未命中 N 次才恢复,默认 1</span>
|
|
|
</el-form-item>
|
|
|
</el-form>
|
|
|
</div>
|
|
|
|
|
|
<template #footer>
|
|
|
- <!-- 主动提报:单按钮跳转 -->
|
|
|
<template v-if="isManualReport">
|
|
|
<el-button @click="visible = false">关闭</el-button>
|
|
|
<el-button type="primary" @click="goManualReport">前往主动提报</el-button>
|
|
|
@@ -362,7 +608,7 @@ function goManualReport() {
|
|
|
<template v-else>
|
|
|
<el-button v-if="step > 0" @click="prevStep">上一步</el-button>
|
|
|
<el-button @click="visible = false">取消</el-button>
|
|
|
- <el-button v-if="step < 3" type="primary" @click="nextStep">下一步</el-button>
|
|
|
+ <el-button v-if="step < 4" type="primary" @click="nextStep">下一步</el-button>
|
|
|
<el-button v-else type="primary" :loading="saving" @click="save">保存(停用)</el-button>
|
|
|
</template>
|
|
|
</template>
|
|
|
@@ -406,9 +652,4 @@ function goManualReport() {
|
|
|
color: var(--el-text-color-secondary);
|
|
|
font-size: 12px;
|
|
|
}
|
|
|
-.wizard-type-code-hint {
|
|
|
- margin-left: 8px;
|
|
|
- color: var(--el-text-color-placeholder);
|
|
|
- font-size: 11px;
|
|
|
-}
|
|
|
</style>
|