فهرست منبع

feat(s8): add field semantics dictionary and tooltips

YY968XX 3 ماه پیش
والد
کامیت
122119a7d0

+ 1 - 1
Web/package.json

@@ -1,7 +1,7 @@
 {
 	"name": "admin.net",
 	"type": "module",
-	"version": "2.4.132",
+	"version": "2.4.133",
 	"packageManager": "pnpm@10.32.1",
 	"lastBuildTime": "2026.03.15",
 	"description": "Admin.NET 站在巨人肩膀上的 .NET 通用权限开发框架",

+ 102 - 0
Web/src/views/aidop/s8/components/S8FieldTooltip.vue

@@ -0,0 +1,102 @@
+<!--
+  TASK-003-023-S8-FIELD-SEMANTICS-TOOLTIP-1
+  通用 S8 字段语义 tooltip:按 fieldKey 从 s8_field_semantics 字典取语义;
+  查不到时只渲染 fallbackLabel 或默认 slot,不显示 tooltip。
+-->
+<script setup lang="ts">
+import { computed, onMounted, ref, shallowRef, watch } from 'vue';
+import { getS8FieldSemantics, preloadS8FieldSemantics, type S8FieldSemantics } from '../utils/s8FieldSemantics';
+
+const props = defineProps<{
+	fieldKey: string;
+	fallbackLabel?: string;
+	placement?: 'top' | 'bottom' | 'left' | 'right';
+}>();
+
+const ready = ref(false);
+const semantics = shallowRef<S8FieldSemantics | null>(null);
+
+function refresh() {
+	semantics.value = getS8FieldSemantics(props.fieldKey);
+}
+
+onMounted(async () => {
+	await preloadS8FieldSemantics();
+	ready.value = true;
+	refresh();
+});
+
+watch(
+	() => props.fieldKey,
+	() => {
+		if (ready.value) refresh();
+	},
+);
+
+const displayLabel = computed(() => semantics.value?.label || props.fallbackLabel || props.fieldKey);
+const hasTooltip = computed(() => Boolean(semantics.value));
+</script>
+
+<template>
+	<el-tooltip
+		v-if="hasTooltip"
+		:placement="placement ?? 'top'"
+		effect="dark"
+		:show-after="200"
+		popper-class="s8-field-tooltip-popper"
+	>
+		<template #content>
+			<div class="s8-field-tooltip-body">
+				<div class="s8-field-tooltip-label">{{ semantics?.label }}</div>
+				<div v-if="semantics?.name" class="s8-field-tooltip-name">{{ semantics?.name }}</div>
+				<div v-if="semantics?.remark" class="s8-field-tooltip-remark">{{ semantics?.remark }}</div>
+				<div v-if="semantics?.example" class="s8-field-tooltip-example">例:{{ semantics?.example }}</div>
+			</div>
+		</template>
+		<span class="s8-field-tooltip-anchor">
+			<slot>{{ displayLabel }}</slot>
+		</span>
+	</el-tooltip>
+	<span v-else class="s8-field-tooltip-anchor">
+		<slot>{{ displayLabel }}</slot>
+	</span>
+</template>
+
+<style scoped>
+.s8-field-tooltip-anchor {
+	display: inline-flex;
+	align-items: center;
+	gap: 2px;
+	cursor: help;
+	border-bottom: 1px dotted var(--el-color-info-light-5, #c0c4cc);
+}
+</style>
+
+<style>
+.s8-field-tooltip-popper.el-popper {
+	max-width: 320px;
+}
+.s8-field-tooltip-body {
+	display: flex;
+	flex-direction: column;
+	gap: 4px;
+	line-height: 1.5;
+}
+.s8-field-tooltip-label {
+	font-weight: 600;
+	font-size: 13px;
+}
+.s8-field-tooltip-name {
+	font-size: 12px;
+	color: var(--el-color-info-light-7, #a8abb2);
+}
+.s8-field-tooltip-remark {
+	font-size: 12px;
+	white-space: normal;
+	word-break: break-word;
+}
+.s8-field-tooltip-example {
+	font-size: 12px;
+	color: var(--el-color-success-light-3, #95d475);
+}
+</style>

+ 22 - 0
Web/src/views/aidop/s8/config/S8WatchRuleConfigPage.vue

@@ -2,6 +2,7 @@
 import { computed, onActivated, onDeactivated, onMounted, onUnmounted, reactive, ref, watch } from 'vue';
 import { ElMessage, ElMessageBox } from 'element-plus';
 import AidopDemoShell from '../../components/AidopDemoShell.vue';
+import S8FieldTooltip from '../components/S8FieldTooltip.vue';
 import WatchRuleWizardDialog from './components/WatchRuleWizardDialog.vue';
 import WizardDraftDrawer from './components/WizardDraftDrawer.vue';
 import {
@@ -1190,28 +1191,46 @@ onDeactivated(() => stopAutoRefresh());
 				<template #default="{ row }">{{ watchRuleDisplayName(row.ruleCode) }}</template>
 			</el-table-column>
 			<el-table-column label="报警机制" width="110">
+				<template #header>
+					<S8FieldTooltip field-key="rule_mechanism" fallback-label="报警机制" />
+				</template>
 				<template #default="{ row }">
 					<el-tag v-if="row.ruleMechanism" size="small" type="info">{{ ruleMechanismLabel(row.ruleMechanism) }}</el-tag>
 					<span v-else>—</span>
 				</template>
 			</el-table-column>
 			<el-table-column label="阶段维度" width="140" show-overflow-tooltip>
+				<template #header>
+					<S8FieldTooltip field-key="stage_code" fallback-label="阶段维度" />
+				</template>
 				<template #default="{ row }">{{ stageNodeLabel(row.stageCode) }}</template>
 			</el-table-column>
 			<el-table-column label="订单流程" width="140" show-overflow-tooltip>
+				<template #header>
+					<S8FieldTooltip field-key="order_flow_code" fallback-label="订单流程" />
+				</template>
 				<template #default="{ row }">{{ orderFlowNodeLabel(row.orderFlowCode) }}</template>
 			</el-table-column>
 			<el-table-column label="监控对象" width="130" show-overflow-tooltip>
+				<template #header>
+					<S8FieldTooltip field-key="monitor_object" fallback-label="监控对象" />
+				</template>
 				<template #default="{ row }">
 					<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 #header>
+					<S8FieldTooltip field-key="monitor_metric" fallback-label="监控指标" />
+				</template>
 				<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 #header>
+					<S8FieldTooltip field-key="condition_standard" fallback-label="判定标准" />
+				</template>
 				<template #default="{ row }">
 					<span :class="{ 'rule-biz-empty': parseRuleBizFields(row).thresholdDisplay === '未配置' }">{{ parseRuleBizFields(row).thresholdDisplay }}</span>
 				</template>
@@ -1228,6 +1247,9 @@ onDeactivated(() => stopAutoRefresh());
 				</template>
 			</el-table-column>
 			<el-table-column label="检测频率" width="100">
+				<template #header>
+					<S8FieldTooltip field-key="poll_interval_seconds" fallback-label="检测频率" />
+				</template>
 				<template #default="{ row }">{{ pollIntervalLabel(row.pollIntervalSeconds) }}</template>
 			</el-table-column>
 			<el-table-column prop="ruleCode" label="内部编码" width="200" show-overflow-tooltip>

+ 61 - 12
Web/src/views/aidop/s8/config/components/WatchRuleWizardDialog.vue

@@ -16,6 +16,7 @@ import {
 	type S8WizardDraftDetail,
 } from '../../api/s8ConfigApi';
 import InlineCreateExceptionTypeDialog from './InlineCreateExceptionTypeDialog.vue';
+import S8FieldTooltip from '../../components/S8FieldTooltip.vue';
 
 interface DataSourceLike {
 	id: number;
@@ -872,12 +873,18 @@ function handleInlineExceptionTypeCreated(payload: { typeCode: string; typeName:
 		<!-- Step 1: 业务维度 -->
 		<div v-else-if="step === 1">
 			<el-form label-position="top" size="small">
-				<el-form-item label="阶段维度(S1-S7)" required>
+				<el-form-item required>
+					<template #label>
+						<S8FieldTooltip field-key="stage_code" fallback-label="阶段维度(S1-S7)" />
+					</template>
 					<el-select v-model="form.stageCode" clearable filterable placeholder="选择阶段维度" style="width: 100%">
 						<el-option v-for="n in stageNodes" :key="n.nodeCode" :label="n.nodeName" :value="n.nodeCode" :disabled="n.disabled" />
 					</el-select>
 				</el-form-item>
-				<el-form-item label="订单流程节点(可选)">
+				<el-form-item>
+					<template #label>
+						<S8FieldTooltip field-key="order_flow_code" fallback-label="订单流程节点(可选)" />
+					</template>
 					<el-select v-model="form.orderFlowCode" clearable filterable placeholder="选择订单流程节点(可不选)" style="width: 100%">
 						<el-option v-for="n in orderFlowNodes" :key="n.nodeCode" :label="n.nodeName" :value="n.nodeCode" :disabled="n.disabled" />
 					</el-select>
@@ -896,7 +903,10 @@ function handleInlineExceptionTypeCreated(payload: { typeCode: string; typeName:
 				:title="monitorEmptyHint"
 			/>
 			<el-form label-position="top" size="small">
-				<el-form-item label="监控对象" required>
+				<el-form-item required>
+					<template #label>
+						<S8FieldTooltip field-key="monitor_object" fallback-label="监控对象" />
+					</template>
 					<el-select v-model="form.objectIndex" placeholder="选择监控对象" style="width: 100%" :disabled="filteredObjects.length === 0">
 						<el-option
 							v-for="(o, idx) in filteredObjects"
@@ -906,7 +916,10 @@ function handleInlineExceptionTypeCreated(payload: { typeCode: string; typeName:
 						/>
 					</el-select>
 				</el-form-item>
-				<el-form-item label="监控指标" required>
+				<el-form-item required>
+					<template #label>
+						<S8FieldTooltip field-key="monitor_metric" fallback-label="监控指标" />
+					</template>
 					<el-select v-model="form.metricCode" placeholder="选择监控指标" style="width: 100%" :disabled="form.objectIndex < 0">
 						<el-option
 							v-for="m in filteredMetrics"
@@ -990,12 +1003,42 @@ function handleInlineExceptionTypeCreated(payload: { typeCode: string; typeName:
 		<!-- 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="订单流程">{{ orderFlowNodeLabel }}</el-descriptions-item>
-				<el-descriptions-item label="监控对象">{{ selectedObject?.objectLabel || fallbackObjectLabel || '—' }}</el-descriptions-item>
-				<el-descriptions-item label="监控指标">{{ selectedMetric?.metricLabel || fallbackMetricLabel || '—' }}</el-descriptions-item>
-				<el-descriptions-item label="判定标准">{{ thresholdDisplay }}</el-descriptions-item>
+				<el-descriptions-item>
+					<template #label>
+						<S8FieldTooltip field-key="rule_mechanism" fallback-label="报警机制" />
+					</template>
+					{{ mechanismLabel }}
+				</el-descriptions-item>
+				<el-descriptions-item>
+					<template #label>
+						<S8FieldTooltip field-key="stage_code" fallback-label="所属阶段" />
+					</template>
+					{{ stageNodeLabel }}
+				</el-descriptions-item>
+				<el-descriptions-item>
+					<template #label>
+						<S8FieldTooltip field-key="order_flow_code" fallback-label="订单流程" />
+					</template>
+					{{ orderFlowNodeLabel }}
+				</el-descriptions-item>
+				<el-descriptions-item>
+					<template #label>
+						<S8FieldTooltip field-key="monitor_object" fallback-label="监控对象" />
+					</template>
+					{{ selectedObject?.objectLabel || fallbackObjectLabel || '—' }}
+				</el-descriptions-item>
+				<el-descriptions-item>
+					<template #label>
+						<S8FieldTooltip field-key="monitor_metric" fallback-label="监控指标" />
+					</template>
+					{{ selectedMetric?.metricLabel || fallbackMetricLabel || '—' }}
+				</el-descriptions-item>
+				<el-descriptions-item>
+					<template #label>
+						<S8FieldTooltip field-key="condition_standard" fallback-label="判定标准" />
+					</template>
+					{{ thresholdDisplay }}
+				</el-descriptions-item>
 				<el-descriptions-item label="异常类型">{{ exceptionTypeLabel }}</el-descriptions-item>
 				<el-descriptions-item label="严重度">{{ severityLabel }}</el-descriptions-item>
 				<el-descriptions-item label="保存后状态">
@@ -1009,10 +1052,16 @@ function handleInlineExceptionTypeCreated(payload: { typeCode: string; typeName:
 					<el-input v-model="draftName" placeholder="留空则自动生成" maxlength="128" show-word-limit />
 					<span class="wizard-hint">草稿名仅用于草稿配置列表展示,可随时修改</span>
 				</el-form-item>
-				<el-form-item label="规则编码" required>
+				<el-form-item required>
+					<template #label>
+						<S8FieldTooltip field-key="rule_code" fallback-label="规则编码" />
+					</template>
 					<el-input v-model="form.ruleCode" :placeholder="ruleCodePlaceholder" maxlength="64" show-word-limit />
 				</el-form-item>
-				<el-form-item label="检测频率(秒)">
+				<el-form-item>
+					<template #label>
+						<S8FieldTooltip field-key="poll_interval_seconds" fallback-label="检测频率(秒)" />
+					</template>
 					<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>

+ 59 - 10
Web/src/views/aidop/s8/exceptions/S8TaskDetailPage.vue

@@ -3,6 +3,7 @@ import { computed, onMounted, reactive, ref } from 'vue';
 import { ElMessage } from 'element-plus';
 import { useRoute, useRouter } from 'vue-router';
 import AidopDemoShell from '../../components/AidopDemoShell.vue';
+import S8FieldTooltip from '../components/S8FieldTooltip.vue';
 import ApprovalPanel from '/@/views/approvalFlow/component/ApprovalPanel.vue';
 import { s8ExceptionApi, type S8DecisionRow, type S8EvidenceRow } from '../api/s8ExceptionApi';
 import { useUserInfo } from '/@/stores/userInfo';
@@ -245,8 +246,18 @@ onMounted(async () => {
 				<el-col :span="16">
 					<el-descriptions :column="2" border>
 						<el-descriptions-item label="编号">{{ detail.exceptionCode }}</el-descriptions-item>
-						<el-descriptions-item label="状态">{{ detail.statusLabel }}</el-descriptions-item>
-						<el-descriptions-item label="严重度">{{ detail.severityLabel }}</el-descriptions-item>
+						<el-descriptions-item>
+							<template #label>
+								<S8FieldTooltip field-key="exception_status" fallback-label="状态" />
+							</template>
+							{{ detail.statusLabel }}
+						</el-descriptions-item>
+						<el-descriptions-item>
+							<template #label>
+								<S8FieldTooltip field-key="severity" fallback-label="严重度" />
+							</template>
+							{{ detail.severityLabel }}
+						</el-descriptions-item>
 						<el-descriptions-item label="模块">{{ detail.moduleName || detail.moduleCode || detail.sceneName || detail.sceneCode || '—' }}</el-descriptions-item>
 						<!-- BUG-9/24:补异常类型,优先 typeName,回退 typeCode,无则 '—' -->
 						<el-descriptions-item label="异常类型">{{ detail.exceptionTypeName || detail.exceptionTypeCode || '—' }}</el-descriptions-item>
@@ -257,8 +268,18 @@ onMounted(async () => {
 						<el-descriptions-item label="检验人">{{ detail.verifierName || detail.verifierId || '—' }}</el-descriptions-item>
 						<el-descriptions-item label="检验结果">{{ detail.verificationResult || '—' }}</el-descriptions-item>
 						<el-descriptions-item label="提报人">{{ detail.reporterName || detail.reporterId || '—' }}</el-descriptions-item>
-						<el-descriptions-item label="优先级">{{ detail.priorityLevel }} / {{ detail.priorityScore }}</el-descriptions-item>
-						<el-descriptions-item label="SLA 截止">{{ detail.slaDeadline || '—' }}</el-descriptions-item>
+						<el-descriptions-item>
+							<template #label>
+								<S8FieldTooltip field-key="priority_level" fallback-label="优先级" />
+							</template>
+							{{ detail.priorityLevel }} / {{ detail.priorityScore }}
+						</el-descriptions-item>
+						<el-descriptions-item>
+							<template #label>
+								<S8FieldTooltip field-key="sla_deadline" fallback-label="SLA 截止" />
+							</template>
+							{{ detail.slaDeadline || '—' }}
+						</el-descriptions-item>
 						<el-descriptions-item label="标题" :span="2">{{ detail.title }}</el-descriptions-item>
 						<el-descriptions-item label="描述" :span="2">{{ detail.description || '—' }}</el-descriptions-item>
 					</el-descriptions>
@@ -310,17 +331,45 @@ onMounted(async () => {
 							>查看订单链路 ›</el-button>
 						</template>
 						<el-descriptions :column="3" border>
-							<el-descriptions-item label="规则类型">
+							<el-descriptions-item>
+								<template #label>
+									<S8FieldTooltip field-key="rule_type" fallback-label="规则类型" />
+								</template>
 								<el-tag v-if="detail.ruleType" :type="ruleTypeTagType(detail.ruleType)" size="small">{{ ruleTypeLabel(detail.ruleType) }}</el-tag>
 								<span v-else>—</span>
 							</el-descriptions-item>
-							<el-descriptions-item label="规则编码">{{ detail.sourceRuleCode || '—' }}</el-descriptions-item>
-							<el-descriptions-item label="来源对象类型">{{ detail.sourceObjectType || '—' }}</el-descriptions-item>
+							<el-descriptions-item>
+								<template #label>
+									<S8FieldTooltip field-key="rule_code" fallback-label="规则编码" />
+								</template>
+								{{ detail.sourceRuleCode || '—' }}
+							</el-descriptions-item>
+							<el-descriptions-item>
+								<template #label>
+									<S8FieldTooltip field-key="source_type" fallback-label="来源对象类型" />
+								</template>
+								{{ detail.sourceObjectType || '—' }}
+							</el-descriptions-item>
 							<el-descriptions-item label="来源对象 ID">{{ detail.sourceObjectId || '—' }}</el-descriptions-item>
 							<!-- t3j:订单链路字段 -->
-							<el-descriptions-item label="订单号">{{ detail.relatedObjectCode || '—' }}</el-descriptions-item>
-							<el-descriptions-item label="链路阶段">{{ detail.orderFlowName || orderFlowLabel(detail.orderFlowCode) || '—' }}</el-descriptions-item>
-							<el-descriptions-item label="规则机制">{{ detail.ruleMechanism || '—' }}</el-descriptions-item>
+							<el-descriptions-item>
+								<template #label>
+									<S8FieldTooltip field-key="related_object_code" fallback-label="订单号" />
+								</template>
+								{{ detail.relatedObjectCode || '—' }}
+							</el-descriptions-item>
+							<el-descriptions-item>
+								<template #label>
+									<S8FieldTooltip field-key="order_flow_code" fallback-label="链路阶段" />
+								</template>
+								{{ detail.orderFlowName || orderFlowLabel(detail.orderFlowCode) || '—' }}
+							</el-descriptions-item>
+							<el-descriptions-item>
+								<template #label>
+									<S8FieldTooltip field-key="rule_mechanism" fallback-label="规则机制" />
+								</template>
+								{{ detail.ruleMechanism || '—' }}
+							</el-descriptions-item>
 							<el-descriptions-item label="最近检测时间">{{ detail.lastDetectedAt || '—' }}</el-descriptions-item>
 							<el-descriptions-item label="恢复时间">
 								<el-tag v-if="detail.recoveredAt" type="success" size="small">{{ detail.recoveredAt }}</el-tag>

+ 135 - 0
Web/src/views/aidop/s8/utils/s8FieldSemantics.ts

@@ -0,0 +1,135 @@
+// TASK-003-023-S8-FIELD-SEMANTICS-TOOLTIP-1
+// S8 字段语义字典访问工具:复用 SysDictType + SysDictData(code='s8_field_semantics')
+// 优先从 userStore.dictList 读;缺关键字段(name/remark/extData)时回退到 /api/sysDictData/dataList/{code}。
+
+import { useUserInfo } from '/@/stores/userInfo';
+import { getAPI } from '/@/utils/axios-utils';
+import { SysDictDataApi } from '/@/api-services/api';
+
+export const S8_FIELD_DICT_CODE = 's8_field_semantics';
+
+export interface S8FieldSemantics {
+	code: string;
+	label: string;
+	value: string;
+	name: string | null;
+	remark: string | null;
+	example: string | null;
+	category: string | null;
+	source: string | null;
+	relatedDict: string | null;
+	pages: string[];
+}
+
+interface RawDictItem {
+	code?: string | null;
+	label?: string | null;
+	value?: string | null;
+	name?: string | null;
+	remark?: string | null;
+	extData?: string | null;
+}
+
+let cache: Map<string, S8FieldSemantics> | null = null;
+let pendingLoad: Promise<void> | null = null;
+
+function parseExt(extData: string | null | undefined): Record<string, any> {
+	if (!extData) return {};
+	try {
+		const parsed = JSON.parse(extData);
+		return parsed && typeof parsed === 'object' ? parsed : {};
+	} catch {
+		return {};
+	}
+}
+
+function toSemantics(item: RawDictItem): S8FieldSemantics {
+	const ext = parseExt(item.extData);
+	return {
+		code: String(item.code ?? ''),
+		label: String(item.label ?? ''),
+		value: String(item.value ?? ''),
+		name: item.name ?? (typeof ext.fullName === 'string' ? ext.fullName : null),
+		remark: item.remark ?? null,
+		example: typeof ext.example === 'string' ? ext.example : null,
+		category: typeof ext.category === 'string' ? ext.category : null,
+		source: typeof ext.source === 'string' ? ext.source : null,
+		relatedDict: typeof ext.relatedDict === 'string' ? ext.relatedDict : null,
+		pages: Array.isArray(ext.pages) ? ext.pages.filter((p: unknown) => typeof p === 'string') : [],
+	};
+}
+
+function indexInto(map: Map<string, S8FieldSemantics>, items: RawDictItem[]) {
+	items.forEach((it) => {
+		const sem = toSemantics(it);
+		if (sem.code) map.set(sem.code, sem);
+		if (sem.value && !map.has(sem.value)) map.set(sem.value, sem);
+	});
+}
+
+function isReadyEnough(items: RawDictItem[]): boolean {
+	// userStore 的 AllDictList 不返回 Name 字段;只要任意一条带 remark 或 extData 就视为足够。
+	return items.some((it) => (it.remark && it.remark.length > 0) || (it.extData && it.extData.length > 0));
+}
+
+async function ensureCache(): Promise<void> {
+	if (cache) return;
+	if (pendingLoad) return pendingLoad;
+
+	pendingLoad = (async () => {
+		const map = new Map<string, S8FieldSemantics>();
+
+		// 优先:userStore.dictList
+		try {
+			const userStore = useUserInfo();
+			const raw = userStore.getDictDataByCode(S8_FIELD_DICT_CODE) as RawDictItem[];
+			if (Array.isArray(raw) && raw.length > 0 && isReadyEnough(raw)) {
+				indexInto(map, raw);
+				cache = map;
+				return;
+			}
+		} catch {
+			// 静默:未登录或 store 未初始化,走 API 回退
+		}
+
+		// 回退:/api/sysDictData/dataList/{code}
+		try {
+			const res = await getAPI(SysDictDataApi).apiSysDictDataDataListCodeGet(S8_FIELD_DICT_CODE);
+			const list = (res?.data?.result ?? []) as RawDictItem[];
+			indexInto(map, list);
+		} catch {
+			// 回退也失败:保留空 cache,tooltip 全部回退到 fallbackLabel
+		}
+
+		cache = map;
+	})().finally(() => {
+		pendingLoad = null;
+	});
+
+	return pendingLoad;
+}
+
+/**
+ * 同步读取字段语义;首次调用若 cache 未就绪则触发异步加载并返回 null。
+ * 调用方应在挂载阶段先 await preloadS8FieldSemantics()。
+ */
+export function getS8FieldSemantics(fieldKey: string): S8FieldSemantics | null {
+	if (!fieldKey) return null;
+	if (!cache) {
+		// 触发异步加载但不等待
+		void ensureCache();
+		return null;
+	}
+	return cache.get(fieldKey) ?? null;
+}
+
+/** 在页面挂载时调用,确保 cache 就绪。 */
+export async function preloadS8FieldSemantics(): Promise<void> {
+	await ensureCache();
+}
+
+/** 测试 / 重置缓存(开发场景)。 */
+export function _resetS8FieldSemanticsCache(): void {
+	cache = null;
+	pendingLoad = null;
+}

+ 3 - 3
server/Admin.NET.Web.Entry/Admin.NET.Web.Entry.csproj

@@ -11,9 +11,9 @@
     <GenerateSatelliteAssembliesForCore>true</GenerateSatelliteAssembliesForCore>
     <Copyright>Admin.NET</Copyright>
     <Description>Admin.NET 通用权限开发平台</Description>
-    <AssemblyVersion>1.0.102</AssemblyVersion>
-    <FileVersion>1.0.102</FileVersion>
-    <Version>1.0.102</Version>
+    <AssemblyVersion>1.0.103</AssemblyVersion>
+    <FileVersion>1.0.103</FileVersion>
+    <Version>1.0.103</Version>
   </PropertyGroup>
 
   <ItemGroup>

+ 224 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/SeedData/S8FieldSemanticsSeedData.cs

@@ -0,0 +1,224 @@
+namespace Admin.NET.Plugin.AiDOP;
+
+/// <summary>
+/// S8 字段语义字典类型种子(TASK-003-023-S8-FIELD-SEMANTICS-TOOLTIP-1)。
+/// 复用 SysDictType + SysDictData 承载字段语义;不新建 ado_s8_*_field_dict。
+/// 与配套 <see cref="S8FieldSemanticsDictDataSeedData"/> 一起灌入 19 条字段语义。
+/// </summary>
+[IncreSeed]
+public class S8FieldSemanticsSeedData : ISqlSugarEntitySeedData<SysDictType>
+{
+    public const long DictTypeId = 1329900008001L;
+
+    public IEnumerable<SysDictType> HasData()
+    {
+        var ct = DateTime.Parse("2026-05-14 00:00:00");
+        return new[]
+        {
+            new SysDictType
+            {
+                Id = DictTypeId,
+                Name = "S8字段语义字典",
+                Code = "s8_field_semantics",
+                SysFlag = YesNoEnum.N,
+                IsTenant = YesNoEnum.N,
+                OrderNo = 600,
+                Remark = "S8 配置中心、异常处理、主动提报、监控看板字段语义说明",
+                Status = StatusEnum.Enable,
+                CreateTime = ct,
+            },
+        };
+    }
+}
+
+/// <summary>
+/// S8 字段语义字典数据种子(19 条 A 类业务术语)。
+/// 字段约定:
+///   Code  = 字段编码(snake_case,前端按此精确匹配)
+///   Label = UI 中文短名(tooltip 标题)
+///   Value = 前端字段 key(camelCase,前端按此回退匹配)
+///   Name  = 英文全称或业务全称
+///   Remark = 中文业务说明(tooltip 主体)
+///   ExtData = JSON:pages / category / example / source / relatedDict / uiTargets
+/// </summary>
+[IncreSeed]
+public class S8FieldSemanticsDictDataSeedData : ISqlSugarEntitySeedData<SysDictData>
+{
+    public IEnumerable<SysDictData> HasData()
+    {
+        var ct = DateTime.Parse("2026-05-14 00:00:00");
+        var typeId = S8FieldSemanticsSeedData.DictTypeId;
+        long seq = 1329900008101L;
+
+        return new[]
+        {
+            F(seq++, typeId, "sla_deadline", "SLA 截止时间", "slaDeadline",
+                "Service Level Agreement Deadline",
+                "异常处理的最晚截止时间。超过该时间未关闭将触发超时升级。",
+                "时效", "2026-05-20 18:00",
+                "ado_s8_exception.sla_deadline", null,
+                new[] { "S8TaskDetailPage", "S8ExceptionListPage" }, 100, ct),
+
+            F(seq++, typeId, "sla_minutes", "SLA 时长(分钟)", "slaMinutes",
+                "Service Level Agreement Minutes",
+                "异常类型的默认 SLA 时长(分钟),新建异常时按此推算截止时间。",
+                "时效", "60",
+                "ado_s8_exception_type.sla_minutes", null,
+                new[] { "S8ExceptionTypeConfigPage", "WatchRuleWizardDialog" }, 101, ct),
+
+            F(seq++, typeId, "owner_role_code", "责任角色", "ownerRoleCode",
+                "Owner Role Code",
+                "异常发生后默认接收处理的角色(不是部门)。指向 ado_s8_role_permission_config 中已配置的角色编码。",
+                "角色", "quality_engineer",
+                "ado_s8_exception_type.owner_role_code", "ado_s8_role_permission_config",
+                new[] { "S8TaskDetailPage", "S8ExceptionTypeConfigPage" }, 102, ct),
+
+            F(seq++, typeId, "escalate_role_code", "升级角色", "escalateRoleCode",
+                "Escalate Role Code",
+                "SLA 超时后异常自动升级到的角色,用于二级响应。",
+                "角色", "quality_manager",
+                "ado_s8_exception_type.escalate_role_code", "ado_s8_role_permission_config",
+                new[] { "S8ExceptionTypeConfigPage", "S8TaskDetailPage" }, 103, ct),
+
+            F(seq++, typeId, "type_code", "异常类型编码", "typeCode",
+                "Exception Type Code",
+                "异常类型的唯一业务编码,是异常类型主数据的主键。",
+                "标识", "ORDER_CHANGE",
+                "ado_s8_exception_type.type_code", "ado_s8_exception_type",
+                new[] { "S8ExceptionTypeConfigPage", "WatchRuleWizardDialog" }, 104, ct),
+
+            F(seq++, typeId, "rule_mechanism", "报警机制", "ruleMechanism",
+                "Rule Mechanism",
+                "规则的检测方式:DATE 日期型 / RATIO 比例型 / VALUE_RANGE 数值超差型 / MANUAL_REPORT 人工主动提报。",
+                "机制", "DATE",
+                "ado_s8_watch_rule.rule_mechanism", null,
+                new[] { "S8WatchRuleConfigPage", "WatchRuleWizardDialog", "S8TaskDetailPage" }, 105, ct),
+
+            F(seq++, typeId, "stage_code", "阶段维度", "stageCode",
+                "Stage Code (S1-S7)",
+                "S8 监控的 7 个阶段维度:S1 产销协同 / S2 生产准备 / S3 供应协同 / S4 质量检验 / S5 仓储供应 / S6 本体生产 / S7 总装发货。",
+                "维度", "S1",
+                "ado_s8_watch_rule.stage_code", "ado_s8_dimension_node(S_STAGE)",
+                new[] { "S8WatchRuleConfigPage", "WatchRuleWizardDialog", "S8ExceptionListPage" }, 106, ct),
+
+            F(seq++, typeId, "order_flow_code", "订单流程节点", "orderFlowCode",
+                "Order Flow Code",
+                "订单全流程 5 节点:评审排产测算 / 产品设计 / 材料采购 / 本体生产 / 总装发货。",
+                "维度", "BODY_PRODUCTION",
+                "ado_s8_watch_rule.order_flow_code", "ado_s8_dimension_node(ORDER_FLOW)",
+                new[] { "S8WatchRuleConfigPage", "WatchRuleWizardDialog", "S8TaskDetailPage" }, 107, ct),
+
+            F(seq++, typeId, "monitor_object", "监控对象", "monitorObject",
+                "Monitor Object",
+                "规则监控的业务实体(如销售订单 / 工单 / 采购单),决定数据源表。",
+                "机制", "SALES_ORDER_FLOW_STAGE",
+                "ado_s8_monitor_object.object_code", "ado_s8_monitor_object",
+                new[] { "WatchRuleWizardDialog", "S8WatchRuleConfigPage" }, 108, ct),
+
+            F(seq++, typeId, "monitor_metric", "监控指标", "monitorMetric",
+                "Monitor Metric",
+                "监控对象上被检测的具体指标(如到期时间 / 完工率 / 实测值),与监控对象联动。",
+                "机制", "ORDER_FLOW_STAGE_DUE",
+                "ado_s8_monitor_metric.metric_code", "ado_s8_monitor_metric",
+                new[] { "WatchRuleWizardDialog", "S8WatchRuleConfigPage" }, 109, ct),
+
+            F(seq++, typeId, "condition_standard", "判定标准", "conditionStandard",
+                "Condition / Threshold Standard",
+                "触发异常的业务条件摘要,如阈值范围、宽限时间、比例下限。",
+                "机制", "公差 0.95–1.05",
+                "ado_s8_watch_rule.params_json 摘要", null,
+                new[] { "WatchRuleWizardDialog", "S8WatchRuleConfigPage" }, 110, ct),
+
+            F(seq++, typeId, "poll_interval_seconds", "检测频率", "pollIntervalSeconds",
+                "Poll Interval (seconds)",
+                "规则的轮询周期(秒)。数值越小检测越频繁,对数据库压力越大。",
+                "机制", "300",
+                "ado_s8_watch_rule.poll_interval_seconds", null,
+                new[] { "S8WatchRuleConfigPage", "WatchRuleWizardDialog" }, 111, ct),
+
+            F(seq++, typeId, "rule_code", "规则编码", "ruleCode",
+                "Rule Code",
+                "规则的内部唯一编码,用于检索与故障定位。",
+                "标识", "RULE_S7_DELIVERY_DELAY_DATE",
+                "ado_s8_watch_rule.rule_code", null,
+                new[] { "S8WatchRuleConfigPage", "S8TaskDetailPage" }, 112, ct),
+
+            F(seq++, typeId, "exception_status", "异常状态", "exceptionStatus",
+                "Exception Status",
+                "异常生命周期状态:新建 / 已指派 / 处理中 / 已处理 / 待检验 / 已关闭 / 已驳回 / 已升级。",
+                "状态", "IN_PROGRESS",
+                "ado_s8_exception.status", "S8DictionaryService.S8_EXCEPTION_STATUS",
+                new[] { "S8ExceptionListPage", "S8TaskDetailPage" }, 113, ct),
+
+            F(seq++, typeId, "severity", "严重程度", "severity",
+                "Severity",
+                "异常严重度:FOLLOW 关注 / SERIOUS 严重。",
+                "状态", "SERIOUS",
+                "ado_s8_exception.severity", "S8DictionaryService.S8_EXCEPTION_SEVERITY",
+                new[] { "S8TaskDetailPage", "S8ExceptionListPage", "S8ManualReportPage" }, 114, ct),
+
+            F(seq++, typeId, "priority_level", "优先级", "priorityLevel",
+                "Priority Level",
+                "P1 最高 / P2 高 / P3 中 / P4 低,由严重度与 SLA 共同推算。",
+                "优先级", "P2",
+                "ado_s8_exception.priority_level", "S8DictionaryService.S8_PRIORITY_LEVEL",
+                new[] { "S8ExceptionListPage", "S8TaskDetailPage" }, 115, ct),
+
+            F(seq++, typeId, "related_object_code", "关联对象编码", "relatedObjectCode",
+                "Related Object Code",
+                "异常关联的业务对象编码(订单号 / 工单号 / 采购单号),用于跨系统定位。",
+                "关联", "SO-2026-001",
+                "ado_s8_exception.related_object_code", null,
+                new[] { "S8ManualReportPage", "S8TaskDetailPage", "S8ExceptionListPage" }, 116, ct),
+
+            F(seq++, typeId, "source_type", "异常来源", "sourceType",
+                "Source Type",
+                "异常的产生方式:MANUAL 人工提报 / RULE 规则触发 / SYSTEM 系统自动。",
+                "状态", "RULE",
+                "ado_s8_exception.source_type", "S8DictionaryService.S8_SOURCE_TYPE",
+                new[] { "S8TaskDetailPage" }, 117, ct),
+
+            F(seq++, typeId, "rule_type", "规则类型", "ruleType",
+                "Rule Type",
+                "规则评估器类型:OUT_OF_RANGE 阈值越界 / TIMEOUT 超时 / SHORTAGE 短缺。",
+                "机制", "TIMEOUT",
+                "ado_s8_watch_rule.rule_type", null,
+                new[] { "S8ExceptionListPage", "S8TaskDetailPage" }, 118, ct),
+        };
+    }
+
+    private static SysDictData F(
+        long id, long typeId, string code, string label, string value,
+        string fullName, string remark,
+        string category, string? example,
+        string? source, string? relatedDict,
+        string[] pages, int order, DateTime ct)
+    {
+        var extObj = new
+        {
+            pages,
+            category,
+            example,
+            source,
+            relatedDict,
+            uiTargets = System.Array.Empty<string>(),
+            fullName,
+        };
+        var extJson = System.Text.Json.JsonSerializer.Serialize(extObj);
+
+        return new SysDictData
+        {
+            Id = id,
+            DictTypeId = typeId,
+            Code = code,
+            Label = label,
+            Value = value,
+            Name = fullName,
+            Remark = remark,
+            ExtData = extJson,
+            OrderNo = order,
+            Status = StatusEnum.Enable,
+            CreateTime = ct,
+        };
+    }
+}