Ver Fonte

feat(s8): add order execution chain shell

YY968XX há 1 semana atrás
pai
commit
1318368f59

+ 40 - 0
Web/src/utils/aidopMenuDisplay.ts

@@ -74,6 +74,19 @@ const S8_MONITORING_VISIBLE_ROUTES: Array<{ path: string; title: string; compone
 	},
 ];
 
+/**
+ * S8 异常监控目录下的隐藏路由(侧栏不显示)。
+ * 用于「订单链路全景」等通过页面内跳转进入、不需要侧栏入口的二级路由。
+ */
+const S8_MONITORING_HIDDEN_ROUTES: Array<{ path: string; title: string; component: string; name: string }> = [
+	{
+		path: '/aidop/s8/monitoring/order-execution/chain',
+		title: '订单链路全景',
+		component: '/aidop/s8/monitoring/OrderChainOverviewPage',
+		name: 'aidopS8OrderExecutionChain',
+	},
+];
+
 /** S8 配置中心中的隐藏详情页;库中若缺少授权或未同步新种子,前端补齐后可避免点卡片进入 404 */
 const S8_CONFIG_HIDDEN_ROUTES: Array<{ path: string; title: string; component: string; name: string }> = [
 	{
@@ -211,6 +224,32 @@ function patchS8MonitoringVisibleRoutesIfMissing(routes: AMenu[] | undefined): v
 	}
 }
 
+/** 为 S8 异常监控目录补隐藏路由(侧栏不显示,仅用于直达 URL 与页面跳转)。 */
+function patchS8MonitoringHiddenRoutesIfMissing(routes: AMenu[] | undefined): void {
+	if (!routes?.length) return;
+	const aidopRoot = routes.find((x) => x.path === '/aidop' || x.name === 'aidopRoot');
+	if (!aidopRoot?.children?.length) return;
+
+	const s8Root = aidopRoot.children.find(
+		(x: AMenu) => x.path === '/aidop/s8' || x.name === 'aidopDirS8' || x.name === 'aidopDirM09',
+	);
+	if (!s8Root?.children?.length) return;
+
+	const monitoringDir = s8Root.children.find(
+		(x: AMenu) => x.path === '/aidop/s8/monitoring' || x.name === 'aidopS8MonitoringDir',
+	);
+	if (!monitoringDir) return;
+
+	for (const route of S8_MONITORING_HIDDEN_ROUTES) {
+		upsertMenu(monitoringDir, {
+			path: route.path,
+			name: route.name,
+			component: route.component,
+			meta: { title: route.title, icon: 'ele-Connection', isHide: true },
+		});
+	}
+}
+
 /** 为 S8 配置中心补隐藏子路由,兼容数据库未刷新到最新菜单/角色授权时的直达 404 */
 function patchS8ConfigHiddenRoutesIfMissing(routes: AMenu[] | undefined): void {
 	if (!routes?.length) return;
@@ -267,6 +306,7 @@ export function patchAidopMenuTitles(routes: any[] | undefined): void {
 	if (!routes?.length) return;
 	patchAidopCustomMenusIfMissing(routes as AMenu[]);
 	patchS8MonitoringVisibleRoutesIfMissing(routes as AMenu[]);
+	patchS8MonitoringHiddenRoutesIfMissing(routes as AMenu[]);
 	patchS8ConfigHiddenRoutesIfMissing(routes as AMenu[]);
 	patchSmartOpsRouteComponents(routes as AMenu[]);
 	patchS8ConfigRouteComponents(routes as AMenu[]);

+ 218 - 0
Web/src/views/aidop/s8/monitoring/OrderChainOverviewPage.vue

@@ -0,0 +1,218 @@
+<script setup lang="ts" name="aidopS8OrderExecutionChain">
+// ORDER-FLOW-CHAIN-SHELL-1:订单链路全景页壳。
+// 入口:总览页订单卡「链路全景」按钮 → store.selectOrderForChain + router.push。
+// 刷新恢复:onMounted 调 store.restoreChainSelection() 从 sessionStorage 重建 selectedOrderNo。
+import { computed, onMounted } from 'vue';
+import { useRouter } from 'vue-router';
+import { useOrderExecutionStore } from '/@/stores/orderExecution';
+import StageMatrixCard from './components/order-execution/StageMatrixCard.vue';
+import NodeDetailCard from './components/order-execution/NodeDetailCard.vue';
+
+const store = useOrderExecutionStore();
+const router = useRouter();
+
+onMounted(() => {
+	store.loadFixture();
+	store.restoreChainSelection();
+});
+
+const order = computed(() => store.selectedOrder);
+
+const focusStage = computed(() => {
+	const o = order.value;
+	if (!o) return null;
+	const lifecycle = o.lifecycle ?? [];
+	if (lifecycle.length === 0) return null;
+	return (
+		lifecycle.find((s) => s.key === o.focusNodeKey) ??
+		lifecycle.find((s) => s.key === o.currentNodeKey) ??
+		lifecycle[0] ??
+		null
+	);
+});
+
+const stages = computed(() => order.value?.lifecycle ?? []);
+
+function goBack() {
+	if (window.history.length > 1) {
+		router.back();
+		return;
+	}
+	router.push('/aidop/s8/monitoring/order-execution');
+}
+</script>
+
+<template>
+	<div class="oe-chain">
+		<header class="oe-chain__header">
+			<div class="oe-chain__title">
+				<h1 class="oe-chain__title-main">订单链路全景</h1>
+				<p v-if="order" class="oe-chain__title-sub">
+					<span class="oe-chain__so">{{ order.soNo }}</span>
+					<span class="oe-chain__sep">·</span>
+					<span>{{ order.productName }}</span>
+				</p>
+				<p v-else class="oe-chain__title-sub">尚未选择订单</p>
+			</div>
+			<el-button class="oe-chain__back" @click="goBack">返回总览</el-button>
+		</header>
+
+		<template v-if="order">
+			<section class="oe-chain__summary">
+				<div class="oe-chain__summary-cell">
+					<span class="oe-chain__summary-label">客户</span>
+					<span class="oe-chain__summary-value">{{ order.customerName }}</span>
+				</div>
+				<div class="oe-chain__summary-cell">
+					<span class="oe-chain__summary-label">产品</span>
+					<span class="oe-chain__summary-value">{{ order.productName }}</span>
+				</div>
+				<div class="oe-chain__summary-cell">
+					<span class="oe-chain__summary-label">当前节点</span>
+					<span class="oe-chain__summary-value">{{ order.currentNode }}</span>
+				</div>
+				<div class="oe-chain__summary-cell">
+					<span class="oe-chain__summary-label">异常次数</span>
+					<span class="oe-chain__summary-value oe-chain__summary-value--accent">
+						{{ order.exceptionCount }}
+					</span>
+				</div>
+				<div class="oe-chain__summary-cell">
+					<span class="oe-chain__summary-label">工作流</span>
+					<span class="oe-chain__summary-value">
+						{{ order.workflowStatus === 'in_progress' ? '执行中' : '已完成' }}
+					</span>
+				</div>
+			</section>
+
+			<StageMatrixCard :stages="stages" :focus-key="focusStage?.key ?? null" />
+			<NodeDetailCard :stage="focusStage" />
+		</template>
+
+		<section v-else class="oe-chain__empty">
+			<p>未找到订单数据,请从订单执行档案总览进入。</p>
+			<el-button type="primary" @click="goBack">返回总览</el-button>
+		</section>
+	</div>
+</template>
+
+<style scoped>
+.oe-chain {
+	--order-bg: #10131a;
+	--order-panel: rgba(25, 28, 34, 0.72);
+	--order-border: rgba(144, 144, 151, 0.18);
+	--order-accent: #7bd0ff;
+	--order-text-primary: #e1e2eb;
+	--order-text-secondary: #c6c6cd;
+	--order-text-muted: #909097;
+
+	min-height: calc(100vh - 108px);
+	padding: 18px 22px 28px;
+	box-sizing: border-box;
+	color: var(--order-text-primary);
+	background:
+		radial-gradient(circle at top left, rgba(123, 208, 255, 0.08), transparent 30%),
+		radial-gradient(circle at bottom right, rgba(109, 224, 57, 0.06), transparent 26%),
+		linear-gradient(180deg, #10131a 0%, #161b25 100%);
+	display: flex;
+	flex-direction: column;
+	gap: 16px;
+	overflow-x: hidden;
+	overflow-y: auto;
+}
+
+.oe-chain__header {
+	display: flex;
+	align-items: flex-end;
+	justify-content: space-between;
+	gap: 16px;
+	padding: 12px 16px;
+	border-radius: 14px;
+	background: var(--order-panel);
+	border: 1px solid var(--order-border);
+}
+
+.oe-chain__title-main {
+	margin: 0 0 4px;
+	font-size: 22px;
+	font-weight: 700;
+}
+
+.oe-chain__title-sub {
+	margin: 0;
+	font-size: 13px;
+	color: var(--order-text-secondary);
+	display: flex;
+	align-items: center;
+	gap: 8px;
+}
+
+.oe-chain__so {
+	font-family: 'JetBrains Mono', 'Cascadia Code', Menlo, monospace;
+	color: var(--order-accent);
+}
+
+.oe-chain__sep {
+	color: var(--order-text-muted);
+}
+
+.oe-chain__summary {
+	display: grid;
+	grid-template-columns: repeat(5, minmax(0, 1fr));
+	gap: 12px;
+	padding: 14px 16px;
+	border-radius: 12px;
+	background: var(--order-panel);
+	border: 1px solid var(--order-border);
+}
+
+.oe-chain__summary-cell {
+	display: flex;
+	flex-direction: column;
+	gap: 4px;
+}
+
+.oe-chain__summary-label {
+	font-size: 11px;
+	color: var(--order-text-muted);
+}
+
+.oe-chain__summary-value {
+	font-size: 15px;
+	font-weight: 700;
+	color: var(--order-text-primary);
+}
+
+.oe-chain__summary-value--accent {
+	color: #ffc107;
+}
+
+.oe-chain__empty {
+	display: flex;
+	flex-direction: column;
+	align-items: center;
+	gap: 14px;
+	padding: 48px 24px;
+	border-radius: 12px;
+	background: var(--order-panel);
+	border: 1px dashed var(--order-border);
+	color: var(--order-text-secondary);
+}
+
+@media (max-width: 1080px) {
+	.oe-chain__summary {
+		grid-template-columns: repeat(3, minmax(0, 1fr));
+	}
+}
+
+@media (max-width: 640px) {
+	.oe-chain__summary {
+		grid-template-columns: repeat(2, minmax(0, 1fr));
+	}
+
+	.oe-chain__header {
+		flex-direction: column;
+		align-items: flex-start;
+	}
+}
+</style>

+ 13 - 1
Web/src/views/aidop/s8/monitoring/SoOrderExecutionDashboardPage.vue

@@ -3,12 +3,18 @@
 // 总览页壳 + 7 维筛选条(status/node/keyword/customer/productLine/region/severity)。
 // KPI / 命中订单 / 客户分组随 store.filteredOrders 变化;空结果时 KPI 显示 --。
 import { computed, onMounted, ref } from 'vue';
+import { useRouter } from 'vue-router';
 import { useOrderExecutionStore } from '/@/stores/orderExecution';
-import type { OrderExecutionFilters } from '/@/views/aidop/s8/monitoring/data/order-execution/types';
+import type {
+	OrderExecutionFilters,
+	SalesOrderExecution,
+} from '/@/views/aidop/s8/monitoring/data/order-execution/types';
 import OrderExecutionKpiCards from './components/order-execution/OrderExecutionKpiCards.vue';
 import OrderExecutionCustomerGroups from './components/order-execution/OrderExecutionCustomerGroups.vue';
 import OrderExecutionFiltersBar from './components/order-execution/OrderExecutionFilters.vue';
 
+const router = useRouter();
+
 const store = useOrderExecutionStore();
 
 onMounted(() => {
@@ -35,6 +41,11 @@ function onFiltersClear() {
 	store.clearFilters();
 }
 
+function onOpenChain(order: SalesOrderExecution) {
+	store.selectOrderForChain(order);
+	router.push('/aidop/s8/monitoring/order-execution/chain');
+}
+
 const activeTab = ref<'overview' | 'execution' | 'source-dept' | 'handle-dept'>('overview');
 </script>
 
@@ -84,6 +95,7 @@ const activeTab = ref<'overview' | 'execution' | 'source-dept' | 'handle-dept'>(
 						v-if="!isEmptyResult"
 						:customers="filteredCustomers"
 						:preview-limit="3"
+						@open-chain="onOpenChain"
 					/>
 					<div v-else class="oe-dashboard__empty">
 						当前筛选无匹配订单,可点击「重置」恢复全部。

+ 193 - 0
Web/src/views/aidop/s8/monitoring/components/order-execution/NodeDetailCard.vue

@@ -0,0 +1,193 @@
+<script setup lang="ts" name="NodeDetailCard">
+// ORDER-FLOW-CHAIN-SHELL-1:链路页节点详情简版(focus 节点的基础字段)。
+import { computed } from 'vue';
+import type { StageSnapshot } from '/@/views/aidop/s8/monitoring/data/order-execution/types';
+
+interface Props {
+	stage: StageSnapshot | null;
+}
+
+const props = defineProps<Props>();
+
+const STATUS_LABEL: Record<StageSnapshot['status'], string> = {
+	green: '正常',
+	yellow: '关注',
+	red: '严重',
+	pending: '未开始',
+};
+
+const detail = computed(() => {
+	const s = props.stage;
+	if (!s) return null;
+	return {
+		name: s.name,
+		targetDate: s.targetDate || '--',
+		actualReachedAt: s.actualReachedAt ?? '尚未达成',
+		actualDays: s.actualDays != null ? `${s.actualDays.toFixed(1)} 天` : '--',
+		varianceText:
+			s.nodeVarianceDays == null
+				? '--'
+				: (s.nodeVarianceDays > 0 ? '+' : '') + s.nodeVarianceDays.toFixed(1) + ' 天',
+		statusLabel: STATUS_LABEL[s.status],
+		status: s.status,
+	};
+});
+</script>
+
+<template>
+	<section class="oe-node-detail">
+		<header class="oe-node-detail__head">
+			<span class="oe-node-detail__bar" />
+			<h2 class="oe-node-detail__title">当前节点详情</h2>
+		</header>
+		<div v-if="detail" class="oe-node-detail__body" :class="`oe-node-detail--${detail.status}`">
+			<div class="oe-node-detail__name">
+				{{ detail.name }}
+				<span class="oe-node-detail__chip" :class="`oe-node-detail__chip--${detail.status}`">
+					{{ detail.statusLabel }}
+				</span>
+			</div>
+			<dl class="oe-node-detail__grid">
+				<div class="oe-node-detail__cell">
+					<dt>计划截止</dt>
+					<dd>{{ detail.targetDate }}</dd>
+				</div>
+				<div class="oe-node-detail__cell">
+					<dt>实际达成</dt>
+					<dd>{{ detail.actualReachedAt }}</dd>
+				</div>
+				<div class="oe-node-detail__cell">
+					<dt>实际耗时</dt>
+					<dd>{{ detail.actualDays }}</dd>
+				</div>
+				<div class="oe-node-detail__cell">
+					<dt>偏差</dt>
+					<dd>{{ detail.varianceText }}</dd>
+				</div>
+			</dl>
+		</div>
+		<div v-else class="oe-node-detail__empty">未找到当前节点。</div>
+	</section>
+</template>
+
+<style scoped>
+.oe-node-detail {
+	display: flex;
+	flex-direction: column;
+	gap: 12px;
+}
+
+.oe-node-detail__head {
+	display: flex;
+	align-items: center;
+	gap: 10px;
+}
+
+.oe-node-detail__bar {
+	width: 5px;
+	height: 18px;
+	border-radius: 999px;
+	background: var(--order-accent, #7bd0ff);
+}
+
+.oe-node-detail__title {
+	margin: 0;
+	font-size: 16px;
+	font-weight: 700;
+}
+
+.oe-node-detail__body {
+	display: flex;
+	flex-direction: column;
+	gap: 10px;
+	padding: 14px 16px;
+	border-radius: 12px;
+	background: var(--order-panel, rgba(25, 28, 34, 0.72));
+	border: 1px solid var(--order-border, rgba(144, 144, 151, 0.18));
+	border-left-width: 3px;
+}
+
+.oe-node-detail--green { border-left-color: #88fd54; }
+.oe-node-detail--yellow { border-left-color: #ffc107; }
+.oe-node-detail--red { border-left-color: #ffb4ab; }
+.oe-node-detail--pending { border-left-color: rgba(144, 144, 151, 0.35); }
+
+.oe-node-detail__name {
+	display: flex;
+	align-items: center;
+	gap: 10px;
+	font-size: 16px;
+	font-weight: 700;
+	color: var(--order-text-primary, #e1e2eb);
+}
+
+.oe-node-detail__chip {
+	padding: 1px 10px;
+	border-radius: 999px;
+	font-size: 11px;
+	line-height: 1.6;
+	font-weight: 400;
+}
+
+.oe-node-detail__chip--green {
+	background: rgba(109, 224, 57, 0.12);
+	color: #88fd54;
+}
+
+.oe-node-detail__chip--yellow {
+	background: rgba(255, 193, 7, 0.12);
+	color: #ffc107;
+}
+
+.oe-node-detail__chip--red {
+	background: rgba(255, 180, 171, 0.12);
+	color: #ffb4ab;
+}
+
+.oe-node-detail__chip--pending {
+	background: rgba(144, 144, 151, 0.12);
+	color: var(--order-text-muted, #909097);
+}
+
+.oe-node-detail__grid {
+	display: grid;
+	grid-template-columns: repeat(4, minmax(0, 1fr));
+	gap: 10px;
+	margin: 0;
+}
+
+.oe-node-detail__cell {
+	display: flex;
+	flex-direction: column;
+	gap: 4px;
+	margin: 0;
+}
+
+.oe-node-detail__cell dt {
+	font-size: 11px;
+	color: var(--order-text-muted, #909097);
+}
+
+.oe-node-detail__cell dd {
+	margin: 0;
+	font-size: 14px;
+	color: var(--order-text-primary, #e1e2eb);
+	font-weight: 600;
+}
+
+.oe-node-detail__empty {
+	padding: 14px 16px;
+	border-radius: 12px;
+	background: var(--order-panel, rgba(25, 28, 34, 0.72));
+	border: 1px dashed var(--order-border, rgba(144, 144, 151, 0.18));
+	color: var(--order-text-secondary, #c6c6cd);
+	font-size: 13px;
+	text-align: center;
+}
+
+@media (max-width: 768px) {
+	.oe-node-detail__grid {
+		grid-template-columns: repeat(2, minmax(0, 1fr));
+	}
+}
+</style>

+ 31 - 0
Web/src/views/aidop/s8/monitoring/components/order-execution/OrderExecutionCustomerGroups.vue

@@ -14,6 +14,14 @@ const props = withDefaults(defineProps<Props>(), {
 	previewLimit: 3,
 });
 
+const emit = defineEmits<{
+	'open-chain': [order: SalesOrderExecution];
+}>();
+
+function onOpenChain(order: SalesOrderExecution) {
+	emit('open-chain', order);
+}
+
 const customerCardData = computed(() =>
 	props.customers.map((customer) => ({
 		id: customer.id,
@@ -60,6 +68,13 @@ const nodeStatusClass = (status: SalesOrderExecution['nodeStatus']) =>
 					<div class="oe-order-row__top">
 						<span class="oe-order-row__so">{{ order.soNo }}</span>
 						<span class="oe-order-row__product">{{ order.productName }}</span>
+						<button
+							type="button"
+							class="oe-order-row__chain-btn"
+							@click="onOpenChain(order)"
+						>
+							链路全景
+						</button>
 					</div>
 					<div class="oe-order-row__bottom">
 						<span class="oe-order-row__node" :class="nodeStatusClass(order.nodeStatus)">
@@ -175,6 +190,22 @@ const nodeStatusClass = (status: SalesOrderExecution['nodeStatus']) =>
 	margin-bottom: 4px;
 }
 
+.oe-order-row__chain-btn {
+	margin-left: auto;
+	padding: 2px 10px;
+	border-radius: 999px;
+	border: 1px solid rgba(123, 208, 255, 0.32);
+	background: rgba(123, 208, 255, 0.08);
+	color: #7bd0ff;
+	font-size: 11px;
+	cursor: pointer;
+	flex: 0 0 auto;
+}
+
+.oe-order-row__chain-btn:hover {
+	background: rgba(123, 208, 255, 0.18);
+}
+
 .oe-order-row__so {
 	font-family: 'JetBrains Mono', 'Cascadia Code', Menlo, monospace;
 	font-size: 12px;

+ 200 - 0
Web/src/views/aidop/s8/monitoring/components/order-execution/StageMatrixCard.vue

@@ -0,0 +1,200 @@
+<script setup lang="ts" name="StageMatrixCard">
+// ORDER-FLOW-CHAIN-SHELL-1:链路页五阶段矩阵。
+// StageSnapshot 没有直接的 plannedDays 字段,KPI 目标天用 actualDays - nodeVarianceDays 反推。
+import { computed } from 'vue';
+import type { StageSnapshot } from '/@/views/aidop/s8/monitoring/data/order-execution/types';
+
+interface Props {
+	stages: StageSnapshot[];
+	focusKey?: string | null;
+}
+
+const props = defineProps<Props>();
+
+const STATUS_LABEL: Record<StageSnapshot['status'], string> = {
+	green: '正常',
+	yellow: '关注',
+	red: '严重',
+	pending: '未开始',
+};
+
+const rows = computed(() =>
+	props.stages.map((stage) => {
+		const kpi =
+			stage.actualDays != null && stage.nodeVarianceDays != null
+				? Number((stage.actualDays - stage.nodeVarianceDays).toFixed(1))
+				: null;
+		return {
+			key: stage.key,
+			name: stage.name,
+			kpiText: kpi != null ? kpi.toFixed(1) : '--',
+			actualText: stage.actualDays != null ? stage.actualDays.toFixed(1) : '--',
+			status: stage.status,
+			statusLabel: STATUS_LABEL[stage.status],
+			varianceText:
+				stage.nodeVarianceDays == null
+					? '--'
+					: (stage.nodeVarianceDays > 0 ? '+' : '') + stage.nodeVarianceDays.toFixed(1),
+			varianceTone:
+				stage.nodeVarianceDays == null
+					? 'neutral'
+					: stage.nodeVarianceDays > 0
+						? 'over'
+						: 'under',
+			focused: !!props.focusKey && stage.key === props.focusKey,
+		};
+	}),
+);
+</script>
+
+<template>
+	<section class="oe-stage-matrix">
+		<header class="oe-stage-matrix__head">
+			<span class="oe-stage-matrix__bar" />
+			<h2 class="oe-stage-matrix__title">五阶段矩阵</h2>
+		</header>
+		<div class="oe-stage-matrix__grid">
+			<div
+				v-for="row in rows"
+				:key="row.key"
+				class="oe-stage-cell"
+				:class="[`oe-stage-cell--${row.status}`, row.focused && 'oe-stage-cell--focus']"
+			>
+				<div class="oe-stage-cell__name">{{ row.name }}</div>
+				<div class="oe-stage-cell__metrics">
+					<div class="oe-stage-cell__metric">
+						<span class="oe-stage-cell__label">KPI 目标</span>
+						<span class="oe-stage-cell__value">{{ row.kpiText }}<span class="oe-stage-cell__unit"> 天</span></span>
+					</div>
+					<div class="oe-stage-cell__metric">
+						<span class="oe-stage-cell__label">实际</span>
+						<span class="oe-stage-cell__value">{{ row.actualText }}<span class="oe-stage-cell__unit"> 天</span></span>
+					</div>
+				</div>
+				<footer class="oe-stage-cell__footer">
+					<span class="oe-stage-cell__status">{{ row.statusLabel }}</span>
+					<span class="oe-stage-cell__variance" :class="`oe-stage-cell__variance--${row.varianceTone}`">
+						偏差 {{ row.varianceText }}
+					</span>
+				</footer>
+			</div>
+		</div>
+	</section>
+</template>
+
+<style scoped>
+.oe-stage-matrix {
+	display: flex;
+	flex-direction: column;
+	gap: 12px;
+}
+
+.oe-stage-matrix__head {
+	display: flex;
+	align-items: center;
+	gap: 10px;
+}
+
+.oe-stage-matrix__bar {
+	width: 5px;
+	height: 18px;
+	border-radius: 999px;
+	background: var(--order-accent, #7bd0ff);
+}
+
+.oe-stage-matrix__title {
+	margin: 0;
+	font-size: 16px;
+	font-weight: 700;
+}
+
+.oe-stage-matrix__grid {
+	display: grid;
+	grid-template-columns: repeat(5, minmax(0, 1fr));
+	gap: 12px;
+}
+
+.oe-stage-cell {
+	display: flex;
+	flex-direction: column;
+	gap: 10px;
+	padding: 14px 14px 12px;
+	border-radius: 12px;
+	background: var(--order-panel, rgba(25, 28, 34, 0.72));
+	border: 1px solid var(--order-border, rgba(144, 144, 151, 0.18));
+	min-height: 132px;
+	transition: border-color 120ms ease;
+}
+
+.oe-stage-cell--green { border-left: 3px solid #88fd54; }
+.oe-stage-cell--yellow { border-left: 3px solid #ffc107; }
+.oe-stage-cell--red { border-left: 3px solid #ffb4ab; }
+.oe-stage-cell--pending { border-left: 3px solid rgba(144, 144, 151, 0.35); }
+
+.oe-stage-cell--focus {
+	box-shadow: 0 0 0 1px rgba(123, 208, 255, 0.55);
+}
+
+.oe-stage-cell__name {
+	font-size: 14px;
+	font-weight: 700;
+	color: var(--order-text-primary, #e1e2eb);
+}
+
+.oe-stage-cell__metrics {
+	display: grid;
+	grid-template-columns: 1fr 1fr;
+	gap: 8px;
+}
+
+.oe-stage-cell__metric {
+	display: flex;
+	flex-direction: column;
+	gap: 2px;
+}
+
+.oe-stage-cell__label {
+	font-size: 11px;
+	color: var(--order-text-muted, #909097);
+}
+
+.oe-stage-cell__value {
+	font-size: 17px;
+	font-weight: 700;
+	color: var(--order-text-primary, #e1e2eb);
+}
+
+.oe-stage-cell__unit {
+	font-size: 11px;
+	color: var(--order-text-muted, #909097);
+	margin-left: 2px;
+	font-weight: 400;
+}
+
+.oe-stage-cell__footer {
+	display: flex;
+	justify-content: space-between;
+	align-items: center;
+	font-size: 11px;
+}
+
+.oe-stage-cell__status {
+	color: var(--order-text-secondary, #c6c6cd);
+}
+
+.oe-stage-cell__variance--over { color: #ffb4ab; }
+.oe-stage-cell__variance--under { color: #88fd54; }
+.oe-stage-cell__variance--neutral { color: var(--order-text-muted, #909097); }
+
+@media (max-width: 1080px) {
+	.oe-stage-matrix__grid {
+		grid-template-columns: repeat(2, minmax(0, 1fr));
+	}
+}
+
+@media (max-width: 640px) {
+	.oe-stage-matrix__grid {
+		grid-template-columns: 1fr;
+	}
+}
+</style>

+ 16 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/SeedData/SysMenuSeedData.cs

@@ -790,6 +790,22 @@ public class SysMenuSeedData : ISqlSugarEntitySeedData<SysMenu>
             OrderNo = 14,
             Remark = "SO 订单执行档案总览(fixture 演示)"
         };
+        // ORDER-FLOW-CHAIN-SHELL-1:链路全景隐藏路由(侧栏不显示,由总览页订单卡跳转进入)。
+        yield return new SysMenu
+        {
+            Id = s8MonitoringDirId + 6,
+            Pid = s8MonitoringDirId,
+            Title = "订单链路全景",
+            Path = "/aidop/s8/monitoring/order-execution/chain",
+            Name = "aidopS8OrderExecutionChain",
+            Component = "/aidop/s8/monitoring/OrderChainOverviewPage",
+            Icon = "ele-Connection",
+            Type = MenuTypeEnum.Menu,
+            CreateTime = ct,
+            OrderNo = 15,
+            IsHide = true,
+            Remark = "SO 订单链路全景(隐藏路由,由总览页跳转进入)"
+        };
 
         // 可见业务页(「异常监控看板」复用自动生成的 1322000000027 菜单位)
         yield return new SysMenu