Bläddra i källkod

fix(s8): optimize stage card semantic layout

YY968XX 4 månader sedan
förälder
incheckning
10ecbe24bb

+ 6 - 0
Web/src/views/aidop/s8/monitoring/S8MonitoringDeliveryPage.vue

@@ -273,6 +273,9 @@ async function loadPageConfig() {
 }
 
 const moduleMap = computed(() => new Map(moduleData.list.map((m) => [m.moduleCode, m])));
+// S8-STAGE-CARD-SEMANTIC-LAYOUT-EXEC-1:summary.byModule 提供每模块 timeout(S8ModuleOrderSummary 不含),
+// 拼到 stage card 的 timeoutCount 上。
+const moduleTimeoutMap = computed(() => new Map((summary.byModule ?? []).map((m) => [m.moduleCode, m.timeout ?? 0])));
 
 // S8-MONITORING-PAGES-STAGE-CARD-SEMANTICS-UNIFY-1:与 overview 口径一致:
 // - healthRate = closeRate;total=0 时空态显示 `闭环率 --`
@@ -284,6 +287,7 @@ const stageCards = computed(() =>
 		const total  = Math.max(m?.total ?? 0, 0);
 		const red    = Math.max(m?.red ?? 0, 0);
 		const yellow = Math.max(m?.yellow ?? 0, 0);
+		const timeout = Math.max(moduleTimeoutMap.value.get(meta.code) ?? 0, 0);
 		const closeRate = Math.max(0, Math.min(100, Math.round(m?.closeRate ?? 0)));
 		const avgProcessHours = m?.avgProcessHours ?? 0;
 		const tone: 'green' | 'yellow' | 'red' = red > 0 ? 'red' : yellow > 0 ? 'yellow' : 'green';
@@ -306,6 +310,8 @@ const stageCards = computed(() =>
 			healthRate: closeRate,
 			healthRateText: hasSamples ? undefined : '--',
 			statusLabel: `${red + yellow} 异常`,
+			// S8-STAGE-CARD-SEMANTIC-LAYOUT-EXEC-1:透传超时数字给 StageCard summary-row。
+			timeoutCount: timeout,
 			progress: denom > 0
 				? { green: (normal / denom) * 100, yellow: (yellow / denom) * 100, red: (red / denom) * 100 }
 				: { green: 0, yellow: 0, red: 0 },

+ 2 - 0
Web/src/views/aidop/s8/monitoring/S8MonitoringOverviewPage.vue

@@ -513,6 +513,8 @@ function buildStageCard(
 		healthRate: closeRate,
 		healthRateText: hasSamples ? undefined : '--',
 		statusLabel: timeout > 0 ? `${timeout} 超时` : `${anomalyCount} 异常`,
+		// S8-STAGE-CARD-SEMANTIC-LAYOUT-EXEC-1:透传超时数字给 StageCard summary-row。
+		timeoutCount: timeout,
 		progress: toProgress(normal, yellow, red),
 		tone,
 		wide: meta.wide ?? false,

+ 5 - 0
Web/src/views/aidop/s8/monitoring/S8MonitoringProductionPage.vue

@@ -263,6 +263,8 @@ async function loadPageConfig() {
 }
 
 const moduleMap = computed(() => new Map(moduleData.list.map((m) => [m.moduleCode, m])));
+// S8-STAGE-CARD-SEMANTIC-LAYOUT-EXEC-1:summary.byModule 提供每模块 timeout,注入到 stage card timeoutCount。
+const moduleTimeoutMap = computed(() => new Map((summary.byModule ?? []).map((m) => [m.moduleCode, m.timeout ?? 0])));
 
 // S8-MONITORING-PAGES-STAGE-CARD-SEMANTICS-UNIFY-1:与 overview 口径一致。S2 主数字改为 total(去除 frequency)。
 const stageCards = computed(() =>
@@ -271,6 +273,7 @@ const stageCards = computed(() =>
 		const total  = Math.max(m?.total ?? 0, 0);
 		const red    = Math.max(m?.red ?? 0, 0);
 		const yellow = Math.max(m?.yellow ?? 0, 0);
+		const timeout = Math.max(moduleTimeoutMap.value.get(meta.code) ?? 0, 0);
 		const closeRate = Math.max(0, Math.min(100, Math.round(m?.closeRate ?? 0)));
 		const avgProcessHours = m?.avgProcessHours ?? 0;
 		const tone: 'green' | 'yellow' | 'red' = red > 0 ? 'red' : yellow > 0 ? 'yellow' : 'green';
@@ -293,6 +296,8 @@ const stageCards = computed(() =>
 			healthRate: closeRate,
 			healthRateText: hasSamples ? undefined : '--',
 			statusLabel: `${red + yellow} 异常`,
+			// S8-STAGE-CARD-SEMANTIC-LAYOUT-EXEC-1:透传超时数字给 StageCard summary-row。
+			timeoutCount: timeout,
 			progress: denom > 0
 				? { green: (normal / denom) * 100, yellow: (yellow / denom) * 100, red: (red / denom) * 100 }
 				: { green: 0, yellow: 0, red: 0 },

+ 5 - 0
Web/src/views/aidop/s8/monitoring/S8MonitoringSupplyPage.vue

@@ -274,6 +274,8 @@ async function loadPageConfig() {
 }
 
 const moduleMap = computed(() => new Map(moduleData.list.map((m) => [m.moduleCode, m])));
+// S8-STAGE-CARD-SEMANTIC-LAYOUT-EXEC-1:summary.byModule 提供每模块 timeout,注入到 stage card timeoutCount。
+const moduleTimeoutMap = computed(() => new Map((summary.byModule ?? []).map((m) => [m.moduleCode, m.timeout ?? 0])));
 
 // S8-MONITORING-PAGES-STAGE-CARD-SEMANTICS-UNIFY-1:与 overview 口径一致。
 // S4/S5 主数字一律改为 total(不再用 avgHours / closeRate%),均时/闭环率统一在底部 wideRateLabel 展示。
@@ -283,6 +285,7 @@ const stageCards = computed(() =>
 		const total  = Math.max(m?.total ?? 0, 0);
 		const red    = Math.max(m?.red ?? 0, 0);
 		const yellow = Math.max(m?.yellow ?? 0, 0);
+		const timeout = Math.max(moduleTimeoutMap.value.get(meta.code) ?? 0, 0);
 		const closeRate = Math.max(0, Math.min(100, Math.round(m?.closeRate ?? 0)));
 		const avgProcessHours = m?.avgProcessHours ?? 0;
 		const tone: 'green' | 'yellow' | 'red' = red > 0 ? 'red' : yellow > 0 ? 'yellow' : 'green';
@@ -305,6 +308,8 @@ const stageCards = computed(() =>
 			healthRate: closeRate,
 			healthRateText: hasSamples ? undefined : '--',
 			statusLabel: `${red + yellow} 异常`,
+			// S8-STAGE-CARD-SEMANTIC-LAYOUT-EXEC-1:透传超时数字给 StageCard summary-row。
+			timeoutCount: timeout,
 			progress: denom > 0
 				? { green: (normal / denom) * 100, yellow: (yellow / denom) * 100, red: (red / denom) * 100 }
 				: { green: 0, yellow: 0, red: 0 },

+ 2 - 0
Web/src/views/aidop/s8/monitoring/components/S8MonitoringModulesGrid.vue

@@ -60,6 +60,8 @@ interface StageCardData {
   // S8-MODULE-CARD-ZERO-TOTAL-EMPTY-STATE-1:空态文本(如 "--"),优先于 `${healthRate}%` 渲染。
   healthRateText?: string
   statusLabel: string
+  // S8-STAGE-CARD-SEMANTIC-LAYOUT-EXEC-1:超时计数(数字,独立于旧 statusLabel 字符串)。
+  timeoutCount?: number
   progress: { green: number; yellow: number; red: number }
   tone: 'green' | 'yellow' | 'red'
   wide?: boolean

+ 118 - 30
Web/src/views/aidop/s8/monitoring/components/S8MonitoringStageCard.vue

@@ -2,7 +2,12 @@
 import { computed, type Component } from 'vue';
 import CardBlockLayout from './CardBlockLayout.vue';
 
-const DEFAULT_BLOCK_ORDER = ['metric-label', 'values', 'health-rate', 'status-label', 'progress', 'wide-meter'];
+// S8-STAGE-CARD-SEMANTIC-LAYOUT-EXEC-1:
+// 默认布局移除 `health-rate` / `status-label`:
+//   - 「闭环率」原本被同时渲染在 health-rate 块和 wide-meter 块两次,去重后仅保留 wide-meter。
+//   - status-label 原显示 "N 异常" / "N 超时" 字符串,改由 values 块内"关注/严重/超时"摘要行替代。
+// 旧值仍保留作为已有 localStorage 配置的兼容渲染目标,但默认顺序不再包含。
+const DEFAULT_BLOCK_ORDER = ['metric-label', 'values', 'progress', 'wide-meter'];
 
 const props = defineProps<{
 	code: string;
@@ -17,6 +22,10 @@ const props = defineProps<{
 	// S8-MODULE-CARD-ZERO-TOTAL-EMPTY-STATE-1:空态文本(如「--」),优先于 `${healthRate}%` 渲染。
 	healthRateText?: string;
 	statusLabel: string;
+	// S8-STAGE-CARD-SEMANTIC-LAYOUT-EXEC-1:
+	// 旧版 statusLabel 是「N 异常」/「N 超时」字符串,新布局需要分别拆出"关注/严重/超时"三个数字。
+	// 不解析 statusLabel 字符串,而是新增 timeoutCount 数字 prop(关注/严重直接复用 values.yellow/red)。
+	timeoutCount?: number;
 	progress: {
 		green: number;
 		yellow: number;
@@ -57,6 +66,18 @@ const shouldShowMetricLabel = computed(() => props.showMetricLabel ?? true);
 const shouldShowStatusLabel = computed(() => props.showStatusLabel ?? true);
 const shouldShowProgress = computed(() => props.showProgress ?? true);
 const isWide = computed(() => (props.cardSpan ?? 1) > 1);
+
+// S8-STAGE-CARD-SEMANTIC-LAYOUT-EXEC-1:从 values 解析自然数(绿=正常,黄=关注,红=严重)。
+function safeNum(v: unknown): number {
+	const n = Number(v);
+	return Number.isFinite(n) && n >= 0 ? Math.floor(n) : 0;
+}
+
+const normalCount = computed(() => safeNum(props.values?.green));
+const followCount = computed(() => safeNum(props.values?.yellow));
+const seriousCount = computed(() => safeNum(props.values?.red));
+const pendingCount = computed(() => followCount.value + seriousCount.value);
+const timeoutDisplayCount = computed(() => safeNum(props.timeoutCount));
 </script>
 
 <template>
@@ -75,32 +96,46 @@ const isWide = computed(() => (props.cardSpan ?? 1) > 1);
 			@update:blocks="emit('update:blockOrder', $event)"
 		>
 			<template #metric-label>
-				<div v-if="shouldShowMetricLabel" class="stage-card__metric" :style="metricLabelFontSize ? { fontSize: `${metricLabelFontSize}px` } : {}">{{ metricLabel }}</div>
+				<div v-if="shouldShowMetricLabel" class="stage-card__pending">
+					<div class="stage-card__pending-head">
+						<span class="stage-card__pending-label">当前待处理异常</span>
+						<span class="stage-card__pending-num">{{ pendingCount }}</span>
+					</div>
+				</div>
 			</template>
 
 			<template #values>
-				<div v-if="values" class="stage-card__values" :class="{ 'stage-card__values--wide': isWide }">
-					<span class="stage-card__value-item stage-card__value-item--green">
-						<span v-if="valueLabels?.green" class="stage-card__value-label">{{ valueLabels.green }},</span>{{ values.green }}
+				<div v-if="values" class="stage-card__summary-row">
+					<span class="stage-card__summary-item stage-card__summary-item--yellow">
+						<span class="stage-card__summary-key">关注</span>
+						<span class="stage-card__summary-val">{{ followCount }}</span>
 					</span>
-					<span v-if="!isWide" class="stage-card__value-sep"> </span>
-					<span class="stage-card__value-item stage-card__value-item--yellow">
-						<span v-if="valueLabels?.yellow" class="stage-card__value-label">{{ valueLabels.yellow }},</span>{{ values.yellow }}
+					<span class="stage-card__summary-item stage-card__summary-item--red">
+						<span class="stage-card__summary-key">严重</span>
+						<span class="stage-card__summary-val">{{ seriousCount }}</span>
 					</span>
-					<span v-if="!isWide" class="stage-card__value-sep"> </span>
-					<span class="stage-card__value-item stage-card__value-item--red">
-						<span v-if="valueLabels?.red" class="stage-card__value-label">{{ valueLabels.red }},</span>{{ values.red }}
+					<span class="stage-card__summary-item stage-card__summary-item--orange">
+						<span class="stage-card__summary-key">超时</span>
+						<span class="stage-card__summary-val">{{ timeoutDisplayCount }}</span>
 					</span>
 				</div>
 				<div v-else class="stage-card__value" :class="{ 'stage-card__value--wide': isWide }">{{ value }}</div>
+				<div v-if="values" class="stage-card__normal-row">
+					<span class="stage-card__normal-key">正常执行订单</span>
+					<span class="stage-card__normal-val">{{ normalCount }}</span>
+				</div>
 			</template>
 
+			<!-- S8-STAGE-CARD-SEMANTIC-LAYOUT-EXEC-1:health-rate 旧块("闭环率 X%")已与 wide-meter 合并去重;
+			     仅当用户的 localStorage 中显式包含 `health-rate` 时才会渲染(向后兼容)。 -->
 			<template #health-rate>
 				<div class="stage-card__stats stage-card__stats--health">
 					<span>闭环率 {{ healthRateText ?? `${healthRate}%` }}</span>
 				</div>
 			</template>
 
+			<!-- S8-STAGE-CARD-SEMANTIC-LAYOUT-EXEC-1:status-label 旧块("N 异常 / N 超时")已被 summary-row 替代;
+			     仅当 stored blockOrder 显式包含 `status-label` 时才会渲染。 -->
 			<template #status-label>
 				<div v-if="shouldShowStatusLabel" class="stage-card__stats stage-card__stats--status">
 					<span>{{ statusLabel }}</span>
@@ -116,7 +151,8 @@ const isWide = computed(() => (props.cardSpan ?? 1) > 1);
 			</template>
 
 			<template #wide-meter>
-				<div v-if="isWide" class="stage-card__wide-meter">
+				<div class="stage-card__wide-meter">
+					<div class="stage-card__efficiency-title">处理效率</div>
 					<div class="stage-card__wide-label">{{ wideRateLabel || `闭环率 ${healthRateText ?? `${healthRate}%`}` }}</div>
 					<div class="stage-card__wide-track">
 						<div class="stage-card__wide-fill" :style="{ width: `${clampedWideRate}%` }" />
@@ -249,42 +285,83 @@ const isWide = computed(() => (props.cardSpan ?? 1) > 1);
 	justify-content: space-between;
 }
 
-.stage-card__values {
+/* S8-STAGE-CARD-SEMANTIC-LAYOUT-EXEC-1:当前待处理异常 主指标块 */
+.stage-card__pending {
+	display: flex;
+	flex-direction: column;
+	gap: 4px;
+}
+
+.stage-card__pending-head {
 	display: flex;
 	align-items: baseline;
-	gap: 12px;
+	gap: 10px;
 }
 
-.stage-card__values--wide {
-	flex-wrap: wrap;
+.stage-card__pending-label {
+	font-size: 12px;
+	color: #c6c6cd;
+	letter-spacing: 0.04em;
 }
 
-.stage-card__value-item {
-	display: inline-flex;
-	align-items: baseline;
-	gap: 2px;
-	min-width: 56px;
-	font-size: 28px;
+.stage-card__pending-num {
+	font-size: 32px;
 	line-height: 1;
 	font-weight: 700;
+	color: #e1e2eb;
+}
+
+/* S8-STAGE-CARD-SEMANTIC-LAYOUT-EXEC-1:摘要行(关注 / 严重 / 超时) */
+.stage-card__summary-row {
+	display: flex;
+	align-items: baseline;
+	gap: 16px;
+	flex-wrap: wrap;
+	margin-top: 4px;
 }
 
-.stage-card__value-label {
+.stage-card__summary-item {
+	display: inline-flex;
+	align-items: baseline;
+	gap: 4px;
 	font-size: 13px;
+	font-weight: 600;
+}
+
+.stage-card__summary-key {
 	font-weight: 500;
+	color: #c6c6cd;
 	opacity: 0.85;
 }
 
-.stage-card__value-item--green {
-	color: #6de039;
+.stage-card__summary-val {
+	font-family: 'Roboto Mono', monospace;
+	font-size: 16px;
+	line-height: 1;
 }
 
-.stage-card__value-item--yellow {
-	color: #ffc107;
+.stage-card__summary-item--yellow .stage-card__summary-val { color: #ffc107; }
+.stage-card__summary-item--red .stage-card__summary-val { color: #ff6450; }
+.stage-card__summary-item--orange .stage-card__summary-val { color: #ff9b5a; }
+
+/* S8-STAGE-CARD-SEMANTIC-LAYOUT-EXEC-1:正常执行订单 次级信息 */
+.stage-card__normal-row {
+	display: flex;
+	align-items: baseline;
+	gap: 6px;
+	margin-top: 6px;
+	font-size: 12px;
+	color: #c6c6cd;
+}
+
+.stage-card__normal-key {
+	opacity: 0.85;
 }
 
-.stage-card__value-item--red {
-	color: #ff6450;
+.stage-card__normal-val {
+	font-family: 'Roboto Mono', monospace;
+	font-weight: 600;
+	color: #6de039;
 }
 
 .stage-card__value {
@@ -300,6 +377,7 @@ const isWide = computed(() => (props.cardSpan ?? 1) > 1);
 	font-size: 42px;
 }
 
+/* 旧 metric 单行(保留作为 health-rate / status-label 兼容样式根) */
 .stage-card__metric {
 	margin-top: 6px;
 	font-size: 12px;
@@ -343,12 +421,22 @@ const isWide = computed(() => (props.cardSpan ?? 1) > 1);
 .stage-card__wide-meter {
 	flex: 1;
 	max-width: 100%;
+	margin-top: 4px;
+}
+
+/* S8-STAGE-CARD-SEMANTIC-LAYOUT-EXEC-1:处理效率小标题 */
+.stage-card__efficiency-title {
+	font-size: 12px;
+	font-weight: 600;
+	color: #c6c6cd;
+	letter-spacing: 0.04em;
+	margin-bottom: 4px;
 }
 
 .stage-card__wide-label {
 	display: flex;
 	justify-content: space-between;
-	margin-bottom: 10px;
+	margin-bottom: 6px;
 	font-size: 11px;
 	color: #c6c6cd;
 }

+ 4 - 0
Web/src/views/aidop/s8/monitoring/useS8StageConfig.ts

@@ -57,6 +57,8 @@ export interface StageCardPreviewData {
 	// S8-MODULE-CARD-ZERO-TOTAL-EMPTY-STATE-1:空态文本(如 "--"),优先于 `${healthRate}%` 渲染。
 	healthRateText?: string;
 	statusLabel: string;
+	// S8-STAGE-CARD-SEMANTIC-LAYOUT-EXEC-1:超时计数(数字,独立于 statusLabel 字符串)。
+	timeoutCount?: number;
 	progress: StageProgress;
 	tone: StageTone;
 	wide?: boolean;
@@ -310,6 +312,8 @@ export function useS8StageConfig() {
 			showMetricLabel: card.showMetricLabel ?? true,
 			showStatusLabel: card.showStatusLabel ?? true,
 			showProgress: card.showProgress ?? true,
+			// S8-STAGE-CARD-SEMANTIC-LAYOUT-EXEC-1:透传超时计数。
+			timeoutCount: card.timeoutCount,
 		};
 	}