Jelajahi Sumber

feat(s8): add business condition wizard for watch rules

YY968XX 4 bulan lalu
induk
melakukan
86098cd2a7

+ 2 - 0
Web/src/views/aidop/s8/api/s8ConfigApi.ts

@@ -168,6 +168,8 @@ export interface S8WatchRuleCreatePayload {
 	consecutiveFailureCount: number;
 	enabled: boolean;
 	paramsJson: string;
+	// CONFIG-RULE-WIZARD-BIZ-CONDITION-MVP-1:占位 SQL 表达式(WHERE 1=0),保证启用后不真命中。
+	expression?: string;
 }
 
 export const s8ConfigApi = {

+ 327 - 86
Web/src/views/aidop/s8/config/components/WatchRuleWizardDialog.vue

@@ -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>