Explorar el Código

feat(s8): improve watch rule center usability

YY968XX hace 5 meses
padre
commit
ba75de6c82
Se han modificado 1 ficheros con 185 adiciones y 51 borrados
  1. 185 51
      Web/src/views/aidop/s8/config/S8WatchRuleConfigPage.vue

+ 185 - 51
Web/src/views/aidop/s8/config/S8WatchRuleConfigPage.vue

@@ -1,5 +1,5 @@
 <script setup lang="ts" name="aidopS8WatchRuleConfig">
-import { computed, onActivated, onDeactivated, onMounted, onUnmounted, reactive, ref } from 'vue';
+import { computed, onActivated, onDeactivated, onMounted, onUnmounted, reactive, ref, watch } from 'vue';
 import { ElMessage, ElMessageBox } from 'element-plus';
 import AidopDemoShell from '../../components/AidopDemoShell.vue';
 import WatchRuleWizardDialog from './components/WatchRuleWizardDialog.vue';
@@ -22,9 +22,15 @@ const exceptionTypes = ref<S8ExceptionTypeConfigRow[]>([]);
 const drawerOpen = ref(false);
 const editingRow = ref<S8WatchRuleConfigRow | null>(null);
 
-// A-03: filter state
-const filterSceneCode = ref('');
-const filterSeverity = ref('');
+// CONFIG-RULE-CENTER-SHELL-MVP-1:业务化筛选 + 前端本地分页 + 运行态列折叠。
+const searchKeyword = ref('');
+const filterMechanism = ref<'' | 'DATE' | 'VALUE_RANGE' | 'RATIO'>('');
+const filterStageCode = ref('');
+const filterEnabledStatus = ref<'' | 'ENABLED' | 'DISABLED'>('');
+const showAdvancedColumns = ref(false);
+const currentPage = ref(1);
+const pageSize = ref(10);
+const PAGE_SIZE_OPTIONS = [10, 20, 50];
 
 // B-05: advanced fields collapse
 const advancedExpanded = ref(false);
@@ -180,17 +186,71 @@ function stopAutoRefresh() {
 	}
 }
 
-// A-03: filtered rows
+// CONFIG-RULE-CENTER-SHELL-MVP-1:业务化筛选 + 前端分页 + 业务字段解析。
+function parseRuleBizFields(row: S8WatchRuleConfigRow): { objectLabel: string; metricLabel: string; thresholdDisplay: string } {
+	const fallback = { objectLabel: '未配置', metricLabel: '未配置', thresholdDisplay: '未配置' };
+	if (!row.paramsJson) return fallback;
+	try {
+		const p = JSON.parse(row.paramsJson) as Record<string, unknown>;
+		return {
+			objectLabel: typeof p.objectLabel === 'string' && p.objectLabel ? p.objectLabel : '未配置',
+			metricLabel: typeof p.metricLabel === 'string' && p.metricLabel ? p.metricLabel : '未配置',
+			thresholdDisplay: typeof p.thresholdDisplay === 'string' && p.thresholdDisplay ? p.thresholdDisplay : '未配置',
+		};
+	} catch {
+		return fallback;
+	}
+}
+
 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);
+	const kw = searchKeyword.value.trim().toLowerCase();
+	if (kw) {
+		r = r.filter(x => {
+			const code = (x.ruleCode ?? '').toLowerCase();
+			const display = watchRuleDisplayName(x.ruleCode).toLowerCase();
+			return code.includes(kw) || display.includes(kw);
+		});
+	}
+	if (filterMechanism.value) r = r.filter(x => x.ruleMechanism === filterMechanism.value);
+	if (filterStageCode.value) r = r.filter(x => x.stageCode === filterStageCode.value);
+	if (filterEnabledStatus.value === 'ENABLED') r = r.filter(x => x.enabled === true);
+	else if (filterEnabledStatus.value === 'DISABLED') r = r.filter(x => x.enabled === false);
 	return r;
 });
 
-const sceneCodeOptions = computed(() =>
-	[...new Set(rows.value.map(r => r.sceneCode).filter((c): c is string => !!c))].sort(),
-);
+const paginatedRows = computed(() => {
+	const start = (currentPage.value - 1) * pageSize.value;
+	return filteredRows.value.slice(start, start + pageSize.value);
+});
+
+// 6 chip 统计始终基于全量 rows,不受筛选影响。
+const centerStats = computed(() => ({
+	total: rows.value.length,
+	enabled: rows.value.filter(x => x.enabled === true).length,
+	disabled: rows.value.filter(x => x.enabled === false).length,
+	date: rows.value.filter(x => x.ruleMechanism === 'DATE').length,
+	valueRange: rows.value.filter(x => x.ruleMechanism === 'VALUE_RANGE').length,
+	ratio: rows.value.filter(x => x.ruleMechanism === 'RATIO').length,
+}));
+
+function ensureCurrentPageInRange() {
+	const total = filteredRows.value.length;
+	const maxPage = Math.max(1, Math.ceil(total / pageSize.value));
+	if (currentPage.value > maxPage) currentPage.value = maxPage;
+}
+
+function resetFilters() {
+	searchKeyword.value = '';
+	filterMechanism.value = '';
+	filterStageCode.value = '';
+	filterEnabledStatus.value = '';
+	currentPage.value = 1;
+}
+
+// 任一筛选/搜索变化后回到第一页。
+watch([searchKeyword, filterMechanism, filterStageCode, filterEnabledStatus], () => { currentPage.value = 1; });
+watch(pageSize, () => { currentPage.value = 1; });
 
 // S8-SEVERITY-FOLLOW-SERIOUS-STANDARDIZE-EXEC-1:业务枚举两档;旧值兜底兼容回显。
 function severityLabel(s?: string | null): string {
@@ -478,6 +538,8 @@ async function loadRows() {
 	loading.value = true;
 	try {
 		rows.value = await s8ConfigApi.watchRules.list({ tenantId: TENANT_ID, factoryId: FACTORY_ID });
+		// CONFIG-RULE-CENTER-SHELL-MVP-1:自动刷新后若 currentPage 越界则回退到最后一页,不打断用户翻页。
+		ensureCurrentPageInRange();
 	} catch (e: any) {
 		ElMessage.error(e?.message ?? '加载监视规则失败');
 	} finally {
@@ -930,6 +992,8 @@ function openWizard() {
 }
 async function onWizardSaved() {
 	await loadRows();
+	// CONFIG-RULE-CENTER-SHELL-MVP-1:保存新规则后回到第 1 页,让用户看到新建结果。
+	currentPage.value = 1;
 }
 
 onMounted(() => {
@@ -945,19 +1009,39 @@ onDeactivated(() => stopAutoRefresh());
 </script>
 
 <template>
-	<AidopDemoShell title="监视规则配置" subtitle="S8 / 配置 / 监视规则(参数模板化编辑 + 调度运行态;列表 30 秒自动刷新)">
-		<!-- A-03: toolbar with scene/severity filters; CONFIG-RULE-WIZARD-MVP-1: 新增"添加规则"入口 -->
+	<AidopDemoShell title="规则配置中心" subtitle="配置异常监控规则、报警机制、监控对象与判定标准。新建后默认停用,演示前由人工启用。">
+		<!-- CONFIG-RULE-CENTER-SHELL-MVP-1:主操作 + 统计 chip + 业务化筛选 + 前端分页 -->
 		<div class="rule-toolbar">
-			<el-button size="small" type="primary" @click="openWizard">添加规则</el-button>
+			<el-button size="small" type="primary" @click="openWizard">新建监控配置</el-button>
 			<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-checkbox v-model="showAdvancedColumns" size="small" style="margin-left: 8px">显示运行态</el-checkbox>
+			<span class="rule-toolbar__hint">每 30 秒自动刷新;编辑抽屉打开期间暂停刷新</span>
+		</div>
+
+		<div class="rule-stats">
+			<el-tag size="small" type="info" effect="plain">全部 {{ centerStats.total }}</el-tag>
+			<el-tag size="small" type="success" effect="plain">启用 {{ centerStats.enabled }}</el-tag>
+			<el-tag size="small" type="info" effect="plain">停用 {{ centerStats.disabled }}</el-tag>
+			<el-tag size="small" type="warning" effect="plain">日期类 {{ centerStats.date }}</el-tag>
+			<el-tag size="small" type="warning" effect="plain">数值超差类 {{ centerStats.valueRange }}</el-tag>
+			<el-tag size="small" type="warning" effect="plain">比例类 {{ centerStats.ratio }}</el-tag>
+		</div>
+
+		<div class="rule-filters">
+			<el-input v-model="searchKeyword" size="small" clearable placeholder="搜索规则名称 / 编码" style="width: 240px" />
+			<el-select v-model="filterMechanism" size="small" clearable placeholder="报警机制" style="width: 150px">
+				<el-option label="日期类" value="DATE" />
+				<el-option label="数值超差类" value="VALUE_RANGE" />
+				<el-option label="比例类" value="RATIO" />
 			</el-select>
-			<el-select v-model="filterSeverity" size="small" clearable placeholder="严重度筛选" style="width: 120px">
-				<el-option label="关注" value="FOLLOW" />
-				<el-option label="严重" value="SERIOUS" />
+			<el-select v-model="filterStageCode" size="small" clearable filterable placeholder="阶段维度" style="width: 170px">
+				<el-option v-for="n in stageNodes" :key="n.nodeCode" :label="n.nodeName" :value="n.nodeCode" />
 			</el-select>
-			<span class="rule-toolbar__hint">每 30 秒自动刷新;编辑抽屉打开期间暂停刷新</span>
+			<el-select v-model="filterEnabledStatus" size="small" clearable placeholder="状态" style="width: 130px">
+				<el-option label="启用" value="ENABLED" />
+				<el-option label="停用" value="DISABLED" />
+			</el-select>
+			<el-button size="small" @click="resetFilters">重置</el-button>
 		</div>
 
 		<WatchRuleWizardDialog
@@ -971,21 +1055,12 @@ onDeactivated(() => stopAutoRefresh());
 			@saved="onWizardSaved"
 		/>
 
-		<!-- 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="170" show-overflow-tooltip>
+		<!-- CONFIG-RULE-CENTER-SHELL-MVP-1:业务台账(默认隐藏运行态列) -->
+		<el-table v-loading="loading" :data="paginatedRows" border stripe size="small">
+			<el-table-column label="规则名称" width="180" show-overflow-tooltip>
 				<template #default="{ row }">{{ watchRuleDisplayName(row.ruleCode) }}</template>
 			</el-table-column>
-			<el-table-column label="规则类型" width="100">
-				<template #default="{ row }">
-					<el-tag :type="row.ruleType ? 'success' : 'info'" size="small">
-						{{ ruleTypeLabel(row.ruleType) }}
-					</el-tag>
-				</template>
-			</el-table-column>
-			<el-table-column prop="sceneCode" label="场景" width="120" show-overflow-tooltip />
-			<el-table-column label="机制" width="100">
+			<el-table-column label="报警机制" width="110">
 				<template #default="{ row }">
 					<el-tag v-if="row.ruleMechanism" size="small" type="info">{{ ruleMechanismLabel(row.ruleMechanism) }}</el-tag>
 					<span v-else>—</span>
@@ -994,34 +1069,55 @@ onDeactivated(() => stopAutoRefresh());
 			<el-table-column label="阶段维度" width="140" show-overflow-tooltip>
 				<template #default="{ row }">{{ stageNodeLabel(row.stageCode) }}</template>
 			</el-table-column>
-			<el-table-column label="订单流程" width="160" show-overflow-tooltip>
+			<el-table-column label="订单流程" width="140" show-overflow-tooltip>
 				<template #default="{ row }">{{ orderFlowNodeLabel(row.orderFlowCode) }}</template>
 			</el-table-column>
-			<el-table-column label="严重度" width="80">
+			<el-table-column label="监控对象" width="130" show-overflow-tooltip>
 				<template #default="{ row }">
-					<el-tag v-if="row.severity" :type="severityTagType(row.severity)" size="small">
-						{{ severityLabel(row.severity) }}
-					</el-tag>
-					<span v-else>—</span>
+					<span :class="{ 'rule-biz-empty': parseRuleBizFields(row).objectLabel === '未配置' }">{{ parseRuleBizFields(row).objectLabel }}</span>
+				</template>
+			</el-table-column>
+			<el-table-column label="监控指标" width="140" show-overflow-tooltip>
+				<template #default="{ row }">
+					<span :class="{ 'rule-biz-empty': parseRuleBizFields(row).metricLabel === '未配置' }">{{ parseRuleBizFields(row).metricLabel }}</span>
+				</template>
+			</el-table-column>
+			<el-table-column label="判定标准" width="140" show-overflow-tooltip>
+				<template #default="{ row }">
+					<span :class="{ 'rule-biz-empty': parseRuleBizFields(row).thresholdDisplay === '未配置' }">{{ parseRuleBizFields(row).thresholdDisplay }}</span>
 				</template>
 			</el-table-column>
-			<el-table-column label="启用" width="80">
+			<el-table-column label="状态" width="80">
 				<template #default="{ row }">
-					<el-tag :type="row.enabled ? 'success' : 'info'" size="small">
-						{{ row.enabled ? '启用' : '停用' }}
-					</el-tag>
+					<el-tag :type="row.enabled ? 'success' : 'info'" size="small">{{ row.enabled ? '启用' : '停用' }}</el-tag>
+				</template>
+			</el-table-column>
+			<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="100">
 				<template #default="{ row }">{{ pollIntervalLabel(row.pollIntervalSeconds) }}</template>
 			</el-table-column>
-			<el-table-column label="持续异常次数" width="115">
+			<el-table-column prop="ruleCode" label="内部编码" width="200" show-overflow-tooltip>
+				<template #default="{ row }"><span class="rule-code-mono">{{ row.ruleCode }}</span></template>
+			</el-table-column>
+			<!-- 运行态高级列:默认隐藏,由"显示运行态"开关控制 -->
+			<el-table-column v-if="showAdvancedColumns" label="规则类型" width="100">
+				<template #default="{ row }">
+					<el-tag :type="row.ruleType ? 'success' : 'info'" size="small">{{ ruleTypeLabel(row.ruleType) }}</el-tag>
+				</template>
+			</el-table-column>
+			<el-table-column v-if="showAdvancedColumns" prop="sceneCode" label="场景" width="120" show-overflow-tooltip />
+			<el-table-column v-if="showAdvancedColumns" label="持续异常次数" width="115">
 				<template #default="{ row }">连续 {{ row.triggerCountRequired ?? 1 }} 次</template>
 			</el-table-column>
-			<el-table-column label="持续正常次数" width="115">
+			<el-table-column v-if="showAdvancedColumns" label="持续正常次数" width="115">
 				<template #default="{ row }">连续 {{ row.recoverCountRequired ?? 1 }} 次</template>
 			</el-table-column>
-			<el-table-column label="上次执行" width="160">
+			<el-table-column v-if="showAdvancedColumns" label="上次执行" width="160">
 				<template #default="{ row }">
 					<el-tooltip v-if="row.lastRunAt" :content="String(row.lastRunAt)" placement="top">
 						<span>{{ row.lastRunAt }}</span>
@@ -1029,30 +1125,29 @@ onDeactivated(() => stopAutoRefresh());
 					<span v-else>—</span>
 				</template>
 			</el-table-column>
-			<el-table-column label="下次执行" width="160">
+			<el-table-column v-if="showAdvancedColumns" label="下次执行" width="160">
 				<template #default="{ row }">
 					<el-tag v-if="isPaused(row)" type="warning" size="small">已暂停</el-tag>
 					<el-tag v-else-if="isRunning(row)" type="info" size="small">执行中</el-tag>
 					<span v-else>{{ nextRunDisplay(row) }}</span>
 				</template>
 			</el-table-column>
-			<el-table-column label="上次状态" width="100">
+			<el-table-column v-if="showAdvancedColumns" label="上次状态" width="100">
 				<template #default="{ row }">
 					<el-tag v-if="row.lastStatus" :type="lastStatusTagType(row.lastStatus)" size="small">{{ lastStatusLabel(row.lastStatus) }}</el-tag>
 					<span v-else>—</span>
 				</template>
 			</el-table-column>
-			<el-table-column label="失败次数" width="90">
+			<el-table-column v-if="showAdvancedColumns" label="失败次数" width="90">
 				<template #default="{ row }">
 					<el-badge v-if="(row.consecutiveFailureCount ?? 0) > 0" :value="row.consecutiveFailureCount" type="danger" />
 					<span v-else>0</span>
 				</template>
 			</el-table-column>
-			<el-table-column label="耗时" width="80">
+			<el-table-column v-if="showAdvancedColumns" 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>
+			<el-table-column v-if="showAdvancedColumns" 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">{{ mapLastError(row.lastError) }}</span>
@@ -1074,6 +1169,17 @@ onDeactivated(() => stopAutoRefresh());
 			</el-table-column>
 		</el-table>
 
+		<div class="rule-pagination">
+			<el-pagination
+				v-model:current-page="currentPage"
+				v-model:page-size="pageSize"
+				:page-sizes="PAGE_SIZE_OPTIONS"
+				:total="filteredRows.length"
+				layout="total, sizes, prev, pager, next, jumper"
+				background
+			/>
+		</div>
+
 		<el-drawer v-model="drawerOpen" :title="formTitle" size="540px" direction="rtl" :before-close="(done: () => void) => { closeDrawer(); done(); }">
 			<div v-if="editingRow" class="rule-edit">
 
@@ -1381,6 +1487,34 @@ onDeactivated(() => stopAutoRefresh());
 </template>
 
 <style scoped>
+/* CONFIG-RULE-CENTER-SHELL-MVP-1 */
+.rule-stats {
+	display: flex;
+	gap: 8px;
+	flex-wrap: wrap;
+	margin: 8px 0 12px;
+}
+.rule-filters {
+	display: flex;
+	gap: 8px;
+	flex-wrap: wrap;
+	align-items: center;
+	margin-bottom: 12px;
+}
+.rule-biz-empty {
+	color: var(--el-text-color-placeholder);
+	font-size: 12px;
+}
+.rule-code-mono {
+	font-family: var(--el-font-family-mono, ui-monospace, monospace);
+	font-size: 12px;
+	color: var(--el-text-color-secondary);
+}
+.rule-pagination {
+	display: flex;
+	justify-content: flex-end;
+	margin-top: 12px;
+}
 .rule-edit {
 	padding: 0 4px 80px;
 }