Эх сурвалжийг харах

feat(s6): align IPQC detail layout with FQC

以 S7 fqc/fqcInspResultDetail.vue 为页面模板,把 S6 IPQC 独立详情页的区块顺序、
Descriptions 布局、检验项目主表、展开样本、流程区与操作区做成同一套页面体验。
只对齐 UI:接口、DTO、枚举、检规快照、处置类型全部沿用 S6 自己的,未调用任何 S7 接口。

区块:基础信息 → 生产信息 → 检规信息 → 检验项目明细 → 流程信息 → 分角色操作区
→ QE 处置结论(仅展示)→ 审核历史。
撤掉原来风格突兀的独立「检验结果」大区(整单状态/结果/项目数折入基础信息);
descriptions 统一 :column="2" border size="small";缺值占位由「—」改「-」,均与 FQC 同款。

主表改为 FQC 14 列同序:展开 / 序号 / 检验项目 / 接收标准 / 技术要求 / 抽样方案 /
样本量 / 上限 / 下限 / 规格标准 / 判定 / 非数值OK / 非数值NG / 备注。
实测样本从主表大单元格移入展开行(编辑态也在展开行),工序 / 检验方法 / 录入类型 /
录入状态降级为展开行二级信息,不再占据主表业务列。

字段缺口按实际来源分三类处理,不伪造:
- MAPPING GAP(已修):指令单数量接既有 S6 指令单只读接口、技术要求接 tech_standard、
  样本量接 sample_count、上下限拆两列、序号在 sort_no 为 NULL 时按行序回退。
- BACKEND FIELD GAP(只登记,未改后端):bill_no、SAP工单号、规格标准、
  单头级合格/不合格数量、行级备注。
- LEGACY DATA GAP(非 UI bug):部分单据 item_name 是字面量 'null'(X-003 残留)、
  specification / lot_serial 为 NULL;bill 1 这些字段有真值且已正常显示。
非数值OK/NG 与备注是对已录样本的派生聚合,不是新造字段。

有意保留的三处 FQC delta(已在文件头注明):区块标题用「基础信息/检验项目明细」而非
FQC 域专有的「基本信息/成品信息/品检项目明细」;流程信息多一行流程实例;末尾保留
FQC 没有的「审核历史」。

编辑权限仍是后端 canEdit 单点决定,并由 :disabled 置灰强化为「只读态不渲染编辑控件」。
契约测试冻结区块顺序、descriptions props、主表列序、操作区位置与展开行降级字段。

Runtime(租户 838257186181189):
bill 1 只读态字段/展开样本/流程/历史 PASS|bill 9 首提→展开行录入→保存落库→完成检验
→提交审核→N2 只读(表内 0 输入控件)→退回→N1 恢复可录入→改样2=20.3 落库、同实例仍在
N1 PASS|bill 2 QE 处置结论区 PASS|直链硬刷新 PASS|列表→详情→返回保留筛选 PASS|
与 FQC 逐项对照:主表列清单与 el-table class 串逐字相同、两页 descriptions 每行 2 列
且均 size=small+border|console 0 error / 0 warn。
YY968XX 1 сар өмнө
parent
commit
de74f22544

+ 1 - 1
Web/package.json

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

+ 164 - 104
Web/src/views/aidop/s6/process-quality/components/IpqcBillItemPanel.vue

@@ -1,97 +1,106 @@
 <template>
 	<div class="item-panel">
-		<div class="panel-head">
-			<span class="panel-title">检验项目明细</span>
+		<div class="items-title">
+			检验项目明细
 			<el-tag v-if="canEdit" type="warning" size="small" effect="plain" disable-transitions>可录入</el-tag>
 			<el-tag v-else type="info" size="small" effect="plain" disable-transitions>只读</el-tag>
-			<span class="panel-hint">判定与整单结果一律以后端计算为准,前端仅作即时反馈</span>
 		</div>
 
-		<el-table :data="lines" border stripe size="small" row-key="id" max-height="460">
-			<el-table-column type="index" label="#" width="46" align="center" />
-			<el-table-column prop="processCode" label="工序" min-width="80" show-overflow-tooltip>
-				<template #default="{ row }">{{ dash(row.processCode) }}</template>
-			</el-table-column>
-			<el-table-column prop="inspectionItem" label="检验项目" min-width="120" show-overflow-tooltip />
-			<el-table-column label="录入类型" min-width="96" align="center">
-				<template #default="{ row }">
-					<el-tag size="small" :type="row.resultType === 'NUMERIC' ? 'success' : 'info'" disable-transitions>
-						{{ row.resultType === 'NUMERIC' ? '数值' : row.resultType === 'NON_NUMERIC' ? '非数值' : dash(row.resultType) }}
-					</el-tag>
-				</template>
-			</el-table-column>
-			<el-table-column label="标准 / 上下限" min-width="140" class-name="wrap-cell">
-				<template #default="{ row }">
-					<span v-if="row.resultType === 'NUMERIC'">[{{ dash(row.lowerLimit) }} , {{ dash(row.upperLimit) }}]</span>
-					<span v-else>{{ dash(row.techStandard || row.specValue) }}</span>
-				</template>
-			</el-table-column>
-			<el-table-column prop="inspectionFrequency" label="频次" min-width="76" show-overflow-tooltip>
-				<template #default="{ row }">{{ dash(row.inspectionFrequency) }}</template>
-			</el-table-column>
-			<el-table-column label="实测录入" min-width="320">
+		<div v-if="canEdit" class="insp-toolbar">
+			<el-button :loading="saving" @click="emit('save', buildSamples())">保存</el-button>
+			<el-button type="primary" :loading="completing" @click="emit('complete', buildSamples())">完成检验</el-button>
+			<span class="insp-tip">展开左侧箭头录入实测值;只提交实测值,判定与整单结果一律由后端计算。</span>
+		</div>
+
+		<el-table :data="lines" border stripe size="small" row-key="id">
+			<el-table-column type="expand">
 				<template #default="{ row }">
-					<template v-if="row.resultType === 'NUMERIC'">
+					<div class="expand-meta">
+						<span class="lbl">工序</span>{{ dash(row.processCode) }}<template v-if="row.processName"> / {{ row.processName }}</template>
+						<span class="lbl">检验方法</span>{{ dash(row.method) }}
+						<span class="lbl">录入类型</span>{{ resultTypeText(row.resultType) }}
+						<span class="lbl">录入状态</span>{{ itemStatusText(row.itemStatus) }}
+					</div>
+
+					<!-- 录入态:数值型逐样本输入(上下限来自建单时冻结的快照)-->
+					<div v-if="canEdit && row.resultType === 'NUMERIC'" class="samples-wrap">
+						<span class="samples-label">样本值(上限 {{ dash(row.upperLimit) }} / 下限 {{ dash(row.lowerLimit) }}):</span>
 						<span v-for="i in sampleCountOf(row)" :key="i" class="sample-cell">
-							<el-input
-								v-model="entry[keyOf(row.id, i)]"
-								size="small"
-								style="width: 76px"
-								:placeholder="'样' + i"
-								:disabled="!canEdit"
-							/>
+							<el-input v-model="entry[keyOf(row.id, i)]" size="small" class="sample" :placeholder="'样本' + i" />
 							<el-tag v-if="liveJudge(row, i)" size="small" :type="judgeTag(liveJudge(row, i))" disable-transitions>
 								{{ shortJudge(liveJudge(row, i)) }}
 							</el-tag>
 						</span>
-					</template>
-					<template v-else>
-						<el-select
-							v-model="entry[keyOf(row.id, 1)]"
-							size="small"
-							style="width: 104px"
-							placeholder="OK / NG"
-							clearable
-							:disabled="!canEdit"
-						>
+					</div>
+
+					<!-- 录入态:非数值型录 OK / NG(S6 按样本记值,不记合格数/不合格数)-->
+					<div v-else-if="canEdit" class="samples-wrap">
+						<span class="samples-label">判定录入:</span>
+						<el-select v-model="entry[keyOf(row.id, 1)]" size="small" class="sample-sel" placeholder="OK / NG" clearable>
 							<el-option label="OK" value="OK" />
 							<el-option label="NG" value="NG" />
 						</el-select>
-						<el-tag v-if="liveJudge(row, 1)" size="small" :type="judgeTag(liveJudge(row, 1))" disable-transitions class="ml6">
+						<el-tag v-if="liveJudge(row, 1)" size="small" :type="judgeTag(liveJudge(row, 1))" disable-transitions>
 							{{ shortJudge(liveJudge(row, 1)) }}
 						</el-tag>
-					</template>
+					</div>
+
+					<!-- 只读态:后端已录样本 + 后端判定 -->
+					<div v-else class="samples-wrap">
+						<template v-if="(row.samples || []).length">
+							<span class="samples-label">样品值:</span>
+							<el-tag v-for="s in row.samples || []" :key="s.sampleIndex" class="sample-tag" type="info" disable-transitions>
+								样品{{ s.sampleIndex }}: {{ sampleValueOf(row, s) }}
+								<template v-if="shortJudge(s.judgement)"> {{ shortJudge(s.judgement) }}</template>
+								<template v-if="s.remark"> · {{ s.remark }}</template>
+							</el-tag>
+						</template>
+						<span v-else class="samples-empty">无样品数据</span>
+					</div>
 				</template>
 			</el-table-column>
-			<el-table-column label="后端判定" min-width="150" class-name="wrap-cell">
-				<template #default="{ row }">
-					<span v-if="(row.samples || []).length === 0" class="muted">暂无</span>
-					<span v-for="s in row.samples || []" :key="s.sampleIndex" class="sample-cell">
-						<el-tag size="small" :type="judgeTag(s.judgement)" effect="plain" disable-transitions>
-							{{ s.sampleIndex }}:{{ shortJudge(s.judgement) || dash(s.judgement) }}
-						</el-tag>
-					</span>
-				</template>
+			<el-table-column label="序号" width="56" resizable>
+				<template #default="{ row, $index }">{{ row.sortNo ?? $index + 1 }}</template>
 			</el-table-column>
-			<el-table-column label="单项结果" min-width="108" align="center">
-				<template #default="{ row }">
-					<el-tag :type="judgeTag(row.itemResult)" size="small" disable-transitions>{{ resultText(row.itemResult) }}</el-tag>
-					<div class="item-status">{{ itemStatusText(row.itemStatus) }}</div>
-				</template>
+			<el-table-column prop="inspectionItem" label="检验项目" min-width="130" class-name="wrap-cell" resizable />
+			<el-table-column label="接收标准" min-width="150" class-name="wrap-cell" resizable>
+				<template #default="{ row }">{{ dash(row.specValue) }}</template>
+			</el-table-column>
+			<el-table-column label="技术要求" min-width="150" class-name="wrap-cell" resizable>
+				<template #default="{ row }">{{ dash(row.techStandard) }}</template>
+			</el-table-column>
+			<el-table-column label="抽样方案" min-width="120" class-name="wrap-cell" resizable>
+				<template #default="{ row }">{{ dash(row.inspectionFrequency) }}</template>
+			</el-table-column>
+			<el-table-column label="样本量" min-width="90" resizable>
+				<template #default="{ row }">{{ dash(row.sampleCount) }}</template>
+			</el-table-column>
+			<el-table-column label="上限" min-width="90" resizable>
+				<template #default="{ row }">{{ dash(row.upperLimit) }}</template>
+			</el-table-column>
+			<el-table-column label="下限" min-width="90" resizable>
+				<template #default="{ row }">{{ dash(row.lowerLimit) }}</template>
+			</el-table-column>
+			<!-- 规格标准:S6 快照无独立尺寸规格列(spec_value 已用于「接收标准」),一律 '-',见 BACKEND FIELD GAP -->
+			<el-table-column label="规格标准" min-width="120" class-name="wrap-cell" resizable>
+				<template #default>-</template>
+			</el-table-column>
+			<el-table-column label="判定" min-width="90" resizable>
+				<template #default="{ row }">{{ resultText(row.itemResult) }}</template>
+			</el-table-column>
+			<el-table-column label="非数值OK" min-width="100" resizable>
+				<template #default="{ row }">{{ nonNumericCount(row, 'OK') }}</template>
+			</el-table-column>
+			<el-table-column label="非数值NG" min-width="100" resizable>
+				<template #default="{ row }">{{ nonNumericCount(row, 'NG') }}</template>
+			</el-table-column>
+			<el-table-column label="备注" min-width="140" class-name="wrap-cell" resizable>
+				<template #default="{ row }">{{ lineRemark(row) }}</template>
 			</el-table-column>
 			<template #empty>
 				<el-empty description="暂无检验项目" :image-size="60" />
 			</template>
 		</el-table>
-
-		<div v-if="canEdit" class="panel-foot">
-			<span class="panel-hint">只提交实测值,不提交判定</span>
-			<el-button :loading="saving" @click="emit('save', buildSamples())">保存</el-button>
-			<el-button type="primary" :loading="completing" @click="emit('complete', buildSamples())">完成检验</el-button>
-		</div>
-		<div v-else class="panel-foot">
-			<span class="panel-hint">已提交审核,检验结果只读</span>
-		</div>
 	</div>
 </template>
 
@@ -99,21 +108,27 @@
 /**
  * S6 IPQC 检查单 · 检验项目明细面板。
  *
- * 从原「生产指令单列表」页 Drawer 里的录入表格抽出,供独立详情页复用 ——
- * 抽的目的不是为了复用次数,而是不让详情页变成又一个几百行的巨型单文件
- * (S7 FQC 的 fqcInspResultDetail.vue 592 行全在一处,本项目 coding-style 要求 200-400 行)。
+ * 结构以 S7 `fqcInspResultDetail.vue` 的只读明细表为模板(S6-IPQC-FQC-PARITY-1,2026-09-14):
+ * 主列顺序、展开行、wrap-cell 换行、`#empty` 空态都与 FQC 一致;
+ * 实测样本从原来的主表大单元格移入**展开行**(编辑态也在展开行),
+ * 让主表第一眼看到的是「检验项目 / 标准 / 抽样 / 上下限 / 判定」这类业务列,
+ * 而不是工序 / 录入类型 / 频次这类系统字段。
+ *
+ * 只对齐 UI,不引入 FQC 业务模型:
+ * - 不用 FQC 的「合格数 / 不合格数」录入(S6 非数值型按样本记 OK/NG,不记计数);
+ * - 不给用户「判定」下拉(S6 的判定一律后端算,FQC 那个 select 不能照抄);
+ * - 「非数值OK / 非数值NG」两列是对已录样本的**派生计数**,不是新造字段。
  *
  * 两条 authority 约定(与 S5 IqcInspBillItemPanel 同款,勿改):
  * - `canEdit` 是本组件**唯一**的编辑开关,组件内不按 currentNodeCode / flowStatus 自行推断;
- * - 样本判定一律由后端算。组件里的 `liveJudge` 只是给录入者的即时反馈,
- *   **不参与提交**(`buildSamples` 只吐实测值),后端判定单独在「后端判定」列回显。
- *   两列并存是刻意的:录入时看到自己的即时反馈,保存后看到后端的权威判定,不一致立刻可见。
+ * - 样本判定一律由后端算。`liveJudge` 只是录入者的即时反馈,**不参与提交**
+ *   (`buildSamples` 只吐实测值),后端判定在展开行的样品标签上回显。
  *
  * 录入态由本组件自持(`entry`),从 `lines[].samples` 初始化,刷新可恢复;
  * 父组件不需要知道 key 编码方式,只在 save/complete 时收到构造好的 samples。
  */
 import { reactive, watch } from 'vue';
-import type { ProcessInspectionSnapshotLine, SaveResultSample } from '../../api/productionInstruction';
+import type { ProcessInspectionResultSample, ProcessInspectionSnapshotLine, SaveResultSample } from '../../api/productionInstruction';
 
 const props = withDefaults(
 	defineProps<{
@@ -155,7 +170,7 @@ watch(() => props.lines, syncEntryFromLines, { immediate: true, deep: false });
 
 /**
  * 录入者的即时反馈,**非真值**:NUMERIC 闭区间;NON_NUMERIC OK→PASS / NG→FAIL;其余 INVALID。
- * 与后端判定口径一致是有意的,但一旦两者显示不一致,以「后端判定」列为准。
+ * 与后端判定口径一致是有意的,但一旦两者显示不一致,以后端回显为准。
  */
 function liveJudge(row: ProcessInspectionSnapshotLine, i: number): string {
 	const raw = (entry[keyOf(row.id, i)] || '').trim();
@@ -189,6 +204,26 @@ function buildSamples(): SaveResultSample[] {
 	return out;
 }
 
+function sampleValueOf(row: ProcessInspectionSnapshotLine, s: ProcessInspectionResultSample) {
+	return row.resultType === 'NUMERIC' ? dash(s.actualNumeric) : dash(s.actualNonNumeric);
+}
+
+/**
+ * 「非数值OK / 非数值NG」= 对已录样本的派生计数(S6 无 qualifiedQty / unqualifiedQty 字段)。
+ * 数值型项目一律 '-',与 FQC 主表同款。
+ */
+function nonNumericCount(row: ProcessInspectionSnapshotLine, want: 'OK' | 'NG') {
+	if (row.resultType !== 'NON_NUMERIC') return '-';
+	const n = (row.samples ?? []).filter((s) => String(s.actualNonNumeric ?? '').trim().toUpperCase() === want).length;
+	return String(n);
+}
+
+/** S6 无行级备注列,此处拼接该项目各样本的备注;都为空则 '-'。 */
+function lineRemark(row: ProcessInspectionSnapshotLine) {
+	const rs = (row.samples ?? []).map((s) => (s.remark ?? '').trim()).filter((r) => r !== '');
+	return rs.length ? rs.join(';') : '-';
+}
+
 function judgeTag(result?: string): 'success' | 'danger' | 'info' | 'warning' {
 	if (result === 'PASS') return 'success';
 	if (result === 'FAIL') return 'danger';
@@ -211,66 +246,91 @@ function resultText(v?: string) {
 	return dash(v);
 }
 
+function resultTypeText(v?: string) {
+	if (v === 'NUMERIC') return '数值型';
+	if (v === 'NON_NUMERIC') return '非数值型';
+	return dash(v);
+}
+
 function itemStatusText(v?: string) {
 	if (v === 'COMPLETED') return '已录满';
 	if (v === 'IN_PROGRESS') return '录入中';
 	if (v === 'NOT_STARTED') return '未开始';
-	return '';
+	return '-';
 }
 
-/** 缺字段一律显示 —,不让 undefined / null / 字面量 'null' 漏到界面上。 */
+/** 缺字段一律显示 '-'(与 FQC 同款),不让 undefined / null / 字面量 'null' 漏到界面上。 */
 function dash(v?: unknown) {
-	if (v === null || v === undefined) return '—';
+	if (v === null || v === undefined) return '-';
 	const s = String(v).trim();
-	if (s === '' || s === 'null' || s === 'undefined') return '—';
+	if (s === '' || s === 'null' || s === 'undefined') return '-';
 	return s;
 }
 </script>
 
 <style scoped>
-.panel-head {
+/* 区块标题与工具条:与 S7 FQC 的 .items-title / .insp-toolbar 同款 */
+.items-title {
 	display: flex;
 	align-items: center;
 	gap: 8px;
+	font-weight: bold;
+	font-size: 14px;
 	margin-bottom: 8px;
 }
-.panel-title {
-	font-weight: 600;
-	font-size: 14px;
+.insp-toolbar {
+	display: flex;
+	align-items: center;
+	gap: 10px;
+	flex-wrap: wrap;
+	margin-bottom: 8px;
+}
+.insp-tip {
+	color: var(--el-text-color-secondary);
+	font-size: 12px;
 }
-.panel-hint {
+.expand-meta {
+	padding: 6px 12px 0;
 	font-size: 12px;
+	color: var(--el-text-color-regular);
+}
+.expand-meta .lbl {
 	color: var(--el-text-color-secondary);
+	margin: 0 4px 0 14px;
+}
+.expand-meta .lbl:first-child {
+	margin-left: 0;
 }
-.panel-foot {
-	margin-top: 12px;
+.samples-wrap {
+	padding: 6px 12px;
 	display: flex;
+	flex-wrap: wrap;
 	align-items: center;
-	justify-content: flex-end;
-	gap: 10px;
+	gap: 6px;
+}
+.samples-label {
+	color: var(--el-text-color-secondary);
+	font-size: 12px;
+}
+.sample-tag {
+	margin: 0;
 }
-.panel-foot .panel-hint {
-	margin-right: auto;
+.samples-empty {
+	color: var(--el-text-color-secondary);
+	font-size: 12px;
 }
 .sample-cell {
 	display: inline-flex;
 	align-items: center;
 	gap: 3px;
-	margin: 2px 6px 2px 0;
 }
-.ml6 {
-	margin-left: 6px;
+.sample {
+	width: 96px;
 }
-.item-status {
-	font-size: 11px;
-	color: var(--el-text-color-secondary);
-	margin-top: 2px;
-}
-.muted {
-	font-size: 12px;
-	color: var(--el-text-color-secondary);
+.sample-sel {
+	width: 104px;
 }
-/* 与 S7 FQC 同款:标准/判定列用换行替代 tooltip 省略号,避免上下限被截断看不全 */
+/* 文本列自动换行(替代 show-overflow-tooltip 的省略号截断),与 FQC 同款 */
 :deep(.wrap-cell .cell) {
 	white-space: pre-wrap;
 	word-break: break-word;

+ 175 - 124
Web/src/views/aidop/s6/process-quality/ipqcBillDetail.vue

@@ -1,17 +1,10 @@
 <template>
 	<AidopDemoShell title="IPQC 检查单详情" subtitle="过程检验单(工序级)· 检验结果与审核流">
-		<div class="page-head">
-			<el-button size="small" @click="goBack">← 返回</el-button>
+		<div class="detail-topbar">
+			<el-button @click="goBack">← 返回</el-button>
 			<span class="bill-no">{{ headTitle }}</span>
-			<el-tag v-if="review?.displayStatus" :type="flowTagType" size="small" effect="plain" disable-transitions>
-				{{ review.displayStatus }}
-			</el-tag>
-			<el-tag v-if="review?.qualityText || detail?.overallResult" :type="judgeTag(detail?.overallResult)" size="small" disable-transitions>
-				{{ review?.qualityText || detail?.overallResult }}
-			</el-tag>
-			<el-tag v-if="review?.currentNodeName" size="small" type="info" effect="plain" disable-transitions>
-				当前节点:{{ review.currentNodeName }}
-			</el-tag>
+			<el-tag v-if="review?.displayStatus" :type="flowTagType" size="small" disable-transitions>{{ review.displayStatus }}</el-tag>
+			<el-tag v-if="review?.currentNodeName" size="small" disable-transitions>{{ review.currentNodeName }}</el-tag>
 		</div>
 
 		<div v-if="invalidId" class="detail-body">
@@ -21,36 +14,34 @@
 		</div>
 
 		<div v-else class="detail-body" v-loading="detailLoading">
-			<el-descriptions :column="3" border size="small" title="基础信息">
-				<el-descriptions-item label="工单编号">{{ dash(detail?.workOrderNo) }}</el-descriptions-item>
-				<el-descriptions-item label="批次号">{{ dash(detail?.lotSerial) }}</el-descriptions-item>
+			<el-descriptions title="基础信息" :column="2" border size="small">
+				<el-descriptions-item label="检验单号">{{ dash(detail?.billId) }}</el-descriptions-item>
+				<el-descriptions-item label="单据状态">{{ dash(detail?.inspectionStatus) }}</el-descriptions-item>
 				<el-descriptions-item label="物料编码">{{ dash(detail?.itemCode) }}</el-descriptions-item>
 				<el-descriptions-item label="物料名称">{{ dash(detail?.itemName) }}</el-descriptions-item>
 				<el-descriptions-item label="规格型号">{{ dash(detail?.specification) }}</el-descriptions-item>
-				<el-descriptions-item label="单据状态">{{ dash(detail?.inspectionStatus) }}</el-descriptions-item>
+				<el-descriptions-item label="生产批号">{{ dash(detail?.lotSerial) }}</el-descriptions-item>
 				<el-descriptions-item label="建单人">{{ dash(detail?.createdByName) }}</el-descriptions-item>
 				<el-descriptions-item label="建单时间">{{ fmtDateTime(detail?.createTime) }}</el-descriptions-item>
-				<el-descriptions-item label="检验单 id">{{ dash(detail?.billId) }}</el-descriptions-item>
+				<el-descriptions-item label="整单状态">{{ overallStatusText }}</el-descriptions-item>
+				<el-descriptions-item label="整单结果">{{ overallResultText }}</el-descriptions-item>
+				<el-descriptions-item label="检验项目数">{{ (detail?.lines || []).length }}</el-descriptions-item>
+			</el-descriptions>
+
+			<el-descriptions title="生产信息" :column="2" border size="small" class="mt8">
+				<el-descriptions-item label="生产指令单">{{ dash(detail?.workOrderNo) }}</el-descriptions-item>
+				<el-descriptions-item label="指令单数量">{{ dash(workOrder?.orderQty) }}</el-descriptions-item>
+				<!-- SAP工单号:S6 过程检验单与生产指令单均无该字段,见 BACKEND FIELD GAP -->
+				<el-descriptions-item label="SAP工单号">-</el-descriptions-item>
 			</el-descriptions>
 
-			<el-descriptions class="mt8" :column="3" border size="small" title="检规信息(建单时冻结的快照)">
+			<el-descriptions title="检规信息" :column="2" border size="small" class="mt8">
 				<el-descriptions-item label="检规编号">{{ dash(detail?.specCode) }}</el-descriptions-item>
 				<el-descriptions-item label="检规版本">{{ dash(detail?.specVersion) }}</el-descriptions-item>
 				<el-descriptions-item label="生效日期">{{ fmtDate(detail?.effectiveDate) }}</el-descriptions-item>
 			</el-descriptions>
 
-			<el-descriptions class="mt8" :column="3" border size="small" title="检验结果">
-				<el-descriptions-item label="整单状态">{{ overallStatusText }}</el-descriptions-item>
-				<el-descriptions-item label="整单结果">
-					<el-tag v-if="detail?.overallResult" :type="judgeTag(detail.overallResult)" size="small" disable-transitions>
-						{{ review?.qualityText || detail.overallResult }}
-					</el-tag>
-					<span v-else>—</span>
-				</el-descriptions-item>
-				<el-descriptions-item label="检验项目数">{{ (detail?.lines || []).length }}</el-descriptions-item>
-			</el-descriptions>
-
-			<div class="mt12">
+			<div class="mt8">
 				<IpqcBillItemPanel
 					:lines="detail?.lines || []"
 					:can-edit="canEdit"
@@ -61,28 +52,66 @@
 				/>
 			</div>
 
-			<el-descriptions class="mt12" :column="3" border size="small" title="流程信息" v-loading="reviewLoading">
+			<el-descriptions title="流程信息" :column="2" border size="small" class="mt8" v-loading="reviewLoading">
 				<el-descriptions-item label="流程状态">{{ dash(review?.displayStatus) }}</el-descriptions-item>
 				<el-descriptions-item label="当前节点">{{ dash(review?.currentNodeName) }}</el-descriptions-item>
-				<el-descriptions-item label="当前处理角色">{{ dash(review?.currentAssigneeRole) }}</el-descriptions-item>
+				<el-descriptions-item label="当前角色">{{ dash(review?.currentAssigneeRole) }}</el-descriptions-item>
 				<el-descriptions-item label="流程实例">{{ dash(review?.instanceId) }}</el-descriptions-item>
 				<el-descriptions-item label="说明" :span="2">{{ dash(review?.message) }}</el-descriptions-item>
 			</el-descriptions>
 
-			<el-descriptions v-if="review?.disposition" class="mt8" :column="3" border size="small" title="质量处置结论">
+			<!-- 操作区:位置跟随 FQC —— 紧接流程信息之后,按角色分块,不在页面别处再放一套 -->
+			<div v-if="review?.canSubmitReview" class="flow-op mt12">
+				<div class="flow-op-title">检验员提交检验结果(转检验主管审核)</div>
+				<el-button type="primary" :loading="flowSubmitting" @click="onSubmitReview">提交审核</el-button>
+			</div>
+
+			<div v-if="review?.canSupervisorApprove || review?.canSupervisorReturn" class="flow-op mt12">
+				<div class="flow-op-title">检验主管审核</div>
+				<el-button v-if="review?.canSupervisorApprove" type="success" :loading="flowSubmitting" @click="onSupervisorApprove">通过</el-button>
+				<el-button v-if="review?.canSupervisorReturn" type="warning" :loading="flowSubmitting" @click="onSupervisorReturn">退回</el-button>
+			</div>
+
+			<div v-if="review?.canSubmitDisposition" class="flow-op mt12">
+				<div class="flow-op-title">QE 质量处置(不合格)</div>
+				<el-form :model="qeForm" label-width="96px" size="small">
+					<el-form-item label="处置类型">
+						<el-select v-model="qeForm.dispositionType" style="width: 180px">
+							<el-option value="REWORK" label="返工" />
+							<el-option value="CONCESSION" label="让步接收" />
+							<el-option value="SCRAP" label="报废" />
+							<el-option value="OTHER" label="其它" />
+						</el-select>
+					</el-form-item>
+					<el-form-item label="处置数量">
+						<el-input-number v-model="qeForm.dispositionQty" :min="0" :controls="false" style="width: 180px" />
+					</el-form-item>
+					<el-form-item label="责任部门"><el-input v-model="qeForm.responsibilityDept" style="width: 240px" /></el-form-item>
+					<el-form-item label="责任人"><el-input v-model="qeForm.responsibleUser" style="width: 180px" /></el-form-item>
+					<el-form-item label="处置意见">
+						<el-input v-model="qeForm.dispositionOpinion" type="textarea" :rows="2" style="width: 320px" />
+					</el-form-item>
+					<el-form-item>
+						<el-button type="primary" :loading="flowSubmitting" @click="onQeDisposition">提交 QE 处置</el-button>
+					</el-form-item>
+				</el-form>
+			</div>
+
+			<el-descriptions v-if="review?.disposition" title="QE 处置结论(仅展示)" :column="2" border size="small" class="mt12">
 				<el-descriptions-item label="处置类型">
-					<el-tag type="warning" size="small" disable-transitions>
-						{{ review.disposition.dispositionTypeText || review.disposition.dispositionType }}
-					</el-tag>
+					{{ dash(review.disposition.dispositionTypeText || review.disposition.dispositionType) }}
 				</el-descriptions-item>
-				<el-descriptions-item label="处置人">{{ dash(review.disposition.operatorName) }}</el-descriptions-item>
-				<el-descriptions-item label="提交时间">{{ fmtDateTime(review.disposition.submittedAt) }}</el-descriptions-item>
+				<el-descriptions-item label="处置数量">{{ dash(review.disposition.dispositionQty) }}</el-descriptions-item>
 				<el-descriptions-item label="责任部门">{{ dash(review.disposition.responsibilityDept) }}</el-descriptions-item>
 				<el-descriptions-item label="责任人">{{ dash(review.disposition.responsibleUser) }}</el-descriptions-item>
-				<el-descriptions-item label="处置数量">{{ dash(review.disposition.dispositionQty) }}</el-descriptions-item>
-				<el-descriptions-item label="处置意见" :span="3">{{ dash(review.disposition.dispositionOpinion) }}</el-descriptions-item>
+				<el-descriptions-item label="处置人">{{ dash(review.disposition.operatorName) }}</el-descriptions-item>
+				<el-descriptions-item label="提交时间">{{ fmtDateTime(review.disposition.submittedAt) }}</el-descriptions-item>
+				<el-descriptions-item label="处置意见" :span="2">
+					<span class="pre">{{ dash(review.disposition.dispositionOpinion) }}</span>
+				</el-descriptions-item>
 			</el-descriptions>
 
+			<!-- 审核历史:FQC 详情页没有这一区,是 S6 独有(正式 ApprovalFlow 时间线),置于末尾不打断 FQC 区块顺序 -->
 			<div class="mt12 hist-block">
 				<div class="hist-title">审核历史</div>
 				<el-timeline v-if="(review?.history || []).length">
@@ -102,35 +131,6 @@
 				</el-timeline>
 				<el-empty v-else description="暂无审核历史" :image-size="60" />
 			</div>
-
-			<div class="mt12 flow-op" v-if="hasAnyAction">
-				<div class="flow-op-title">操作</div>
-				<div class="flow-op-row">
-					<el-button v-if="review?.canSubmitReview" type="primary" :loading="flowSubmitting" @click="onSubmitReview">
-						提交审核
-					</el-button>
-					<el-button v-if="review?.canSupervisorApprove" type="success" :loading="flowSubmitting" @click="onSupervisorApprove">
-						主管通过
-					</el-button>
-					<el-button v-if="review?.canSupervisorReturn" :loading="flowSubmitting" @click="onSupervisorReturn">退回</el-button>
-				</div>
-				<div v-if="review?.canSubmitDisposition" class="flow-op-row mt8">
-					<el-select v-model="dispType" size="small" style="width: 168px" placeholder="处置类型">
-						<el-option label="返工 REWORK" value="REWORK" />
-						<el-option label="让步 CONCESSION" value="CONCESSION" />
-						<el-option label="报废 SCRAP" value="SCRAP" />
-						<el-option label="其它 OTHER" value="OTHER" />
-					</el-select>
-					<el-input v-model="dispOpinion" size="small" style="width: 320px" placeholder="处置意见(必填)" />
-					<el-button type="warning" :loading="flowSubmitting" @click="onQeDisposition">提交处置</el-button>
-				</div>
-			</div>
-
-			<div class="mt12" v-if="review?.isCompleted">
-				<el-tag :type="review?.flowStatus === 'Approved' ? 'success' : 'info'" size="small" disable-transitions>
-					{{ review?.flowStatus === 'Approved' ? '审核已闭环(CLOSED)' : `流程已结束(${review?.flowStatus})` }}
-				</el-tag>
-			</div>
 		</div>
 	</AidopDemoShell>
 </template>
@@ -144,14 +144,25 @@
  * 另一条 `IpqcInspection*` 链(源 `qms_gcjyd`、单头级汇总、节点 N1_INSPECT/.../N3_QUALITY_DISPOSITION)
  * 在 `process-quality/processInspectionList.vue`,与本页无共享代码 —— 改错文件会白做。
  *
- * 信息架构对齐 S7 FQC 详情页(整页 + 顶部返回/单号/状态 + 区块化 descriptions + 项目明细 + 流程 + 操作),
- * 但业务模型、接口、字段、枚举全部沿用 S6 自己的,**不引入任何 FQC 语义**
- * (无 j1..j50、无 FQC DTO/表名/BizType、处置枚举用 S6 的 REWORK/CONCESSION/SCRAP/OTHER 字符串)。
+ * ── 布局基线(S6-IPQC-FQC-PARITY-1,2026-09-14)──
+ * 以 S7 `fqc/fqcInspResultDetail.vue` 为**页面模板**:topbar(返回/单号/状态 tag/节点 tag)→
+ * descriptions 区块(`:column="2" border size="small"`,`.mt8` 间距)→ 明细标题裸 div →
+ * 主表(含展开行)→ 流程信息 → **紧随其后的分角色 `.flow-op` 操作块** → QE 处置结论(仅展示)。
+ *
+ * 与 FQC 的三处有意差异(已知且刻意):
+ * - 区块标题用「基础信息 / 生产信息 / 检规信息 / 检验项目明细 / 流程信息 / 审核历史」,
+ *   而非 FQC 字面的「基本信息 / 成品信息 / 品检项目明细」—— 后两个是 FQC 域专有说法,
+ *   S6 检的是工序在制品,不是成品;FQC 的「基本信息 + 成品信息」在此合为「基础信息」。
+ * - 流程信息多一行「流程实例」(FQC 无此行,但 S6 有真实 instanceId 且排障要用)。
+ * - 末尾多「审核历史」区(FQC 详情页完全没有这一区,S6 有正式 ApprovalFlow 时间线,不删)。
+ *
+ * 只对齐 UI,不引入任何 FQC 业务模型:接口、DTO、枚举、检规快照、处置类型
+ * (REWORK/CONCESSION/SCRAP/OTHER)全部沿用 S6 自己的,不调用任何 S7 接口。
  *
  * 刻意**不照抄** S7 的三处实现(改用 S5 IQC 已修正的做法):
  * - 路由用 path param 且菜单 `IsKeepAlive=0`。详情页是强状态页(可录入 → 只读 → 退回 → 可录入),
  *   S7 的 `IsKeepAlive=1` + 只有 onMounted 会在换单时显示上一单数据;本页另加 `watch(route.params.id)` 兜底。
- * - 状态 tag 按流程状态配色(S7 硬写 type="info" 且直出英文枚举)。
+ * - 状态 tag 按流程状态配色,且显示后端拥有的中文 `displayStatus`(S7 硬写 type="info" 且直出英文枚举)。
  * - 保存/动作失败不再叠加 ElMessage —— 交给 `request.ts` 拦截器统一弹后端真实 message,
  *   避免同时弹出业务文案与 axios 技术文案。
  *
@@ -159,13 +170,14 @@
  * 上一版曾因后端无该字段而在本页本地推断(`flowStatus` 非 NotStarted 即锁录入),
  * 导致主管退回回到 N1 后仍是只读 —— 已改为后端 Authority + 前端纯消费,详见 `canEdit` 处注释。
  */
-import { computed, onActivated, onMounted, ref, watch } from 'vue';
+import { computed, onActivated, onMounted, reactive, ref, watch } from 'vue';
 import { useRoute, useRouter } from 'vue-router';
 import { ElMessage, ElMessageBox } from 'element-plus';
 import AidopDemoShell from '/@/views/aidop/components/AidopDemoShell.vue';
 import IpqcBillItemPanel from './components/IpqcBillItemPanel.vue';
 import {
 	fetchProcessInspectionDetail,
+	fetchProductionInstructionDetail,
 	saveProcessInspectionResult,
 	completeProcessInspection,
 	fetchReviewState,
@@ -174,6 +186,7 @@ import {
 	reviewSupervisorReturn,
 	reviewQeDisposition,
 	type ProcessInspectionDetail,
+	type ProductionInstructionRow,
 	type ReviewState,
 	type SaveResultSample,
 } from '../api/productionInstruction';
@@ -185,13 +198,29 @@ const router = useRouter();
 
 const detail = ref<ProcessInspectionDetail | null>(null);
 const review = ref<ReviewState | null>(null);
+/** 生产指令单(只读):过程检验单详情不带指令单数量,从既有 S6 指令单接口取,取不到则「-」。 */
+const workOrder = ref<ProductionInstructionRow | null>(null);
 const detailLoading = ref(false);
 const reviewLoading = ref(false);
 const saving = ref(false);
 const completing = ref(false);
 const flowSubmitting = ref(false);
-const dispType = ref('');
-const dispOpinion = ref('');
+
+const qeForm = reactive<{
+	dispositionType: string;
+	dispositionQty: number | null;
+	dispositionOpinion: string;
+	responsibilityDept: string;
+	responsibleUser: string;
+}>({ dispositionType: 'REWORK', dispositionQty: null, dispositionOpinion: '', responsibilityDept: '', responsibleUser: '' });
+
+function resetQeForm() {
+	qeForm.dispositionType = 'REWORK';
+	qeForm.dispositionQty = null;
+	qeForm.dispositionOpinion = '';
+	qeForm.responsibilityDept = '';
+	qeForm.responsibleUser = '';
+}
 
 /**
  * 路由参数先按「正整数字符串」校验再转数字。
@@ -204,7 +233,7 @@ const billId = computed(() => (invalidId.value ? 0 : Number(billIdRaw.value)));
 
 const headTitle = computed(() => {
 	const wo = detail.value?.workOrderNo;
-	return wo ? `IPQC 检查单 · ${wo}` : `IPQC 检查单 #${billIdRaw.value || '—'}`;
+	return wo ? `IPQC 检查单 · ${wo}` : `IPQC 检查单 #${billIdRaw.value || '-'}`;
 });
 
 /**
@@ -216,12 +245,6 @@ const headTitle = computed(() => {
  */
 const canEdit = computed(() => review.value?.canEdit === true);
 
-const hasAnyAction = computed(
-	() =>
-		!!review.value &&
-		(review.value.canSubmitReview || review.value.canSupervisorApprove || review.value.canSupervisorReturn || review.value.canSubmitDisposition)
-);
-
 const flowTagType = computed<'success' | 'danger' | 'warning' | 'info'>(() => {
 	switch (review.value?.flowStatus) {
 		case 'Approved':
@@ -250,11 +273,28 @@ const overallStatusText = computed(() => {
 	}
 });
 
+/** 整单结果文案优先用后端拥有的中文(qualityText),不在前端重新翻译枚举。 */
+const overallResultText = computed(() => {
+	if (review.value?.qualityText) return review.value.qualityText;
+	switch (detail.value?.overallResult) {
+		case 'PASS':
+			return '合格';
+		case 'FAIL':
+			return '不合格';
+		case 'PENDING':
+			return '检验中';
+		case 'INVALID':
+			return '无效';
+		default:
+			return dash(detail.value?.overallResult);
+	}
+});
+
 function resetPageState() {
 	detail.value = null;
 	review.value = null;
-	dispType.value = '';
-	dispOpinion.value = '';
+	workOrder.value = null;
+	resetQeForm();
 }
 
 async function loadReview() {
@@ -269,7 +309,20 @@ async function loadReview() {
 	}
 }
 
-/** 详情与流程态可并发;两者都不依赖对方的结果。 */
+/** 指令单数量等生产信息取自既有指令单接口;失败只让「生产信息」退化为 '-',不影响本页其余部分。 */
+async function loadWorkOrder(workOrderNo?: string) {
+	if (!workOrderNo) {
+		workOrder.value = null;
+		return;
+	}
+	try {
+		workOrder.value = await fetchProductionInstructionDetail(workOrderNo);
+	} catch {
+		workOrder.value = null;
+	}
+}
+
+/** 详情与流程态可并发;两者都不依赖对方的结果。生产指令单需要 workOrderNo,故在详情之后取。 */
 async function loadPage() {
 	if (invalidId.value) {
 		resetPageState();
@@ -279,8 +332,10 @@ async function loadPage() {
 	try {
 		const [d] = await Promise.all([fetchProcessInspectionDetail(billId.value), loadReview()]);
 		detail.value = d;
+		await loadWorkOrder(d?.workOrderNo);
 	} catch {
 		detail.value = null;
+		workOrder.value = null;
 	} finally {
 		detailLoading.value = false;
 	}
@@ -317,7 +372,7 @@ async function onComplete(samples: SaveResultSample[]) {
 		await saveProcessInspectionResult(billId.value, samples);
 		const r = await completeProcessInspection(billId.value);
 		await reloadAfterAction();
-		ElMessage.success(`已完成检验:整单 ${r?.overallResult || detail.value?.overallResult || '—'}`);
+		ElMessage.success(`已完成检验:整单 ${r?.overallResult || detail.value?.overallResult || '-'}`);
 	} catch (e: any) {
 		const msg = String(e?.response?.data?.message || e?.message || '');
 		if (msg.includes('INSPECTION_INCOMPLETE')) ElMessage.warning('尚有检验项未按频次录满,无法完成检验。');
@@ -377,18 +432,21 @@ async function onSupervisorReturn() {
 
 async function onQeDisposition() {
 	if (!billId.value || flowSubmitting.value) return;
-	if (!dispType.value) return ElMessage.warning('请选择处置类型');
-	if (!dispOpinion.value.trim()) return ElMessage.warning('处置意见必填');
+	if (!qeForm.dispositionType) return ElMessage.warning('请选择处置类型');
+	if (!qeForm.dispositionOpinion.trim()) return ElMessage.warning('处置意见必填');
+	if (qeForm.dispositionQty !== null && qeForm.dispositionQty < 0) return ElMessage.warning('处置数量不能为负');
 	flowSubmitting.value = true;
 	try {
 		review.value = await reviewQeDisposition({
 			billId: billId.value,
-			dispositionType: dispType.value,
-			dispositionOpinion: dispOpinion.value.trim(),
+			dispositionType: qeForm.dispositionType,
+			dispositionQty: qeForm.dispositionQty,
+			dispositionOpinion: qeForm.dispositionOpinion.trim(),
+			responsibilityDept: qeForm.responsibilityDept.trim() || undefined,
+			responsibleUser: qeForm.responsibleUser.trim() || undefined,
 		});
 		ElMessage.success('已提交质量处置');
-		dispType.value = '';
-		dispOpinion.value = '';
+		resetQeForm();
 		await reloadAfterAction();
 	} catch {
 		await loadReview();
@@ -402,28 +460,22 @@ function goBack() {
 	else router.push(IPQC_LIST_PATH);
 }
 
-function judgeTag(result?: string): 'success' | 'danger' | 'info' | 'warning' {
-	if (result === 'PASS') return 'success';
-	if (result === 'FAIL') return 'danger';
-	if (result === 'INVALID') return 'warning';
-	return 'info';
-}
-
+/** 缺字段一律显示 '-'(与 FQC 同款),不让 undefined / null / 字面量 'null' 漏到界面上。 */
 function dash(v?: unknown) {
-	if (v === null || v === undefined) return '—';
+	if (v === null || v === undefined) return '-';
 	const s = String(v).trim();
-	if (s === '' || s === 'null' || s === 'undefined') return '—';
+	if (s === '' || s === 'null' || s === 'undefined') return '-';
 	return s;
 }
 
 function fmtDate(v?: string) {
-	if (!v) return '—';
+	if (!v) return '-';
 	const s = String(v);
 	return s.length >= 10 ? s.slice(0, 10) : s;
 }
 
 function fmtDateTime(v?: string) {
-	if (!v) return '—';
+	if (!v) return '-';
 	return String(v).replace('T', ' ').slice(0, 19);
 }
 
@@ -441,16 +493,17 @@ watch(
 </script>
 
 <style scoped>
-.page-head {
+/* topbar / 间距 / flow-op 与 S7 FQC 详情页同款 */
+.detail-topbar {
 	display: flex;
 	align-items: center;
-	gap: 12px;
+	gap: 10px;
 	flex-wrap: wrap;
 	margin-bottom: 12px;
 }
 .bill-no {
-	font-weight: 600;
-	font-size: 16px;
+	font-weight: bold;
+	font-size: 15px;
 }
 .detail-body {
 	min-height: 160px;
@@ -461,6 +514,20 @@ watch(
 .mt12 {
 	margin-top: 12px;
 }
+.flow-op {
+	padding: 12px;
+	border: 1px solid var(--el-border-color-lighter);
+	border-radius: 4px;
+	background: var(--el-fill-color-light);
+}
+.flow-op-title {
+	font-weight: bold;
+	margin-bottom: 10px;
+}
+.pre {
+	white-space: pre-wrap;
+	word-break: break-word;
+}
 .hist-block {
 	padding: 12px;
 	border: 1px solid var(--el-border-color-lighter);
@@ -468,7 +535,7 @@ watch(
 	background: var(--el-fill-color-light);
 }
 .hist-title {
-	font-weight: 600;
+	font-weight: bold;
 	font-size: 14px;
 	margin-bottom: 10px;
 }
@@ -484,20 +551,4 @@ watch(
 	font-size: 12px;
 	color: var(--el-text-color-secondary);
 }
-.flow-op {
-	padding: 12px;
-	border: 1px solid var(--el-border-color-lighter);
-	border-radius: 4px;
-	background: var(--el-fill-color-light);
-}
-.flow-op-title {
-	font-weight: 600;
-	margin-bottom: 10px;
-}
-.flow-op-row {
-	display: flex;
-	align-items: center;
-	flex-wrap: wrap;
-	gap: 8px;
-}
 </style>

+ 99 - 5
server/Plugins/Admin.NET.Plugin.AiDOP.Tests/S6/Manufacturing/S6ProcessInspectionEditAuthorityContractTests.cs

@@ -255,20 +255,114 @@ public class S6ProcessInspectionEditAuthorityContractTests
     }
 
     /// <summary>
-    /// §12:录入区的输入框与保存/完成页脚必须由**同一个** canEdit 控制 ——
-    /// 不允许出现"按钮 disabled 但 input 还能改"。
+    /// 录入控件与保存/完成工具条必须由**同一个** canEdit 控制 ——
+    /// 不允许出现"按钮没了但 input 还能改"。
+    /// 2026-09-14(FQC Parity)结构变更:实测样本从主表大单元格移入展开行,
+    /// 编辑控件改为**仅在 canEdit 分支下渲染**(不再靠 `:disabled` 置灰),
+    /// 三个分支 v-if / v-else-if / v-else 互斥,只读态根本不产出输入控件。
     /// </summary>
     [Fact]
-    public void ItemPanel_SingleEditSwitchGovernsInputsAndFooter()
+    public void ItemPanel_SingleEditSwitchGovernsInputsAndToolbar()
     {
         var s = VueSource.CodeOnly(ItemPanel());
-        Assert.Contains(":disabled=\"!canEdit\"", s);
-        Assert.Contains("v-if=\"canEdit\" class=\"panel-foot\"", s);
+        Assert.Contains("v-if=\"canEdit\" class=\"insp-toolbar\"", s);
+        Assert.Contains("v-if=\"canEdit && row.resultType === 'NUMERIC'\"", s);
+        Assert.Contains("v-else-if=\"canEdit\"", s);
+        // 只读分支必须是 v-else(保证三分支互斥,不会与编辑分支同时出现)
+        Assert.Contains("v-else class=\"samples-wrap\"", s);
         // 组件内不得自己看流程(去注释后断言:注释里成段解释"不按 flowStatus 推断",不算实现)
         Assert.DoesNotContain("flowStatus", s);
         Assert.DoesNotContain("currentNodeCode", s);
     }
 
+    // ── FQC 布局对齐冻结(S6-IPQC-FQC-PARITY-1)──
+
+    /// <summary>区块标题与顺序:基础信息 → 生产信息 → 检规信息 → 检验项目明细 → 流程信息 → 审核历史。</summary>
+    [Fact]
+    public void Layout_SectionOrderFollowsFqcTemplate()
+    {
+        var s = VueSource.CodeOnly(DetailPage());
+        var order = new[] { "title=\"基础信息\"", "title=\"生产信息\"", "title=\"检规信息\"", "IpqcBillItemPanel", "title=\"流程信息\"", "审核历史" };
+        var last = -1;
+        foreach (var token in order)
+        {
+            var at = s.IndexOf(token, StringComparison.Ordinal);
+            Assert.True(at > last, $"区块顺序不符:{token} 应出现在前一区块之后(at={at}, last={last})");
+            last = at;
+        }
+    }
+
+    /// <summary>Descriptions 样式与 FQC 一致:两列 + border + size=small。</summary>
+    [Fact]
+    public void Layout_DescriptionsMatchFqcProps()
+    {
+        var s = VueSource.CodeOnly(DetailPage());
+        Assert.Equal(0, CountOf(s, ":column=\"3\""));
+        Assert.True(CountOf(s, ":column=\"2\" border size=\"small\"") >= 4, "至少 4 个区块应使用 FQC 的 descriptions props");
+    }
+
+    /// <summary>操作区必须紧随流程信息(FQC 位置),且不在审核历史之后再放一套。</summary>
+    [Fact]
+    public void Layout_OperationBlocksFollowFlowSection()
+    {
+        var s = VueSource.CodeOnly(DetailPage());
+        var flow = s.IndexOf("title=\"流程信息\"", StringComparison.Ordinal);
+        var firstOp = s.IndexOf("class=\"flow-op mt12\"", StringComparison.Ordinal);
+        var history = s.IndexOf("审核历史", StringComparison.Ordinal);
+        Assert.True(flow < firstOp && firstOp < history, $"操作区应在流程信息之后、审核历史之前(flow={flow}, op={firstOp}, hist={history})");
+    }
+
+    /// <summary>主表列顺序与 FQC 只读明细表一致。</summary>
+    [Fact]
+    public void Layout_MainTableColumnsFollowFqcOrder()
+    {
+        var s = VueSource.CodeOnly(ItemPanel());
+        var cols = new[] { "type=\"expand\"", "label=\"序号\"", "label=\"检验项目\"", "label=\"接收标准\"", "label=\"技术要求\"",
+            "label=\"抽样方案\"", "label=\"样本量\"", "label=\"上限\"", "label=\"下限\"", "label=\"规格标准\"",
+            "label=\"判定\"", "label=\"非数值OK\"", "label=\"非数值NG\"", "label=\"备注\"" };
+        var last = -1;
+        foreach (var c in cols)
+        {
+            var at = s.IndexOf(c, StringComparison.Ordinal);
+            Assert.True(at > last, $"主表列顺序不符:{c}(at={at}, last={last})");
+            last = at;
+        }
+    }
+
+    /// <summary>主表不得再把工序 / 录入类型 / 频次这类系统字段摆在业务列前面(§16)。</summary>
+    [Theory]
+    [InlineData("label=\"工序\"")]
+    [InlineData("label=\"录入类型\"")]
+    [InlineData("label=\"频次\"")]
+    [InlineData("label=\"实测录入\"")]
+    [InlineData("label=\"后端判定\"")]
+    [InlineData("label=\"单项结果\"")]
+    public void Layout_MainTableDropsSystemOrientedColumns(string forbidden)
+    {
+        Assert.DoesNotContain(forbidden, VueSource.CodeOnly(ItemPanel()));
+    }
+
+    /// <summary>S6 特有信息(工序/方法/录入类型/录入状态)降级到展开行,未丢失。</summary>
+    [Fact]
+    public void Layout_S6SpecificFieldsMovedToExpandRow()
+    {
+        var s = VueSource.CodeOnly(ItemPanel());
+        var expand = Slice(s, "class=\"expand-meta\"", "</div>");
+        foreach (var token in new[] { "工序", "检验方法", "录入类型", "录入状态" })
+            Assert.Contains(token, expand);
+    }
+
+    /// <summary>不得为"像 FQC"调用 S7 接口或引入 FQC 语义。</summary>
+    [Theory]
+    [InlineData("fqc")]
+    [InlineData("Fqc")]
+    [InlineData("s7")]
+    public void Layout_DoesNotBorrowS7Implementation(string forbidden)
+    {
+        Assert.DoesNotContain(forbidden, VueSource.CodeOnly(DetailPage()));
+        Assert.DoesNotContain(forbidden, VueSource.CodeOnly(ItemPanel()));
+    }
+
     /// <summary>子组件 canEdit 默认必须是 false(fail-closed)。</summary>
     [Fact]
     public void ItemPanel_DefaultsToReadOnly()