Explorar o código

feat(s8): improve watch rule config ux

YY968XX hai 5 meses
pai
achega
7fb9426a09
Modificáronse 1 ficheiros con 437 adicións e 79 borrados
  1. 437 79
      Web/src/views/aidop/s8/config/S8WatchRuleConfigPage.vue

+ 437 - 79
Web/src/views/aidop/s8/config/S8WatchRuleConfigPage.vue

@@ -20,6 +20,13 @@ const exceptionTypes = ref<S8ExceptionTypeConfigRow[]>([]);
 const drawerOpen = ref(false);
 const editingRow = ref<S8WatchRuleConfigRow | null>(null);
 
+// A-03: filter state
+const filterSceneCode = ref('');
+const filterSeverity = ref('');
+
+// B-05: advanced fields collapse
+const advancedExpanded = ref(false);
+
 interface TimeoutForm {
 	dueAtField: string;
 	statusField: string;
@@ -29,13 +36,14 @@ interface TimeoutForm {
 	graceMinutes: number;
 	exceptionTypeCode: string;
 }
+// A-06: toleranceRatio renamed to toleranceRatioPct (stores 0–100 in UI, converts to 0–1 on save)
 interface ShortageForm {
 	targetQtyField: string;
 	actualQtyField: string;
 	objectCodeField: string;
 	objectIdField: string;
 	toleranceAbs: number;
-	toleranceRatio: number;
+	toleranceRatioPct: number;
 	exceptionTypeCode: string;
 }
 interface OutOfRangeForm {
@@ -47,7 +55,7 @@ interface OutOfRangeForm {
 	lowerBoundField: string;
 	upperBoundField: string;
 	toleranceAbs: number;
-	toleranceRatio: number;
+	toleranceRatioPct: number;
 	exceptionTypeCode: string;
 }
 
@@ -66,7 +74,7 @@ const shortageForm = reactive<ShortageForm>({
 	objectCodeField: '',
 	objectIdField: '',
 	toleranceAbs: 0,
-	toleranceRatio: 0,
+	toleranceRatioPct: 0,
 	exceptionTypeCode: '',
 });
 const outOfRangeForm = reactive<OutOfRangeForm>({
@@ -78,7 +86,7 @@ const outOfRangeForm = reactive<OutOfRangeForm>({
 	lowerBoundField: '',
 	upperBoundField: '',
 	toleranceAbs: 0,
-	toleranceRatio: 0,
+	toleranceRatioPct: 0,
 	exceptionTypeCode: '',
 });
 const enabledForm = ref(true);
@@ -131,6 +139,36 @@ function stopAutoRefresh() {
 	}
 }
 
+// A-03: filtered rows
+const filteredRows = computed(() => {
+	let r = rows.value;
+	if (filterSceneCode.value) r = r.filter(x => x.sceneCode === filterSceneCode.value);
+	if (filterSeverity.value) r = r.filter(x => (x.severity ?? '').toUpperCase() === filterSeverity.value);
+	return r;
+});
+
+const sceneCodeOptions = computed(() =>
+	[...new Set(rows.value.map(r => r.sceneCode).filter((c): c is string => !!c))].sort(),
+);
+
+// A-03: severity helpers
+function severityLabel(s?: string | null): string {
+	switch ((s ?? '').toUpperCase()) {
+		case 'HIGH': return '高';
+		case 'MEDIUM': return '中';
+		case 'LOW': return '低';
+		default: return s || '—';
+	}
+}
+function severityTagType(s?: string | null): 'danger' | 'warning' | 'info' | '' {
+	switch ((s ?? '').toUpperCase()) {
+		case 'HIGH': return 'danger';
+		case 'MEDIUM': return 'warning';
+		case 'LOW': return 'info';
+		default: return '';
+	}
+}
+
 const ruleTypeOf = (row: S8WatchRuleConfigRow | null): S8WatchRuleType | '' =>
 	(row?.ruleType ?? '').toUpperCase();
 
@@ -139,11 +177,12 @@ const formTitle = computed(() => {
 	return `编辑规则参数 — ${editingRow.value.ruleCode}`;
 });
 
+// A-01: OUT_OF_RANGE label changed from '数据超差' to '数值超限'
 const ruleTypeLabel = (t?: string | null) => {
 	switch ((t ?? '').toUpperCase()) {
 		case 'TIMEOUT': return '超时';
 		case 'SHORTAGE': return '数量不足';
-		case 'OUT_OF_RANGE': return '数据超差';
+		case 'OUT_OF_RANGE': return '数值超限';
 		default: return t || '—';
 	}
 };
@@ -154,6 +193,101 @@ const exceptionTypeOptions = computed(() =>
 		.map((t) => ({ label: `${t.typeName} (${t.typeCode})`, value: t.typeCode })),
 );
 
+// A-07: exception type name lookup
+function exceptionTypeName(code: string): string {
+	const found = exceptionTypes.value.find(t => t.typeCode === code);
+	return found ? found.typeName : code;
+}
+
+// A-07: rule summary in plain Chinese
+const ruleSummary = computed<{ text: string; complete: boolean }>(() => {
+	const row = editingRow.value;
+	if (!row) return { text: '', complete: false };
+	const t = ruleTypeOf(row);
+	if (t === 'TIMEOUT') {
+		const f = timeoutForm;
+		if (!f.dueAtField || !f.statusField || !f.exceptionTypeCode) {
+			return { text: '超时规则(参数不完整,请填写必填字段)', complete: false };
+		}
+		const typeName = exceptionTypeName(f.exceptionTypeCode);
+		const grace = f.graceMinutes > 0 ? `,宽限 ${f.graceMinutes} 分钟` : '';
+		return {
+			text: `检测 "${f.dueAtField}" 是否超期${grace},触发"${typeName}"异常`,
+			complete: true,
+		};
+	}
+	if (t === 'SHORTAGE') {
+		const f = shortageForm;
+		if (!f.targetQtyField || !f.actualQtyField || !f.exceptionTypeCode) {
+			return { text: '数量不足规则(参数不完整,请填写必填字段)', complete: false };
+		}
+		const typeName = exceptionTypeName(f.exceptionTypeCode);
+		return {
+			text: `"${f.actualQtyField}" 低于 "${f.targetQtyField}"(绝对容差 ${f.toleranceAbs},比例容差 ${f.toleranceRatioPct}%),触发"${typeName}"异常`,
+			complete: true,
+		};
+	}
+	if (t === 'OUT_OF_RANGE') {
+		const f = outOfRangeForm;
+		if (!f.measuredValueField || !f.exceptionTypeCode) {
+			return { text: '数值超限规则(参数不完整,请填写必填字段)', complete: false };
+		}
+		const typeName = exceptionTypeName(f.exceptionTypeCode);
+		const boundDesc = f.lowerBoundField || f.upperBoundField
+			? `行内字段限(${[f.lowerBoundField && `下限字段: ${f.lowerBoundField}`, f.upperBoundField && `上限字段: ${f.upperBoundField}`].filter(Boolean).join(',')})`
+			: `固定区间 [${f.lowerBound ?? '—'}, ${f.upperBound ?? '—'}]`;
+		return {
+			text: `"${f.measuredValueField}" 超出 ${boundDesc},触发"${typeName}"异常`,
+			complete: true,
+		};
+	}
+	return { text: '规则类型未知', complete: false };
+});
+
+// A-05: lastError Chinese mapping
+function mapLastError(raw: string | null | undefined): string {
+	if (!raw) return '—';
+	const s = raw.trim();
+	if (/params_json.*invalid/i.test(s) || /json.*parse/i.test(s)) return '配置参数格式错误,请重新保存';
+	if (/connection.*refused/i.test(s) || /cannot.*connect/i.test(s) || /连接.*失败/i.test(s)) return '数据库连接失败';
+	if (/query.*timeout|execution.*timeout|command.*timeout|sql.*timeout/i.test(s)) return '查询超时';
+	if (/column.*not.*found/i.test(s) || /invalid.*column/i.test(s) || /列名.*无效/i.test(s)) return '字段名配置有误,列不存在';
+	if (/table.*not.*found/i.test(s) || /object.*not.*found/i.test(s)) return '数据源表不存在';
+	if (/permission.*denied/i.test(s) || /access.*denied/i.test(s)) return '数据库权限不足';
+	return s.length > 60 ? s.substring(0, 60) + '…' : s;
+}
+
+// A-06: toleranceRatio conversion helpers
+function ratioToPercent(v: number | null | undefined): number {
+	return parseFloat(((v ?? 0) * 100).toFixed(4));
+}
+function percentToRatio(v: number | null | undefined): number {
+	return parseFloat(((v ?? 0) / 100).toFixed(4));
+}
+
+// B-01: pre-save validation
+function validateBeforeSave(): string | null {
+	const t = ruleTypeOf(editingRow.value);
+	if (t === 'TIMEOUT') {
+		if (!timeoutForm.dueAtField.trim()) return '到期时间字段名不能为空';
+		if (!timeoutForm.statusField.trim()) return '状态字段名不能为空';
+		if (!timeoutForm.exceptionTypeCode) return '请选择异常类型';
+	} else if (t === 'SHORTAGE') {
+		if (!shortageForm.targetQtyField.trim()) return '目标数量字段名不能为空';
+		if (!shortageForm.actualQtyField.trim()) return '实际数量字段名不能为空';
+		if (!shortageForm.exceptionTypeCode) return '请选择异常类型';
+		if (shortageForm.toleranceRatioPct < 0 || shortageForm.toleranceRatioPct > 100) return '比例容差须在 0–100% 范围内';
+	} else if (t === 'OUT_OF_RANGE') {
+		if (!outOfRangeForm.measuredValueField.trim()) return '测量值字段名不能为空';
+		if (!outOfRangeForm.exceptionTypeCode) return '请选择异常类型';
+		if (outOfRangeForm.toleranceRatioPct < 0 || outOfRangeForm.toleranceRatioPct > 100) return '比例容差须在 0–100% 范围内';
+		const noUpper = outOfRangeForm.upperBound == null && !outOfRangeForm.upperBoundField.trim();
+		const noLower = outOfRangeForm.lowerBound == null && !outOfRangeForm.lowerBoundField.trim();
+		if (noUpper && noLower) return '请至少配置一个上限或下限(固定值或行内字段)';
+	}
+	return null;
+}
+
 async function loadRows() {
 	loading.value = true;
 	try {
@@ -186,13 +320,13 @@ function resetForms() {
 	Object.assign(shortageForm, {
 		targetQtyField: '', actualQtyField: '',
 		objectCodeField: '', objectIdField: '',
-		toleranceAbs: 0, toleranceRatio: 0, exceptionTypeCode: '',
+		toleranceAbs: 0, toleranceRatioPct: 0, exceptionTypeCode: '',
 	});
 	Object.assign(outOfRangeForm, {
 		measuredValueField: '', objectCodeField: '', objectIdField: '',
 		lowerBound: null, upperBound: null,
 		lowerBoundField: '', upperBoundField: '',
-		toleranceAbs: 0, toleranceRatio: 0, exceptionTypeCode: '',
+		toleranceAbs: 0, toleranceRatioPct: 0, exceptionTypeCode: '',
 	});
 }
 
@@ -230,7 +364,8 @@ function loadFormFromRow(row: S8WatchRuleConfigRow) {
 			objectCodeField: parsed.objectCodeField ?? '',
 			objectIdField: parsed.objectIdField ?? '',
 			toleranceAbs: typeof parsed.toleranceAbs === 'number' ? parsed.toleranceAbs : 0,
-			toleranceRatio: typeof parsed.toleranceRatio === 'number' ? parsed.toleranceRatio : 0,
+			// A-06: convert stored 0–1 to 0–100 for display
+			toleranceRatioPct: ratioToPercent(typeof parsed.toleranceRatio === 'number' ? parsed.toleranceRatio : 0),
 			exceptionTypeCode: parsed.exceptionTypeCode ?? '',
 		});
 	} else if (t === 'OUT_OF_RANGE') {
@@ -252,7 +387,8 @@ function loadFormFromRow(row: S8WatchRuleConfigRow) {
 			lowerBoundField: parsed.lowerBoundField ?? '',
 			upperBoundField: parsed.upperBoundField ?? '',
 			toleranceAbs: typeof parsed.toleranceAbs === 'number' ? parsed.toleranceAbs : 0,
-			toleranceRatio: typeof parsed.toleranceRatio === 'number' ? parsed.toleranceRatio : 0,
+			// A-06: convert stored 0–1 to 0–100 for display
+			toleranceRatioPct: ratioToPercent(typeof parsed.toleranceRatio === 'number' ? parsed.toleranceRatio : 0),
 			exceptionTypeCode: parsed.exceptionTypeCode ?? 'EQUIP_FAULT',
 		});
 	}
@@ -261,6 +397,7 @@ function loadFormFromRow(row: S8WatchRuleConfigRow) {
 function openEdit(row: S8WatchRuleConfigRow) {
 	editingRow.value = row;
 	loadFormFromRow(row);
+	advancedExpanded.value = false;
 	// S8-SCHED-SAVE-SPLIT-1:写入快照。schedule 走数值归一化;params 走 buildPayloadParamsJson
 	// 自身的稳定 key 顺序,因此打开时与保存时两次调用即可字节级比较。
 	originalScheduleSnapshot.value = {
@@ -319,7 +456,8 @@ function buildPayloadParamsJson(): string | null {
 			objectCodeField: shortageForm.objectCodeField,
 			objectIdField: shortageForm.objectIdField,
 			toleranceAbs: Number(shortageForm.toleranceAbs) || 0,
-			toleranceRatio: Number(shortageForm.toleranceRatio) || 0,
+			// A-06: convert 0–100 back to 0–1 for storage
+			toleranceRatio: percentToRatio(shortageForm.toleranceRatioPct),
 			exceptionTypeCode: shortageForm.exceptionTypeCode,
 		});
 	}
@@ -333,7 +471,8 @@ function buildPayloadParamsJson(): string | null {
 			lowerBoundField: outOfRangeForm.lowerBoundField || undefined,
 			upperBoundField: outOfRangeForm.upperBoundField || undefined,
 			toleranceAbs: Number(outOfRangeForm.toleranceAbs) || 0,
-			toleranceRatio: Number(outOfRangeForm.toleranceRatio) || 0,
+			// A-06: convert 0–100 back to 0–1 for storage
+			toleranceRatio: percentToRatio(outOfRangeForm.toleranceRatioPct),
 			exceptionTypeCode: outOfRangeForm.exceptionTypeCode,
 		});
 	}
@@ -342,6 +481,12 @@ function buildPayloadParamsJson(): string | null {
 
 async function save() {
 	if (!editingRow.value) return;
+	// B-01: validate before submitting
+	const validationError = validateBeforeSave();
+	if (validationError) {
+		ElMessage.error(validationError);
+		return;
+	}
 	const scheduleChanged = hasScheduleChanged();
 	const paramsChanged = hasParamsChanged();
 	if (!scheduleChanged && !paramsChanged) {
@@ -402,10 +547,8 @@ async function save() {
 		if (scheduleChanged && paramsChanged) {
 			if (scheduleErr) {
 				ElMessage.error(`调度设置保存失败,规则参数未提交:${apiMsg(scheduleErr)}`);
-				// 双方都未落库,列表无需刷新;抽屉保持打开供用户重试。
 			} else if (paramsErr) {
 				ElMessage.error(`调度设置已保存,但规则参数保存失败,请检查后重试:${apiMsg(paramsErr)}`);
-				// schedule 已落库,刷新列表展示新 schedule;抽屉保持打开避免误以为全部成功。
 				await loadRows();
 			} else {
 				ElMessage.success('保存成功');
@@ -544,11 +687,22 @@ onDeactivated(() => stopAutoRefresh());
 
 <template>
 	<AidopDemoShell title="监视规则配置" subtitle="S8 / 配置 / 监视规则(参数模板化编辑 + 调度运行态;列表 30 秒自动刷新)">
+		<!-- A-03: toolbar with scene/severity filters -->
 		<div class="rule-toolbar">
 			<el-button size="small" @click="loadRows" :loading="loading">手动刷新</el-button>
+			<el-select v-model="filterSceneCode" size="small" clearable placeholder="场景筛选" style="width: 140px">
+				<el-option v-for="c in sceneCodeOptions" :key="c" :label="c" :value="c" />
+			</el-select>
+			<el-select v-model="filterSeverity" size="small" clearable placeholder="严重度筛选" style="width: 120px">
+				<el-option label="高" value="HIGH" />
+				<el-option label="中" value="MEDIUM" />
+				<el-option label="低" value="LOW" />
+			</el-select>
 			<span class="rule-toolbar__hint">每 30 秒自动刷新;编辑抽屉打开期间暂停刷新</span>
 		</div>
-		<el-table v-loading="loading" :data="rows" border stripe size="small">
+
+		<!-- A-02: updated labels; A-03: filteredRows + sceneCode/severity columns -->
+		<el-table v-loading="loading" :data="filteredRows" border stripe size="small">
 			<el-table-column prop="ruleCode" label="规则编码" width="180" />
 			<el-table-column label="规则类型" width="100">
 				<template #default="{ row }">
@@ -557,6 +711,15 @@ onDeactivated(() => stopAutoRefresh());
 					</el-tag>
 				</template>
 			</el-table-column>
+			<el-table-column prop="sceneCode" label="场景" width="120" show-overflow-tooltip />
+			<el-table-column label="严重度" width="80">
+				<template #default="{ row }">
+					<el-tag v-if="row.severity" :type="severityTagType(row.severity)" size="small">
+						{{ severityLabel(row.severity) }}
+					</el-tag>
+					<span v-else>—</span>
+				</template>
+			</el-table-column>
 			<el-table-column label="启用" width="80">
 				<template #default="{ row }">
 					<el-tag :type="row.enabled ? 'success' : 'info'" size="small">
@@ -564,14 +727,14 @@ onDeactivated(() => stopAutoRefresh());
 					</el-tag>
 				</template>
 			</el-table-column>
-			<el-table-column label="轮询间隔" width="100">
+			<el-table-column label="检测频率" width="100">
 				<template #default="{ row }">{{ pollIntervalLabel(row.pollIntervalSeconds) }}</template>
 			</el-table-column>
-			<el-table-column label="触发阈值" width="110">
-				<template #default="{ row }">连续 {{ row.triggerCountRequired ?? 1 }} 次命中</template>
+			<el-table-column label="持续异常次数" width="115">
+				<template #default="{ row }">连续 {{ row.triggerCountRequired ?? 1 }} 次</template>
 			</el-table-column>
-			<el-table-column label="恢复阈值" width="120">
-				<template #default="{ row }">连续 {{ row.recoverCountRequired ?? 1 }} 次未命中</template>
+			<el-table-column label="持续正常次数" width="115">
+				<template #default="{ row }">连续 {{ row.recoverCountRequired ?? 1 }} 次</template>
 			</el-table-column>
 			<el-table-column label="上次执行" width="160">
 				<template #default="{ row }">
@@ -603,10 +766,11 @@ onDeactivated(() => stopAutoRefresh());
 			<el-table-column label="耗时" width="80">
 				<template #default="{ row }">{{ row.lastDurationMs != null ? `${row.lastDurationMs}ms` : '—' }}</template>
 			</el-table-column>
+			<!-- A-05: mapLastError() applied -->
 			<el-table-column label="错误摘要" min-width="180" show-overflow-tooltip>
 				<template #default="{ row }">
 					<el-tooltip v-if="row.lastError" :content="String(row.lastError)" placement="top">
-						<span class="rule-error">{{ row.lastError }}</span>
+						<span class="rule-error">{{ mapLastError(row.lastError) }}</span>
 					</el-tooltip>
 					<span v-else>—</span>
 				</template>
@@ -625,105 +789,249 @@ onDeactivated(() => stopAutoRefresh());
 			</el-table-column>
 		</el-table>
 
-		<el-drawer v-model="drawerOpen" :title="formTitle" size="520px" direction="rtl" :before-close="(done: () => void) => { closeDrawer(); done(); }">
+		<el-drawer v-model="drawerOpen" :title="formTitle" size="540px" direction="rtl" :before-close="(done: () => void) => { closeDrawer(); done(); }">
 			<div v-if="editingRow" class="rule-edit">
+
+				<!-- A-07: rule summary block -->
+				<div class="rule-summary" :class="{ 'rule-summary--incomplete': !ruleSummary.complete }">
+					<span class="rule-summary__label">规则摘要:</span>{{ ruleSummary.text }}
+				</div>
+
+				<el-divider content-position="left">基本信息</el-divider>
 				<el-descriptions :column="1" size="small" border class="rule-meta">
 					<el-descriptions-item label="规则编码">{{ editingRow.ruleCode }}</el-descriptions-item>
 					<el-descriptions-item label="规则类型">{{ ruleTypeLabel(editingRow.ruleType) }}</el-descriptions-item>
-					<el-descriptions-item label="场景">{{ editingRow.sceneCode }}</el-descriptions-item>
-					<el-descriptions-item label="源对象">{{ editingRow.sourceObjectType ?? editingRow.watchObjectType ?? '—' }}</el-descriptions-item>
-					<el-descriptions-item label="表达式(只读)">
-						<pre class="rule-expr">{{ editingRow.expression || '—' }}</pre>
+					<!-- A-04: severity readonly display -->
+					<el-descriptions-item label="严重度">
+						<el-tag v-if="editingRow.severity" :type="severityTagType(editingRow.severity)" size="small">{{ severityLabel(editingRow.severity) }}</el-tag>
+						<span v-else>—</span>
+						<span class="rule-hint">如需调整严重度,请联系实施人员</span>
 					</el-descriptions-item>
+					<el-descriptions-item label="场景">{{ editingRow.sceneCode ?? '—' }}</el-descriptions-item>
+					<el-descriptions-item label="源对象">{{ editingRow.sourceObjectType ?? editingRow.watchObjectType ?? '—' }}</el-descriptions-item>
 				</el-descriptions>
 
-				<el-divider content-position="left">参数</el-divider>
+				<el-divider content-position="left">触发条件</el-divider>
 
+				<!-- TIMEOUT form -->
 				<el-form v-if="ruleTypeOf(editingRow) === 'TIMEOUT'" label-position="top" size="small">
-					<el-form-item label="dueAtField(到期时间字段名)">
-						<el-input v-model="timeoutForm.dueAtField" />
+					<el-form-item>
+						<template #label>
+							到期时间字段名
+							<el-tooltip content="填写 SQL 结果集中表示到期时间的列名,如 due_date、expected_finish_time" placement="top">
+								<span class="rule-field-hint">(?)</span>
+							</el-tooltip>
+						</template>
+						<el-input v-model="timeoutForm.dueAtField" placeholder="如 due_date" />
 					</el-form-item>
-					<el-form-item label="statusField(状态字段名)">
-						<el-input v-model="timeoutForm.statusField" />
+					<el-form-item>
+						<template #label>
+							状态字段名
+							<el-tooltip content="填写 SQL 结果集中表示当前状态的列名,如 status、order_status" placement="top">
+								<span class="rule-field-hint">(?)</span>
+							</el-tooltip>
+						</template>
+						<el-input v-model="timeoutForm.statusField" placeholder="如 status" />
 					</el-form-item>
-					<el-form-item label="completedStates(完成状态,逗号分隔)">
+					<!-- B-04: completedStates risk tip -->
+					<div class="rule-tip">
+						注意:<strong>完成状态值</strong>必须与数据库中实际值完全匹配(区分大小写)。拼写错误将导致规则持续命中,产生大量误报异常。
+					</div>
+					<el-form-item>
+						<template #label>
+							完成状态值(逗号分隔)
+							<el-tooltip content="填写表示「已完成」的状态枚举值,多个用逗号分隔。须与数据库实际存储值完全一致(区分大小写)" placement="top">
+								<span class="rule-field-hint">(?)</span>
+							</el-tooltip>
+						</template>
 						<el-input v-model="timeoutForm.completedStates" placeholder="如 DONE,CLOSED" />
 					</el-form-item>
-					<el-form-item label="objectCodeField">
-						<el-input v-model="timeoutForm.objectCodeField" />
-					</el-form-item>
-					<el-form-item label="objectIdField">
-						<el-input v-model="timeoutForm.objectIdField" />
-					</el-form-item>
-					<el-form-item label="graceMinutes(宽限分钟,≥0)">
+					<el-form-item label="宽限分钟(≥0)">
 						<el-input-number v-model="timeoutForm.graceMinutes" :min="0" />
+						<span class="rule-hint">超过到期时间后再等待 N 分钟才触发,0 表示立即触发</span>
 					</el-form-item>
-					<el-form-item label="exceptionTypeCode">
-						<el-select v-model="timeoutForm.exceptionTypeCode" filterable placeholder="选择异常类型">
+					<el-form-item label="异常类型">
+						<el-select v-model="timeoutForm.exceptionTypeCode" filterable clearable placeholder="选择异常类型" style="width: 100%">
 							<el-option v-for="o in exceptionTypeOptions" :key="o.value" :label="o.label" :value="o.value" />
 						</el-select>
 					</el-form-item>
+					<!-- B-05: advanced fields collapse -->
+					<div class="rule-advanced-toggle" @click="advancedExpanded = !advancedExpanded">
+						{{ advancedExpanded ? '▲ 收起高级字段' : '▼ 展开高级字段(对象编码/ID 映射)' }}
+					</div>
+					<template v-if="advancedExpanded">
+						<el-form-item>
+							<template #label>
+								对象编码字段名
+								<el-tooltip content="SQL 结果集中用于关联业务对象编码的列名,通常无需修改" placement="top">
+									<span class="rule-field-hint">(?)</span>
+								</el-tooltip>
+							</template>
+							<el-input v-model="timeoutForm.objectCodeField" placeholder="如 order_code" />
+						</el-form-item>
+						<el-form-item>
+							<template #label>
+								对象 ID 字段名
+								<el-tooltip content="SQL 结果集中用于关联业务对象主键的列名,通常无需修改" placement="top">
+									<span class="rule-field-hint">(?)</span>
+								</el-tooltip>
+							</template>
+							<el-input v-model="timeoutForm.objectIdField" placeholder="如 order_id" />
+						</el-form-item>
+					</template>
 				</el-form>
 
+				<!-- SHORTAGE form -->
 				<el-form v-else-if="ruleTypeOf(editingRow) === 'SHORTAGE'" label-position="top" size="small">
-					<el-form-item label="targetQtyField">
-						<el-input v-model="shortageForm.targetQtyField" />
-					</el-form-item>
-					<el-form-item label="actualQtyField">
-						<el-input v-model="shortageForm.actualQtyField" />
+					<!-- B-02: AND logic -->
+					<el-alert type="info" :closable="false" show-icon title="触发逻辑说明">
+						<template #default>
+							缺料触发为 <strong>AND 逻辑</strong>:实际缺口必须同时超过绝对容差 <em>且</em> 超过比例容差,才触发异常;任一未超过则不触发。
+						</template>
+					</el-alert>
+					<el-form-item>
+						<template #label>
+							目标数量字段名
+							<el-tooltip content="填写 SQL 结果集中表示计划/目标数量的列名,如 plan_qty、target_quantity" placement="top">
+								<span class="rule-field-hint">(?)</span>
+							</el-tooltip>
+						</template>
+						<el-input v-model="shortageForm.targetQtyField" placeholder="如 plan_qty" />
 					</el-form-item>
-					<el-form-item label="objectCodeField">
-						<el-input v-model="shortageForm.objectCodeField" />
+					<el-form-item>
+						<template #label>
+							实际数量字段名
+							<el-tooltip content="填写 SQL 结果集中表示实际/到货数量的列名,如 actual_qty、received_qty" placement="top">
+								<span class="rule-field-hint">(?)</span>
+							</el-tooltip>
+						</template>
+						<el-input v-model="shortageForm.actualQtyField" placeholder="如 actual_qty" />
 					</el-form-item>
-					<el-form-item label="objectIdField">
-						<el-input v-model="shortageForm.objectIdField" />
-					</el-form-item>
-					<el-form-item label="toleranceAbs(绝对容差,≥0)">
+					<el-form-item label="绝对容差(≥0)">
 						<el-input-number v-model="shortageForm.toleranceAbs" :min="0" :step="0.1" />
+						<span class="rule-hint">缺口不超过此值时不触发</span>
 					</el-form-item>
-					<el-form-item label="toleranceRatio(比例容差,0–1)">
-						<el-input-number v-model="shortageForm.toleranceRatio" :min="0" :max="1" :step="0.01" />
+					<el-form-item label="比例容差(0–100%)">
+						<el-input-number v-model="shortageForm.toleranceRatioPct" :min="0" :max="100" :step="1" />
+						<span class="rule-hint">% 缺口占目标数量的比例不超过此值时不触发;两项均须超过才触发</span>
 					</el-form-item>
-					<el-form-item label="exceptionTypeCode">
-						<el-select v-model="shortageForm.exceptionTypeCode" filterable placeholder="选择异常类型">
+					<el-form-item label="异常类型">
+						<el-select v-model="shortageForm.exceptionTypeCode" filterable clearable placeholder="选择异常类型" style="width: 100%">
 							<el-option v-for="o in exceptionTypeOptions" :key="o.value" :label="o.label" :value="o.value" />
 						</el-select>
 					</el-form-item>
+					<!-- B-05: advanced fields collapse -->
+					<div class="rule-advanced-toggle" @click="advancedExpanded = !advancedExpanded">
+						{{ advancedExpanded ? '▲ 收起高级字段' : '▼ 展开高级字段(对象编码/ID 映射)' }}
+					</div>
+					<template v-if="advancedExpanded">
+						<el-form-item>
+							<template #label>
+								对象编码字段名
+								<el-tooltip content="SQL 结果集中用于关联业务对象编码的列名,通常无需修改" placement="top">
+									<span class="rule-field-hint">(?)</span>
+								</el-tooltip>
+							</template>
+							<el-input v-model="shortageForm.objectCodeField" placeholder="如 order_code" />
+						</el-form-item>
+						<el-form-item>
+							<template #label>
+								对象 ID 字段名
+								<el-tooltip content="SQL 结果集中用于关联业务对象主键的列名,通常无需修改" placement="top">
+									<span class="rule-field-hint">(?)</span>
+								</el-tooltip>
+							</template>
+							<el-input v-model="shortageForm.objectIdField" placeholder="如 order_id" />
+						</el-form-item>
+					</template>
 				</el-form>
 
+				<!-- OUT_OF_RANGE form -->
 				<el-form v-else-if="ruleTypeOf(editingRow) === 'OUT_OF_RANGE'" label-position="top" size="small">
-					<el-form-item label="measuredValueField">
-						<el-input v-model="outOfRangeForm.measuredValueField" />
-					</el-form-item>
-					<el-form-item label="objectCodeField">
-						<el-input v-model="outOfRangeForm.objectCodeField" />
+					<!-- B-02: AND logic -->
+					<el-alert type="info" :closable="false" show-icon title="触发逻辑说明">
+						<template #default>
+							超限触发为 <strong>AND 逻辑</strong>:超限幅度必须同时超过绝对容差 <em>且</em> 超过比例容差,才触发异常;任一未超过则不触发。
+						</template>
+					</el-alert>
+					<!-- B-03: bound priority -->
+					<el-alert type="warning" :closable="false" show-icon title="上下限优先级">
+						<template #default>
+							<strong>行内字段优先</strong>:若填写了行内上/下限字段名,则优先取该行对应列的值作为限值;固定上下限仅在字段名为空时生效。
+						</template>
+					</el-alert>
+					<el-form-item>
+						<template #label>
+							测量值字段名
+							<el-tooltip content="填写 SQL 结果集中表示被监测数值的列名,如 measured_value、sensor_reading" placement="top">
+								<span class="rule-field-hint">(?)</span>
+							</el-tooltip>
+						</template>
+						<el-input v-model="outOfRangeForm.measuredValueField" placeholder="如 measured_value" />
 					</el-form-item>
-					<el-form-item label="objectIdField">
-						<el-input v-model="outOfRangeForm.objectIdField" />
-					</el-form-item>
-					<el-form-item label="upperBound(固定上限,可空)">
+					<el-form-item label="固定上限(可空)">
 						<el-input-number v-model="outOfRangeForm.upperBound" :step="0.1" />
+						<span class="rule-hint">行内上限字段不为空时此值不生效</span>
 					</el-form-item>
-					<el-form-item label="lowerBound(固定下限,可空)">
+					<el-form-item label="固定下限(可空)">
 						<el-input-number v-model="outOfRangeForm.lowerBound" :step="0.1" />
+						<span class="rule-hint">行内下限字段不为空时此值不生效</span>
 					</el-form-item>
-					<el-form-item label="upperBoundField(行内上限字段,可空)">
-						<el-input v-model="outOfRangeForm.upperBoundField" />
+					<el-form-item>
+						<template #label>
+							行内上限字段名(可空)
+							<el-tooltip content="若每行有自己的上限列,填写该列名,优先级高于固定上限" placement="top">
+								<span class="rule-field-hint">(?)</span>
+							</el-tooltip>
+						</template>
+						<el-input v-model="outOfRangeForm.upperBoundField" placeholder="如 upper_limit_col" />
 					</el-form-item>
-					<el-form-item label="lowerBoundField(行内下限字段,可空)">
-						<el-input v-model="outOfRangeForm.lowerBoundField" />
+					<el-form-item>
+						<template #label>
+							行内下限字段名(可空)
+							<el-tooltip content="若每行有自己的下限列,填写该列名,优先级高于固定下限" placement="top">
+								<span class="rule-field-hint">(?)</span>
+							</el-tooltip>
+						</template>
+						<el-input v-model="outOfRangeForm.lowerBoundField" placeholder="如 lower_limit_col" />
 					</el-form-item>
-					<el-form-item label="toleranceAbs(绝对容差,≥0)">
+					<el-form-item label="绝对容差(≥0)">
 						<el-input-number v-model="outOfRangeForm.toleranceAbs" :min="0" :step="0.1" />
+						<span class="rule-hint">超限幅度不超过此值时不触发</span>
 					</el-form-item>
-					<el-form-item label="toleranceRatio(比例容差,0–1)">
-						<el-input-number v-model="outOfRangeForm.toleranceRatio" :min="0" :max="1" :step="0.01" />
+					<el-form-item label="比例容差(0–100%)">
+						<el-input-number v-model="outOfRangeForm.toleranceRatioPct" :min="0" :max="100" :step="1" />
+						<span class="rule-hint">% 超限幅度占限值的比例不超过此值时不触发;两项均须超过才触发</span>
 					</el-form-item>
-					<el-form-item label="exceptionTypeCode">
-						<el-select v-model="outOfRangeForm.exceptionTypeCode" filterable placeholder="选择异常类型">
+					<el-form-item label="异常类型">
+						<el-select v-model="outOfRangeForm.exceptionTypeCode" filterable clearable placeholder="选择异常类型" style="width: 100%">
 							<el-option v-for="o in exceptionTypeOptions" :key="o.value" :label="o.label" :value="o.value" />
 						</el-select>
 					</el-form-item>
+					<!-- B-05: advanced fields collapse -->
+					<div class="rule-advanced-toggle" @click="advancedExpanded = !advancedExpanded">
+						{{ advancedExpanded ? '▲ 收起高级字段' : '▼ 展开高级字段(对象编码/ID 映射)' }}
+					</div>
+					<template v-if="advancedExpanded">
+						<el-form-item>
+							<template #label>
+								对象编码字段名
+								<el-tooltip content="SQL 结果集中用于关联业务对象编码的列名,通常无需修改" placement="top">
+									<span class="rule-field-hint">(?)</span>
+								</el-tooltip>
+							</template>
+							<el-input v-model="outOfRangeForm.objectCodeField" placeholder="如 device_code" />
+						</el-form-item>
+						<el-form-item>
+							<template #label>
+								对象 ID 字段名
+								<el-tooltip content="SQL 结果集中用于关联业务对象主键的列名,通常无需修改" placement="top">
+									<span class="rule-field-hint">(?)</span>
+								</el-tooltip>
+							</template>
+							<el-input v-model="outOfRangeForm.objectIdField" placeholder="如 device_id" />
+						</el-form-item>
+					</template>
 				</el-form>
 
 				<el-alert v-else type="warning" :closable="false" show-icon>
@@ -731,18 +1039,21 @@ onDeactivated(() => stopAutoRefresh());
 					<pre class="rule-raw">{{ rawParamsJson || '(空)' }}</pre>
 				</el-alert>
 
-				<el-divider content-position="left">调度设置</el-divider>
+				<el-divider content-position="left">检测 SQL(只读)</el-divider>
+				<pre class="rule-expr">{{ editingRow.expression || '—' }}</pre>
+
+				<el-divider content-position="left">检测调度</el-divider>
 				<el-form label-position="top" size="small">
-					<el-form-item label="轮询间隔(poll_interval_seconds,60–86400 秒)">
+					<el-form-item label="检测频率(60–86400 秒)">
 						<el-select v-model="scheduleForm.pollIntervalSeconds" allow-create filterable placeholder="选择或输入秒数">
 							<el-option v-for="o in POLL_INTERVAL_PRESETS" :key="o.value" :label="o.label" :value="o.value" />
 						</el-select>
 					</el-form-item>
-					<el-form-item label="触发阈值(trigger_count_required,1–10)">
+					<el-form-item label="持续异常次数(1–10)">
 						<el-input-number v-model="scheduleForm.triggerCountRequired" :min="1" :max="10" />
 						<span class="rule-hint">连续命中 N 次后才建单</span>
 					</el-form-item>
-					<el-form-item label="恢复阈值(recover_count_required,1–10)">
+					<el-form-item label="持续正常次数(1–10)">
 						<el-input-number v-model="scheduleForm.recoverCountRequired" :min="1" :max="10" />
 						<span class="rule-hint">连续未命中 N 次后才标 recovered</span>
 					</el-form-item>
@@ -771,10 +1082,30 @@ onDeactivated(() => stopAutoRefresh());
 .rule-meta {
 	margin-bottom: 12px;
 }
+.rule-summary {
+	background: var(--el-color-primary-light-9);
+	border-left: 3px solid var(--el-color-primary);
+	padding: 8px 12px;
+	border-radius: 4px;
+	font-size: 13px;
+	margin-bottom: 4px;
+	line-height: 1.6;
+}
+.rule-summary--incomplete {
+	background: var(--el-color-warning-light-9);
+	border-left-color: var(--el-color-warning);
+}
+.rule-summary__label {
+	font-weight: 600;
+	color: var(--el-text-color-primary);
+}
 .rule-expr {
 	white-space: pre-wrap;
 	word-break: break-all;
-	margin: 0;
+	margin: 0 0 8px;
+	padding: 8px;
+	background: var(--el-fill-color-light);
+	border-radius: 4px;
 	font-family: var(--el-font-family-mono, monospace);
 	font-size: 12px;
 	color: var(--el-text-color-secondary);
@@ -799,6 +1130,7 @@ onDeactivated(() => stopAutoRefresh());
 	align-items: center;
 	gap: 12px;
 	margin-bottom: 8px;
+	flex-wrap: wrap;
 }
 .rule-toolbar__hint {
 	font-size: 12px;
@@ -809,8 +1141,34 @@ onDeactivated(() => stopAutoRefresh());
 	color: var(--el-text-color-secondary);
 	margin-left: 8px;
 }
+.rule-field-hint {
+	font-size: 11px;
+	color: var(--el-color-primary);
+	cursor: pointer;
+	margin-left: 4px;
+}
 .rule-error {
 	color: var(--el-color-danger);
 	font-size: 12px;
 }
+.rule-tip {
+	background: var(--el-color-warning-light-9);
+	border: 1px solid var(--el-color-warning-light-5);
+	border-radius: 4px;
+	padding: 8px 12px;
+	font-size: 12px;
+	color: var(--el-text-color-regular);
+	margin-bottom: 12px;
+	line-height: 1.6;
+}
+.rule-advanced-toggle {
+	font-size: 12px;
+	color: var(--el-color-primary);
+	cursor: pointer;
+	margin: 8px 0 4px;
+	user-select: none;
+}
+.rule-advanced-toggle:hover {
+	text-decoration: underline;
+}
 </style>