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

feat(s8): refine monitoring grid cards and order status display

TASK-004: 9-card 3×3 grid; S8/S9 demoted to SummaryCard; sidebar/analysis panels hidden; LAYOUT_VERSION→2
TASK-005: severity-before-attention ordering; red bar left of yellow; remove health-order/timeout display; add S8SeverityAttentionRows component
TASK-006: order status auxiliary row (mock) on all S1-S7 cards via ORDER_STATUS_MOCK; field registration in useS8StageConfig
YY968XX 2 недель назад
Родитель
Сommit
a44dff1fe1

+ 3 - 1
Web/src/views/aidop/s8/monitoring/S8MonitoringDeliveryPage.vue

@@ -118,7 +118,7 @@ import S8MonitoringEditToolbar from './components/S8MonitoringEditToolbar.vue';
 import S8MonitoringStageConfigDrawer from './components/S8MonitoringStageConfigDrawer.vue';
 import S8DeliveryTrendChart from './components/S8DeliveryTrendChart.vue';
 import { useS8UnsavedLayoutEditGuard } from './useS8UnsavedLayoutEditGuard';
-import { useS8StageConfig, buildModuleTriColorStats, DEFAULT_NORMAL_WORK_ORDER_COUNT } from './useS8StageConfig';
+import { useS8StageConfig, buildModuleTriColorStats, DEFAULT_NORMAL_WORK_ORDER_COUNT, ORDER_STATUS_MOCK } from './useS8StageConfig';
 import { useS8CategoryConfig } from './useS8CategoryConfig';
 import { useS8PageConfigDriver } from './useS8PageConfigDriver';
 
@@ -322,6 +322,8 @@ const stageCards = computed(() =>
 			wideRate: closeRate,
 			wideRateLabel: `闭环率 ${closeRateText} / 均时 ${avgHoursText}`,
 			avgProcessHours,
+			// TASK-006 [MOCK]:S1-S7 全部卡片注入订单状态辅助行
+			orderStatus: ORDER_STATUS_MOCK,
 		};
 		return applyConfig(raw);
 	}),

+ 70 - 124
Web/src/views/aidop/s8/monitoring/S8MonitoringOverviewPage.vue

@@ -13,12 +13,15 @@
 		<S8MonitoringResizableShell
 			:shell="draftLayout.shell"
 			:editable="editMode"
+			:hide-sidebar="true"
+			:hide-analysis="true"
 			@update:shell="onShellUpdate"
 		>
 			<template #modules>
 				<section class="s8-monitor__modules glass-panel">
 					<S8MonitoringModulesGrid
 						:cards="allStageCards"
+						:summary-cards="allSummaryCards"
 						:layout="draftLayout.modules"
 						:editable="editMode"
 						:row-height="60"
@@ -29,94 +32,6 @@
 					/>
 				</section>
 			</template>
-
-			<template #sidebar>
-				<aside class="s8-monitor__sidebar glass-panel">
-					<template v-if="pageConfig.hasDeptCluster.value">
-					<div class="section-title">
-						<div class="section-title__bar" />
-						<h2 class="section-title__text">S9 部门效率分析</h2>
-					</div>
-
-					<div class="s8-monitor__dept-list">
-						<article
-							v-for="dept in configuredDeptCards"
-							:key="dept.deptName"
-							class="dept-card"
-							:class="[`dept-card--${dept.tone}`, { 'dept-card--dragging': deptDraggingKey === dept.deptName, 'dept-card--dragover': deptDragOverKey === dept.deptName }]"
-							draggable="true"
-							@dragstart="onDeptDragStart(dept.deptName)"
-							@dragover.prevent="onDeptDragOver(dept.deptName)"
-							@drop.prevent="onDeptDrop(dept.deptName)"
-							@dragend="onDeptDragEnd"
-						>
-							<div class="dept-card__head">
-								<span class="dept-card__name">{{ dept.deptName }}</span>
-								<span class="dept-card__badge" :class="`dept-card__badge--${dept.tone}`">
-									{{ dept.levelLabel }}
-								</span>
-							</div>
-
-							<div class="dept-card__metrics">
-								<div class="dept-card__metric">
-									<div class="dept-card__metric-label">异常数</div>
-									<div class="dept-card__metric-value">{{ dept.totalText }}</div>
-								</div>
-								<div class="dept-card__metric">
-									<div class="dept-card__metric-label">均时</div>
-									<div class="dept-card__metric-value">{{ dept.avgHoursText }}</div>
-								</div>
-								<div class="dept-card__metric">
-									<div class="dept-card__metric-label">关闭率</div>
-									<div class="dept-card__metric-value dept-card__metric-value--accent">{{ dept.closeRateText }}</div>
-								</div>
-							</div>
-						</article>
-					</div>
-					</template>
-
-					<div class="s8-monitor__efficiency">
-						<div class="s8-monitor__efficiency-head">
-							<span>整体响应效能</span>
-							<span :class="`s8-monitor__efficiency-label s8-monitor__efficiency-label--${overallEfficiency.tone}`">
-								{{ overallEfficiency.label }}
-							</span>
-						</div>
-						<div class="s8-monitor__efficiency-track">
-							<div class="s8-monitor__efficiency-fill" :style="{ width: `${overallEfficiency.score}%` }" />
-						</div>
-					</div>
-				</aside>
-			</template>
-
-			<template #analysis>
-				<section class="s8-monitor__analysis glass-panel">
-					<div class="s8-monitor__analysis-head">
-						<div class="section-title">
-							<div class="section-title__bar section-title__bar--dim" />
-							<h2 class="section-title__text">S8 功能异常多维分析</h2>
-						</div>
-
-					<div class="s8-monitor__tabs" aria-hidden="true">
-						<!-- S8-MONITORING-DEMO-SEMANTICS-CLOSURE-1:原「最近24小时 / 实时快照」装饰文案与实际全量口径不符,改为「累计统计」。 -->
-						<span class="s8-monitor__tab s8-monitor__tab--active">累计统计</span>
-					</div>
-				</div>
-
-					<div class="s8-monitor__analysis-grid">
-						<S8MonitoringCategoryGrid
-							:cards="configuredCategoryCards"
-							:layout="draftLayout.analysis"
-							:editable="editMode"
-							:row-height="56"
-							:gap="12"
-							@update:layout="onAnalysisLayoutUpdate"
-						/>
-					</div>
-
-					<!-- S8-MONITORING-TOP-STATUS-STRIP-REMOVE-1:移除底部全局状态条(与模块卡内三色重复且口径不清晰)。 -->
-				</section>
-			</template>
 		</S8MonitoringResizableShell>
 
 		<S8MonitoringStageConfigDrawer
@@ -151,7 +66,6 @@ import {
 } from '@element-plus/icons-vue';
 import {
 	s8MonitoringApi,
-	type S8CategorySummary,
 	type S8DeptSummary,
 	type S8ModuleOrderSummary,
 	type S8ModuleSummaryItem,
@@ -162,11 +76,10 @@ import { deepClone, useS8Layout } from './useS8Layout';
 import type { S8LayoutSchema } from './useS8Layout';
 import { DEMO_LAYOUT } from './useS8Layout';
 import S8MonitoringResizableShell from './components/S8MonitoringResizableShell.vue';
-import S8MonitoringModulesGrid from './components/S8MonitoringModulesGrid.vue';
-import S8MonitoringCategoryGrid, { type CategoryGridCardData } from './components/S8MonitoringCategoryGrid.vue';
+import S8MonitoringModulesGrid, { type SummaryCardData } from './components/S8MonitoringModulesGrid.vue';
 import S8MonitoringEditToolbar from './components/S8MonitoringEditToolbar.vue';
 import S8MonitoringStageConfigDrawer from './components/S8MonitoringStageConfigDrawer.vue';
-import { useS8StageConfig, buildModuleTriColorStats, DEFAULT_NORMAL_WORK_ORDER_COUNT } from './useS8StageConfig';
+import { useS8StageConfig, buildModuleTriColorStats, DEFAULT_NORMAL_WORK_ORDER_COUNT, ORDER_STATUS_MOCK } from './useS8StageConfig';
 import { useS8CategoryConfig } from './useS8CategoryConfig';
 import { useS9DeptConfig } from './useS9DeptConfig';
 import { useS8UnsavedLayoutEditGuard } from './useS8UnsavedLayoutEditGuard';
@@ -249,7 +162,7 @@ async function loadPageConfig() {
 const { layout: persistedLayout, save, resetToDefault, restoreDemo } = useS8Layout();
 const { stageConfigState, initializeFromCards, applyConfig, reset: resetStageConfigState } = useS8StageConfig();
 const { categoryConfigState, initializeFromCategories, applyConfig: applyCategoryConfig, reset: resetCategoryConfigState } = useS8CategoryConfig();
-const { deptConfigState, initializeFromDepts, applyConfig: applyDeptConfig, reorder: reorderDept, reset: resetDeptConfigState } = useS9DeptConfig();
+const { deptConfigState, initializeFromDepts, applyConfig: applyDeptConfig, reset: resetDeptConfigState } = useS9DeptConfig();
 
 const editMode = shallowRef(false);
 const configDrawerVisible = shallowRef(false);
@@ -354,9 +267,6 @@ function onModulesLayoutUpdate(modules: S8LayoutSchema['modules']) {
 	draftLayout.modules.splice(0, draftLayout.modules.length, ...modules);
 }
 
-function onAnalysisLayoutUpdate(analysis: S8LayoutSchema['analysis']) {
-	draftLayout.analysis.splice(0, draftLayout.analysis.length, ...analysis);
-}
 
 const layoutVars = computed<CSSProperties>(() => ({
 	'--grid-gap': '24px',
@@ -394,6 +304,66 @@ const stageCards = computed(() => {
 
 const allStageCards = computed(() => stageCards.value.map((card) => applyConfig(card)));
 
+// ─── S8/S9 同级摘要卡 ───────────────────────────────────────────────────────
+
+const s8SummaryCard = computed<SummaryCardData>(() => {
+	const cats = gridData.byCategory;
+	const totalAnomalies = cats.reduce((sum, c) => sum + Math.max(c.total, 0), 0);
+	const topCat = cats.length ? [...cats].sort((a, b) => b.total - a.total)[0] : null;
+	const avgCloseRate = cats.length
+		? cats.reduce((sum, c) => sum + clampPercent(c.closeRate), 0) / cats.length
+		: 0;
+	const avgHours = cats.length
+		? cats.reduce((sum, c) => sum + Math.max(c.avgProcessHours ?? 0, 0), 0) / cats.length
+		: 0;
+	const hasData = totalAnomalies > 0 || cats.length > 0;
+	const tone: SummaryCardData['tone'] = avgCloseRate >= 90 ? 'good' : avgCloseRate >= 70 ? 'warn' : 'danger';
+	return {
+		code: 'S8',
+		title: '功能异常多维分析',
+		subtitle: '按异常类型汇总',
+		icon: DataAnalysis,
+		tone: hasData ? tone : 'warn',
+		mainLabel: '累计异常数',
+		mainValue: formatInteger(totalAnomalies),
+		metrics: [
+			{ label: '异常类型数', value: String(cats.length || '--') },
+			{ label: '平均关闭率', value: hasData ? formatPercent(avgCloseRate) : '--', accent: true },
+			{ label: '平均处理时长', value: hasData ? formatHours(avgHours) : '--' },
+			{ label: '最高风险类别', value: topCat?.category ?? '--' },
+		],
+	};
+});
+
+const s9SummaryCard = computed<SummaryCardData>(() => {
+	const depts = deptCards.value;
+	const pressedCount = depts.filter((d) => d.tone === 'danger').length;
+	const topDept = depts.length ? depts[0] : null;
+	const avgCloseRate = depts.length
+		? depts.reduce((sum, d) => sum + clampPercent(parseFloat(d.closeRateText) || 0), 0) / depts.length
+		: 0;
+	const eff = overallEfficiency.value;
+	const tone: SummaryCardData['tone'] = eff.tone === 'good' ? 'good' : eff.tone === 'warn' ? 'warn' : 'danger';
+	return {
+		code: 'S9',
+		title: '部门效率分析',
+		subtitle: '按部门响应效率汇总',
+		icon: TrendCharts,
+		tone,
+		mainLabel: '整体响应效能',
+		mainValue: String(eff.score),
+		mainSuffix: '%',
+		metrics: [
+			{ label: '部门数', value: String(depts.length || '--') },
+			{ label: '承压部门数', value: String(pressedCount), accent: pressedCount > 0 },
+			{ label: '平均关闭率', value: depts.length ? formatPercent(avgCloseRate) : '--' },
+			{ label: '异常最多部门', value: topDept?.deptName ?? '--' },
+		],
+	};
+});
+
+const allSummaryCards = computed<SummaryCardData[]>(() => [s8SummaryCard.value, s9SummaryCard.value]);
+
 const deptCards = computed<DeptDisplay[]>(() => {
 	const source = gridData.byDept.length
 		? [...gridData.byDept].sort((a, b) => b.total - a.total)
@@ -447,7 +417,7 @@ const overallEfficiency = computed(() => {
 	return { score, label: '承压', tone: 'danger' as const };
 });
 
-const categoryCards = computed<CategoryGridCardData[]>(() => {
+const categoryCards = computed(() => {
 	const sourceMap = new Map(gridData.byCategory.map((item) => [item.category, item]));
 
 	return effectiveCategoryDefs.value.map((category, index) => {
@@ -523,6 +493,8 @@ function buildStageCard(
 		wideRate: closeRate,
 		wideRateLabel: `闭环率 ${closeRateText} / 均时 ${avgHoursText}`,
 		avgProcessHours,
+		// TASK-006 [MOCK]:S1-S7 全部卡片注入订单状态辅助行
+		orderStatus: ORDER_STATUS_MOCK,
 	};
 }
 
@@ -574,7 +546,7 @@ function resolveDeptLevelLabel(item: S8DeptSummary, tone: 'good' | 'warn' | 'dan
 	return '滞后';
 }
 
-function resolveCategoryTone(item: S8CategorySummary): 'good' | 'warn' | 'danger' {
+function resolveCategoryTone(item: { closeRate: number }): 'good' | 'warn' | 'danger' {
 	if (item.closeRate >= 95) return 'good';
 	if (item.closeRate >= 80) return 'warn';
 	return 'danger';
@@ -644,32 +616,6 @@ function resetDeptConfig() {
 	resetDeptConfigState(deptCards.value);
 }
 
-// ── S9 拖拽排序 ──────────────────────────────────────────────────────────────
-const deptDraggingKey = shallowRef('');
-const deptDragOverKey = shallowRef('');
-
-function onDeptDragStart(deptName: string) {
-	deptDraggingKey.value = deptName;
-}
-
-function onDeptDragOver(deptName: string) {
-	if (deptDraggingKey.value && deptDraggingKey.value !== deptName) {
-		deptDragOverKey.value = deptName;
-	}
-}
-
-function onDeptDrop(targetName: string) {
-	if (deptDraggingKey.value && deptDraggingKey.value !== targetName) {
-		reorderDept(deptDraggingKey.value, targetName);
-	}
-	deptDraggingKey.value = '';
-	deptDragOverKey.value = '';
-}
-
-function onDeptDragEnd() {
-	deptDraggingKey.value = '';
-	deptDragOverKey.value = '';
-}
 
 function onBlockOrderUpdate(code: string, order: string[]) {
 	if (stageConfigState.items[code]) {

+ 3 - 1
Web/src/views/aidop/s8/monitoring/S8MonitoringProductionPage.vue

@@ -112,7 +112,7 @@ import S8MonitoringEditToolbar from './components/S8MonitoringEditToolbar.vue';
 import S8MonitoringStageConfigDrawer from './components/S8MonitoringStageConfigDrawer.vue';
 import S8ProductionTrendChart from './components/S8ProductionTrendChart.vue';
 import { useS8UnsavedLayoutEditGuard } from './useS8UnsavedLayoutEditGuard';
-import { useS8StageConfig, buildModuleTriColorStats, DEFAULT_NORMAL_WORK_ORDER_COUNT } from './useS8StageConfig';
+import { useS8StageConfig, buildModuleTriColorStats, DEFAULT_NORMAL_WORK_ORDER_COUNT, ORDER_STATUS_MOCK } from './useS8StageConfig';
 import { useS8CategoryConfig } from './useS8CategoryConfig';
 import { useS8PageConfigDriver } from './useS8PageConfigDriver';
 
@@ -308,6 +308,8 @@ const stageCards = computed(() =>
 			wideRate: closeRate,
 			wideRateLabel: `闭环率 ${closeRateText} / 均时 ${avgHoursText}`,
 			avgProcessHours,
+			// TASK-006 [MOCK]:S1-S7 全部卡片注入订单状态辅助行
+			orderStatus: ORDER_STATUS_MOCK,
 		};
 		return applyConfig(raw);
 	}),

+ 3 - 1
Web/src/views/aidop/s8/monitoring/S8MonitoringSupplyPage.vue

@@ -112,7 +112,7 @@ import S8MonitoringEditToolbar from './components/S8MonitoringEditToolbar.vue';
 import S8MonitoringStageConfigDrawer from './components/S8MonitoringStageConfigDrawer.vue';
 import S8SupplyTrendChart from './components/S8SupplyTrendChart.vue';
 import { useS8UnsavedLayoutEditGuard } from './useS8UnsavedLayoutEditGuard';
-import { useS8StageConfig, buildModuleTriColorStats, DEFAULT_NORMAL_WORK_ORDER_COUNT } from './useS8StageConfig';
+import { useS8StageConfig, buildModuleTriColorStats, DEFAULT_NORMAL_WORK_ORDER_COUNT, ORDER_STATUS_MOCK } from './useS8StageConfig';
 import { useS8CategoryConfig } from './useS8CategoryConfig';
 import { useS8PageConfigDriver } from './useS8PageConfigDriver';
 
@@ -320,6 +320,8 @@ const stageCards = computed(() =>
 			wideRate: closeRate,
 			wideRateLabel: `闭环率 ${closeRateText} / 均时 ${avgHoursText}`,
 			avgProcessHours,
+			// TASK-006 [MOCK]:S1-S7 全部卡片注入订单状态辅助行
+			orderStatus: ORDER_STATUS_MOCK,
 		};
 		return applyConfig(raw);
 	}),

+ 26 - 3
Web/src/views/aidop/s8/monitoring/components/S8MonitoringModulesGrid.vue

@@ -27,14 +27,20 @@
         :static="!editable"
       >
         <S8MonitoringStageCard
-          v-if="cardMap.get(item.i)"
-          v-bind="cardMap.get(item.i)!"
+          v-if="stageCardMap.get(item.i)"
+          v-bind="stageCardMap.get(item.i)!"
           :card-span="item.w"
           :min-height="item.h * rowHeight - gap * 2"
           :blocks-editable="blocksEditable"
           style="width:100%;height:100%"
           @update:block-order="(order) => emit('update:blockOrder', item.i, order)"
         />
+        <S8MonitoringSummaryCard
+          v-else-if="summaryCardMap.get(item.i)"
+          v-bind="summaryCardMap.get(item.i)!"
+          :min-height="item.h * rowHeight - gap * 2"
+          style="width:100%;height:100%"
+        />
       </grid-item>
     </grid-layout>
   </div>
@@ -44,6 +50,7 @@
 import { computed, nextTick, onActivated, onMounted, ref, watch } from 'vue'
 import { useResizeObserver } from '@vueuse/core'
 import S8MonitoringStageCard from './S8MonitoringStageCard.vue'
+import S8MonitoringSummaryCard, { type SummaryMetric } from './S8MonitoringSummaryCard.vue'
 import type { ModuleGridItem } from '../useS8Layout'
 import type { Component } from 'vue'
 
@@ -72,10 +79,25 @@ interface StageCardData {
   showProgress?: boolean
   blockOrder?: string[]
   metricLabelFontSize?: number
+  // TASK-006 [MOCK] 订单状态补充行(S1/S7 专用,其他卡片不传)
+  orderStatus?: { completed: number; inProgress: number; delayed: number }
+}
+
+export interface SummaryCardData {
+  code: string
+  title: string
+  subtitle?: string
+  icon: Component
+  tone: 'good' | 'warn' | 'danger'
+  mainLabel: string
+  mainValue: string
+  mainSuffix?: string
+  metrics?: SummaryMetric[]
 }
 
 const props = defineProps<{
   cards: StageCardData[]
+  summaryCards?: SummaryCardData[]
   layout: ModuleGridItem[]
   editable: boolean
   rowHeight?: number
@@ -174,7 +196,8 @@ function onLayoutUpdated(newLayout: ModuleGridItem[]) {
   emit('update:layout', newLayout.map((item) => ({ ...item })))
 }
 
-const cardMap = computed(() => new Map(props.cards.map((c) => [c.code, c])))
+const stageCardMap = computed(() => new Map(props.cards.map((c) => [c.code, c])))
+const summaryCardMap = computed(() => new Map((props.summaryCards ?? []).map((c) => [c.code, c])))
 
 useResizeObserver(containerRef, () => {
   syncGridWidth()

+ 23 - 4
Web/src/views/aidop/s8/monitoring/components/S8MonitoringResizableShell.vue

@@ -2,6 +2,7 @@
   <div
     ref="shellRef"
     class="resizable-shell"
+    :class="{ 'resizable-shell--single-col': hideSidebar, 'resizable-shell--single-row': hideAnalysis }"
     :style="gridStyle"
   >
     <div class="resizable-shell__modules">
@@ -9,23 +10,23 @@
     </div>
 
     <div
-      v-if="editable"
+      v-if="editable && !hideAnalysis"
       class="resizable-shell__h-handle"
       @mousedown.prevent="startRowDrag"
     >
       <div class="resizable-shell__h-handle-bar" />
     </div>
 
-    <div class="resizable-shell__sidebar">
+    <div v-if="!hideSidebar" class="resizable-shell__sidebar">
       <slot name="sidebar" />
     </div>
 
-    <div class="resizable-shell__analysis">
+    <div v-if="!hideAnalysis" class="resizable-shell__analysis">
       <slot name="analysis" />
     </div>
 
     <div
-      v-if="editable"
+      v-if="editable && !hideSidebar"
       class="resizable-shell__v-handle"
       @mousedown.prevent="startColDrag"
     >
@@ -44,6 +45,8 @@ const GRID_GAP = 24
 const props = defineProps<{
   shell: ShellLayout
   editable: boolean
+  hideSidebar?: boolean
+  hideAnalysis?: boolean
 }>()
 
 const emit = defineEmits<{
@@ -187,12 +190,28 @@ useEventListener(document, 'mouseup', () => {
   padding-top: 16px;
 }
 
+.resizable-shell--single-col {
+  grid-template-columns: 1fr;
+}
+
+.resizable-shell--single-row {
+  grid-template-rows: 1fr;
+}
+
 .resizable-shell__modules {
   grid-column: 1;
   grid-row: 1;
   overflow: hidden;
 }
 
+.resizable-shell--single-col .resizable-shell__modules {
+  grid-column: 1 / -1;
+}
+
+.resizable-shell--single-row .resizable-shell__modules {
+  grid-row: 1 / -1;
+}
+
 .resizable-shell__sidebar {
   grid-column: 2;
   grid-row: 1 / span 2;

+ 57 - 22
Web/src/views/aidop/s8/monitoring/components/S8MonitoringStageCard.vue

@@ -1,6 +1,7 @@
 <script setup lang="ts">
 import { computed, type Component } from 'vue';
 import CardBlockLayout from './CardBlockLayout.vue';
+import S8SeverityAttentionRows from './S8SeverityAttentionRows.vue';
 
 // S8-STAGE-CARD-SEMANTIC-LAYOUT-EXEC-1:
 // 默认布局移除 `health-rate` / `status-label`:
@@ -42,6 +43,8 @@ const props = defineProps<{
 	blockOrder?: string[];
 	blocksEditable?: boolean;
 	metricLabelFontSize?: number;
+	// TASK-006 [MOCK] 订单状态补充行(S1/S7 专用,其他卡片不传此 prop)
+	orderStatus?: { completed: number; inProgress: number; delayed: number };
 }>();
 
 const emit = defineEmits<{
@@ -67,6 +70,13 @@ const shouldShowStatusLabel = computed(() => props.showStatusLabel ?? true);
 const shouldShowProgress = computed(() => props.showProgress ?? true);
 const isWide = computed(() => (props.cardSpan ?? 1) > 1);
 
+// TASK-006:orderStatus 存在时在末尾追加 order-status 块,避免非订单卡片多出空占位
+const activeBlockOrder = computed(() => {
+	const base = props.blockOrder ?? DEFAULT_BLOCK_ORDER;
+	if (!props.orderStatus) return base;
+	return base.includes('order-status') ? base : [...base, 'order-status'];
+});
+
 // S8-STAGE-CARD-SEMANTIC-LAYOUT-EXEC-1:从 values 解析自然数(绿=正常,黄=关注,红=严重)。
 function safeNum(v: unknown): number {
 	const n = Number(v);
@@ -78,6 +88,17 @@ 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));
+
+// TASK-005:主进度条仅显示红(严重)+ 黄(关注),归一化为满宽
+const _progressDenom = computed(() => props.progress.red + props.progress.yellow);
+const redBarWidth = computed(() => {
+  const d = _progressDenom.value;
+  return d > 0 ? (props.progress.red / d) * 100 : 0;
+});
+const yellowBarWidth = computed(() => {
+  const d = _progressDenom.value;
+  return d > 0 ? (props.progress.yellow / d) * 100 : 0;
+});
 </script>
 
 <template>
@@ -90,7 +111,7 @@ const timeoutDisplayCount = computed(() => safeNum(props.timeoutCount));
 		</div>
 
 		<CardBlockLayout
-			:blocks="blockOrder ?? DEFAULT_BLOCK_ORDER"
+			:blocks="activeBlockOrder"
 			:editable="blocksEditable ?? false"
 			class="stage-card__body"
 			@update:blocks="emit('update:blockOrder', $event)"
@@ -102,25 +123,15 @@ const timeoutDisplayCount = computed(() => safeNum(props.timeoutCount));
 			</template>
 
 			<template #values>
-				<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 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 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>
+				<!-- TASK-005:严重在上关注在下,各附延误值;移除超时/健康订单展示 -->
+				<S8SeverityAttentionRows
+					v-if="values"
+					:serious-count="seriousCount"
+					:attention-count="followCount"
+					:serious-delay-count="seriousCount"
+					:attention-delay-count="followCount"
+				/>
 				<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 合并去重;
@@ -140,10 +151,10 @@ const timeoutDisplayCount = computed(() => safeNum(props.timeoutCount));
 			</template>
 
 			<template #progress>
+				<!-- TASK-005:红色严重段在左,黄色关注段在右;不显示绿色/紫色段 -->
 				<div v-if="shouldShowProgress" class="stage-card__stack">
-					<span class="stage-card__seg stage-card__seg--green" :style="{ width: `${progress.green}%` }" />
-					<span class="stage-card__seg stage-card__seg--yellow" :style="{ width: `${progress.yellow}%` }" />
-					<span class="stage-card__seg stage-card__seg--red" :style="{ width: `${progress.red}%` }" />
+					<span class="stage-card__seg stage-card__seg--red" :style="{ width: `${redBarWidth}%` }" />
+					<span class="stage-card__seg stage-card__seg--yellow" :style="{ width: `${yellowBarWidth}%` }" />
 				</div>
 			</template>
 
@@ -155,6 +166,13 @@ const timeoutDisplayCount = computed(() => safeNum(props.timeoutCount));
 					</div>
 				</div>
 			</template>
+
+			<!-- TASK-006 [MOCK]:订单状态补充行,仅 orderStatus prop 存在时渲染(S1/S7 订单类卡片) -->
+			<template #order-status>
+				<div v-if="orderStatus" class="stage-card__order-status">
+					订单状态:已完成 <span class="stage-card__order-status-num">{{ orderStatus.completed }}</span> 进行中 <span class="stage-card__order-status-num">{{ orderStatus.inProgress }}</span> 延误 <span class="stage-card__order-status-num">{{ orderStatus.delayed }}</span>
+				</div>
+			</template>
 		</CardBlockLayout>
 	</article>
 </template>
@@ -447,4 +465,21 @@ const timeoutDisplayCount = computed(() => safeNum(props.timeoutCount));
 		max-width: none;
 	}
 }
+
+/* TASK-006 [MOCK]:订单状态补充行样式 */
+.stage-card__order-status {
+	font-size: 11px;
+	color: rgba(198, 198, 205, 0.6);
+	letter-spacing: 0.02em;
+	white-space: nowrap;
+	overflow: hidden;
+	text-overflow: ellipsis;
+	margin-top: 2px;
+}
+
+.stage-card__order-status-num {
+	font-family: 'Roboto Mono', monospace;
+	font-size: 12px;
+	color: rgba(198, 198, 205, 0.85);
+}
 </style>

+ 185 - 0
Web/src/views/aidop/s8/monitoring/components/S8MonitoringSummaryCard.vue

@@ -0,0 +1,185 @@
+<script setup lang="ts">
+import { computed, type Component } from 'vue';
+
+export interface SummaryMetric {
+  label: string;
+  value: string;
+  accent?: boolean;
+}
+
+const props = defineProps<{
+  code: string;
+  title: string;
+  subtitle?: string;
+  icon: Component;
+  tone: 'good' | 'warn' | 'danger';
+  mainLabel: string;
+  mainValue: string;
+  mainSuffix?: string;
+  metrics?: SummaryMetric[];
+  minHeight?: number;
+}>();
+
+const cardStyle = computed(() => ({
+  minHeight: `${Math.max(120, props.minHeight ?? 160)}px`,
+}));
+</script>
+
+<template>
+  <article class="summary-card" :class="`summary-card--${tone}`" :style="cardStyle">
+    <div class="summary-card__glow" />
+    <div class="summary-card__header">
+      <div class="summary-card__title-wrap">
+        <span class="summary-card__code">{{ code }}</span>
+        <span class="summary-card__title">{{ title }}</span>
+      </div>
+      <el-icon class="summary-card__icon">
+        <component :is="icon" />
+      </el-icon>
+    </div>
+
+    <div class="summary-card__main">
+      <div class="summary-card__main-value">{{ mainValue }}<span v-if="mainSuffix" class="summary-card__main-suffix">{{ mainSuffix }}</span></div>
+      <div class="summary-card__main-label">{{ mainLabel }}</div>
+    </div>
+
+    <div v-if="metrics && metrics.length" class="summary-card__metrics">
+      <div v-for="m in metrics" :key="m.label" class="summary-card__metric">
+        <span class="summary-card__metric-label">{{ m.label }}</span>
+        <span class="summary-card__metric-value" :class="{ 'summary-card__metric-value--accent': m.accent }">{{ m.value }}</span>
+      </div>
+    </div>
+
+    <div v-if="subtitle" class="summary-card__subtitle">{{ subtitle }}</div>
+  </article>
+</template>
+
+<style scoped>
+.summary-card {
+  position: relative;
+  overflow: hidden;
+  border-radius: 18px;
+  width: 100%;
+  height: 100%;
+  box-sizing: border-box;
+  padding: 16px;
+  display: flex;
+  flex-direction: column;
+  gap: 10px;
+  background: rgba(22, 26, 36, 0.85);
+  border: 1px solid rgba(144, 144, 151, 0.14);
+  transition: border-color 0.2s;
+}
+
+.summary-card--good  { border-color: rgba(109, 224, 57, 0.22); }
+.summary-card--warn  { border-color: rgba(255, 193, 7, 0.22); }
+.summary-card--danger { border-color: rgba(255, 180, 171, 0.22); }
+
+.summary-card__glow {
+  position: absolute;
+  inset: 0;
+  pointer-events: none;
+  border-radius: inherit;
+}
+.summary-card--good  .summary-card__glow { background: radial-gradient(ellipse at top right, rgba(109,224,57,0.07), transparent 60%); }
+.summary-card--warn  .summary-card__glow { background: radial-gradient(ellipse at top right, rgba(255,193,7,0.07), transparent 60%); }
+.summary-card--danger .summary-card__glow { background: radial-gradient(ellipse at top right, rgba(255,100,80,0.07), transparent 60%); }
+
+.summary-card__header {
+  display: flex;
+  align-items: flex-start;
+  justify-content: space-between;
+  gap: 8px;
+}
+
+.summary-card__title-wrap {
+  display: flex;
+  flex-direction: column;
+  gap: 2px;
+  min-width: 0;
+}
+
+.summary-card__code {
+  font-size: 11px;
+  font-weight: 700;
+  color: rgba(123, 208, 255, 0.7);
+  letter-spacing: 0.08em;
+}
+
+.summary-card__title {
+  font-size: 13px;
+  font-weight: 700;
+  color: #e1e2eb;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+
+.summary-card__icon {
+  font-size: 20px;
+  flex-shrink: 0;
+  color: rgba(123, 208, 255, 0.5);
+}
+
+.summary-card__main {
+  display: flex;
+  flex-direction: column;
+  gap: 2px;
+}
+
+.summary-card__main-value {
+  font-size: 32px;
+  font-weight: 800;
+  color: #e1e2eb;
+  line-height: 1;
+}
+
+.summary-card__main-suffix {
+  font-size: 16px;
+  font-weight: 600;
+  color: #909097;
+  margin-left: 2px;
+}
+
+.summary-card__main-label {
+  font-size: 11px;
+  color: #909097;
+}
+
+.summary-card__metrics {
+  display: grid;
+  grid-template-columns: 1fr 1fr;
+  gap: 6px 10px;
+  margin-top: auto;
+}
+
+.summary-card__metric {
+  display: flex;
+  flex-direction: column;
+  gap: 1px;
+}
+
+.summary-card__metric-label {
+  font-size: 10px;
+  color: #909097;
+}
+
+.summary-card__metric-value {
+  font-size: 13px;
+  font-weight: 700;
+  color: #c6c6cd;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+
+.summary-card__metric-value--accent {
+  color: #7bd0ff;
+}
+
+.summary-card__subtitle {
+  font-size: 10px;
+  color: rgba(144, 144, 151, 0.6);
+  margin-top: auto;
+}
+</style>

+ 84 - 0
Web/src/views/aidop/s8/monitoring/components/S8SeverityAttentionRows.vue

@@ -0,0 +1,84 @@
+<script setup lang="ts">
+defineProps<{
+  seriousCount: number
+  attentionCount: number
+  seriousDelayCount: number
+  attentionDelayCount: number
+}>()
+
+function safe(n: number): number {
+  const v = Number(n)
+  return Number.isFinite(v) && v >= 0 ? Math.floor(v) : 0
+}
+</script>
+
+<template>
+  <div class="sa-rows">
+    <div class="sa-row sa-row--red">
+      <span class="sa-label">严重</span>
+      <span class="sa-value">{{ safe(seriousCount) }}</span>
+      <span class="sa-delay-label">延误</span>
+      <span class="sa-delay-value">{{ safe(seriousDelayCount) }}</span>
+    </div>
+    <div class="sa-row sa-row--yellow">
+      <span class="sa-label">关注</span>
+      <span class="sa-value">{{ safe(attentionCount) }}</span>
+      <span class="sa-delay-label">延误</span>
+      <span class="sa-delay-value">{{ safe(attentionDelayCount) }}</span>
+    </div>
+  </div>
+</template>
+
+<style scoped>
+.sa-rows {
+  display: flex;
+  flex-direction: column;
+  gap: 5px;
+  margin-top: 4px;
+}
+
+.sa-row {
+  display: flex;
+  align-items: baseline;
+  gap: 5px;
+  font-size: 13px;
+  font-weight: 600;
+}
+
+.sa-label,
+.sa-delay-label {
+  font-weight: 500;
+}
+
+.sa-value {
+  font-family: 'Roboto Mono', monospace;
+  font-size: 16px;
+  line-height: 1;
+}
+
+.sa-delay-label {
+  margin-left: 10px;
+  font-size: 12px;
+  opacity: 0.75;
+}
+
+.sa-delay-value {
+  font-family: 'Roboto Mono', monospace;
+  font-size: 14px;
+  line-height: 1;
+}
+
+.sa-row--red .sa-label,
+.sa-row--red .sa-value,
+.sa-row--red .sa-delay-label,
+.sa-row--red .sa-delay-value {
+  color: #ff6450;
+}
+
+.sa-row--yellow .sa-label,
+.sa-row--yellow .sa-value,
+.sa-row--yellow .sa-delay-label,
+.sa-row--yellow .sa-delay-value {
+  color: #ffc107;
+}
+</style>

+ 13 - 22
Web/src/views/aidop/s8/monitoring/useS8Layout.ts

@@ -27,32 +27,28 @@ export interface S8LayoutSchema {
 }
 
 // ─── 版本常量(破坏性升级时递增) ───────────────────────────────────────────
-export const LAYOUT_VERSION = 1
+export const LAYOUT_VERSION = 2
 
 // ─── 演示基线布局(只读,硬编码) ─────────────────────────────────────────
 export const DEMO_LAYOUT: S8LayoutSchema = {
   version: LAYOUT_VERSION,
   shell: {
-    colFr: [3.25, 0.95],
-    rowFr: [1.72, 0.88],
+    colFr: [4, 0.001],
+    rowFr: [4, 0.001],
   },
   modules: [
-    { i: 'S1', x: 0, y: 0, w: 4, h: 3 },
+    { i: 'S3', x: 0, y: 0, w: 4, h: 3 },
     { i: 'S2', x: 4, y: 0, w: 4, h: 3 },
-    { i: 'S3', x: 8, y: 0, w: 4, h: 3 },
+    { i: 'S1', x: 8, y: 0, w: 4, h: 3 },
     { i: 'S4', x: 0, y: 3, w: 4, h: 3 },
     { i: 'S5', x: 4, y: 3, w: 4, h: 3 },
-    { i: 'S6', x: 8, y: 3, w: 4, h: 3 },
+    { i: 'S8', x: 8, y: 3, w: 4, h: 3 },
+    { i: 'S6', x: 0, y: 6, w: 4, h: 3 },
     { i: 'S7', x: 4, y: 6, w: 4, h: 3 },
+    { i: 'S9', x: 8, y: 6, w: 4, h: 3 },
   ],
   sidebar: [],
-  analysis: [
-    { i: 'order-review',      x: 0, y: 0, w: 4, h: 3 },
-    { i: 'product-design',    x: 4, y: 0, w: 4, h: 3 },
-    { i: 'material-purchase', x: 8, y: 0, w: 4, h: 3 },
-    { i: 'body-production',   x: 2, y: 3, w: 4, h: 3 },
-    { i: 'final-assembly',    x: 6, y: 3, w: 4, h: 3 },
-  ],
+  analysis: [],
 }
 
 /** 向后兼容别名 */
@@ -70,6 +66,7 @@ const LEGACY_STORAGE_KEYS = [
   'aidop_s8_layout_v5',
   'aidop_s8_layout_v6',
   'aidop_s8_layout_v7',
+  'aidop_s8_layout_v8',
 ]
 
 function clearLegacyStorage(): void {
@@ -89,14 +86,8 @@ const GRID_COLS  = 12
 const GRID_MIN_W = 4
 const GRID_MIN_H = 3
 
-const EXPECTED_MODULES = new Set(['S1', 'S2', 'S3', 'S4', 'S5', 'S6', 'S7'])
-const EXPECTED_ANALYSIS = new Set([
-  'order-review',
-  'product-design',
-  'material-purchase',
-  'body-production',
-  'final-assembly',
-])
+const EXPECTED_MODULES = new Set(['S1', 'S2', 'S3', 'S4', 'S5', 'S6', 'S7', 'S8', 'S9'])
+const EXPECTED_ANALYSIS = new Set<string>()
 
 /**
  * 合并式网格校验:保留用户坐标,对缺失/损坏/多余的 ID 作最小修复。
@@ -155,7 +146,7 @@ function validateShell(shell: unknown): shell is ShellLayout {
   const [left, right] = s.colFr ?? []
   const [top, bottom] = s.rowFr ?? []
   if (![left, right, top, bottom].every((v) => Number.isFinite(v) && v > 0)) return false
-  if (left < 2.4 || right < 0.7 || top < 1.2 || bottom < 0.7) return false
+  if (left < 0.001 || right < 0.001 || top < 0.001 || bottom < 0.001) return false
   return true
 }
 

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

@@ -73,6 +73,11 @@ export interface StageCardPreviewData {
 	showProgress?: boolean;
 	blockOrder?: string[];
 	metricLabelFontSize?: number;
+	// TASK-006 [MOCK] 订单状态补充行 — 真实字段未打通
+	// 适用范围:S1-S7 全部阶段卡片;展示:订单状态:已完成 N 进行中 N 延误 N
+	// 真实字段:completedCount / inProgressCount / delayedCount(后端未实现)
+	// 替换点:Overview buildStageCard / Delivery+Production+Supply stageCards
+	orderStatus?: { completed: number; inProgress: number; delayed: number };
 }
 
 export const STAGE_ICON_COMPONENTS: Record<string, Component> = {
@@ -114,6 +119,22 @@ const STORAGE_KEY = 'aidop_s8_stage_config';
  */
 export const DEFAULT_NORMAL_WORK_ORDER_COUNT = 20;
 
+/**
+ * [MOCK] 订单状态补充行数据 — 真实后端字段尚未打通
+ * 适用范围:S1-S7 全部阶段卡片
+ * 展示格式:订单状态:已完成 N 进行中 N 延误 N
+ * 未打通字段:completedCount / inProgressCount / delayedCount
+ * 后续接入真实数据时在以下位置替换:
+ *   - S8MonitoringOverviewPage buildStageCard
+ *   - S8MonitoringDeliveryPage / ProductionPage / SupplyPage stageCards
+ *   - StageCardPreviewData.orderStatus / StageCardData.orderStatus / S8MonitoringStageCard.orderStatus prop
+ */
+export const ORDER_STATUS_MOCK = {
+	completed: 14,
+	inProgress: 0,
+	delayed: 6,
+} as const;
+
 /**
  * 从模块原始指标派生三色业务数字(自然数字符串)。
  * 语义恒等:warningCount + seriousCount = totalExceptionCount。