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

feat(s5): migrate IQC inspection detail to independent page

将 S5 IQC 检验单详情由「列表页内嵌 el-drawer」收敛为与 S7 FQC 同类的独立整页
/aidop/s5/iqc/detail/:id(隐藏 SysMenu 路由 + 独立 Tab),旧 Drawer 完全删除,
不保留两套详情入口。server 1.0.535 / Web 2.4.397。

前端
- 新增 iqcInspBillDetail.vue:AidopDemoShell 整页,顶部「返回 / 单号 / 流程状态」,
  区块顺序 基本信息 → 物料与供应商 → 检规信息(自物料块拆出)→ 检验数量 →
  检验执行 → 不合格与偏差 → 检验项目明细 → 审核信息 → 流程信息 → 节点操作区;
  descriptions 由 2 列改 4 列以利用宽屏
- N1 提交 / N2 审核 / N3 SQE 三段操作自 Drawer 平移,校验逻辑逐字不变,
  emit 中转改为页面内直接调用 API
- 项目级保存链路由三层缩短为两层:ItemPanel → 页面 → save-detail → 重取 detail
- 表单重置由「可见性 watch」改为「加载函数内联重置」(对齐 S7 resetForms 机制)
- iqcTaskList.vue:三个详情入口(待检任务 Tab / 检验单 Tab / 生成后自动进入)
  统一 router.push;移除全部详情态 state 与流程 handler

路由与授权
- SysMenuSeedData 局部新增一条隐藏菜单 1329015010004,显式 IsKeepAlive=false
  (实体默认 true)——详情页是强状态页面,缓存会残留旧 canEdit
- UpdateScripts/1.0.535.sql 三段幂等:SysMenu / SysTenantMenu / SysRoleMenu;
  授权口径 = 直接继承 IQC 任务列表入口菜单的授权面(子查询继承而非硬编码 Id),
  实测 10 租户 / 23 角色,与入口页严格相等
- 配套 1.0.535.verify.sql 14 条断言,含「入口可见必详情可见」与「详情不得越权」双向校验

保持不变
- IqcInspBillItemPanel.vue 零改动复用:NUMERIC/NON_NUMERIC、不定长 Sample、
  AUTO/MANUAL、INVALID、类型切换确认、LEGACY 空态全部保留,未引入 FQC 的 j1..j50
- 后端 S5 业务代码、DTO、Flow、MDP、DB schema 均未改动

例外说明
- api/iqcInspBill.ts 与 iqcInspBillFlow.ts 的 id 类签名由 number 放宽为 number | string:
  路由 path param 为字符串,且库中存在 101991258985070726 这类超出
  Number.MAX_SAFE_INTEGER 的检验单 id,转 number 会丢精度
YY968XX 1 месяц назад
Родитель
Сommit
6aee5cd2e7

+ 1 - 1
Web/package.json

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

+ 5 - 2
Web/src/views/aidop/s5/api/iqcInspBill.ts

@@ -62,7 +62,8 @@ export function fetchIqcInspBillList(params: any) {
 // 约 35 展示字段 + 流程占位 bizType/bizId/bizCode。字典全 raw。id 不存在返回 null。
 export interface IqcInspBillDetail {
 	// A 基本信息
-	id?: number;
+	/** 后端 long → JSON 字符串(大 id 超出 JS 安全整数范围,禁止 Number 转换) */
+	id?: number | string;
 	billNo?: string;
 	sourceBillNo?: string;
 	billStatus?: string;
@@ -193,7 +194,9 @@ export interface IqcInspBillLineSummary {
 	unjudgedCount?: number;
 }
 
-export function fetchIqcInspBillDetail(id: number) {
+// id 放宽为 number | string:detail 路由用 path param(字符串),且库中存在
+// 101991258985070726 这类 > Number.MAX_SAFE_INTEGER 的检验单 id,Number() 会丢精度。
+export function fetchIqcInspBillDetail(id: number | string) {
 	return service
 		.get<IqcInspBillDetail | null>('/api/S5IqcInspBill/detail', { params: { id } })
 		.then((r) => r.data);

+ 6 - 5
Web/src/views/aidop/s5/api/iqcInspBillFlow.ts

@@ -2,6 +2,7 @@ import service from '/@/utils/request';
 import { withAidopTenantParams } from '../../api/aidopTenant';
 
 // ── S5 来料检验单 检验员+主管最小审批流(写接口,正常鉴权)──
+// id 类一律 number | string:详情页走 path param,且大 id 超出 JS 安全整数范围(见 iqcInspBill.ts)
 // 后端 api/S5IqcInspBillFlow。BizType=S5_IQC_INSPBILL。不做 SQE/仓储/入库/导出。
 export interface IqcFlowState {
 	bizType?: string;
@@ -26,7 +27,7 @@ export interface IqcFlowState {
 }
 
 export interface IqcSqeDispositionPayload {
-	id: number;
+	id: number | string;
 	dispositionType: number;
 	dispositionQty?: number | null;
 	concessionQty?: number | null;
@@ -39,7 +40,7 @@ export interface IqcSqeDispositionPayload {
 }
 
 export interface IqcSubmitResultPayload {
-	id: number;
+	id: number | string;
 	pd: number;
 	dhsl: number;
 	bhgsl: number;
@@ -47,7 +48,7 @@ export interface IqcSubmitResultPayload {
 	comment?: string;
 }
 
-export function fetchIqcInspBillFlowState(id: number) {
+export function fetchIqcInspBillFlowState(id: number | string) {
 	return service
 		.get<IqcFlowState>('/api/S5IqcInspBillFlow/state', { params: withAidopTenantParams({ id }) })
 		.then((r) => r.data);
@@ -57,11 +58,11 @@ export function submitIqcInspBillResult(payload: IqcSubmitResultPayload) {
 	return service.post<IqcFlowState>('/api/S5IqcInspBillFlow/submit-result', payload).then((r) => r.data);
 }
 
-export function approveIqcInspBill(id: number, comment?: string) {
+export function approveIqcInspBill(id: number | string, comment?: string) {
 	return service.post<IqcFlowState>('/api/S5IqcInspBillFlow/supervisor-approve', { id, comment }).then((r) => r.data);
 }
 
-export function rejectIqcInspBill(id: number, comment: string) {
+export function rejectIqcInspBill(id: number | string, comment: string) {
 	return service.post<IqcFlowState>('/api/S5IqcInspBillFlow/supervisor-reject', { id, comment }).then((r) => r.data);
 }
 

+ 0 - 380
Web/src/views/aidop/s5/iqc/components/IqcInspBillDetailDrawer.vue

@@ -1,380 +0,0 @@
-<template>
-	<!-- size 由 620 放宽至 1200:项目明细表 10 列总宽约 1170px,窄于此会出现横向滚动,
-	     而展开行的录入区会随表体一起横向偏移。不新增路由/页面。 -->
-	<el-drawer v-model="innerVisible" :size="1200" direction="rtl" @closed="onClosed">
-		<template #header>
-			<div class="drawer-title">
-				<span class="t1">来料检验单详情</span>
-				<span class="t2">{{ detail?.billNo || '-' }}</span>
-			</div>
-		</template>
-
-		<div v-loading="loading" class="detail-body">
-			<template v-if="detail">
-				<el-descriptions title="基本信息" :column="2" border size="small">
-					<el-descriptions-item label="检验单号">{{ dash(detail.billNo) }}</el-descriptions-item>
-					<el-descriptions-item label="来源单号">{{ dash(detail.sourceBillNo) }}</el-descriptions-item>
-					<el-descriptions-item label="单据状态">{{ dash(detail.billStatus) }}</el-descriptions-item>
-					<el-descriptions-item label="组织">{{ dash(detail.orgId) }}</el-descriptions-item>
-					<el-descriptions-item label="创建时间">{{ fmtDateTime(detail.createTime) }}</el-descriptions-item>
-					<el-descriptions-item label="更新时间">{{ fmtDateTime(detail.updateTime) }}</el-descriptions-item>
-				</el-descriptions>
-
-				<el-descriptions title="物料与供应商" :column="2" border size="small" class="mt12">
-					<el-descriptions-item label="物料编码">{{ dash(detail.materialCode) }}</el-descriptions-item>
-					<el-descriptions-item label="物料名称">{{ dash(detail.materialName) }}</el-descriptions-item>
-					<el-descriptions-item label="规格型号">{{ dash(detail.spec) }}</el-descriptions-item>
-					<el-descriptions-item label="供应商">{{ dash(detail.supplierName) }}</el-descriptions-item>
-					<el-descriptions-item label="批次">{{ dash(detail.lotNo) }}</el-descriptions-item>
-					<el-descriptions-item label="检规编号">{{ dash(detail.inspectionSpecNo) }}</el-descriptions-item>
-					<el-descriptions-item label="检规版本">{{ dash(detail.inspectionSpecVersion) }}</el-descriptions-item>
-					<el-descriptions-item label="检规生效日期">{{ dash(detail.inspectionSpecEffectiveDate) }}</el-descriptions-item>
-				</el-descriptions>
-
-				<el-descriptions title="检验数量" :column="2" border size="small" class="mt12">
-					<el-descriptions-item label="报检数量">{{ dashNum(detail.reportQty) }}</el-descriptions-item>
-					<el-descriptions-item label="检验数量">{{ dashNum(detail.inspectQty) }}</el-descriptions-item>
-					<el-descriptions-item label="样本数量">{{ dashNum(detail.sampleQty) }}</el-descriptions-item>
-					<el-descriptions-item label="测试数量">{{ dashNum(detail.testQty) }}</el-descriptions-item>
-					<el-descriptions-item label="合格数量">{{ dashNum(detail.qualifiedQty) }}</el-descriptions-item>
-					<el-descriptions-item label="不合格数量">{{ dashNum(detail.unqualifiedQty) }}</el-descriptions-item>
-					<el-descriptions-item label="破坏数量">{{ dashNum(detail.destructiveQty) }}</el-descriptions-item>
-					<el-descriptions-item label="留样数量">{{ dashNum(detail.retainSampleQty) }}</el-descriptions-item>
-				</el-descriptions>
-
-				<el-descriptions title="检验执行" :column="2" border size="small" class="mt12">
-					<el-descriptions-item label="检验员">{{ dash(detail.inspector) }}</el-descriptions-item>
-					<el-descriptions-item label="检验人">{{ dash(detail.inspectorName) }}</el-descriptions-item>
-					<el-descriptions-item label="检验开始时间">{{ fmtDateTime(detail.inspectStartDate) }}</el-descriptions-item>
-					<el-descriptions-item label="检验结束时间">{{ fmtDateTime(detail.inspectEndDate) }}</el-descriptions-item>
-					<el-descriptions-item label="判定">{{ dash(detail.judgement) }}</el-descriptions-item>
-					<el-descriptions-item label="处理方式">{{ dash(detail.processMethod) }}</el-descriptions-item>
-				</el-descriptions>
-
-				<el-descriptions title="不合格与偏差" :column="1" border size="small" class="mt12">
-					<el-descriptions-item label="不合格描述">
-						<span class="pre">{{ dash(detail.unqualifiedDescription) }}</span>
-					</el-descriptions-item>
-					<el-descriptions-item label="不良分类">{{ dash(detail.defectCategoryName) }}</el-descriptions-item>
-					<el-descriptions-item label="退货原因">
-						<span class="pre">{{ dash(detail.returnReason) }}</span>
-					</el-descriptions-item>
-					<el-descriptions-item label="偏差申请">{{ dash(detail.deviationApply) }}</el-descriptions-item>
-					<el-descriptions-item label="偏差说明">
-						<span class="pre">{{ dash(detail.deviationDescription) }}</span>
-					</el-descriptions-item>
-				</el-descriptions>
-
-				<el-descriptions title="审核信息(仅展示,不作为流程依据)" :column="2" border size="small" class="mt12">
-					<el-descriptions-item label="主管候选">{{ dash(detail.leader) }}</el-descriptions-item>
-					<el-descriptions-item label="审核人">{{ dash(detail.auditorId) }}</el-descriptions-item>
-					<el-descriptions-item label="审核时间">{{ fmtDateTime(detail.auditDate) }}</el-descriptions-item>
-				</el-descriptions>
-
-				<!-- 项目级检验结果(后端 lines/lineSummary/canEdit 驱动;canEdit 为唯一编辑 authority)-->
-				<IqcInspBillItemPanel
-					class="mt12"
-					:bill-id="detail.id"
-					:lines="detail.lines"
-					:summary="detail.lineSummary"
-					:can-edit="detail.canEdit === true"
-					:saving-item-id="savingItemId"
-					@save="onSaveItemDetail"
-				/>
-
-				<el-descriptions title="流程信息" :column="2" border size="small" class="mt12" v-loading="flowLoading">
-					<el-descriptions-item label="BizType">{{ dash(detail.bizType) }}</el-descriptions-item>
-					<el-descriptions-item label="BizCode">{{ dash(detail.bizCode) }}</el-descriptions-item>
-					<el-descriptions-item label="流程状态">{{ dash(flowState?.flowStatus) }}</el-descriptions-item>
-					<el-descriptions-item label="当前节点">{{ dash(flowState?.currentNodeName) }}</el-descriptions-item>
-					<el-descriptions-item label="当前角色">{{ dash(flowState?.currentAssigneeRole) }}</el-descriptions-item>
-					<el-descriptions-item label="说明">{{ dash(flowState?.message) }}</el-descriptions-item>
-				</el-descriptions>
-
-				<!-- 检验员录入结果(仅 canSubmitResult 显示)-->
-				<div v-if="flowState?.canSubmitResult" class="flow-op mt12">
-					<div class="flow-op-title">检验员录入检验结果</div>
-					<el-form :model="resultForm" label-width="96px" size="small">
-						<el-form-item label="判定">
-							<el-select v-model="resultForm.pd" style="width: 180px" @change="onPdChange">
-								<el-option :value="0" label="合格" />
-								<el-option :value="1" label="不合格" />
-							</el-select>
-						</el-form-item>
-						<el-form-item label="合格数量">
-							<el-input-number v-model="resultForm.dhsl" :min="0" :controls="false" style="width: 180px" />
-						</el-form-item>
-						<el-form-item label="不合格数量">
-							<el-input-number v-model="resultForm.bhgsl" :min="0" :controls="false" :disabled="resultForm.pd === 0" style="width: 180px" />
-						</el-form-item>
-						<el-form-item label="处理方式" v-if="resultForm.pd === 1">
-							<el-select v-model="resultForm.clfs" style="width: 180px" clearable>
-								<el-option :value="0" label="让步接收" />
-								<el-option :value="1" label="挑选" />
-								<el-option :value="2" label="报废" />
-							</el-select>
-						</el-form-item>
-						<el-form-item label="备注">
-							<el-input v-model="resultForm.comment" type="textarea" :rows="2" style="width: 320px" />
-						</el-form-item>
-						<el-form-item>
-							<el-button type="primary" :loading="submitting" @click="onSubmitResult">提交检验结果</el-button>
-						</el-form-item>
-					</el-form>
-				</div>
-
-				<!-- 检验主管审核(仅 canSupervisorApprove/Reject 显示)-->
-				<div v-if="flowState?.canSupervisorApprove || flowState?.canSupervisorReject" class="flow-op mt12">
-					<div class="flow-op-title">检验主管审核</div>
-					<el-button type="success" :loading="submitting" @click="onApprove">通过</el-button>
-					<el-button type="warning" :loading="submitting" @click="onReject">退回</el-button>
-				</div>
-
-				<!-- SQE 处置(仅 canSqeSubmit 显示)-->
-				<div v-if="flowState?.canSqeSubmit" class="flow-op mt12">
-					<div class="flow-op-title">SQE 处置(不合格)</div>
-					<div class="sqe-hint">不合格数量 {{ dashNum(detail.unqualifiedQty) }} · 检验员处理方式(clfs) {{ dash(detail.processMethod) }}</div>
-					<el-form :model="sqeForm" label-width="96px" size="small" class="mt12">
-						<el-form-item label="处置方式">
-							<el-select v-model="sqeForm.dispositionType" style="width: 180px">
-								<el-option :value="0" label="让步接收" />
-								<el-option :value="1" label="挑选" />
-								<el-option :value="2" label="报废" />
-							</el-select>
-						</el-form-item>
-						<el-form-item label="处置数量">
-							<el-input-number v-model="sqeForm.dispositionQty" :min="0" :controls="false" style="width: 180px" />
-						</el-form-item>
-						<el-form-item label="让步数量">
-							<el-input-number v-model="sqeForm.concessionQty" :min="0" :controls="false" style="width: 180px" />
-						</el-form-item>
-						<el-form-item label="挑选数量">
-							<el-input-number v-model="sqeForm.selectionQty" :min="0" :controls="false" style="width: 180px" />
-						</el-form-item>
-						<el-form-item label="报废数量">
-							<el-input-number v-model="sqeForm.scrapQty" :min="0" :controls="false" style="width: 180px" />
-						</el-form-item>
-						<el-form-item label="处置意见">
-							<el-input v-model="sqeForm.dispositionOpinion" type="textarea" :rows="2" style="width: 320px" />
-						</el-form-item>
-						<el-form-item label="供应商回复">
-							<el-input v-model="sqeForm.supplierResponse" type="textarea" :rows="2" style="width: 320px" />
-						</el-form-item>
-						<el-form-item>
-							<el-button type="primary" :loading="submitting" @click="onSqeSubmit">提交 SQE 处置</el-button>
-						</el-form-item>
-					</el-form>
-				</div>
-			</template>
-			<el-empty v-else-if="!loading" description="暂无详情数据" />
-		</div>
-	</el-drawer>
-</template>
-
-<script setup lang="ts" name="IqcInspBillDetailDrawer">
-import { computed, reactive, watch } from 'vue';
-import { ElMessage, ElMessageBox } from 'element-plus';
-import type { IqcInspBillDetail } from '../../api/iqcInspBill';
-import type { IqcFlowState, IqcSubmitResultPayload, IqcSqeDispositionPayload } from '../../api/iqcInspBillFlow';
-import type { IqcSaveDetailPayload } from '../../api/iqcInspection';
-import IqcInspBillItemPanel from './IqcInspBillItemPanel.vue';
-
-const props = defineProps<{
-	modelValue: boolean;
-	detail: IqcInspBillDetail | null;
-	loading?: boolean;
-	flowState?: IqcFlowState | null;
-	flowLoading?: boolean;
-	submitting?: boolean;
-	/** 正在保存的项目实例 id(父组件持有,用于单项 loading);后端 long → JSON 字符串 */
-	savingItemId?: number | string | null;
-}>();
-const emit = defineEmits<{
-	(e: 'update:modelValue', v: boolean): void;
-	(e: 'closed'): void;
-	(e: 'submit-result', payload: IqcSubmitResultPayload): void;
-	(e: 'approve'): void;
-	(e: 'reject', comment: string): void;
-	(e: 'sqe-submit', payload: IqcSqeDispositionPayload): void;
-	(e: 'save-item-detail', payload: IqcSaveDetailPayload, itemId: number | string): void;
-}>();
-
-const innerVisible = computed({
-	get: () => props.modelValue,
-	set: (v: boolean) => emit('update:modelValue', v),
-});
-
-// 检验员录入表单(只读详情批不含,本批新增,仅 canSubmitResult 时可见)
-const resultForm = reactive<{ pd: number; dhsl: number; bhgsl: number; clfs: number | null; comment: string }>({
-	pd: 0,
-	dhsl: 0,
-	bhgsl: 0,
-	clfs: null,
-	comment: '',
-});
-
-// SQE 处置表单(仅 canSqeSubmit 时可见)
-const sqeForm = reactive<{
-	dispositionType: number;
-	dispositionQty: number | null;
-	concessionQty: number | null;
-	selectionQty: number | null;
-	scrapQty: number | null;
-	dispositionOpinion: string;
-	supplierResponse: string;
-}>({
-	dispositionType: 0,
-	dispositionQty: null,
-	concessionQty: null,
-	selectionQty: null,
-	scrapQty: null,
-	dispositionOpinion: '',
-	supplierResponse: '',
-});
-
-// 每次打开 drawer 重置录入表单
-watch(
-	() => props.modelValue,
-	(v) => {
-		if (v) {
-			resultForm.pd = 0;
-			resultForm.dhsl = 0;
-			resultForm.bhgsl = 0;
-			resultForm.clfs = null;
-			resultForm.comment = '';
-			sqeForm.dispositionType = 0;
-			sqeForm.dispositionQty = null;
-			sqeForm.concessionQty = null;
-			sqeForm.selectionQty = null;
-			sqeForm.scrapQty = null;
-			sqeForm.dispositionOpinion = '';
-			sqeForm.supplierResponse = '';
-		}
-	}
-);
-
-function onSqeSubmit() {
-	if (sqeForm.dispositionType !== 0 && sqeForm.dispositionType !== 1 && sqeForm.dispositionType !== 2)
-		return ElMessage.error('请选择处置方式');
-	if (!sqeForm.dispositionOpinion || !sqeForm.dispositionOpinion.trim()) return ElMessage.error('请填写处置意见');
-	for (const q of [sqeForm.dispositionQty, sqeForm.concessionQty, sqeForm.selectionQty, sqeForm.scrapQty]) {
-		if (q !== null && q !== undefined && q < 0) return ElMessage.error('处置数量不能为负');
-	}
-	const id = props.detail?.id;
-	if (!id) return ElMessage.error('检验单 id 缺失');
-	emit('sqe-submit', {
-		id,
-		dispositionType: sqeForm.dispositionType,
-		dispositionQty: sqeForm.dispositionQty,
-		concessionQty: sqeForm.concessionQty,
-		selectionQty: sqeForm.selectionQty,
-		scrapQty: sqeForm.scrapQty,
-		dispositionOpinion: sqeForm.dispositionOpinion.trim(),
-		supplierResponse: sqeForm.supplierResponse || undefined,
-	});
-}
-
-function onPdChange() {
-	if (resultForm.pd === 0) {
-		resultForm.bhgsl = 0;
-		resultForm.clfs = null;
-	}
-}
-
-function onSubmitResult() {
-	if (resultForm.pd !== 0 && resultForm.pd !== 1) return ElMessage.error('请选择判定');
-	if (resultForm.dhsl < 0) return ElMessage.error('合格数量不能为负');
-	if (resultForm.bhgsl < 0) return ElMessage.error('不合格数量不能为负');
-	if (resultForm.pd === 0 && resultForm.bhgsl !== 0) return ElMessage.error('判定合格时不合格数量必须为 0');
-	if (resultForm.pd === 1 && resultForm.bhgsl <= 0) return ElMessage.error('判定不合格时不合格数量必须大于 0');
-	if (resultForm.pd === 1 && (resultForm.clfs === null || resultForm.clfs === undefined)) return ElMessage.error('判定不合格时请选择处理方式');
-	const id = props.detail?.id;
-	if (!id) return ElMessage.error('检验单 id 缺失');
-	emit('submit-result', {
-		id,
-		pd: resultForm.pd,
-		dhsl: resultForm.dhsl,
-		bhgsl: resultForm.bhgsl,
-		clfs: resultForm.pd === 1 ? resultForm.clfs : null,
-		comment: resultForm.comment,
-	});
-}
-
-function onSaveItemDetail(payload: IqcSaveDetailPayload, itemId: number | string) {
-	emit('save-item-detail', payload, itemId);
-}
-
-function onApprove() {
-	emit('approve');
-}
-
-async function onReject() {
-	try {
-		const { value } = await ElMessageBox.prompt('请填写退回意见', '退回', {
-			confirmButtonText: '确定退回',
-			cancelButtonText: '取消',
-			inputType: 'textarea',
-			inputValidator: (v: string) => (v && v.trim() ? true : '退回意见不能为空'),
-		});
-		emit('reject', value.trim());
-	} catch {
-		/* 用户取消 */
-	}
-}
-
-function dash(v: unknown) {
-	if (v === null || v === undefined || v === '') return '-';
-	return String(v);
-}
-function dashNum(v?: number | null) {
-	if (v === null || v === undefined) return '-';
-	return String(v);
-}
-function fmtDateTime(v?: string | null) {
-	if (!v) return '-';
-	return String(v).replace('T', ' ').slice(0, 19);
-}
-function onClosed() {
-	emit('closed');
-}
-</script>
-
-<style scoped lang="scss">
-.drawer-title {
-	display: flex;
-	align-items: baseline;
-	gap: 10px;
-	.t1 {
-		font-weight: bold;
-		font-size: 16px;
-	}
-	.t2 {
-		color: var(--el-text-color-secondary);
-		font-size: 13px;
-	}
-}
-.detail-body {
-	min-height: 120px;
-}
-.mt12 {
-	margin-top: 12px;
-}
-.pre {
-	white-space: pre-wrap;
-	word-break: break-word;
-}
-.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;
-}
-.sqe-hint {
-	color: var(--el-text-color-secondary);
-	font-size: 12px;
-}
-</style>

+ 580 - 0
Web/src/views/aidop/s5/iqc/iqcInspBillDetail.vue

@@ -0,0 +1,580 @@
+<template>
+	<AidopDemoShell :title="pageTitle" subtitle="检验项目明细录入 → 提交 → 主管审核 → SQE 处置">
+		<!-- 顶部:返回 / 单号 / 流程状态 -->
+		<div class="page-head">
+			<el-button size="small" @click="goBack">← 返回</el-button>
+			<span class="bill-no">{{ detail?.billNo || (invalidId ? '-' : '加载中…') }}</span>
+			<el-tag v-if="flowState?.flowStatus" :type="flowTagType" size="small" effect="plain">
+				{{ flowState.flowStatus }}<template v-if="flowState.currentNodeName"> · {{ flowState.currentNodeName }}</template>
+			</el-tag>
+			<el-tag v-if="detail?.billStatus" size="small" type="info" effect="plain">{{ detail.billStatus }}</el-tag>
+		</div>
+
+		<!-- 非法路由参数:不发任何请求 -->
+		<el-result v-if="invalidId" icon="warning" title="检验单参数非法" :sub-title="`路由参数 id = ${rawId ?? '(空)'},无法加载检验单`">
+			<template #extra>
+				<el-button type="primary" @click="goList">返回来料检验任务列表</el-button>
+			</template>
+		</el-result>
+
+		<div v-else v-loading="loading" class="detail-body">
+			<template v-if="detail">
+				<el-descriptions title="基本信息" :column="4" border size="small">
+					<el-descriptions-item label="检验单号">{{ dash(detail.billNo) }}</el-descriptions-item>
+					<el-descriptions-item label="来源单号">{{ dash(detail.sourceBillNo) }}</el-descriptions-item>
+					<el-descriptions-item label="单据状态">{{ dash(detail.billStatus) }}</el-descriptions-item>
+					<el-descriptions-item label="组织">{{ dash(detail.orgId) }}</el-descriptions-item>
+					<el-descriptions-item label="创建时间">{{ fmtDateTime(detail.createTime) }}</el-descriptions-item>
+					<el-descriptions-item label="更新时间">{{ fmtDateTime(detail.updateTime) }}</el-descriptions-item>
+					<el-descriptions-item label="创建人">{{ dash(detail.createUserId) }}</el-descriptions-item>
+					<el-descriptions-item label="修改人">{{ dash(detail.updateUserId) }}</el-descriptions-item>
+				</el-descriptions>
+
+				<el-descriptions title="物料与供应商" :column="4" border size="small" class="mt12">
+					<el-descriptions-item label="物料编码">{{ dash(detail.materialCode) }}</el-descriptions-item>
+					<el-descriptions-item label="物料名称">{{ dash(detail.materialName) }}</el-descriptions-item>
+					<el-descriptions-item label="规格型号">{{ dash(detail.spec) }}</el-descriptions-item>
+					<el-descriptions-item label="供应商">{{ dash(detail.supplierName) }}</el-descriptions-item>
+					<el-descriptions-item label="批次">{{ dash(detail.lotNo) }}</el-descriptions-item>
+				</el-descriptions>
+
+				<!-- 检规信息:由原「物料与供应商」混合块拆出为独立区域 -->
+				<el-descriptions title="检规信息" :column="4" border size="small" class="mt12">
+					<el-descriptions-item label="检规编号">{{ dash(detail.inspectionSpecNo) }}</el-descriptions-item>
+					<el-descriptions-item label="检规版本">{{ dash(detail.inspectionSpecVersion) }}</el-descriptions-item>
+					<el-descriptions-item label="检规生效日期">{{ dash(detail.inspectionSpecEffectiveDate) }}</el-descriptions-item>
+				</el-descriptions>
+
+				<el-descriptions title="检验数量" :column="4" border size="small" class="mt12">
+					<el-descriptions-item label="报检数量">{{ dashNum(detail.reportQty) }}</el-descriptions-item>
+					<el-descriptions-item label="检验数量">{{ dashNum(detail.inspectQty) }}</el-descriptions-item>
+					<el-descriptions-item label="样本数量">{{ dashNum(detail.sampleQty) }}</el-descriptions-item>
+					<el-descriptions-item label="测试数量">{{ dashNum(detail.testQty) }}</el-descriptions-item>
+					<el-descriptions-item label="合格数量">{{ dashNum(detail.qualifiedQty) }}</el-descriptions-item>
+					<el-descriptions-item label="不合格数量">{{ dashNum(detail.unqualifiedQty) }}</el-descriptions-item>
+					<el-descriptions-item label="破坏数量">{{ dashNum(detail.destructiveQty) }}</el-descriptions-item>
+					<el-descriptions-item label="留样数量">{{ dashNum(detail.retainSampleQty) }}</el-descriptions-item>
+				</el-descriptions>
+
+				<el-descriptions title="检验执行" :column="4" border size="small" class="mt12">
+					<el-descriptions-item label="检验员">{{ dash(detail.inspector) }}</el-descriptions-item>
+					<el-descriptions-item label="检验人">{{ dash(detail.inspectorName) }}</el-descriptions-item>
+					<el-descriptions-item label="检验开始时间">{{ fmtDateTime(detail.inspectStartDate) }}</el-descriptions-item>
+					<el-descriptions-item label="检验结束时间">{{ fmtDateTime(detail.inspectEndDate) }}</el-descriptions-item>
+					<el-descriptions-item label="判定">{{ dash(detail.judgement) }}</el-descriptions-item>
+					<el-descriptions-item label="处理方式">{{ dash(detail.processMethod) }}</el-descriptions-item>
+				</el-descriptions>
+
+				<el-descriptions title="不合格与偏差" :column="2" border size="small" class="mt12">
+					<el-descriptions-item label="不合格描述" :span="2">
+						<span class="pre">{{ dash(detail.unqualifiedDescription) }}</span>
+					</el-descriptions-item>
+					<el-descriptions-item label="不良分类">{{ dash(detail.defectCategoryName) }}</el-descriptions-item>
+					<el-descriptions-item label="偏差申请">{{ dash(detail.deviationApply) }}</el-descriptions-item>
+					<el-descriptions-item label="退货原因" :span="2">
+						<span class="pre">{{ dash(detail.returnReason) }}</span>
+					</el-descriptions-item>
+					<el-descriptions-item label="偏差说明" :span="2">
+						<span class="pre">{{ dash(detail.deviationDescription) }}</span>
+					</el-descriptions-item>
+				</el-descriptions>
+
+				<!-- 项目级检验结果(后端 lines/lineSummary/canEdit 驱动;canEdit 为唯一编辑 authority)-->
+				<IqcInspBillItemPanel
+					class="mt12"
+					:bill-id="detail.id"
+					:lines="detail.lines"
+					:summary="detail.lineSummary"
+					:can-edit="detail.canEdit === true"
+					:saving-item-id="savingItemId"
+					@save="onSaveItemDetail"
+				/>
+
+				<el-descriptions title="审核信息(仅展示,不作为流程依据)" :column="4" border size="small" class="mt12">
+					<el-descriptions-item label="主管候选">{{ dash(detail.leader) }}</el-descriptions-item>
+					<el-descriptions-item label="审核人">{{ dash(detail.auditorId) }}</el-descriptions-item>
+					<el-descriptions-item label="审核时间">{{ fmtDateTime(detail.auditDate) }}</el-descriptions-item>
+				</el-descriptions>
+
+				<el-descriptions title="流程信息" :column="4" border size="small" class="mt12" v-loading="flowLoading">
+					<el-descriptions-item label="BizType">{{ dash(detail.bizType) }}</el-descriptions-item>
+					<el-descriptions-item label="BizCode">{{ dash(detail.bizCode) }}</el-descriptions-item>
+					<el-descriptions-item label="流程状态">{{ dash(flowState?.flowStatus) }}</el-descriptions-item>
+					<el-descriptions-item label="当前节点">{{ dash(flowState?.currentNodeName) }}</el-descriptions-item>
+					<el-descriptions-item label="当前角色">{{ dash(flowState?.currentAssigneeRole) }}</el-descriptions-item>
+					<el-descriptions-item label="说明" :span="3">{{ dash(flowState?.message) }}</el-descriptions-item>
+				</el-descriptions>
+
+				<!-- 检验员录入结果(仅 canSubmitResult 显示)-->
+				<div v-if="flowState?.canSubmitResult" class="flow-op mt12">
+					<div class="flow-op-title">检验员录入检验结果</div>
+					<el-form :model="resultForm" label-width="96px" size="small" @submit.prevent>
+						<el-form-item label="判定">
+							<el-select v-model="resultForm.pd" style="width: 180px" @change="onPdChange">
+								<el-option :value="0" label="合格" />
+								<el-option :value="1" label="不合格" />
+							</el-select>
+						</el-form-item>
+						<el-form-item label="合格数量">
+							<el-input-number v-model="resultForm.dhsl" :min="0" :controls="false" style="width: 180px" />
+						</el-form-item>
+						<el-form-item label="不合格数量">
+							<el-input-number v-model="resultForm.bhgsl" :min="0" :controls="false" :disabled="resultForm.pd === 0" style="width: 180px" />
+						</el-form-item>
+						<el-form-item label="处理方式" v-if="resultForm.pd === 1">
+							<el-select v-model="resultForm.clfs" style="width: 180px" clearable>
+								<el-option :value="0" label="让步接收" />
+								<el-option :value="1" label="挑选" />
+								<el-option :value="2" label="报废" />
+							</el-select>
+						</el-form-item>
+						<el-form-item label="备注">
+							<el-input v-model="resultForm.comment" type="textarea" :rows="2" style="width: 420px" />
+						</el-form-item>
+						<el-form-item>
+							<el-button type="primary" :loading="flowSubmitting" @click="onSubmitResult">提交检验结果</el-button>
+						</el-form-item>
+					</el-form>
+				</div>
+
+				<!-- 检验主管审核(仅 canSupervisorApprove/Reject 显示)-->
+				<div v-if="flowState?.canSupervisorApprove || flowState?.canSupervisorReject" class="flow-op mt12">
+					<div class="flow-op-title">检验主管审核</div>
+					<el-button type="success" :loading="flowSubmitting" @click="onApprove">通过</el-button>
+					<el-button type="warning" :loading="flowSubmitting" @click="onReject">退回</el-button>
+				</div>
+
+				<!-- SQE 处置(仅 canSqeSubmit 显示)-->
+				<div v-if="flowState?.canSqeSubmit" class="flow-op mt12">
+					<div class="flow-op-title">SQE 处置(不合格)</div>
+					<div class="sqe-hint">不合格数量 {{ dashNum(detail.unqualifiedQty) }} · 检验员处理方式(clfs) {{ dash(detail.processMethod) }}</div>
+					<el-form :model="sqeForm" label-width="96px" size="small" class="mt12" @submit.prevent>
+						<el-form-item label="处置方式">
+							<el-select v-model="sqeForm.dispositionType" style="width: 180px">
+								<el-option :value="0" label="让步接收" />
+								<el-option :value="1" label="挑选" />
+								<el-option :value="2" label="报废" />
+							</el-select>
+						</el-form-item>
+						<el-form-item label="处置数量">
+							<el-input-number v-model="sqeForm.dispositionQty" :min="0" :controls="false" style="width: 180px" />
+						</el-form-item>
+						<el-form-item label="让步数量">
+							<el-input-number v-model="sqeForm.concessionQty" :min="0" :controls="false" style="width: 180px" />
+						</el-form-item>
+						<el-form-item label="挑选数量">
+							<el-input-number v-model="sqeForm.selectionQty" :min="0" :controls="false" style="width: 180px" />
+						</el-form-item>
+						<el-form-item label="报废数量">
+							<el-input-number v-model="sqeForm.scrapQty" :min="0" :controls="false" style="width: 180px" />
+						</el-form-item>
+						<el-form-item label="处置意见">
+							<el-input v-model="sqeForm.dispositionOpinion" type="textarea" :rows="2" style="width: 420px" />
+						</el-form-item>
+						<el-form-item label="供应商回复">
+							<el-input v-model="sqeForm.supplierResponse" type="textarea" :rows="2" style="width: 420px" />
+						</el-form-item>
+						<el-form-item>
+							<el-button type="primary" :loading="flowSubmitting" @click="onSqeSubmit">提交 SQE 处置</el-button>
+						</el-form-item>
+					</el-form>
+				</div>
+			</template>
+
+			<el-empty v-else-if="!loading" :description="loadError || '未找到该检验单(不存在或不属于当前租户)'">
+				<el-button type="primary" @click="goList">返回来料检验任务列表</el-button>
+			</el-empty>
+		</div>
+	</AidopDemoShell>
+</template>
+
+<script setup lang="ts" name="aidopS5IqcInspBillDetail">
+// S5 IQC 检验单详情 · 独立整页(Batch 4B)
+//
+// 由 /aidop/s5/iqc/detail/:id 隐藏路由进入,取代原 IqcInspBillDetailDrawer 侧边抽屉。
+// 与 S7 FQC 详情页同构(整页 + 顶部返回/单号/状态 + 区块化 descriptions),
+// 但业务模型完全沿用 S5 自己的:项目实例 / 不定长样本 / NUMERIC·NON_NUMERIC /
+// AUTO·MANUAL / LEGACY 空态 / 后端 canEdit —— 不引入 FQC 的 j1..j50。
+//
+// 【授权】编辑权唯一 authority 是后端 detail.canEdit(Edit Gate),页面不自行按节点推断。
+// 【租户】前端不提交 tenantId,租户边界完全由 Batch 3 后端 JWT 解析决定。
+// 【缓存】路由 IsKeepAlive=false;此外仍 watch route.params.id 主动重置+重载,双保险防串单。
+import { computed, 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 { fetchIqcInspBillDetail, type IqcInspBillDetail } from '../api/iqcInspBill';
+import {
+	fetchIqcInspBillFlowState,
+	submitIqcInspBillResult,
+	approveIqcInspBill,
+	rejectIqcInspBill,
+	sqeSubmitIqcInspBillDisposition,
+	type IqcFlowState,
+} from '../api/iqcInspBillFlow';
+import { saveIqcInspDetail, type IqcSaveDetailPayload } from '../api/iqcInspection';
+import IqcInspBillItemPanel from './components/IqcInspBillItemPanel.vue';
+
+/** 列表页路由:返回按钮的兜底目标(三个详情入口均在该页) */
+const IQC_LIST_PATH = '/aidop/s5/iqc/task-list';
+
+const route = useRoute();
+const router = useRouter();
+const pageTitle = computed(() => (route.meta?.title as string) || 'IQC检验单详情');
+
+const detail = ref<IqcInspBillDetail | null>(null);
+const flowState = ref<IqcFlowState | null>(null);
+const loading = ref(false);
+const flowLoading = ref(false);
+const flowSubmitting = ref(false);
+const savingItemId = ref<number | string | null>(null);
+const loadError = ref('');
+
+/** 路由原始参数(仅用于错误提示展示,不参与业务) */
+const rawId = computed(() => {
+	const v = route.params.id;
+	return Array.isArray(v) ? v[0] : v;
+});
+
+/**
+ * 业务 identity 唯一来源 = route.params.id。
+ * 后端 long 序列化为 JSON 字符串,这里只做「正整数字符串」校验,不做 Number 转换,
+ * 避免超出 JS 安全整数范围后精度丢失(billId 量级为 15 位以上)。
+ */
+const billId = computed<string>(() => {
+	const v = rawId.value;
+	return typeof v === 'string' ? v.trim() : '';
+});
+const invalidId = computed(() => !/^\d+$/.test(billId.value) || /^0+$/.test(billId.value));
+
+const flowTagType = computed(() => {
+	switch (flowState.value?.flowStatus) {
+		case 'Approved':
+			return 'success';
+		case 'Rejected':
+		case 'Terminated':
+		case 'Cancelled':
+			return 'danger';
+		case 'Running':
+			return 'warning';
+		default:
+			return 'info';
+	}
+});
+
+// ── 录入 / 处置表单(自 Drawer 平移,逻辑不变)──
+
+const resultForm = reactive<{ pd: number; dhsl: number; bhgsl: number; clfs: number | null; comment: string }>({
+	pd: 0,
+	dhsl: 0,
+	bhgsl: 0,
+	clfs: null,
+	comment: '',
+});
+
+const sqeForm = reactive<{
+	dispositionType: number;
+	dispositionQty: number | null;
+	concessionQty: number | null;
+	selectionQty: number | null;
+	scrapQty: number | null;
+	dispositionOpinion: string;
+	supplierResponse: string;
+}>({
+	dispositionType: 0,
+	dispositionQty: null,
+	concessionQty: null,
+	selectionQty: null,
+	scrapQty: null,
+	dispositionOpinion: '',
+	supplierResponse: '',
+});
+
+/**
+ * 表单重置。原 Drawer 靠 watch(modelValue) 在「打开」时重置;
+ * 独立页无可见性概念,改为「加载函数内联重置」(与 S7 fqcInspResultDetail 的 resetForms 同机制)。
+ */
+function resetForms() {
+	resultForm.pd = 0;
+	resultForm.dhsl = 0;
+	resultForm.bhgsl = 0;
+	resultForm.clfs = null;
+	resultForm.comment = '';
+	sqeForm.dispositionType = 0;
+	sqeForm.dispositionQty = null;
+	sqeForm.concessionQty = null;
+	sqeForm.selectionQty = null;
+	sqeForm.scrapQty = null;
+	sqeForm.dispositionOpinion = '';
+	sqeForm.supplierResponse = '';
+}
+
+/** 切换单据时清空一切与上一单相关的状态,杜绝 A 单残留到 B 单 */
+function resetPageState() {
+	detail.value = null;
+	flowState.value = null;
+	loadError.value = '';
+	savingItemId.value = null;
+	flowSubmitting.value = false;
+	resetForms();
+}
+
+// ── 数据加载 ──
+
+async function loadFlowState(id: string) {
+	flowLoading.value = true;
+	try {
+		flowState.value = await fetchIqcInspBillFlowState(id);
+	} catch (e: any) {
+		flowState.value = null;
+		ElMessage.error(e?.message || '加载流程状态失败');
+	} finally {
+		flowLoading.value = false;
+	}
+}
+
+/** 页面统一加载入口:非法参数不发任何请求 */
+async function loadPage() {
+	if (invalidId.value) {
+		resetPageState();
+		return;
+	}
+	const id = billId.value;
+	loading.value = true;
+	loadError.value = '';
+	try {
+		const [data] = await Promise.all([fetchIqcInspBillDetail(id), loadFlowState(id)]);
+		if (!data) {
+			detail.value = null;
+			loadError.value = '未找到该检验单(不存在或不属于当前租户)';
+			return;
+		}
+		detail.value = data;
+		resetForms();
+	} catch (e: any) {
+		detail.value = null;
+		loadError.value = e?.message || '加载检验单详情失败';
+	} finally {
+		loading.value = false;
+	}
+}
+
+/** 仅重取 detail(save-detail 不推进流程,无需刷新 flow state) */
+async function reloadDetailOnly() {
+	if (invalidId.value) return;
+	const data = await fetchIqcInspBillDetail(billId.value);
+	if (data) detail.value = data;
+}
+
+/** 流程动作后:detail + flowState 一起重取,编辑态实时翻转(不依赖任何缓存) */
+async function refreshAfterAction() {
+	if (invalidId.value) return;
+	const [data] = await Promise.all([fetchIqcInspBillDetail(billId.value), loadFlowState(billId.value)]);
+	if (data) detail.value = data;
+	resetForms();
+}
+
+// ── 项目级结果保存 ──
+// 链路已缩短为 ItemPanel → 本页 → API(原 Drawer 中转层已删除)。
+// 失败时后端消息由 request.ts 响应拦截器原样弹出,此处不二次 ElMessage,
+// 避免叠加 axios 的 "Request failed with status code 400" 技术文案。
+async function onSaveItemDetail(payload: IqcSaveDetailPayload, itemId: number | string) {
+	savingItemId.value = itemId;
+	try {
+		const res = await saveIqcInspDetail(payload);
+		if (res?.clearedByTypeChange?.length) ElMessage.warning('录入类型已切换,该项目原有测量事实已清空');
+		if (res?.manualInvalidated?.length) ElMessage.warning('测量结果已变更,原人工判定已复位为自动判定');
+		ElMessage.success('检验项目结果已保存');
+		await reloadDetailOnly();
+	} catch {
+		/* 后端消息已由响应拦截器弹出 */
+	} finally {
+		savingItemId.value = null;
+	}
+}
+
+// ── 流程动作(自 Drawer 平移,校验逻辑逐字不变,emit 改为直接调用 API)──
+
+function onPdChange() {
+	if (resultForm.pd === 0) {
+		resultForm.bhgsl = 0;
+		resultForm.clfs = null;
+	}
+}
+
+async function onSubmitResult() {
+	if (resultForm.pd !== 0 && resultForm.pd !== 1) return ElMessage.error('请选择判定');
+	if (resultForm.dhsl < 0) return ElMessage.error('合格数量不能为负');
+	if (resultForm.bhgsl < 0) return ElMessage.error('不合格数量不能为负');
+	if (resultForm.pd === 0 && resultForm.bhgsl !== 0) return ElMessage.error('判定合格时不合格数量必须为 0');
+	if (resultForm.pd === 1 && resultForm.bhgsl <= 0) return ElMessage.error('判定不合格时不合格数量必须大于 0');
+	if (resultForm.pd === 1 && (resultForm.clfs === null || resultForm.clfs === undefined)) return ElMessage.error('判定不合格时请选择处理方式');
+	const id = detail.value?.id;
+	if (!id) return ElMessage.error('检验单 id 缺失');
+	flowSubmitting.value = true;
+	try {
+		await submitIqcInspBillResult({
+			id,
+			pd: resultForm.pd,
+			dhsl: resultForm.dhsl,
+			bhgsl: resultForm.bhgsl,
+			clfs: resultForm.pd === 1 ? resultForm.clfs : null,
+			comment: resultForm.comment,
+		});
+		ElMessage.success('检验结果已提交(已触发回写 WMS),转检验主管审核');
+		await refreshAfterAction();
+	} catch (e: any) {
+		ElMessage.error(e?.message || '提交检验结果失败');
+	} finally {
+		flowSubmitting.value = false;
+	}
+}
+
+async function onApprove() {
+	const id = detail.value?.id;
+	if (!id) return;
+	flowSubmitting.value = true;
+	try {
+		await approveIqcInspBill(id);
+		ElMessage.success('已通过,流程完成');
+		await refreshAfterAction();
+	} catch (e: any) {
+		ElMessage.error(e?.message || '审核通过失败');
+	} finally {
+		flowSubmitting.value = false;
+	}
+}
+
+async function onReject() {
+	const id = detail.value?.id;
+	if (!id) return;
+	let comment = '';
+	try {
+		const { value } = await ElMessageBox.prompt('请填写退回意见', '退回', {
+			confirmButtonText: '确定退回',
+			cancelButtonText: '取消',
+			inputType: 'textarea',
+			inputValidator: (v: string) => (v && v.trim() ? true : '退回意见不能为空'),
+		});
+		comment = value.trim();
+	} catch {
+		return; /* 用户取消 */
+	}
+	flowSubmitting.value = true;
+	try {
+		await rejectIqcInspBill(id, comment);
+		ElMessage.success('已退回检验员');
+		await refreshAfterAction();
+	} catch (e: any) {
+		ElMessage.error(e?.message || '退回失败');
+	} finally {
+		flowSubmitting.value = false;
+	}
+}
+
+async function onSqeSubmit() {
+	if (sqeForm.dispositionType !== 0 && sqeForm.dispositionType !== 1 && sqeForm.dispositionType !== 2)
+		return ElMessage.error('请选择处置方式');
+	if (!sqeForm.dispositionOpinion || !sqeForm.dispositionOpinion.trim()) return ElMessage.error('请填写处置意见');
+	for (const q of [sqeForm.dispositionQty, sqeForm.concessionQty, sqeForm.selectionQty, sqeForm.scrapQty]) {
+		if (q !== null && q !== undefined && q < 0) return ElMessage.error('处置数量不能为负');
+	}
+	const id = detail.value?.id;
+	if (!id) return ElMessage.error('检验单 id 缺失');
+	flowSubmitting.value = true;
+	try {
+		await sqeSubmitIqcInspBillDisposition({
+			id,
+			dispositionType: sqeForm.dispositionType,
+			dispositionQty: sqeForm.dispositionQty,
+			concessionQty: sqeForm.concessionQty,
+			selectionQty: sqeForm.selectionQty,
+			scrapQty: sqeForm.scrapQty,
+			dispositionOpinion: sqeForm.dispositionOpinion.trim(),
+			supplierResponse: sqeForm.supplierResponse || undefined,
+		});
+		ElMessage.success('SQE 处置已提交,流程完成');
+		await refreshAfterAction();
+	} catch (e: any) {
+		ElMessage.error(e?.message || 'SQE 处置提交失败');
+	} finally {
+		flowSubmitting.value = false;
+	}
+}
+
+// ── 展示格式化(自 Drawer 平移;空值一律 '-',绝不出现 null/undefined 字面量)──
+
+function dash(v: unknown) {
+	if (v === null || v === undefined || v === '') return '-';
+	return String(v);
+}
+function dashNum(v?: number | null) {
+	if (v === null || v === undefined) return '-';
+	return String(v);
+}
+function fmtDateTime(v?: string | null) {
+	if (!v) return '-';
+	return String(v).replace('T', ' ').slice(0, 19);
+}
+
+// ── 导航 ──
+
+function goList() {
+	router.push(IQC_LIST_PATH);
+}
+
+function goBack() {
+	if (window.history.length > 1) router.back();
+	else goList();
+}
+
+// ── 生命周期 ──
+// 路由 IsKeepAlive=false 已保证换单必然重建组件;此 watch 是防御性双保险:
+// 即便未来框架路由复用策略变化,也不会把 A 单状态残留到 B 单。
+watch(
+	() => route.params.id,
+	() => {
+		resetPageState();
+		loadPage();
+	}
+);
+
+onMounted(loadPage);
+</script>
+
+<style scoped lang="scss">
+.page-head {
+	display: flex;
+	align-items: center;
+	gap: 12px;
+	margin-bottom: 12px;
+	.bill-no {
+		font-weight: bold;
+		font-size: 16px;
+	}
+}
+.detail-body {
+	min-height: 160px;
+}
+.mt12 {
+	margin-top: 12px;
+}
+.pre {
+	white-space: pre-wrap;
+	word-break: break-word;
+}
+.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;
+}
+.sqe-hint {
+	color: var(--el-text-color-secondary);
+	font-size: 12px;
+}
+</style>

+ 14 - 166
Web/src/views/aidop/s5/iqc/iqcTaskList.vue

@@ -63,7 +63,7 @@
 							>
 								来料检验
 							</el-button>
-							<el-button v-else type="primary" link @click="openDetailById(row.inspBillId)">查看检验单</el-button>
+							<el-button v-else type="primary" link @click="goDetail(row.inspBillId)">查看检验单</el-button>
 						</template>
 					</el-table-column>
 					<template #empty>
@@ -114,7 +114,7 @@
 					<el-table-column prop="billStatus" label="状态" min-width="90" />
 					<el-table-column label="操作" width="90" fixed="right">
 						<template #default="{ row }">
-							<el-button type="primary" link @click="openDetail(row)">查看</el-button>
+							<el-button type="primary" link @click="goDetail(row.id)">查看</el-button>
 						</template>
 					</el-table-column>
 					<template #empty>
@@ -136,21 +136,6 @@
 			</el-tab-pane>
 		</el-tabs>
 
-		<IqcInspBillDetailDrawer
-			v-model="detailVisible"
-			:detail="currentDetail"
-			:loading="detailLoading"
-			:flow-state="currentFlowState"
-			:flow-loading="flowLoading"
-			:submitting="flowSubmitting"
-			:saving-item-id="savingItemId"
-			@submit-result="onSubmitResult"
-			@approve="onApprove"
-			@reject="onReject"
-			@sqe-submit="onSqeSubmit"
-			@save-item-detail="onSaveItemDetail"
-		/>
-
 		<el-dialog v-model="assignVisible" title="检验员调配" width="420px">
 			<el-form label-width="100px">
 				<el-form-item label="用户 Id">
@@ -179,21 +164,10 @@
 
 <script setup lang="ts" name="aidopS5IqcTaskList">
 import { computed, onActivated, onMounted, reactive, ref } from 'vue';
-import { useRoute } from 'vue-router';
+import { useRoute, useRouter } from 'vue-router';
 import { ElMessage, ElMessageBox } from 'element-plus';
 import AidopDemoShell from '/@/views/aidop/components/AidopDemoShell.vue';
-import { fetchIqcInspBillList, fetchIqcInspBillDetail, type IqcInspBillRow, type IqcInspBillDetail } from '../api/iqcInspBill';
-import {
-	fetchIqcInspBillFlowState,
-	submitIqcInspBillResult,
-	approveIqcInspBill,
-	rejectIqcInspBill,
-	sqeSubmitIqcInspBillDisposition,
-	type IqcFlowState,
-	type IqcSubmitResultPayload,
-	type IqcSqeDispositionPayload,
-} from '../api/iqcInspBillFlow';
-import { saveIqcInspDetail, type IqcSaveDetailPayload } from '../api/iqcInspection';
+import { fetchIqcInspBillList, type IqcInspBillRow } from '../api/iqcInspBill';
 import {
 	fetchIqcTaskEntryList,
 	claimIqcTaskEntries,
@@ -203,9 +177,9 @@ import {
 	generateIqcInspBill,
 	type IqcTaskEntryRow,
 } from '../api/iqcTaskEntry';
-import IqcInspBillDetailDrawer from './components/IqcInspBillDetailDrawer.vue';
 
 const route = useRoute();
+const router = useRouter();
 const pageTitle = computed(() => (route.meta?.title as string) || '来料检验任务列表');
 
 const activeTab = ref('entry');
@@ -249,15 +223,6 @@ const loading = ref(false);
 const rows = ref<IqcInspBillRow[]>([]);
 const total = ref(0);
 
-const detailVisible = ref(false);
-const detailLoading = ref(false);
-const currentDetail = ref<IqcInspBillDetail | null>(null);
-const currentFlowState = ref<IqcFlowState | null>(null);
-const flowLoading = ref(false);
-const flowSubmitting = ref(false);
-const currentId = ref<number | null>(null);
-const savingItemId = ref<number | string | null>(null);
-
 function onTabChange(name: string | number) {
 	if (name === 'entry') loadEntryList();
 	else loadList();
@@ -362,7 +327,7 @@ async function onGenerate(row: IqcTaskEntryRow) {
 		const res = await generateIqcInspBill(row.id);
 		ElMessage.success(`已生成检验单 ${res.inspBillNo}`);
 		await loadEntryList();
-		if (res.inspBillId) await openDetailById(res.inspBillId);
+		if (res.inspBillId) goDetail(res.inspBillId);
 	} catch (e: any) {
 		ElMessage.error(e?.message || '生成检验单失败');
 	} finally {
@@ -370,131 +335,14 @@ async function onGenerate(row: IqcTaskEntryRow) {
 	}
 }
 
-async function loadFlowState(id: number) {
-	flowLoading.value = true;
-	try {
-		currentFlowState.value = await fetchIqcInspBillFlowState(id);
-	} catch (e: any) {
-		currentFlowState.value = null;
-		ElMessage.error(e?.message || '加载流程状态失败');
-	} finally {
-		flowLoading.value = false;
-	}
-}
-
-async function refreshAfterAction() {
-	if (currentId.value) {
-		const [d] = await Promise.all([fetchIqcInspBillDetail(currentId.value), loadFlowState(currentId.value)]);
-		if (d) currentDetail.value = d;
-	}
-	loadList();
-	loadEntryList();
-}
-
-async function reloadDetailOnly() {
-	if (!currentId.value) return;
-	const d = await fetchIqcInspBillDetail(currentId.value);
-	if (d) currentDetail.value = d;
-}
-
-// 项目级结果保存:save-detail 不推进流程,故只重新 GET detail(lines/lineSummary/canEdit 均以后端为准),
-// 不刷新流程态与列表(Header 未变)。失败时后端消息由 request.ts 响应拦截器原样弹出,
-// 此处刻意不再二次 ElMessage —— 否则会叠加 axios 的 "Request failed with status code 400" 技术文案。
-async function onSaveItemDetail(payload: IqcSaveDetailPayload, itemId: number | string) {
-	savingItemId.value = itemId;
-	try {
-		const res = await saveIqcInspDetail(payload);
-		if (res?.clearedByTypeChange?.length) ElMessage.warning('录入类型已切换,该项目原有测量事实已清空');
-		if (res?.manualInvalidated?.length) ElMessage.warning('测量结果已变更,原人工判定已复位为自动判定');
-		ElMessage.success('检验项目结果已保存');
-		await reloadDetailOnly();
-	} catch {
-		/* 后端消息已由响应拦截器弹出 */
-	} finally {
-		savingItemId.value = null;
-	}
-}
-
-async function openDetailById(id: number) {
-	currentId.value = id;
-	detailLoading.value = true;
-	detailVisible.value = true;
-	currentDetail.value = null;
-	currentFlowState.value = null;
-	try {
-		const [data] = await Promise.all([fetchIqcInspBillDetail(id), loadFlowState(id)]);
-		if (!data) {
-			ElMessage.warning('未找到检验单详情');
-			return;
-		}
-		currentDetail.value = data;
-	} catch (e: any) {
-		ElMessage.error(e?.message || '加载检验单详情失败');
-	} finally {
-		detailLoading.value = false;
-	}
-}
-
-async function openDetail(row: IqcInspBillRow) {
-	if (!row?.id) {
-		ElMessage.error('检验单 id 缺失,无法查看详情');
-		return;
-	}
-	await openDetailById(row.id);
-}
-
-async function onSubmitResult(payload: IqcSubmitResultPayload) {
-	flowSubmitting.value = true;
-	try {
-		await submitIqcInspBillResult(payload);
-		ElMessage.success('检验结果已提交(已触发回写 WMS),转检验主管审核');
-		await refreshAfterAction();
-	} catch (e: any) {
-		ElMessage.error(e?.message || '提交检验结果失败');
-	} finally {
-		flowSubmitting.value = false;
-	}
-}
-
-async function onApprove() {
-	if (!currentId.value) return;
-	flowSubmitting.value = true;
-	try {
-		await approveIqcInspBill(currentId.value);
-		ElMessage.success('已通过,流程完成');
-		await refreshAfterAction();
-	} catch (e: any) {
-		ElMessage.error(e?.message || '审核通过失败');
-	} finally {
-		flowSubmitting.value = false;
-	}
-}
-
-async function onReject(comment: string) {
-	if (!currentId.value) return;
-	flowSubmitting.value = true;
-	try {
-		await rejectIqcInspBill(currentId.value, comment);
-		ElMessage.success('已退回检验员');
-		await refreshAfterAction();
-	} catch (e: any) {
-		ElMessage.error(e?.message || '退回失败');
-	} finally {
-		flowSubmitting.value = false;
-	}
-}
-
-async function onSqeSubmit(payload: IqcSqeDispositionPayload) {
-	flowSubmitting.value = true;
-	try {
-		await sqeSubmitIqcInspBillDisposition(payload);
-		ElMessage.success('SQE 处置已提交,流程完成');
-		await refreshAfterAction();
-	} catch (e: any) {
-		ElMessage.error(e?.message || 'SQE 处置提交失败');
-	} finally {
-		flowSubmitting.value = false;
-	}
+/**
+ * 打开检验单详情:统一跳独立 Tab(隐藏路由 /aidop/s5/iqc/detail/:id)。
+ * 待检任务 Tab、检验单 Tab、生成检验单后自动进入,三个入口共用本函数——不再有 Drawer。
+ * id 直接以字符串拼路径:后端 long 序列化为 JSON 字符串,且存在超出 JS 安全整数范围的大 id。
+ */
+function goDetail(id?: number | string | null) {
+	if (id === null || id === undefined || id === '') return ElMessage.error('检验单 id 缺失,无法查看详情');
+	router.push(`/aidop/s5/iqc/detail/${id}`);
 }
 
 async function loadList() {

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

@@ -11,9 +11,9 @@
     <GenerateSatelliteAssembliesForCore>true</GenerateSatelliteAssembliesForCore>
     <Copyright>Admin.NET</Copyright>
     <Description>Admin.NET 通用权限开发平台</Description>
-    <AssemblyVersion>1.0.534</AssemblyVersion>
-    <FileVersion>1.0.534</FileVersion>
-    <Version>1.0.534</Version>
+    <AssemblyVersion>1.0.535</AssemblyVersion>
+    <FileVersion>1.0.535</FileVersion>
+    <Version>1.0.535</Version>
   </PropertyGroup>
 
   <ItemGroup>
@@ -823,6 +823,12 @@
     <None Update="UpdateScripts\1.0.529.verify.sql">
       <CopyToOutputDirectory>Always</CopyToOutputDirectory>
     </None>
+    <None Update="UpdateScripts\1.0.535.sql">
+      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+    </None>
+    <None Update="UpdateScripts\1.0.535.verify.sql">
+      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+    </None>
     <None Update="UpdateScripts\UAT-PLACEHOLDER-MENU-HIDE.ops.sql">
       <CopyToOutputDirectory>Always</CopyToOutputDirectory>
     </None>

+ 43 - 0
server/Admin.NET.Web.Entry/UpdateScripts/1.0.535.sql

@@ -0,0 +1,43 @@
+-- 1.0.535:Batch 4B · S5 IQC 检验单详情统一页 detail/:id 隐藏菜单 + 租户可见 + 角色授权
+-- 背景:原「列表页内嵌 el-drawer」(IqcInspBillDetailDrawer)收敛为单一路由页
+--       /aidop/s5/iqc/detail/:id(组件 /aidop/s5/iqc/iqcInspBillDetail)。
+--       来料检验任务列表的「待检任务」「检验单」两个 Tab、以及生成检验单后的自动进入,
+--       三个入口统一跳此页;页面按后端 detail.canEdit / flowState 决定录入 / 审核 / 处置 / 只读。
+--       与 S7 FQC 的 1.0.382(result-detail 隐藏菜单)同构。
+-- 幂等:菜单按 Id NOT EXISTS;租户可见性按 (TenantId,MenuId) NOT EXISTS;角色授权按 (RoleId,MenuId) NOT EXISTS。
+--       多行 Id 用 MAX(Id) + ROW_NUMBER() 一次性算定 —— 避免 @变量在 MySQL 8 求值顺序未定义导致重复 PK
+--       (窗口函数在 WHERE 之后求值,故行号只覆盖真正会插入的行,不会产生空洞或冲突)。
+-- 授权口径:Detail menu authority = IQC business entry authority。
+--       直接**继承**「来料检验任务级列表」(Id=1329015010001,即三个详情入口所在页) 现有的
+--       SysTenantMenu / SysRoleMenu 授权面,用子查询继承而非硬编码 Id 清单(跨环境 Id 不同也成立)。
+--       实测该入口菜单当前 = 10 个租户 / 23 个角色;三张 IQC 业务页授权面完全一致(并集=交集)。
+--       页面可打开 ≠ 可编辑:真正的编辑权仍由后端 IqcInspBillEditGate(canEdit) 与流程 Gate 控制。
+-- 缓存:IsKeepAlive = 0。详情页是强状态页面(N1 可编辑 → N2 只读 → 退回 → N1 可编辑),
+--       不允许 KeepAlive 缓存旧的 canEdit / flowState / 项目结果。
+-- 边界:本脚本只建路由菜单与其授权,不新增业务数据、不改 qms_* / ado_s5_iqc_* 结构与数据、不改流程数据。
+-- 方言:MySQL(主库 aidopdev)。SysMenu / SysTenantMenu / SysRoleMenu 为框架表 PascalCase。
+
+-- ① 隐藏菜单:IQC检验单详情(Id=1329015010004 = iqcBase+4,Pid=来料检验目录 1322000000015)
+INSERT INTO SysMenu (Id, Pid, Type, Title, Name, Path, Component, Icon, IsIframe, IsHide, IsKeepAlive, IsAffix, OrderNo, Status, CreateTime)
+SELECT 1329015010004, 1322000000015, 2, 'IQC检验单详情', 'aidopS5IqcInspBillDetail',
+       '/aidop/s5/iqc/detail/:id', '/aidop/s5/iqc/iqcInspBillDetail', 'ele-Document',
+       0, 1, 0, 0, 45, 1, NOW()
+WHERE NOT EXISTS (SELECT 1 FROM SysMenu WHERE Id = 1329015010004);
+
+-- ② 租户可见性:继承 1329015010001 的租户面(只插缺失项)
+INSERT INTO SysTenantMenu (Id, TenantId, MenuId)
+SELECT base.m + ROW_NUMBER() OVER (ORDER BY src.TenantId), src.TenantId, 1329015010004
+FROM (SELECT COALESCE(MAX(Id), 0) AS m FROM SysTenantMenu) base
+CROSS JOIN (SELECT DISTINCT TenantId FROM SysTenantMenu WHERE MenuId = 1329015010001) src
+WHERE NOT EXISTS (
+    SELECT 1 FROM SysTenantMenu x WHERE x.TenantId = src.TenantId AND x.MenuId = 1329015010004
+);
+
+-- ③ 角色授权:继承 1329015010001 的角色面(只插缺失项)
+INSERT INTO SysRoleMenu (Id, RoleId, MenuId)
+SELECT base.m + ROW_NUMBER() OVER (ORDER BY src.RoleId), src.RoleId, 1329015010004
+FROM (SELECT COALESCE(MAX(Id), 0) AS m FROM SysRoleMenu) base
+CROSS JOIN (SELECT DISTINCT RoleId FROM SysRoleMenu WHERE MenuId = 1329015010001) src
+WHERE NOT EXISTS (
+    SELECT 1 FROM SysRoleMenu x WHERE x.RoleId = src.RoleId AND x.MenuId = 1329015010004
+);

+ 48 - 0
server/Admin.NET.Web.Entry/UpdateScripts/1.0.535.verify.sql

@@ -0,0 +1,48 @@
+-- ============================================================================
+-- 1.0.535 Batch 4B · S5 IQC 检验单详情隐藏菜单 · VERIFY
+--
+-- 执行契约(AutoVersionUpdate.RunVerifyScriptIfExists):每条语句经
+-- SqlQuerySingle<object> 取首行首列并要求 IsTruthy == true。故:
+--   ① 禁止 SET @var(返回 null 即判失败)—— 一律内联子查询;
+--   ② 每条语句返回「通过为真」的标量;
+--   ③ 文件必须以最后一条 SQL 的分号结束,末尾不得残留 comment-only 块
+--      (本文件注释内刻意不出现字面分号,避免任何 splitter 把注释切成空语句)。
+-- ============================================================================
+
+-- 菜单存在且字段口径完全正确(含 IsHide=1 / IsKeepAlive=0 两个本批关键位)
+SELECT COUNT(*) = 1 FROM SysMenu WHERE Id = 1329015010004 AND Pid = 1322000000015 AND Type = 2 AND Status = 1 AND IsHide = 1 AND IsKeepAlive = 0 AND IsAffix = 0 AND IsIframe = 0 AND OrderNo = 45 AND Title = 'IQC检验单详情' AND Name = 'aidopS5IqcInspBillDetail' AND Path = '/aidop/s5/iqc/detail/:id' AND Component = '/aidop/s5/iqc/iqcInspBillDetail';
+
+-- 路由 Path 唯一(未产生重复菜单)
+SELECT COUNT(*) = 1 FROM SysMenu WHERE Path = '/aidop/s5/iqc/detail/:id';
+
+-- 路由 Name 唯一(vue-router name 冲突会导致动态路由注册异常)
+SELECT COUNT(*) = 1 FROM SysMenu WHERE Name = 'aidopS5IqcInspBillDetail';
+
+-- 租户可见性:与 IQC 任务列表入口的租户面完全一致(继承口径成立)
+SELECT (SELECT COUNT(DISTINCT TenantId) FROM SysTenantMenu WHERE MenuId = 1329015010004) = (SELECT COUNT(DISTINCT TenantId) FROM SysTenantMenu WHERE MenuId = 1329015010001);
+
+-- 租户可见性:不存在「入口可见但详情不可见」的租户
+SELECT COUNT(*) = 0 FROM SysTenantMenu e WHERE e.MenuId = 1329015010001 AND NOT EXISTS (SELECT 1 FROM SysTenantMenu d WHERE d.MenuId = 1329015010004 AND d.TenantId = e.TenantId);
+
+-- 角色授权:与 IQC 任务列表入口的角色面完全一致(继承口径成立)
+SELECT (SELECT COUNT(DISTINCT RoleId) FROM SysRoleMenu WHERE MenuId = 1329015010004) = (SELECT COUNT(DISTINCT RoleId) FROM SysRoleMenu WHERE MenuId = 1329015010001);
+
+-- 角色授权:不存在「能进入口页但进不了详情页」的角色
+SELECT COUNT(*) = 0 FROM SysRoleMenu e WHERE e.MenuId = 1329015010001 AND NOT EXISTS (SELECT 1 FROM SysRoleMenu d WHERE d.MenuId = 1329015010004 AND d.RoleId = e.RoleId);
+
+-- 未越权:详情页授权面没有超出入口页(不得凭空多授角色)
+SELECT COUNT(*) = 0 FROM SysRoleMenu d WHERE d.MenuId = 1329015010004 AND NOT EXISTS (SELECT 1 FROM SysRoleMenu e WHERE e.MenuId = 1329015010001 AND e.RoleId = d.RoleId);
+
+-- 端到端可达性:S5 IQC 检验员角色成员可经角色到达该详情菜单
+SELECT COUNT(*) >= 1 FROM SysRoleMenu rm JOIN SysRole r ON r.Id = rm.RoleId WHERE rm.MenuId = 1329015010004 AND r.Code = 'ROLE_S5_IQC_INSPECTOR';
+
+-- 主键无重复(多行插入的 MAX(Id)+ROW_NUMBER() 写法正确性)
+SELECT COUNT(*) = COUNT(DISTINCT Id) FROM SysTenantMenu WHERE MenuId = 1329015010004;
+SELECT COUNT(*) = COUNT(DISTINCT Id) FROM SysRoleMenu WHERE MenuId = 1329015010004;
+
+-- 回归:来料检验目录下菜单由 3 增至 4,且 OrderNo 无重复
+SELECT COUNT(*) = 4 FROM SysMenu WHERE Pid = 1322000000015 AND Type = 2;
+SELECT COUNT(DISTINCT OrderNo) = COUNT(*) FROM SysMenu WHERE Pid = 1322000000015 AND Type = 2;
+
+-- 回归:三张既有 IQC 业务页仍为可见菜单(未被本脚本误伤为隐藏)
+SELECT COUNT(*) = 3 FROM SysMenu WHERE Id IN (1329015010001, 1329015010002, 1329015010003) AND IsHide = 0 AND Status = 1;

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

@@ -540,6 +540,9 @@ public class SysMenuSeedData : ISqlSugarEntitySeedData<SysMenu>
         yield return new SysMenu { Id = iqcBase + 3, Pid = iqcDirId, Title = "来料检验申请列表", Path = "/aidop/s5/iqc/application-list", Name = "aidopS5IqcApplicationList", Component = "/aidop/s5/iqc/iqcApplicationList", Icon = "ele-Document", Type = MenuTypeEnum.Menu, CreateTime = ct, OrderNo = 5, Remark = "S5 来料检验申请列表(只读列表)" };
         yield return new SysMenu { Id = iqcBase + 1, Pid = iqcDirId, Title = "来料检验任务列表", Path = "/aidop/s5/iqc/task-list", Name = "aidopS5IqcTaskList", Component = "/aidop/s5/iqc/iqcTaskList", Icon = "ele-Document", Type = MenuTypeEnum.Menu, CreateTime = ct, OrderNo = 10, Remark = "S5 来料检验任务列表(只读列表)" };
         yield return new SysMenu { Id = iqcBase + 2, Pid = iqcDirId, Title = "来料检验结果列表", Path = "/aidop/s5/iqc/result-list", Name = "aidopS5IqcResultList", Component = "/aidop/s5/iqc/iqcResultList", Icon = "ele-Document", Type = MenuTypeEnum.Menu, CreateTime = ct, OrderNo = 20, Remark = "S5 来料检验结果列表(只读列表)" };
+        // 隐藏详情路由:不出侧栏,仅参与动态路由注册。IsKeepAlive 必须显式 false(实体默认 true)——
+        // 详情页为强状态页面(N1 可编辑 → N2 只读 → 退回 → N1 可编辑),缓存会残留旧 canEdit。
+        yield return new SysMenu { Id = iqcBase + 4, Pid = iqcDirId, Title = "IQC检验单详情", Path = "/aidop/s5/iqc/detail/:id", Name = "aidopS5IqcInspBillDetail", Component = "/aidop/s5/iqc/iqcInspBillDetail", Icon = "ele-Document", Type = MenuTypeEnum.Menu, IsHide = true, IsKeepAlive = false, CreateTime = ct, OrderNo = 45, Remark = "S5 IQC检验单详情统一页(隐藏路由,来料检验任务列表两个 Tab 与生成后自动进入)" };
 
         // 仓储管理 (1322000000016) 下 5 个三级
         const long warehouseDirId = 1322000000016L;