Просмотр исходного кода

fix(s8): restore config navigation and dashboard periods | Web 2.4.343

关闭最终 20 页运行认证暴露的两个前端 blocker(首次认证 18/20 FAIL)。

① CFG_HUB:正式 9 张配置卡片只呈现 6 张。
DATASRC / NOTIFY / ALERT 三行卡片定义被注释,且全站再无其它入口——
route、page、菜单种子、功能码全都在,用户却只能手输 URL 才能到达这三个正式功能页。
起因是 S8-CONFIG-HUB-DEMO-ENTRY-CLEANUP-REVISION-1 以「演示阶段」为由做的入口收口;
该决定现判为 SUPERSEDED:三页均属正式 20 页产品合同,且都已完成真实 Runtime Certification。
其当时的隐藏理由「通知分层与 ApprovalFlow 通知重叠、整表未被业务消费」已被 CFG_NOTIFY
认证实证推翻(14 条 GLOBAL 生效层确由 S8NotificationLayerResolver 运行时消费),注释一并订正。
ALERT 卡片描述改为「历史兼容只读,运行时判定见监控规则」——该页写能力已退役为 410,
旧描述「阈值与触发条件」与事实不符。其余 6 张卡片、路由、功能码、菜单种子均未改动。

② DASHBOARD:预设时间按钮点了等于没点。
本日/本周/本月/本季度/过去24h/7天/30天 点击后只更新 period,从不重算 beginTime/endTime;
而请求同时携带二者、后端 S8PeriodHelper 的契约是「显式日期优先于 period」,
于是日期框与全部数字纹丝不动。更早一层:dashboardPeriod 写死 'this_month',
而选择器由 createDefaultDateRange() 初始化为近 7 天,页面首次渲染就自相矛盾
(芯片标「本月」,实际按近 7 天取数)。
修复为前端 only:新增 resolvePresetRange() 严格镜像 S8PeriodHelper 语义
(半开区间换算闭区间;周一为周起始、季度起始月 1/4/7/10、last_7d=today-6、last_30d=today-29),
预设点击时同步写回 dateStart/dateEnd;last_24h 是小时级区间、日期粒度无法如实表达,
改为清空显式日期交后端按 period 解析;用户手动改日期则取消预设高亮;初始不选中任何预设。
默认时间窗未改动,仍是 createDefaultDateRange() 的滚动近 7 天。

顺带订正一条长期误解:旧记的「DASHBOARD = 14」是对 /dashboard/overview 的
无日期参数调用结果,从来不是 UI 默认窗口的合同。Golden 异常创建时间跨 07-28~08-26,
会随时间自然滑出短窗口。正确判据是「UI 范围 == 请求范围 == API 范围」且
「UI total == 同范围 API total」,而非某个固定数字。

③ 附带 UI 修复:预设修好后能拉出更宽范围,首次把 status=ASSIGNED 的行带进明细表,
暴露既有缺陷——statusTagType 把 ASSIGNED 映射为空字符串,而 el-tag 只接受
primary/success/info/warning/danger,空串每渲染一行报 2 条 console warning。
改为 'primary',只动展示层 tag type,不涉及状态业务码、显示名、API 与流转逻辑。

另移除生产 UI 中面向实施人员的话术「请先执行数据库初始化脚本…」,
改为「当前时间范围内的异常尚无流程节点与关联对象信息」,不暴露 DB / 脚本 / 迁移等实现细节。

定向认证:CFG_HUB 9/9 卡片导航全过;DASHBOARD 7/7 预设 BEGIN/END/UI/API 逐项一致
(本季度 07-01~09-30 → 14/14,自然得到 Golden 全量,无硬编码);console 0;
DB mutation 0,Golden 14 / UAT 17 / Watch 5-0 / Detection 8 / Notify 14 / 外发 0 全程零漂移。
无后端改动,Server 保持 1.0.429。
YY968XX 17 часов назад
Родитель
Сommit
e83b44b419

+ 1 - 1
Web/package.json

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

+ 13 - 7
Web/src/views/aidop/s8/config/S8ConfigHubPage.vue

@@ -16,9 +16,16 @@ import { useRouter } from 'vue-router';
 import AidopDemoShell from '../../components/AidopDemoShell.vue';
 
 const router = useRouter();
-// S8-CONFIG-HUB-DEMO-ENTRY-CLEANUP-REVISION-1:演示入口收口为 5 张卡片。
-// 隐藏的卡片仅前端入口注释,route / page / controller / entity / 数据 / 菜单全部保留。
-// 隐藏:通知分层(与 ApprovalFlow 通知重叠,整表未被业务消费)、报警规则(演示阶段不展示阈值配置)、数据源(演示阶段不展示连接配置)。
+// 配置中心入口 = 正式的 9 个 S8 配置页,与产品合同的 20 页清单一致。
+//
+// S8-FINAL-BLOCKERS-FIX-AND-TARGETED-CERT-1:恢复通知分层 / 报警规则 / 数据源三张卡片。
+// 此前 S8-CONFIG-HUB-DEMO-ENTRY-CLEANUP-REVISION-1 曾以「演示阶段」为由注释掉这三个入口,
+// 该决定现已 **SUPERSEDED**:三页均已完成真实 Runtime Certification,
+// 继续隐藏等于要求用户手输 URL 才能到达正式功能页。
+//
+// ⚠️ 当时的隐藏理由「通知分层与 ApprovalFlow 通知重叠、整表未被业务消费」**已被实证推翻**:
+//    ado_s8_notification_layer 的 14 条 GLOBAL 生效层确由 S8NotificationLayerResolver
+//    在运行时消费(见 CFG_NOTIFY Runtime Certification)。该论断不得再作为产品行为依据。
 const cards = [
 	{ path: '/aidop/s8/config/scenes', title: '场景基础配置', desc: '场景编码、启用、排序' },
 	{ path: '/aidop/s8/config/exception-types', title: '异常类型', desc: '类型编码、SLA、责任角色' },
@@ -27,10 +34,9 @@ const cards = [
 	// S8-CONFIG-GLOBAL-ROW-SEMANTICS-AND-KPI-TARGET-1:运营指标目标值维护入口。
 	{ path: '/aidop/s8/config/kpi-targets', title: 'S9 KPI 目标值配置', desc: '运营指标目标值,支持按工厂覆盖平台默认' },
 	{ path: '/aidop/s8/config/roles', title: '角色权限 / 操作员绑定', desc: '操作员与员工的绑定维护' },
-	// 演示阶段隐藏(保留代码,后续恢复取消注释即可):
-	// { path: '/aidop/s8/config/notifications', title: '通知分层', desc: '场景 + 严重度 + 层级 + 推送渠道' },
-	// { path: '/aidop/s8/config/alert-rules', title: '报警规则', desc: '阈值与触发条件' },
-	// { path: '/aidop/s8/config/data-sources', title: '数据源', desc: '连接与启用' },
+	{ path: '/aidop/s8/config/data-sources', title: '数据源', desc: '连接与启用' },
+	{ path: '/aidop/s8/config/notifications', title: '通知分层', desc: '场景 + 严重度 + 层级 + 推送渠道' },
+	{ path: '/aidop/s8/config/alert-rules', title: '报警规则', desc: '历史兼容只读,运行时判定见监控规则' },
 ];
 
 function go(path: string) {

+ 87 - 4
Web/src/views/aidop/s8/dashboard/S8DashboardPage.vue

@@ -146,7 +146,66 @@ function savePreferences(config: DashboardPreferences) {
 const preferences = reactive<DashboardPreferences>(normalizePreferences(Local.get(DASHBOARD_PREFERENCE_KEY)));
 
 // ─── 时间筛选 ─────────────────────────────────────────────────
-const dashboardPeriod = shallowRef<S8Period | undefined>('this_month');
+// S8-FINAL-BLOCKERS-FIX-AND-TARGETED-CERT-1:
+// 原先这里写死 'this_month',而日期选择器由 createDefaultDateRange() 初始化为近 7 天,
+// 于是页面**首次渲染就自相矛盾**:芯片高亮「本月」,实际取数却按近 7 天。
+// 改为初始不选中任何预设——默认窗口仍由日期选择器决定(默认取数行为不变),
+// 但芯片不再显示一个与实际口径不符的高亮。
+const dashboardPeriod = shallowRef<S8Period | undefined>(undefined);
+
+/**
+ * 预设 → 日期选择器可表达的**闭区间** [start, end]。
+ *
+ * 语义严格镜像后端 `S8PeriodHelper.Resolve`(半开区间 [from, to)),
+ * 这里把 to 减一天换算成闭区间,保证「界面显示的范围」「请求里的 beginTime/endTime」
+ * 「后端按 period 自行解析的范围」三者一致。**不另造一套时间口径。**
+ *
+ * `last_24h` 返回 null:它是**跨小时**区间(now-24h ~ now),
+ * 日期粒度的选择器无法如实表达;此时清空选择器、只发 period,
+ * 由后端按其权威语义解析(后端契约:显式日期优先于 period,故清空是必要的)。
+ */
+function resolvePresetRange(period: S8Period | undefined): { start: Date; end: Date } | null {
+	if (!period) return null;
+	const today = new Date();
+	today.setHours(0, 0, 0, 0);
+	const day = (base: Date, offset: number) => {
+		const d = new Date(base);
+		d.setDate(d.getDate() + offset);
+		return d;
+	};
+	switch (period) {
+		case 'today':
+			return { start: today, end: today };
+		case 'this_week': {
+			// 与后端一致:周一为周起始
+			const offset = (today.getDay() - 1 + 7) % 7;
+			const monday = day(today, -offset);
+			return { start: monday, end: day(monday, 6) };
+		}
+		case 'this_month': {
+			const start = new Date(today.getFullYear(), today.getMonth(), 1);
+			return { start, end: new Date(today.getFullYear(), today.getMonth() + 1, 0) };
+		}
+		case 'this_quarter': {
+			const qStartMonth = Math.floor(today.getMonth() / 3) * 3;
+			const start = new Date(today.getFullYear(), qStartMonth, 1);
+			return { start, end: new Date(today.getFullYear(), qStartMonth + 3, 0) };
+		}
+		case 'last_7d':
+			return { start: day(today, -6), end: today };
+		case 'last_30d':
+			return { start: day(today, -29), end: today };
+		case 'last_24h':
+			return null; // 见上:交由后端按 period 解析
+		default:
+			return null;
+	}
+}
+
+/** 用户手动改日期后,取消预设高亮——避免芯片继续标注一个已不成立的范围。 */
+function onManualDateChange() {
+	dashboardPeriod.value = undefined;
+}
 
 // ─── 状态 ────────────────────────────────────────────────────
 const loading = ref(false);
@@ -192,7 +251,11 @@ function severityTagType(s: string) {
 	return 'info';
 }
 function statusTagType(s: string) {
-	return ({ NEW: 'info', ASSIGNED: '', IN_PROGRESS: 'warning', RESOLVED: 'success', CLOSED: 'success' } as any)[s] ?? 'info';
+	// S8-FINAL-BLOCKERS-FIX-AND-TARGETED-CERT-1:ASSIGNED 原映射为空字符串,
+	// 而 el-tag 的 type 只接受 primary/success/info/warning/danger,空串会触发
+	// 「Invalid prop: validation failed for prop "type"」告警(每渲染一条 ASSIGNED 行报 2 条)。
+	// 此前预设按钮失效、默认窗口取不到 ASSIGNED 行,该问题一直未暴露。
+	return ({ NEW: 'info', ASSIGNED: 'primary', IN_PROGRESS: 'warning', RESOLVED: 'success', CLOSED: 'success' } as any)[s] ?? 'info';
 }
 function fmtDate(v: string | null | undefined) {
 	return v ? new Date(v).toLocaleString('zh-CN', { hour12: false }) : '—';
@@ -568,7 +631,22 @@ function onDetailPageChange(page: number) {
 	void loadDetail();
 }
 
-watch(dashboardPeriod, () => { void loadOverview(); void loadChartData(); void loadDimTrends(); });
+// S8-FINAL-BLOCKERS-FIX-AND-TARGETED-CERT-1:预设点击必须**真正改变时间范围**。
+// 修复前这里只重新取数,`period` 变了但 dateStart/dateEnd 仍是旧值;
+// 而请求同时携带二者、后端「显式日期优先于 period」,导致预设按钮点了等于没点。
+watch(dashboardPeriod, (period) => {
+	if (period) {
+		const range = resolvePresetRange(period);
+		const filter = currentPanelFilter.value;
+		// last_24h 无法用日期粒度表达 → 清空显式日期,交后端按 period 解析。
+		filter.dateStart = range ? range.start : null;
+		filter.dateEnd = range ? range.end : null;
+	}
+	void loadOverview();
+	void loadChartData();
+	void loadDimTrends();
+	void loadDetail();
+});
 
 onMounted(() => void loadAll());
 onBeforeUnmount(() => { mainChart?.dispose(); trendChart?.dispose(); });
@@ -608,6 +686,7 @@ onBeforeUnmount(() => { mainChart?.dispose(); trendChart?.dispose(); });
 							type="date"
 							placeholder="选择日期"
 							style="width:130px"
+							@change="onManualDateChange"
 						/>
 					</el-form-item>
 					<el-form-item label="结束日期">
@@ -616,6 +695,7 @@ onBeforeUnmount(() => { mainChart?.dispose(); trendChart?.dispose(); });
 							type="date"
 							placeholder="选择日期"
 							style="width:130px"
+							@change="onManualDateChange"
 						/>
 					</el-form-item>
 					<el-form-item v-if="activePanel === 'process'" label="流程节点">
@@ -669,9 +749,12 @@ onBeforeUnmount(() => { mainChart?.dispose(); trendChart?.dispose(); });
 							<el-empty description="暂无数据">
 								<template #description>
 									<p style="color:#64748b;font-size:13px">当前面板暂无数据</p>
+									<!-- S8-FINAL-BLOCKERS-FIX-AND-TARGETED-CERT-1:原文案「请先执行数据库初始化脚本…」
+									     是面向实施人员的内部操作指引,不应出现在业务用户界面。
+									     改为如实描述当前状态,不暴露 DB / 脚本 / 迁移等内部实现。 -->
 									<p v-if="activePanel === 'object' || activePanel === 'process'"
 										style="color:#94a3b8;font-size:12px;margin-top:4px">
-										请先执行数据库初始化脚本,为异常记录分配流程节点和关联对象
+										当前时间范围内的异常尚无流程节点与关联对象信息
 									</p>
 								</template>
 							</el-empty>