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

feat(s8): persist product design drawing chain data

Add S8 product design drawing fact data bound to order codes.

Expose product design drawing aggregation under order-flow.

Connect product design detail panel to backend data.

Preserve category and drawing detail layout.

Remove non-production label from product design detail.
YY968XX 3 месяцев назад
Родитель
Сommit
6c2bafdf02

+ 1 - 1
Web/package.json

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

+ 32 - 0
Web/src/stores/orderExecution.ts

@@ -19,11 +19,14 @@ import type {
 // ORDER-FLOW-S8-INTEGRATED-DOMAIN-RESET-1 t3g:正式 API client + mapper,loadFromDomain 走正式端点。
 // t3h:增订单详情按需加载,展开订单时调 getOrderFlowOrder 注入 lifecycle。
 // t3i:Chain 页 baseline + 单订单 chain(含 substeps/units)切正式 API。
+// S8-ORDER-CHAIN-PRODUCT-DESIGN-DRAWING-PERSIST-1:产品设计图号粒度数据走专用端点。
 import {
 	getOrderFlowAggregate,
 	getOrderFlowChain,
 	getOrderFlowOrder,
 	getOrderFlowOrders,
+	getOrderFlowProductDesignDrawings,
+	type OrderFlowProductDesignDrawings,
 } from '/@/views/aidop/s8/api/s8OrderFlowDomainApi';
 import {
 	mapDomainAggregateToLegacyBaseline,
@@ -71,6 +74,9 @@ interface OrderExecutionState {
 	initialized: boolean;
 	loading: boolean;
 	loadError: string | null;
+	productDesignDetail: OrderFlowProductDesignDrawings | null;
+	productDesignLoading: boolean;
+	productDesignError: string | null;
 }
 
 export const useOrderExecutionStore = defineStore('orderExecution', {
@@ -82,6 +88,9 @@ export const useOrderExecutionStore = defineStore('orderExecution', {
 		initialized: false,
 		loading: false,
 		loadError: null,
+		productDesignDetail: null,
+		productDesignLoading: false,
+		productDesignError: null,
 	}),
 	getters: {
 		allOrders(state): SalesOrderExecution[] {
@@ -269,5 +278,28 @@ export const useOrderExecutionStore = defineStore('orderExecution', {
 		clearChainSelection() {
 			this.selectedOrderNo = null;
 		},
+		// S8-ORDER-CHAIN-PRODUCT-DESIGN-DRAWING-PERSIST-1:按订单范围 + 产品类型拉取产品设计图号详情。
+		// orderCodes 为空数组 → 默认聚合全 20 单(scope=BASELINE_PPT);非空 → 按订单子集聚合。
+		// productType 仅过滤明细列表,summary.categories 始终返回三档。
+		// 失败时仅写 productDesignError,不清空已加载数据,避免页面闪空。
+		async loadProductDesignDrawings(orderCodes: string[] = [], productType: string | null = null) {
+			this.productDesignLoading = true;
+			this.productDesignError = null;
+			try {
+				const params: { orderCodes?: string; productType?: string } = {};
+				if (orderCodes.length > 0) params.orderCodes = orderCodes.join(',');
+				if (productType) params.productType = productType;
+				const detail = await getOrderFlowProductDesignDrawings(params);
+				this.productDesignDetail = detail ?? null;
+			} catch (err: unknown) {
+				this.productDesignError = err instanceof Error ? err.message : String(err);
+			} finally {
+				this.productDesignLoading = false;
+			}
+		},
+		clearProductDesignDetail() {
+			this.productDesignDetail = null;
+			this.productDesignError = null;
+		},
 	},
 });

+ 81 - 0
Web/src/views/aidop/s8/api/s8OrderFlowDomainApi.ts

@@ -189,6 +189,80 @@ export interface OrderFlowProcurementPivot {
 	matrixByMaterial: Record<string, OrderFlowProcurementMatrixRow[]>;
 }
 
+// ────────────────────────────────────────────────────────────
+// Product Design Drawings (S8-ORDER-CHAIN-PRODUCT-DESIGN-DRAWING-PERSIST-1)
+// ────────────────────────────────────────────────────────────
+
+export type OrderFlowProductDesignProductType = 'STANDARD' | 'NON_STANDARD' | 'TOTAL';
+export type OrderFlowProductDesignStatus = 'green' | 'yellow' | 'red' | 'pending';
+
+export interface OrderFlowProductDesignDrawingsQuery {
+	tenantId?: number;
+	factoryId?: number;
+	/** 逗号分隔订单号;为空时默认聚合全 20 单。 */
+	orderCodes?: string;
+	/** STANDARD / NON_STANDARD;为空时返回全量。 */
+	productType?: string;
+}
+
+export interface OrderFlowProductDesignDrawingItem {
+	orderCode: string;
+	drawingNo: string;
+	productType: 'STANDARD' | 'NON_STANDARD';
+	/** 责任单位/岗位(研发中心 / 结构设计组 / 电气设计组 / 工艺设计组 / 设计审核组)。 */
+	responsiblePerson: string;
+	plannedStartDate: string;
+	plannedEndDate: string;
+	actualStartDate: string | null;
+	actualEndDate: string | null;
+	kpiDays: number;
+	actualDays: number | null;
+	isAchieved: boolean;
+	status: OrderFlowProductDesignStatus;
+	productQuantity: number;
+}
+
+export interface OrderFlowProductDesignOverallSummary {
+	drawingCount: number;
+	totalQuantity: number;
+	kpiDays: number;
+	/** 图号 actual_days 算术平均(非加权),后端原始小数;前端格式化为 2 位 + "天"。 */
+	avgActualDays: number;
+	/** product_quantity 加权达成率,小数 0~1;前端格式化为整数百分比。 */
+	achievementRate: number;
+}
+
+export interface OrderFlowProductDesignCategorySummary {
+	productType: OrderFlowProductDesignProductType;
+	name: string;
+	drawingCount: number;
+	totalQuantity: number;
+	/** product_quantity 占比,小数 0~1;前端格式化为 1 位百分比。 */
+	ratio: number;
+	avgActualDays: number;
+	kpiDays: number;
+	achievementRate: number;
+	/** green / yellow / red / ''(空 = 无数据)。 */
+	status: 'green' | 'yellow' | 'red' | '';
+}
+
+export interface OrderFlowProductDesignSummary {
+	overall: OrderFlowProductDesignOverallSummary;
+	categories: OrderFlowProductDesignCategorySummary[];
+}
+
+export interface OrderFlowProductDesignFilter {
+	orderCodes: string[];
+	productType: 'STANDARD' | 'NON_STANDARD' | null;
+}
+
+export interface OrderFlowProductDesignDrawings {
+	scope: string;
+	filter: OrderFlowProductDesignFilter;
+	summary: OrderFlowProductDesignSummary;
+	drawings: OrderFlowProductDesignDrawingItem[];
+}
+
 // ────────────────────────────────────────────────────────────
 // API methods
 // ────────────────────────────────────────────────────────────
@@ -229,10 +303,17 @@ export function getOrderFlowProcurementPivot(query: OrderFlowProcurementPivotQue
 		.then(unwrap);
 }
 
+export function getOrderFlowProductDesignDrawings(query: OrderFlowProductDesignDrawingsQuery = {}) {
+	return service
+		.get<OrderFlowProductDesignDrawings>(`${BASE}/product-design/drawings`, { params: query })
+		.then(unwrap);
+}
+
 export const s8OrderFlowDomainApi = {
 	getOrderFlowOrders,
 	getOrderFlowOrder,
 	getOrderFlowChain,
 	getOrderFlowAggregate,
 	getOrderFlowProcurementPivot,
+	getOrderFlowProductDesignDrawings,
 };

+ 40 - 3
Web/src/views/aidop/s8/monitoring/OrderChainOverviewPage.vue

@@ -21,7 +21,6 @@ import ProcurementDetailPanel from './components/order-execution/ProcurementDeta
 import ManufacturingDetailPanel from './components/order-execution/ManufacturingDetailPanel.vue';
 import FinalAssemblyCollabPanel from './components/order-execution/FinalAssemblyCollabPanel.vue';
 import {
-	DESIGN_DETAIL_FIXTURE,
 	MANUFACTURING_DETAIL_FIXTURE,
 	adaptProcurementPivotFromApi,
 	type ProcurementDetailFromApi,
@@ -364,6 +363,34 @@ watch(
 	},
 	{ immediate: true },
 );
+
+// S8-ORDER-CHAIN-PRODUCT-DESIGN-DRAWING-PERSIST-1:产品设计图号粒度数据加载。
+// 单订单态 → orderCodes=[detailOrder.soNo];多订单态 → orderCodes=filteredChainOrders.map(soNo);
+// 默认基准态(isUnfiltered)→ orderCodes=[],后端走 BASELINE_PPT 聚合全 20 单。
+// 失败由 store.productDesignError 承载并由面板渲染生产级错误文案,不回退到静态数据。
+const productDesignDetail = computed(() => store.productDesignDetail);
+const productDesignLoading = computed(() => store.productDesignLoading);
+const productDesignError = computed(() => store.productDesignError);
+
+async function loadProductDesignDrawings() {
+	if (!showDesignDetail.value) return;
+	const orderCodes = isUnfiltered.value
+		? []
+		: filteredChainOrders.value.map((o) => o.soNo);
+	await store.loadProductDesignDrawings(orderCodes, null);
+}
+
+watch(
+	[
+		() => showDesignDetail.value,
+		() => filteredChainOrders.value,
+		() => isUnfiltered.value,
+	],
+	() => {
+		void loadProductDesignDrawings();
+	},
+	{ immediate: true },
+);
 </script>
 
 <template>
@@ -443,7 +470,12 @@ watch(
 				:breakdown="opinionBreakdown"
 				:title="`${activeSubstepName} — 涉及部门 / 组`"
 			/>
-			<DesignDetailPanel v-if="showDesignDetail" :detail="DESIGN_DETAIL_FIXTURE" />
+			<DesignDetailPanel
+					v-if="showDesignDetail"
+					:detail="productDesignDetail"
+					:loading="productDesignLoading"
+					:error-message="productDesignError"
+				/>
 			<ProcurementDetailPanel
 				v-if="showProcurementDetail"
 				:api-detail="procurementApiDetail"
@@ -504,7 +536,12 @@ watch(
 				:breakdown="opinionBreakdown"
 				:title="`${activeSubstepName} — 涉及部门 / 组`"
 			/>
-			<DesignDetailPanel v-if="showDesignDetail" :detail="DESIGN_DETAIL_FIXTURE" />
+			<DesignDetailPanel
+					v-if="showDesignDetail"
+					:detail="productDesignDetail"
+					:loading="productDesignLoading"
+					:error-message="productDesignError"
+				/>
 			<ProcurementDetailPanel
 				v-if="showProcurementDetail"
 				:api-detail="procurementApiDetail"

+ 266 - 66
Web/src/views/aidop/s8/monitoring/components/order-execution/DesignDetailPanel.vue

@@ -1,15 +1,105 @@
 <script setup lang="ts" name="DesignDetailPanel">
-// ORDER-FLOW-CHAIN-STAGE-DETAIL-MIGRATE-1:产品设计阶段结构化详情。fixture 来源 ecc-sandbox 硬编码;非真实数据。
-import type { DesignDetail, StageDetailStatus } from '/@/views/aidop/s8/monitoring/data/order-execution/stage-detail';
+// S8-ORDER-CHAIN-PRODUCT-DESIGN-DRAWING-PERSIST-1:产品设计阶段图号粒度详情面板。
+// 数据源:ado_s8_order_flow_product_design_drawing → /api/aidop/s8/order-flow/product-design/drawings。
+// 分类汇总 7 列与图号执行明细 9 列 UI 结构保持稳定;activeCategory 控制明细过滤,三档点击即过滤。
+import { computed, ref, watch } from 'vue';
+import type {
+	ProductDesignDetailDto,
+	ProductDesignDrawingItemDto,
+	ProductDesignProductType,
+} from '/@/views/aidop/s8/monitoring/data/order-execution/types';
 
-defineProps<{ detail: DesignDetail }>();
+interface Props {
+	detail: ProductDesignDetailDto | null;
+	loading?: boolean;
+	errorMessage?: string | null;
+}
+
+const props = withDefaults(defineProps<Props>(), {
+	loading: false,
+	errorMessage: null,
+});
+
+const activeCategory = ref<ProductDesignProductType>('TOTAL');
+
+const categoryRows = computed(() => props.detail?.summary.categories ?? []);
+
+const drawings = computed<ProductDesignDrawingItemDto[]>(() => {
+	const all = props.detail?.drawings ?? [];
+	if (activeCategory.value === 'TOTAL') return all;
+	return all.filter((d) => d.productType === activeCategory.value);
+});
+
+const isEmpty = computed(
+	() => !props.loading && !props.errorMessage && (props.detail?.drawings.length ?? 0) === 0,
+);
+
+// detail 切换(筛选订单变化)时如果当前 activeCategory 在新 categories 中已不存在,
+// fallback 到 TOTAL,避免明细表恒空。
+watch(
+	() => props.detail,
+	(next) => {
+		if (!next) return;
+		const allowed = new Set(next.summary.categories.map((c) => c.productType));
+		if (!allowed.has(activeCategory.value)) activeCategory.value = 'TOTAL';
+	},
+);
+
+function formatRatio(value: number | null | undefined): string {
+	if (value == null || Number.isNaN(value)) return '—';
+	return `${(value * 100).toFixed(1)}%`;
+}
+
+function formatAchievementRate(value: number | null | undefined): string {
+	if (value == null || Number.isNaN(value)) return '—';
+	return `${Math.round(value * 100)}%`;
+}
 
-function statusColor(s: StageDetailStatus | null): string {
+function formatDays(value: number | null | undefined): string {
+	if (value == null || Number.isNaN(value)) return '—';
+	return `${value.toFixed(2)} 天`;
+}
+
+function formatKpiDays(value: number | null | undefined): string {
+	if (value == null || Number.isNaN(value)) return '—';
+	return `${value.toFixed(0)} 天`;
+}
+
+function formatInt(value: number | null | undefined): string {
+	if (value == null || Number.isNaN(value)) return '—';
+	return String(value);
+}
+
+function formatDate(iso: string | null | undefined): string {
+	if (!iso) return '—';
+	const d = new Date(iso);
+	if (Number.isNaN(d.getTime())) return '—';
+	const y = d.getFullYear();
+	const m = String(d.getMonth() + 1).padStart(2, '0');
+	const day = String(d.getDate()).padStart(2, '0');
+	return `${y}-${m}-${day}`;
+}
+
+function categoryDisplay(productType: string): string {
+	if (productType === 'STANDARD') return '常规产品';
+	if (productType === 'NON_STANDARD') return '非标产品';
+	return productType;
+}
+
+function statusColor(s: string | null | undefined): string {
 	if (s === 'red') return '#ff4d4f';
 	if (s === 'yellow') return '#ffc107';
 	if (s === 'green') return '#88fd54';
 	return 'var(--order-text-muted, #909097)';
 }
+
+function onSelectCategory(productType: string) {
+	activeCategory.value = productType as ProductDesignProductType;
+}
+
+function isCategoryActive(productType: string): boolean {
+	return activeCategory.value === productType;
+}
 </script>
 
 <template>
@@ -17,77 +107,187 @@ function statusColor(s: StageDetailStatus | null): string {
 		<header class="design-panel__head">
 			<span class="design-panel__bar" />
 			<h2 class="design-panel__title">产品设计 · 阶段详情</h2>
-			<span class="design-panel__demo">演示数据</span>
 		</header>
 
-		<div class="design-panel__block">
-			<div class="design-panel__caption">设计分类汇总</div>
-			<table class="design-panel__table">
-				<thead>
-					<tr>
-						<th>产品类型</th><th>台数</th><th>占比</th><th>KPI</th><th>平均设计周期</th><th>达标</th><th>图号数量</th>
-					</tr>
-				</thead>
-				<tbody>
-					<tr v-for="row in detail.categoryRows" :key="row.type">
-						<td>{{ row.type }}</td>
-						<td>{{ row.count }}</td>
-						<td>{{ row.ratio }}</td>
-						<td>{{ row.kpi }}</td>
-						<td :style="{ color: statusColor(row.status) }">{{ row.avgCycle }}</td>
-						<td>
-							<span v-if="row.status" class="design-panel__dot" :style="{ background: statusColor(row.status) }" />
-							<span v-else>—</span>
-						</td>
-						<td>{{ row.drawingCount }}</td>
-					</tr>
-				</tbody>
-			</table>
+		<div v-if="loading" class="design-panel__hint">产品设计图号数据加载中</div>
+		<div v-else-if="errorMessage" class="design-panel__hint design-panel__hint--error">
+			{{ errorMessage || '产品设计图号数据加载失败' }}
 		</div>
+		<div v-else-if="isEmpty" class="design-panel__hint">当前筛选范围暂无产品设计图号记录</div>
 
-		<div class="design-panel__block">
-			<div class="design-panel__caption">图号执行明细(含两类样例)</div>
-			<table class="design-panel__table">
-				<thead>
-					<tr>
-						<th>负责人</th><th>图号</th><th>类别</th><th>计划开始</th><th>计划结束</th><th>实际开始</th><th>实际结束</th><th>设计周期</th><th>达标</th>
-					</tr>
-				</thead>
-				<tbody>
-					<tr v-for="row in detail.drawings" :key="row.id">
-						<td>{{ row.owner }}</td>
-						<td>{{ row.id }}</td>
-						<td>{{ row.category }}</td>
-						<td>{{ row.planStart }}</td>
-						<td>{{ row.planEnd }}</td>
-						<td>{{ row.actualStart }}</td>
-						<td>{{ row.actualEnd }}</td>
-						<td :style="{ color: statusColor(row.status) }">{{ row.cycle }}</td>
-						<td><span class="design-panel__dot" :style="{ background: statusColor(row.status) }" /></td>
-					</tr>
-				</tbody>
-			</table>
-		</div>
+		<template v-else-if="detail">
+			<div class="design-panel__block">
+				<div class="design-panel__caption">设计分类汇总(点击行可过滤下方图号执行明细)</div>
+				<table class="design-panel__table">
+					<thead>
+						<tr>
+							<th>产品类型</th>
+							<th>台数</th>
+							<th>占比</th>
+							<th>KPI</th>
+							<th>平均设计周期</th>
+							<th>达标</th>
+							<th>图号数量</th>
+						</tr>
+					</thead>
+					<tbody>
+						<tr
+							v-for="row in categoryRows"
+							:key="row.productType"
+							class="design-panel__row"
+							:class="{ 'design-panel__row--active': isCategoryActive(row.productType) }"
+							role="button"
+							tabindex="0"
+							@click="onSelectCategory(row.productType)"
+							@keydown.enter.prevent="onSelectCategory(row.productType)"
+							@keydown.space.prevent="onSelectCategory(row.productType)"
+						>
+							<td>{{ row.name }}</td>
+							<td>{{ formatInt(row.totalQuantity) }}</td>
+							<td>{{ formatRatio(row.ratio) }}</td>
+							<td>{{ formatKpiDays(row.kpiDays) }}</td>
+							<td :style="{ color: statusColor(row.status) }">{{ formatDays(row.avgActualDays) }}</td>
+							<td>
+								<template v-if="row.status">
+									<span class="design-panel__dot" :style="{ background: statusColor(row.status) }" />
+									<span class="design-panel__rate">{{ formatAchievementRate(row.achievementRate) }}</span>
+								</template>
+								<span v-else>—</span>
+							</td>
+							<td>{{ formatInt(row.drawingCount) }}</td>
+						</tr>
+					</tbody>
+				</table>
+			</div>
+
+			<div class="design-panel__block">
+				<div class="design-panel__caption">图号执行明细</div>
+				<table class="design-panel__table">
+					<thead>
+						<tr>
+							<th>负责人</th>
+							<th>图号</th>
+							<th>类别</th>
+							<th>计划开始</th>
+							<th>计划结束</th>
+							<th>实际开始</th>
+							<th>实际结束</th>
+							<th>设计周期</th>
+							<th>达标</th>
+						</tr>
+					</thead>
+					<tbody>
+						<tr v-for="row in drawings" :key="row.drawingNo">
+							<td>{{ row.responsiblePerson }}</td>
+							<td>{{ row.drawingNo }}</td>
+							<td>{{ categoryDisplay(row.productType) }}</td>
+							<td>{{ formatDate(row.plannedStartDate) }}</td>
+							<td>{{ formatDate(row.plannedEndDate) }}</td>
+							<td>{{ formatDate(row.actualStartDate) }}</td>
+							<td>{{ formatDate(row.actualEndDate) }}</td>
+							<td :style="{ color: statusColor(row.status) }">{{ formatDays(row.actualDays) }}</td>
+							<td><span class="design-panel__dot" :style="{ background: statusColor(row.status) }" /></td>
+						</tr>
+					</tbody>
+				</table>
+			</div>
+		</template>
 	</section>
 </template>
 
 <style scoped>
-.design-panel { display: flex; flex-direction: column; gap: 12px; }
-.design-panel__head { display: flex; align-items: center; gap: 10px; }
-.design-panel__bar { width: 5px; height: 18px; border-radius: 999px; background: var(--order-accent, #7bd0ff); }
-.design-panel__title { margin: 0; font-size: 16px; font-weight: 700; }
-.design-panel__demo {
-	margin-left: 6px; padding: 2px 10px; border-radius: 999px; font-size: 11px;
-	color: #ffc107; background: rgba(255,193,7,0.1); border: 1px solid rgba(255,193,7,0.32);
-}
-.design-panel__block { background: rgba(25, 28, 34, 0.55); border: 1px solid rgba(69, 70, 77, 0.28); border-radius: 10px; padding: 12px; overflow-x: auto; }
-.design-panel__caption { font-size: 12px; color: var(--order-text-muted, #909097); margin-bottom: 8px; }
-.design-panel__table { width: 100%; border-collapse: collapse; font-size: 12px; color: var(--order-text-primary, #e1e2eb); }
-.design-panel__table th, .design-panel__table td {
-	padding: 8px 10px; text-align: center;
+.design-panel {
+	display: flex;
+	flex-direction: column;
+	gap: 12px;
+}
+.design-panel__head {
+	display: flex;
+	align-items: center;
+	gap: 10px;
+}
+.design-panel__bar {
+	width: 5px;
+	height: 18px;
+	border-radius: 999px;
+	background: var(--order-accent, #7bd0ff);
+}
+.design-panel__title {
+	margin: 0;
+	font-size: 16px;
+	font-weight: 700;
+}
+.design-panel__hint {
+	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;
+}
+.design-panel__hint--error {
+	color: #ff8a8a;
+	border-style: solid;
+	border-color: rgba(255, 77, 79, 0.32);
+	background: rgba(255, 77, 79, 0.08);
+}
+.design-panel__block {
+	background: rgba(25, 28, 34, 0.55);
+	border: 1px solid rgba(69, 70, 77, 0.28);
+	border-radius: 10px;
+	padding: 12px;
+	overflow-x: auto;
+}
+.design-panel__caption {
+	font-size: 12px;
+	color: var(--order-text-muted, #909097);
+	margin-bottom: 8px;
+}
+.design-panel__table {
+	width: 100%;
+	border-collapse: collapse;
+	font-size: 12px;
+	color: var(--order-text-primary, #e1e2eb);
+}
+.design-panel__table th,
+.design-panel__table td {
+	padding: 8px 10px;
+	text-align: center;
 	border: 1px solid rgba(69, 70, 77, 0.18);
 	font-family: 'JetBrains Mono', 'Cascadia Code', Menlo, monospace;
 }
-.design-panel__table th { background: rgba(15, 19, 26, 0.7); font-weight: 600; font-family: 'PingFang SC', sans-serif; }
-.design-panel__dot { display: inline-block; width: 10px; height: 10px; border-radius: 50%; }
+.design-panel__table th {
+	background: rgba(15, 19, 26, 0.7);
+	font-weight: 600;
+	font-family: 'PingFang SC', sans-serif;
+}
+.design-panel__row {
+	cursor: pointer;
+	user-select: none;
+	transition: background 120ms ease, box-shadow 120ms ease;
+}
+.design-panel__row:hover {
+	background: rgba(123, 208, 255, 0.06);
+}
+.design-panel__row:focus-visible {
+	outline: 2px solid rgba(123, 208, 255, 0.5);
+	outline-offset: -2px;
+}
+.design-panel__row--active {
+	background: rgba(123, 208, 255, 0.16);
+	box-shadow: inset 0 0 0 1px rgba(123, 208, 255, 0.7);
+}
+.design-panel__dot {
+	display: inline-block;
+	width: 10px;
+	height: 10px;
+	border-radius: 50%;
+	vertical-align: middle;
+	margin-right: 6px;
+}
+.design-panel__rate {
+	font-size: 11px;
+	color: var(--order-text-secondary, #c6c6cd);
+}
 </style>

+ 3 - 44
Web/src/views/aidop/s8/monitoring/data/order-execution/stage-detail.ts

@@ -1,33 +1,9 @@
-// ORDER-FLOW-CHAIN-STAGE-DETAIL-MIGRATE-1:产品设计 / 材料采购 / 设备制造(本体生产)阶段静态结构化详情。
-// 来源:ecc-sandbox/frontend/src/pages/OrderChainOverviewPage.tsx 中的硬编码常量;显示侧必须明示「演示数据」。
-// 不连后端,不参与聚合,未来真实数据接入需替换 fixture 来源
+// ORDER-FLOW-CHAIN-STAGE-DETAIL-MIGRATE-1:材料采购 / 设备制造(本体生产)阶段静态结构化详情。
+// 产品设计阶段已迁移至 ado_s8_order_flow_product_design_drawing 表(S8-ORDER-CHAIN-PRODUCT-DESIGN-DRAWING-PERSIST-1),
+// 由 /api/aidop/s8/order-flow/product-design/drawings 接口聚合驱动,本文件不再承载产品设计阶段数据
 
 export type StageDetailStatus = 'green' | 'yellow' | 'red';
 
-export interface DesignDetail {
-	demoData: true;
-	categoryRows: Array<{
-		type: '常规产品' | '非标产品' | '合计';
-		count: number;
-		ratio: string;
-		kpi: string;
-		avgCycle: string;
-		status: StageDetailStatus | null;
-		drawingCount: number;
-	}>;
-	drawings: Array<{
-		id: string;
-		owner: string;
-		category: '常规产品' | '非标产品';
-		planStart: string;
-		planEnd: string;
-		actualStart: string;
-		actualEnd: string;
-		cycle: string;
-		status: StageDetailStatus;
-	}>;
-}
-
 // ORDER-FLOW-CHAIN-PROCUREMENT-PIVOT-1:供应商 × 线材周期透视表列定义。
 // 列顺序固定,模板按此顺序渲染。
 export const PROCUREMENT_MATRIX_COLUMNS = ['L4.5*11.2', 'MT2*6.3', 'MT1*5.4', '合计'] as const;
@@ -95,23 +71,6 @@ export interface ManufacturingDetail {
 	}>;
 }
 
-export const DESIGN_DETAIL_FIXTURE: DesignDetail = {
-	demoData: true,
-	categoryRows: [
-		{ type: '常规产品', count: 354, ratio: '93.5%', kpi: '--', avgCycle: '--', status: 'green', drawingCount: 52 },
-		{ type: '非标产品', count: 28, ratio: '6.5%', kpi: '3天', avgCycle: '9.28天', status: 'red', drawingCount: 11 },
-		{ type: '合计', count: 382, ratio: '100%', kpi: '--', avgCycle: '--', status: null, drawingCount: 63 },
-	],
-	drawings: [
-		{ id: 'A1354', owner: '王工', category: '非标产品', planStart: '8月1日', planEnd: '8月3日', actualStart: '7月30日', actualEnd: '8月3日', cycle: '5天', status: 'red' },
-		{ id: 'A1128', owner: '赵工', category: '非标产品', planStart: '8月2日', planEnd: '8月5日', actualStart: '8月1日', actualEnd: '8月4日', cycle: '3天', status: 'green' },
-		{ id: 'A1153', owner: '张工', category: '非标产品', planStart: '8月3日', planEnd: '8月6日', actualStart: '7月25日', actualEnd: '8月6日', cycle: '12天', status: 'red' },
-		{ id: 'A1287', owner: '李工', category: '常规产品', planStart: '8月5日', planEnd: '8月8日', actualStart: '8月4日', actualEnd: '8月7日', cycle: '3天', status: 'green' },
-		{ id: 'A1205', owner: '刘工', category: '常规产品', planStart: '8月6日', planEnd: '8月8日', actualStart: '8月6日', actualEnd: '8月8日', cycle: '2天', status: 'green' },
-		{ id: 'A1301', owner: '陈工', category: '常规产品', planStart: '8月7日', planEnd: '8月10日', actualStart: '8月7日', actualEnd: '8月9日', cycle: '2天', status: 'green' },
-	],
-};
-
 export const PROCUREMENT_DETAIL_FIXTURE: ProcurementDetail = {
 	demoData: true,
 	keyMaterials: [

+ 15 - 0
Web/src/views/aidop/s8/monitoring/data/order-execution/types.ts

@@ -268,3 +268,18 @@ export interface OrderExecutionKpiAverages {
 	avgProcessing: number;
 	avgLoss: number;
 }
+
+// ── 产品设计图号 (S8-ORDER-CHAIN-PRODUCT-DESIGN-DRAWING-PERSIST-1) ──
+// 类型源头在 s8OrderFlowDomainApi.ts;types.ts 仅 re-export 业务化别名供组件直接 import,
+// 保证全链路单一类型来源,避免 API ↔ 视图层重复声明导致字段漂移。
+export type {
+	OrderFlowProductDesignProductType as ProductDesignProductType,
+	OrderFlowProductDesignStatus as ProductDesignDrawingStatus,
+	OrderFlowProductDesignDrawingItem as ProductDesignDrawingItemDto,
+	OrderFlowProductDesignOverallSummary as ProductDesignOverallSummaryDto,
+	OrderFlowProductDesignCategorySummary as ProductDesignCategorySummaryDto,
+	OrderFlowProductDesignSummary as ProductDesignSummaryDto,
+	OrderFlowProductDesignFilter as ProductDesignFilterDto,
+	OrderFlowProductDesignDrawings as ProductDesignDetailDto,
+	OrderFlowProductDesignDrawingsQuery as ProductDesignDrawingsQuery,
+} from '/@/views/aidop/s8/api/s8OrderFlowDomainApi';

+ 3 - 0
server/Admin.NET.Web.Entry/Admin.NET.Web.Entry.csproj

@@ -64,6 +64,9 @@
     <None Update="UpdateScripts\1.0.121.sql">
       <CopyToOutputDirectory>Always</CopyToOutputDirectory>
     </None>
+    <None Update="UpdateScripts\1.0.122.sql">
+      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+    </None>
   </ItemGroup>
 
   <ItemGroup>

+ 30 - 0
server/Admin.NET.Web.Entry/UpdateScripts/1.0.122.sql

@@ -0,0 +1,30 @@
+-- 1.0.122.sql
+-- S8-ORDER-CHAIN-PRODUCT-DESIGN-DRAWING-PERSIST-1
+-- 无数据变更脚本:仅用于推进 AutoVersionUpdate 历史版本号至 1.0.122。
+--
+-- 实际 Admin.NET 启动顺序为:
+--   1) CodeFirst (EnableInitTable=true) 自动建表 ado_s8_order_flow_product_design_drawing;
+--   2) SeedData ([IncreSeed]) 注入 63 行产品设计阶段基准业务初始化数据;
+--   3) AutoVersionUpdate 执行本 SQL。
+-- 由于 SeedData 步骤先于 AutoVersionUpdate,本脚本若执行任何 DELETE/TRUNCATE 会清空步骤 2 刚注入的图号数据;
+-- 故本脚本不执行任何数据清理,仅推进 AutoVersionUpdate 历史版本至 1.0.122。
+--
+-- 数据来源与维护:
+--   * 实体定义:server/Plugins/Admin.NET.Plugin.AiDOP/Entity/S8/OrderFlow/AdoS8OrderFlowProductDesignDrawing.cs
+--   * 种子定义:server/Plugins/Admin.NET.Plugin.AiDOP/SeedData/S8OrderFlowProductDesignDrawingSeedData.cs
+--   * 服务聚合:S8OrderFlowService.GetProductDesignDrawingsAsync
+--   * 路由:GET /api/aidop/s8/order-flow/product-design/drawings
+--
+-- 安全边界:
+--   * 不动 ado_s8_order_flow_product_design_drawing;
+--   * 不动 ado_s8_order_flow_order / stage / substep / substep_unit;
+--   * 不动 ado_product_design(独立 Order 域表,与 S8 订单无 FK 关联);
+--   * 不动其它 4 个一级阶段相关表(ORDER_REVIEW_PLAN_CALC / MATERIAL_PURCHASE / BODY_PRODUCTION /
+--     FINAL_ASSEMBLY_DELIVERY);
+--   * 无 DELETE、无 TRUNCATE、无 DROP、无 ALTER、无 UPDATE。
+--
+-- Rollback: 无影响(本脚本无数据变更)。
+-- 2026-05-21
+
+-- 无数据变更语句,保证执行器接受脚本并推进历史版本号至 1.0.122。
+SELECT 1;

+ 10 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Controllers/S8/AdoS8OrderFlowController.cs

@@ -50,4 +50,14 @@ public class AdoS8OrderFlowController : ControllerBase
     [HttpGet("procurement-pivot")]
     public async Task<IActionResult> ProcurementPivotAsync([FromQuery] AdoS8OrderFlowProcurementPivotQueryDto query)
         => Ok(await _svc.GetProcurementPivotAsync(query));
+
+    /// <summary>
+    /// S8-ORDER-CHAIN-PRODUCT-DESIGN-DRAWING-PERSIST-1:产品设计图号粒度聚合。
+    /// orderCodes 为空时默认聚合全 20 单(scope=BASELINE_PPT);非空时按订单子集聚合(scope=CURRENT_FILTERED)。
+    /// productType 仅过滤 drawings 明细列表,summary.categories 始终返回常规/非标/合计三档。
+    /// </summary>
+    [HttpGet("product-design/drawings")]
+    public async Task<IActionResult> ProductDesignDrawingsAsync(
+        [FromQuery] AdoS8OrderFlowProductDesignDrawingsQueryDto query)
+        => Ok(await _svc.GetProductDesignDrawingsAsync(query));
 }

+ 90 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Dto/S8/OrderFlow/AdoS8OrderFlowDtos.cs

@@ -252,3 +252,93 @@ public class AdoS8OrderFlowRelatedExceptionQueryDto
 }
 
 #endregion
+
+#region 产品设计图号
+
+/// <summary>
+/// S8-ORDER-CHAIN-PRODUCT-DESIGN-DRAWING-PERSIST-1:产品设计图号查询入参。
+/// orderCodes 为空 → 默认聚合全 20 单(scope=BASELINE_PPT);
+/// orderCodes 非空 → 仅命中订单范围聚合(scope=CURRENT_FILTERED)。
+/// productType 仅过滤 drawings 列表,不改变 summary.categories 的"常规/非标/合计"三档结构。
+/// </summary>
+public class AdoS8OrderFlowProductDesignDrawingsQueryDto
+{
+    public long? TenantId { get; set; }
+    public long? FactoryId { get; set; }
+    /// <summary>逗号分隔订单号;为空时默认全 20 单。</summary>
+    public string? OrderCodes { get; set; }
+    /// <summary>STANDARD / NON_STANDARD;为空时返回全量并保留三档汇总。</summary>
+    public string? ProductType { get; set; }
+}
+
+/// <summary>查询条件回显,便于前端固定下钻状态。</summary>
+public class AdoS8OrderFlowProductDesignFilterDto
+{
+    public List<string> OrderCodes { get; set; } = new();
+    public string? ProductType { get; set; }
+}
+
+public class AdoS8OrderFlowProductDesignDrawingItemDto
+{
+    public string OrderCode { get; set; } = string.Empty;
+    public string DrawingNo { get; set; } = string.Empty;
+    /// <summary>STANDARD / NON_STANDARD。</summary>
+    public string ProductType { get; set; } = string.Empty;
+    /// <summary>责任单位/岗位(研发中心 / 结构设计组 / 电气设计组 / 工艺设计组 / 设计审核组)。</summary>
+    public string ResponsiblePerson { get; set; } = string.Empty;
+    public DateTime PlannedStartDate { get; set; }
+    public DateTime PlannedEndDate { get; set; }
+    public DateTime? ActualStartDate { get; set; }
+    public DateTime? ActualEndDate { get; set; }
+    public decimal KpiDays { get; set; }
+    public decimal? ActualDays { get; set; }
+    public bool IsAchieved { get; set; }
+    /// <summary>green / yellow / red / pending。</summary>
+    public string Status { get; set; } = string.Empty;
+    /// <summary>该图号对应台数;台数/占比/加权达成率三种口径的唯一来源。</summary>
+    public int ProductQuantity { get; set; }
+}
+
+public class AdoS8OrderFlowProductDesignOverallSummaryDto
+{
+    public int DrawingCount { get; set; }
+    public int TotalQuantity { get; set; }
+    public decimal KpiDays { get; set; }
+    /// <summary>图号 actual_days 算术平均(非加权),保留小数原值,由前端格式化为 2 位。</summary>
+    public decimal AvgActualDays { get; set; }
+    /// <summary>按 product_quantity 加权达标率,小数 0~1,由前端格式化为整数百分比。</summary>
+    public decimal AchievementRate { get; set; }
+}
+
+public class AdoS8OrderFlowProductDesignCategorySummaryDto
+{
+    /// <summary>STANDARD / NON_STANDARD / TOTAL。</summary>
+    public string ProductType { get; set; } = string.Empty;
+    public string Name { get; set; } = string.Empty;
+    public int DrawingCount { get; set; }
+    public int TotalQuantity { get; set; }
+    /// <summary>product_quantity 占合计比例,小数 0~1,由前端格式化为 1 位百分比。</summary>
+    public decimal Ratio { get; set; }
+    public decimal AvgActualDays { get; set; }
+    public decimal KpiDays { get; set; }
+    public decimal AchievementRate { get; set; }
+    /// <summary>green / yellow / red。由 achievement_rate 分档得出,不再二次判定。</summary>
+    public string Status { get; set; } = string.Empty;
+}
+
+public class AdoS8OrderFlowProductDesignSummaryDto
+{
+    public AdoS8OrderFlowProductDesignOverallSummaryDto Overall { get; set; } = new();
+    public List<AdoS8OrderFlowProductDesignCategorySummaryDto> Categories { get; set; } = new();
+}
+
+public class AdoS8OrderFlowProductDesignDrawingsDto
+{
+    /// <summary>BASELINE_PPT(默认全 20 单)/ CURRENT_FILTERED(命中订单子集)。</summary>
+    public string Scope { get; set; } = string.Empty;
+    public AdoS8OrderFlowProductDesignFilterDto Filter { get; set; } = new();
+    public AdoS8OrderFlowProductDesignSummaryDto Summary { get; set; } = new();
+    public List<AdoS8OrderFlowProductDesignDrawingItemDto> Drawings { get; set; } = new();
+}
+
+#endregion

+ 103 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Entity/S8/OrderFlow/AdoS8OrderFlowProductDesignDrawing.cs

@@ -0,0 +1,103 @@
+namespace Admin.NET.Plugin.AiDOP.Entity.S8.OrderFlow;
+
+/// <summary>
+/// S8-ORDER-CHAIN-PRODUCT-DESIGN-DRAWING-PERSIST-1:S8 订单链路产品设计图号事实表。
+/// 承载 PRODUCT_DESIGN 阶段图号粒度的产品设计阶段基准业务初始化数据:
+///   - 1 行 = 1 张图号绑定 1 个订单(OrderCode + DrawingNo 为业务唯一);
+///   - 台数 / 占比 / 加权达成率 全部由 product_quantity 字段单一来源驱动;
+///   - 平均设计周期 由 actual_days 算术平均;
+///   - 达标判定 由 is_achieved + 当前 KPI=3 天为阈值(actual_days &lt;= kpi_days);
+/// 设计语义独立,不复用 ado_s8_order_flow_substep / substep_unit 的工时小时口径,
+/// 也不与 ado_product_design(独立 Order 域表)建立物理依赖。
+/// </summary>
+[SugarTable("ado_s8_order_flow_product_design_drawing", "S8 订单链路产品设计图号事实表")]
+[SugarIndex("uk_order_flow_pdd_drawing",
+    nameof(TenantId), OrderByType.Asc,
+    nameof(FactoryId), OrderByType.Asc,
+    nameof(OrderCode), OrderByType.Asc,
+    nameof(DrawingNo), OrderByType.Asc,
+    IsUnique = true)]
+[SugarIndex("idx_order_flow_pdd_order_code",
+    nameof(TenantId), OrderByType.Asc,
+    nameof(FactoryId), OrderByType.Asc,
+    nameof(OrderCode), OrderByType.Asc)]
+[SugarIndex("idx_order_flow_pdd_product_type",
+    nameof(TenantId), OrderByType.Asc,
+    nameof(FactoryId), OrderByType.Asc,
+    nameof(ProductType), OrderByType.Asc)]
+public class AdoS8OrderFlowProductDesignDrawing
+{
+    [SugarColumn(ColumnName = "id", IsPrimaryKey = true, ColumnDataType = "bigint")]
+    public long Id { get; set; }
+
+    [SugarColumn(ColumnName = "tenant_id", ColumnDataType = "bigint")]
+    public long TenantId { get; set; }
+
+    [SugarColumn(ColumnName = "factory_id", ColumnDataType = "bigint")]
+    public long FactoryId { get; set; }
+
+    /// <summary>订单业务键,引用 ado_s8_order_flow_order.order_code(无物理 FK,逻辑一致性由 service 保证)。</summary>
+    [SugarColumn(ColumnName = "order_code", Length = 64)]
+    public string OrderCode { get; set; } = string.Empty;
+
+    /// <summary>图号编码:常规 D-{OrderCode}-G{NN};非标 D-{OrderCode}-X{NN}。租户/工厂内 OrderCode + DrawingNo 业务唯一。</summary>
+    [SugarColumn(ColumnName = "drawing_no", Length = 64)]
+    public string DrawingNo { get; set; } = string.Empty;
+
+    /// <summary>产品类型:STANDARD(常规产品)/ NON_STANDARD(非标产品)。白名单由 service / seed 约束。</summary>
+    [SugarColumn(ColumnName = "product_type", Length = 16)]
+    public string ProductType { get; set; } = string.Empty;
+
+    /// <summary>该图号对应台数。前端"台数 / 占比 / 加权达成率"三种口径的唯一数据源。</summary>
+    [SugarColumn(ColumnName = "product_quantity")]
+    public int ProductQuantity { get; set; }
+
+    /// <summary>负责人字段,承载责任单位/岗位(研发中心 / 结构设计组 / 电气设计组 / 工艺设计组 / 设计审核组)。</summary>
+    [SugarColumn(ColumnName = "responsible_person", Length = 64)]
+    public string ResponsiblePerson { get; set; } = string.Empty;
+
+    /// <summary>图号粒度计划开始日期(订单 PRODUCT_DESIGN 阶段区间内)。</summary>
+    [SugarColumn(ColumnName = "planned_start_date")]
+    public DateTime PlannedStartDate { get; set; }
+
+    /// <summary>图号粒度计划结束日期。</summary>
+    [SugarColumn(ColumnName = "planned_end_date")]
+    public DateTime PlannedEndDate { get; set; }
+
+    /// <summary>图号粒度实际开始日期;未开工时为 null。</summary>
+    [SugarColumn(ColumnName = "actual_start_date", IsNullable = true)]
+    public DateTime? ActualStartDate { get; set; }
+
+    /// <summary>图号粒度实际结束日期;未完成时为 null。</summary>
+    [SugarColumn(ColumnName = "actual_end_date", IsNullable = true)]
+    public DateTime? ActualEndDate { get; set; }
+
+    /// <summary>KPI 标准设计天数。PRODUCT_DESIGN 阶段固定 3 天,落库为常量字段以支持后续 KPI 调整。</summary>
+    [SugarColumn(ColumnName = "kpi_days", DecimalDigits = 2, Length = 6)]
+    public decimal KpiDays { get; set; }
+
+    /// <summary>实际设计周期(天)。actual_end_date - actual_start_date 派生;未完成时为 null。</summary>
+    [SugarColumn(ColumnName = "actual_days", DecimalDigits = 2, Length = 6, IsNullable = true)]
+    public decimal? ActualDays { get; set; }
+
+    /// <summary>是否达标:actual_days &lt;= kpi_days。未完成图号此列 false。</summary>
+    [SugarColumn(ColumnName = "is_achieved", ColumnDataType = "boolean")]
+    public bool IsAchieved { get; set; }
+
+    /// <summary>green / yellow / red / pending。green = actual_days &lt;= kpi_days;yellow = (actual - kpi)/kpi &lt;= 0.20;red = 其余;pending = 未完成。</summary>
+    [SugarColumn(ColumnName = "status", Length = 16)]
+    public string Status { get; set; } = "pending";
+
+    /// <summary>同订单内图号展示顺序,从 1 起。</summary>
+    [SugarColumn(ColumnName = "sort_no")]
+    public int SortNo { get; set; }
+
+    [SugarColumn(ColumnName = "is_deleted", ColumnDataType = "boolean")]
+    public bool IsDeleted { get; set; }
+
+    [SugarColumn(ColumnName = "create_time")]
+    public DateTime CreateTime { get; set; } = DateTime.Now;
+
+    [SugarColumn(ColumnName = "update_time", IsNullable = true)]
+    public DateTime? UpdateTime { get; set; }
+}

+ 193 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/SeedData/S8OrderFlowProductDesignDrawingSeedData.cs

@@ -0,0 +1,193 @@
+using Admin.NET.Plugin.AiDOP.Entity.S8.OrderFlow;
+
+namespace Admin.NET.Plugin.AiDOP.SeedData;
+
+/// <summary>
+/// S8-ORDER-CHAIN-PRODUCT-DESIGN-DRAWING-PERSIST-1:S8 订单链路产品设计阶段基准业务初始化数据种子。
+/// 20 单 × 63 图号;严格满足七项不变量约束:
+///   1) COUNT(DISTINCT drawing_no) = 63;STANDARD=52 / NON_STANDARD=11
+///   2) SUM(product_quantity ALL) = 382;STANDARD=357 / NON_STANDARD=25
+///   3) AVG(actual_days WHERE product_type='STANDARD')     落在 [1.17, 1.21]
+///   4) AVG(actual_days WHERE product_type='NON_STANDARD') 落在 [9.25, 9.31]
+///   5) AVG(actual_days ALL) ≈ 2.60 天
+///   6) SUM(product_quantity WHERE is_achieved=false) 落在 [14, 16]
+///   7) 加权达成率 = 达标 quantity / 总 quantity ≈ 96%
+/// 图号编码:常规 D-{OrderCode}-G{NN};非标 D-{OrderCode}-X{NN}(绑定 SO-2026-001..020 真实 OrderCode)。
+/// 责任单位/岗位 5 选 1:研发中心 / 结构设计组 / 电气设计组 / 工艺设计组 / 设计审核组;不引入个人姓名。
+/// </summary>
+[IncreSeed]
+public class S8OrderFlowProductDesignDrawingSeedData
+    : ISqlSugarEntitySeedData<AdoS8OrderFlowProductDesignDrawing>
+{
+    public IEnumerable<AdoS8OrderFlowProductDesignDrawing> HasData()
+        => S8OrderFlowProductDesignDrawingDataset.BuildDrawings();
+}
+
+internal static class S8OrderFlowProductDesignDrawingDataset
+{
+    /// <summary>图号 id 基址(与其他 S8 种子号段隔离)。</summary>
+    internal const long IdBase = 1329909130000L;
+
+    internal const string ProductTypeStandard = "STANDARD";
+    internal const string ProductTypeNonStandard = "NON_STANDARD";
+
+    /// <summary>常规产品 KPI(天)。</summary>
+    internal const decimal StandardKpiDays = 3.00m;
+
+    /// <summary>非标产品 KPI(天)。业务允许非标设计周期更长,落库为图号粒度独立 KPI。</summary>
+    internal const decimal NonStandardKpiDays = 12.00m;
+
+    /// <summary>责任单位/岗位(5 选 1,按 sort_no 循环分配;不使用个人姓名)。</summary>
+    private static readonly string[] ResponsibleUnits =
+    {
+        "研发中心",
+        "结构设计组",
+        "电气设计组",
+        "工艺设计组",
+        "设计审核组",
+    };
+
+    /// <summary>
+    /// 每订单 (常规图号数, 非标图号数)。20 行合计 52 / 11;其中 4 单 4 图、16 单 3 图。
+    /// 非标集中在大客户、P1 优先级与 in_progress 订单上,与业务直觉一致。
+    /// </summary>
+    private static readonly (int StdCount, int NonStdCount)[] PerOrderConfig =
+    {
+        // SO-2026-001..005
+        (3, 1), (3, 1), (3, 1), (3, 0), (3, 0),
+        // SO-2026-006..010
+        (2, 1), (2, 1), (2, 1), (3, 0), (3, 0),
+        // SO-2026-011..015
+        (3, 0), (2, 1), (3, 0), (3, 0), (3, 0),
+        // SO-2026-016..020
+        (3, 0), (2, 1), (2, 1), (2, 1), (2, 1),
+    };
+
+    /// <summary>52 张常规图号 spec:合计 actual_days=61.88、qty=357、1 张超期 (qty=3)。</summary>
+    private static readonly DrawingSpec[] StandardSequence = BuildStandardSequence();
+
+    /// <summary>11 张非标图号 spec:合计 actual_days=102.08、qty=25、2 张超期 (qty=6 each)。</summary>
+    private static readonly DrawingSpec[] NonStandardSequence = BuildNonStandardSequence();
+
+    /// <summary>单张图号的回卷规格:actual_days / quantity / is_achieved / status。</summary>
+    internal readonly record struct DrawingSpec(decimal ActualDays, int Quantity, bool IsAchieved, string Status);
+
+    private static DrawingSpec[] BuildStandardSequence()
+    {
+        // 52 张分布:24×(1.13, qty=7) + 24×(1.12, qty=7) + 2×(1.13, qty=6) + 1×(1.12, qty=6) + 1×(4.50, qty=3, red)
+        // actual_days 合计 = 24×1.13 + 24×1.12 + 2×1.13 + 1×1.12 + 4.50
+        //                  = 27.12 + 26.88 + 2.26 + 1.12 + 4.50 = 61.88  → avg 1.190 (在 [1.17, 1.21])
+        // quantity 合计    = 24×7 + 24×7 + 2×6 + 1×6 + 3 = 168 + 168 + 12 + 6 + 3 = 357
+        var seq = new DrawingSpec[52];
+        for (var i = 0; i < 24; i++) seq[i] = new DrawingSpec(1.13m, 7, true, "green");
+        for (var i = 24; i < 48; i++) seq[i] = new DrawingSpec(1.12m, 7, true, "green");
+        seq[48] = new DrawingSpec(1.13m, 6, true, "green");
+        seq[49] = new DrawingSpec(1.13m, 6, true, "green");
+        seq[50] = new DrawingSpec(1.12m, 6, true, "green");
+        // 唯一一张超期常规图号:actual=4.50 > kpi=3 → (4.5-3)/3 = 0.50 > 0.20 → red
+        seq[51] = new DrawingSpec(4.50m, 3, false, "red");
+        return seq;
+    }
+
+    private static DrawingSpec[] BuildNonStandardSequence()
+    {
+        // 11 张分布:5×(8.34, qty=1) + 4×(8.34, qty=2) + 2×(13.51, qty=6, yellow)
+        // actual_days 合计 = 9×8.34 + 2×13.51 = 75.06 + 27.02 = 102.08 → avg 9.28 (在 [9.25, 9.31])
+        // quantity 合计    = 5×1 + 4×2 + 2×6 = 5 + 8 + 12 = 25
+        var seq = new DrawingSpec[11];
+        for (var i = 0; i < 5; i++) seq[i] = new DrawingSpec(8.34m, 1, true, "green");
+        for (var i = 5; i < 9; i++) seq[i] = new DrawingSpec(8.34m, 2, true, "green");
+        // 两张超期非标图号:actual=13.51 > kpi=12 → (13.51-12)/12 ≈ 0.126 ≤ 0.20 → yellow
+        seq[9] = new DrawingSpec(13.51m, 6, false, "yellow");
+        seq[10] = new DrawingSpec(13.51m, 6, false, "yellow");
+        return seq;
+    }
+
+    public static IEnumerable<AdoS8OrderFlowProductDesignDrawing> BuildDrawings()
+    {
+        int stdCursor = 0, nonStdCursor = 0;
+        long seq = 0;
+
+        for (var i = 0; i < S8OrderFlowDataset.Specs.Length; i++)
+        {
+            var orderSpec = S8OrderFlowDataset.Specs[i];
+            var (stdN, nonStdN) = PerOrderConfig[i];
+
+            // 图号粒度计划开始 = release_at + 5 天(ORDER_REVIEW KPI=5 天结束后进入 PRODUCT_DESIGN)。
+            // 所有图号同基准开始,actualEnd 由 actualDays 派生,保证 actualEnd - actualStart 严格等于 actualDays。
+            var plannedStart = S8OrderFlowDataset.ReleaseBase.AddDays(5);
+
+            var sortNo = 0;
+
+            for (var g = 0; g < stdN; g++)
+            {
+                sortNo++;
+                var spec = StandardSequence[stdCursor++];
+                yield return BuildDrawing(
+                    seq: ++seq,
+                    orderSpec: orderSpec,
+                    sortNo: sortNo,
+                    drawingNo: $"D-{orderSpec.OrderCode}-G{g + 1:D2}",
+                    productType: ProductTypeStandard,
+                    kpiDays: StandardKpiDays,
+                    plannedStart: plannedStart,
+                    spec: spec);
+            }
+
+            for (var x = 0; x < nonStdN; x++)
+            {
+                sortNo++;
+                var spec = NonStandardSequence[nonStdCursor++];
+                yield return BuildDrawing(
+                    seq: ++seq,
+                    orderSpec: orderSpec,
+                    sortNo: sortNo,
+                    drawingNo: $"D-{orderSpec.OrderCode}-X{x + 1:D2}",
+                    productType: ProductTypeNonStandard,
+                    kpiDays: NonStandardKpiDays,
+                    plannedStart: plannedStart,
+                    spec: spec);
+            }
+        }
+    }
+
+    private static AdoS8OrderFlowProductDesignDrawing BuildDrawing(
+        long seq,
+        S8OrderFlowDataset.OrderSpec orderSpec,
+        int sortNo,
+        string drawingNo,
+        string productType,
+        decimal kpiDays,
+        DateTime plannedStart,
+        DrawingSpec spec)
+    {
+        var plannedEnd = plannedStart.AddDays((double)kpiDays);
+        var actualStart = plannedStart;
+        var actualEnd = actualStart.AddDays((double)spec.ActualDays);
+        var responsible = ResponsibleUnits[(sortNo - 1) % ResponsibleUnits.Length];
+
+        return new AdoS8OrderFlowProductDesignDrawing
+        {
+            Id = IdBase + seq,
+            TenantId = 1,
+            FactoryId = 1,
+            OrderCode = orderSpec.OrderCode,
+            DrawingNo = drawingNo,
+            ProductType = productType,
+            ProductQuantity = spec.Quantity,
+            ResponsiblePerson = responsible,
+            PlannedStartDate = plannedStart,
+            PlannedEndDate = plannedEnd,
+            ActualStartDate = actualStart,
+            ActualEndDate = actualEnd,
+            KpiDays = kpiDays,
+            ActualDays = spec.ActualDays,
+            IsAchieved = spec.IsAchieved,
+            Status = spec.Status,
+            SortNo = sortNo,
+            IsDeleted = false,
+            CreateTime = S8OrderFlowDataset.CreatedAt,
+            UpdateTime = null,
+        };
+    }
+}

+ 168 - 1
server/Plugins/Admin.NET.Plugin.AiDOP/Service/S8/OrderFlow/S8OrderFlowService.cs

@@ -44,6 +44,7 @@ public class S8OrderFlowService : ITransient
     private readonly SqlSugarRepository<AdoS8OrderFlowSnapshot> _snapshotRep;
     private readonly SqlSugarRepository<AdoS8OrderFlowProcurementPivot> _pivotRep;
     private readonly SqlSugarRepository<AdoS8Exception> _exceptionRep;
+    private readonly SqlSugarRepository<AdoS8OrderFlowProductDesignDrawing> _pddRep;
 
     public S8OrderFlowService(
         SqlSugarRepository<AdoS8OrderFlowOrder> orderRep,
@@ -52,7 +53,8 @@ public class S8OrderFlowService : ITransient
         SqlSugarRepository<AdoS8OrderFlowSubstepUnit> unitRep,
         SqlSugarRepository<AdoS8OrderFlowSnapshot> snapshotRep,
         SqlSugarRepository<AdoS8OrderFlowProcurementPivot> pivotRep,
-        SqlSugarRepository<AdoS8Exception> exceptionRep)
+        SqlSugarRepository<AdoS8Exception> exceptionRep,
+        SqlSugarRepository<AdoS8OrderFlowProductDesignDrawing> pddRep)
     {
         _orderRep = orderRep;
         _stageRep = stageRep;
@@ -61,6 +63,7 @@ public class S8OrderFlowService : ITransient
         _snapshotRep = snapshotRep;
         _pivotRep = pivotRep;
         _exceptionRep = exceptionRep;
+        _pddRep = pddRep;
     }
 
     /// <summary>订单档案列表(无分页)。当前 baseline 20 单。</summary>
@@ -690,4 +693,168 @@ public class S8OrderFlowService : ITransient
 
         return result;
     }
+
+    // ──────────────────────────────────────────────────────────────────────
+    // S8-ORDER-CHAIN-PRODUCT-DESIGN-DRAWING-PERSIST-1:产品设计图号粒度聚合。
+    // 单一数据源 = ado_s8_order_flow_product_design_drawing 表;
+    // 台数 / 占比 / 加权达成率 全部由 product_quantity 字段驱动;
+    // 平均设计周期 = actual_days 算术平均(非 quantity 加权);
+    // summary.categories 固定返回 STANDARD / NON_STANDARD / TOTAL 三档;
+    // drawings 按 productType 过滤;未达标优先 + actualDays 降序 + orderCode/drawingNo 升序。
+    // ──────────────────────────────────────────────────────────────────────
+
+    private const string ProductDesignTypeStandard = "STANDARD";
+    private const string ProductDesignTypeNonStandard = "NON_STANDARD";
+    private const string ProductDesignTypeTotal = "TOTAL";
+    private const decimal ProductDesignStageKpiDays = 3m;
+    private const decimal ProductDesignNonStandardKpiDays = 12m;
+
+    public async Task<AdoS8OrderFlowProductDesignDrawingsDto> GetProductDesignDrawingsAsync(
+        AdoS8OrderFlowProductDesignDrawingsQueryDto query)
+    {
+        var tenantId = query.TenantId ?? 1;
+        var factoryId = query.FactoryId ?? 1;
+
+        var orderCodes = ParseOrderCodesCsv(query.OrderCodes);
+        var productTypeFilter = NormalizeProductDesignType(query.ProductType);
+        var scope = orderCodes.Count == 0 ? ScopeBaselinePpt : ScopeCurrentFiltered;
+
+        var rows = await _pddRep.AsQueryable()
+            .Where(d => d.TenantId == tenantId && d.FactoryId == factoryId && !d.IsDeleted)
+            .WhereIF(orderCodes.Count > 0, d => orderCodes.Contains(d.OrderCode))
+            .ToListAsync();
+
+        // summary 始终基于命中订单范围的全分类汇总(不受 productType 过滤影响),让前端可同时渲染三档汇总。
+        var summary = BuildProductDesignSummary(rows);
+
+        var drawingItems = rows.AsEnumerable();
+        if (productTypeFilter != null)
+            drawingItems = drawingItems.Where(d => d.ProductType == productTypeFilter);
+
+        var orderedDrawings = drawingItems
+            .OrderBy(d => d.IsAchieved)
+            .ThenByDescending(d => d.ActualDays ?? decimal.MinValue)
+            .ThenBy(d => d.OrderCode, StringComparer.Ordinal)
+            .ThenBy(d => d.DrawingNo, StringComparer.Ordinal)
+            .Select(MapProductDesignDrawingItem)
+            .ToList();
+
+        return new AdoS8OrderFlowProductDesignDrawingsDto
+        {
+            Scope = scope,
+            Filter = new AdoS8OrderFlowProductDesignFilterDto
+            {
+                OrderCodes = orderCodes,
+                ProductType = productTypeFilter,
+            },
+            Summary = summary,
+            Drawings = orderedDrawings,
+        };
+    }
+
+    private static List<string> ParseOrderCodesCsv(string? raw)
+    {
+        if (string.IsNullOrWhiteSpace(raw)) return new List<string>();
+        return raw.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
+                  .Where(s => !string.IsNullOrWhiteSpace(s))
+                  .Distinct(StringComparer.Ordinal)
+                  .ToList();
+    }
+
+    private static string? NormalizeProductDesignType(string? raw)
+    {
+        if (string.IsNullOrWhiteSpace(raw)) return null;
+        var s = raw.Trim().ToUpperInvariant();
+        return s == ProductDesignTypeStandard || s == ProductDesignTypeNonStandard ? s : null;
+    }
+
+    private static AdoS8OrderFlowProductDesignSummaryDto BuildProductDesignSummary(
+        List<AdoS8OrderFlowProductDesignDrawing> rows)
+    {
+        var stdRows = rows.Where(r => r.ProductType == ProductDesignTypeStandard).ToList();
+        var nonStdRows = rows.Where(r => r.ProductType == ProductDesignTypeNonStandard).ToList();
+        var grandTotalQty = rows.Sum(r => r.ProductQuantity);
+
+        return new AdoS8OrderFlowProductDesignSummaryDto
+        {
+            Overall = BuildProductDesignOverall(rows),
+            Categories = new List<AdoS8OrderFlowProductDesignCategorySummaryDto>
+            {
+                BuildProductDesignCategory(ProductDesignTypeStandard,    "常规产品", ProductDesignStageKpiDays,        stdRows,    grandTotalQty),
+                BuildProductDesignCategory(ProductDesignTypeNonStandard, "非标产品", ProductDesignNonStandardKpiDays,  nonStdRows, grandTotalQty),
+                BuildProductDesignCategory(ProductDesignTypeTotal,       "合计",     ProductDesignStageKpiDays,        rows,       grandTotalQty),
+            },
+        };
+    }
+
+    private static AdoS8OrderFlowProductDesignOverallSummaryDto BuildProductDesignOverall(
+        List<AdoS8OrderFlowProductDesignDrawing> rows)
+    {
+        var totalQty = rows.Sum(r => r.ProductQuantity);
+        var achievedQty = rows.Where(r => r.IsAchieved).Sum(r => r.ProductQuantity);
+        var actualDays = rows.Where(r => r.ActualDays.HasValue).Select(r => r.ActualDays!.Value).ToList();
+
+        return new AdoS8OrderFlowProductDesignOverallSummaryDto
+        {
+            DrawingCount = rows.Select(r => r.DrawingNo).Distinct(StringComparer.Ordinal).Count(),
+            TotalQuantity = totalQty,
+            KpiDays = ProductDesignStageKpiDays,
+            AvgActualDays = actualDays.Count == 0 ? 0m : actualDays.Average(),
+            AchievementRate = totalQty == 0 ? 0m : (decimal)achievedQty / totalQty,
+        };
+    }
+
+    private static AdoS8OrderFlowProductDesignCategorySummaryDto BuildProductDesignCategory(
+        string productType,
+        string name,
+        decimal kpiDays,
+        List<AdoS8OrderFlowProductDesignDrawing> rows,
+        int grandTotalQty)
+    {
+        var totalQty = rows.Sum(r => r.ProductQuantity);
+        var achievedQty = rows.Where(r => r.IsAchieved).Sum(r => r.ProductQuantity);
+        var actualDays = rows.Where(r => r.ActualDays.HasValue).Select(r => r.ActualDays!.Value).ToList();
+        var rate = totalQty == 0 ? 0m : (decimal)achievedQty / totalQty;
+
+        return new AdoS8OrderFlowProductDesignCategorySummaryDto
+        {
+            ProductType = productType,
+            Name = name,
+            DrawingCount = rows.Select(r => r.DrawingNo).Distinct(StringComparer.Ordinal).Count(),
+            TotalQuantity = totalQty,
+            Ratio = grandTotalQty == 0 ? 0m : (decimal)totalQty / grandTotalQty,
+            AvgActualDays = actualDays.Count == 0 ? 0m : actualDays.Average(),
+            KpiDays = kpiDays,
+            AchievementRate = rate,
+            Status = ClassifyProductDesignCategoryStatus(rate, totalQty),
+        };
+    }
+
+    /// <summary>达标率 ≥ 0.95 绿 / ≥ 0.80 黄 / 否则 红;无数据返回空串以表达"未取数"而非状态判定。</summary>
+    private static string ClassifyProductDesignCategoryStatus(decimal achievementRate, int totalQty)
+    {
+        if (totalQty == 0) return string.Empty;
+        if (achievementRate >= 0.95m) return "green";
+        if (achievementRate >= 0.80m) return "yellow";
+        return "red";
+    }
+
+    private static AdoS8OrderFlowProductDesignDrawingItemDto MapProductDesignDrawingItem(
+        AdoS8OrderFlowProductDesignDrawing d)
+        => new()
+        {
+            OrderCode = d.OrderCode,
+            DrawingNo = d.DrawingNo,
+            ProductType = d.ProductType,
+            ResponsiblePerson = d.ResponsiblePerson,
+            PlannedStartDate = d.PlannedStartDate,
+            PlannedEndDate = d.PlannedEndDate,
+            ActualStartDate = d.ActualStartDate,
+            ActualEndDate = d.ActualEndDate,
+            KpiDays = d.KpiDays,
+            ActualDays = d.ActualDays,
+            IsAchieved = d.IsAchieved,
+            Status = d.Status,
+            ProductQuantity = d.ProductQuantity,
+        };
 }