Jelajahi Sumber

fix(s8): refine exception detail fallback and metrics labels

YY968XX 1 bulan lalu
induk
melakukan
c27e50e347

+ 12 - 5
Web/src/views/aidop/s8/api/s8ExceptionApi.ts

@@ -4,6 +4,10 @@ function unwrap<T>(res: { data: T }): T {
 	return res.data;
 }
 
+function silentHeaders(silent?: boolean) {
+	return silent ? { 'X-Silent-Error': '1' } : undefined;
+}
+
 export interface S8Paged<T> {
 	total: number;
 	page: number;
@@ -77,11 +81,14 @@ export const s8ExceptionApi = {
 		service.get<S8Paged<S8ExceptionRow>>('/api/aidop/s8/exceptions', { params }).then(unwrap),
 	filterOptions: (params?: { tenantId?: number; factoryId?: number }) =>
 		service.get('/api/aidop/s8/exceptions/filter-options', { params }).then(unwrap),
-	detail: (id: number, params?: { tenantId?: number; factoryId?: number }) =>
-		service.get(`/api/aidop/s8/exceptions/${id}`, { params }).then(unwrap),
-	timeline: (id: number) => service.get(`/api/aidop/s8/exceptions/${id}/timeline`).then(unwrap),
-	decisions: (id: number) => service.get(`/api/aidop/s8/exceptions/${id}/decisions`).then(unwrap),
-	evidences: (id: number) => service.get(`/api/aidop/s8/exceptions/${id}/evidences`).then(unwrap),
+	detail: (id: number, params?: { tenantId?: number; factoryId?: number }, options?: { silentError?: boolean }) =>
+		service.get(`/api/aidop/s8/exceptions/${id}`, { params, headers: silentHeaders(options?.silentError) }).then(unwrap),
+	timeline: (id: number, options?: { silentError?: boolean }) =>
+		service.get(`/api/aidop/s8/exceptions/${id}/timeline`, { headers: silentHeaders(options?.silentError) }).then(unwrap),
+	decisions: (id: number, options?: { silentError?: boolean }) =>
+		service.get(`/api/aidop/s8/exceptions/${id}/decisions`, { headers: silentHeaders(options?.silentError) }).then(unwrap),
+	evidences: (id: number, options?: { silentError?: boolean }) =>
+		service.get(`/api/aidop/s8/exceptions/${id}/evidences`, { headers: silentHeaders(options?.silentError) }).then(unwrap),
 	employees: (params?: { factoryRefId?: number }) =>
 		service.get('/api/aidop/s8/master-data/employees', { params }).then(unwrap),
 	claim: (id: number, body: { assigneeId: number; remark?: string }) =>

+ 12 - 5
Web/src/views/aidop/s8/exceptions/S8TaskDetailPage.vue

@@ -64,16 +64,23 @@ async function loadDetail() {
 	if (!id) return;
 	loading.value = true;
 	try {
-		const [detailRes, timelineRes, decisionRes, evidenceRes] = await Promise.all([
-			s8ExceptionApi.detail(id),
-			s8ExceptionApi.timeline(id),
-			s8ExceptionApi.decisions(id),
-			s8ExceptionApi.evidences(id),
+		const detailRes = await s8ExceptionApi.detail(id, undefined, { silentError: true });
+		const [timelineRes, decisionRes, evidenceRes] = await Promise.all([
+			s8ExceptionApi.timeline(id, { silentError: true }).catch(() => []),
+			s8ExceptionApi.decisions(id, { silentError: true }).catch(() => []),
+			s8ExceptionApi.evidences(id, { silentError: true }).catch(() => []),
 		]);
 		detail.value = detailRes as Record<string, any>;
 		timeline.value = timelineRes as typeof timeline.value;
 		decisions.value = decisionRes as S8DecisionRow[];
 		evidences.value = evidenceRes as S8EvidenceRow[];
+	} catch {
+		detail.value = null;
+		timeline.value = [];
+		decisions.value = [];
+		evidences.value = [];
+		ElMessage.warning('该异常记录不存在或已被清理,已返回异常列表');
+		router.replace('/aidop/s8/exceptions');
 	} finally {
 		loading.value = false;
 	}

+ 15 - 36
Web/src/views/aidop/s8/monitoring/components/S8MonitoringStageCard.vue

@@ -97,10 +97,7 @@ const timeoutDisplayCount = computed(() => safeNum(props.timeoutCount));
 		>
 			<template #metric-label>
 				<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>
+					<span class="stage-card__pending-label">当前待处理异常</span>
 				</div>
 			</template>
 
@@ -114,14 +111,14 @@ const timeoutDisplayCount = computed(() => safeNum(props.timeoutCount));
 						<span class="stage-card__summary-key">严重</span>
 						<span class="stage-card__summary-val">{{ seriousCount }}</span>
 					</span>
-					<span class="stage-card__summary-item stage-card__summary-item--orange">
+					<span class="stage-card__summary-item stage-card__summary-item--purple">
 						<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-key">健康订单</span>
 					<span class="stage-card__normal-val">{{ normalCount }}</span>
 				</div>
 			</template>
@@ -152,7 +149,6 @@ const timeoutDisplayCount = computed(() => safeNum(props.timeoutCount));
 
 			<template #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}%` }" />
@@ -292,25 +288,13 @@ const timeoutDisplayCount = computed(() => safeNum(props.timeoutCount));
 	gap: 4px;
 }
 
-.stage-card__pending-head {
-	display: flex;
-	align-items: baseline;
-	gap: 10px;
-}
-
 .stage-card__pending-label {
-	font-size: 12px;
+	font-size: 13px;
+	font-weight: 600;
 	color: #c6c6cd;
 	letter-spacing: 0.04em;
 }
 
-.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;
@@ -328,10 +312,9 @@ const timeoutDisplayCount = computed(() => safeNum(props.timeoutCount));
 	font-weight: 600;
 }
 
+/* S8-STAGE-CARD-SEMANTIC-LAYOUT-EXEC-1:标签与数字同色(黄/红/紫) */
 .stage-card__summary-key {
 	font-weight: 500;
-	color: #c6c6cd;
-	opacity: 0.85;
 }
 
 .stage-card__summary-val {
@@ -340,26 +323,31 @@ const timeoutDisplayCount = computed(() => safeNum(props.timeoutCount));
 	line-height: 1;
 }
 
+.stage-card__summary-item--yellow .stage-card__summary-key,
 .stage-card__summary-item--yellow .stage-card__summary-val { color: #ffc107; }
+.stage-card__summary-item--red .stage-card__summary-key,
 .stage-card__summary-item--red .stage-card__summary-val { color: #ff6450; }
-.stage-card__summary-item--orange .stage-card__summary-val { color: #ff9b5a; }
+.stage-card__summary-item--purple .stage-card__summary-key,
+.stage-card__summary-item--purple .stage-card__summary-val { color: #a78bfa; }
 
 /* S8-STAGE-CARD-SEMANTIC-LAYOUT-EXEC-1:正常执行订单 次级信息 */
+/* 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;
+	font-size: 13px;
 }
 
 .stage-card__normal-key {
-	opacity: 0.85;
+	font-weight: 500;
+	color: #6de039;
 }
 
 .stage-card__normal-val {
 	font-family: 'Roboto Mono', monospace;
+	font-size: 16px;
 	font-weight: 600;
 	color: #6de039;
 }
@@ -424,15 +412,6 @@ const timeoutDisplayCount = computed(() => safeNum(props.timeoutCount));
 	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;