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

feat(s7): FQC检验单详情统一路由页 result-detail(Phase 1J)

抽屉 + 旧录入页收敛为单一路由页 /aidop/s7/fqc/result-detail,
页面按 flowState 决定 录入/审核/处置/只读,动作不再由入口来源决定。

- 新增 fqcInspResultDetail.vue 作为唯一业务详情页
- 删除 FqcInspBillDetailDrawer.vue(0 引用)
- fqcInspDetail.vue 退化为兼容重定向壳 insp-detail -> result-detail
- 任务列表/结果列表/审批中心 N1-N3 入口统一指向 result-detail
- 新增隐藏菜单 result-detail 及角色授权(14 并集 + 9 检验角色)
YY968XX 10 часов назад
Родитель
Сommit
3067b3e406

+ 1 - 1
Web/package.json

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

+ 1 - 0
Web/src/views/aidop/s7/api/fqcInspection.ts

@@ -25,6 +25,7 @@ export interface FqcInspHead {
 	fireNo?: string;
 	applicant?: string;
 	applyTime?: string;
+	inspectStartTime?: string;
 	ownerId?: string;
 	ownerName?: string;
 	remark?: string;

+ 0 - 409
Web/src/views/aidop/s7/fqc/components/FqcInspBillDetailDrawer.vue

@@ -1,409 +0,0 @@
-<template>
-	<el-drawer v-model="innerVisible" :size="720" direction="rtl" @closed="onClosed">
-		<template #header>
-			<div class="drawer-title">
-				<span class="t1">FQC检验单详情</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="检验时间">{{ fmtDateTime(detail.inspectTime) }}</el-descriptions-item>
-					<el-descriptions-item label="来源单据编号">{{ dash(detail.sourceBillNo) }}</el-descriptions-item>
-					<el-descriptions-item label="检验人">{{ dash(detail.inspector) }}</el-descriptions-item>
-					<el-descriptions-item label="判定">{{ dash(detail.judgment) }}</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.productionBatchNo) }}</el-descriptions-item>
-				</el-descriptions>
-
-				<el-descriptions title="生产信息" :column="2" border size="small" class="mt12">
-					<el-descriptions-item label="生产指令单">{{ dash(detail.productionOrderNo) }}</el-descriptions-item>
-					<el-descriptions-item label="指令单数量">{{ dashNum(detail.orderQty) }}</el-descriptions-item>
-					<el-descriptions-item label="SAP工单号">{{ dash(detail.sapWorkOrderNo) }}</el-descriptions-item>
-				</el-descriptions>
-
-				<el-descriptions title="检验数量" :column="2" border size="small" class="mt12">
-					<el-descriptions-item label="检验数量">{{ dashNum(detail.inspectQty) }}</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>
-
-				<el-descriptions title="检规信息" :column="2" border size="small" class="mt12">
-					<el-descriptions-item label="击发编号">{{ dash(detail.fireNo) }}</el-descriptions-item>
-					<el-descriptions-item label="检规编号">{{ dash(detail.inspectSpecNo) }}</el-descriptions-item>
-					<el-descriptions-item label="检规版本">{{ dash(detail.inspectSpecVersion) }}</el-descriptions-item>
-				</el-descriptions>
-
-				<div class="items-title mt12">品检项目明细</div>
-				<el-table :data="detail.lines || []" border stripe size="small" row-key="id">
-					<el-table-column type="expand">
-						<template #default="{ row }">
-							<div class="samples-wrap">
-								<template v-if="nonEmptySamples(row).length">
-									<span class="samples-label">样品值:</span>
-									<el-tag
-										v-for="s in nonEmptySamples(row)"
-										:key="s.idx"
-										class="sample-tag"
-										type="info"
-										disable-transitions
-									>样品{{ s.idx }}: {{ s.val }}</el-tag>
-								</template>
-								<span v-else class="samples-empty">无样品数据</span>
-							</div>
-						</template>
-					</el-table-column>
-					<el-table-column prop="seqNo" label="序号" min-width="70" resizable />
-					<el-table-column prop="inspectionItem" label="检验项目" min-width="140" show-overflow-tooltip resizable />
-					<el-table-column prop="acceptStandard" label="接收标准" min-width="140" show-overflow-tooltip resizable />
-					<el-table-column prop="techRequirement" label="技术要求" min-width="140" show-overflow-tooltip resizable />
-					<el-table-column prop="samplingPlan" label="抽样方案" min-width="120" show-overflow-tooltip resizable />
-					<el-table-column prop="sampleQty" label="样本量" min-width="90" resizable />
-					<el-table-column prop="upperLimit" label="上限" min-width="90" resizable />
-					<el-table-column prop="lowerLimit" label="下限" min-width="90" resizable />
-					<el-table-column prop="sizeSpecification" label="规格标准" min-width="120" show-overflow-tooltip resizable />
-					<el-table-column prop="judgment" label="判定" min-width="90" resizable />
-					<el-table-column prop="nonNumericOk" label="非数值OK" min-width="100" resizable />
-					<el-table-column prop="nonNumericNg" label="非数值NG" min-width="100" resizable />
-					<el-table-column prop="remark" label="备注" min-width="140" show-overflow-tooltip resizable />
-					<template #empty>
-						<el-empty description="暂无明细" :image-size="60" />
-					</template>
-				</el-table>
-
-				<el-descriptions title="流程信息" :column="2" border size="small" class="mt12">
-					<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.hgsl" :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>
-
-				<!-- QE 处置(仅 canSubmitDisposition 显示;不合格 pd=1 经主管通过后进入)-->
-				<div v-if="flowState?.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-number v-model="qeForm.reworkQty" :min="0" :controls="false" style="width: 180px" />
-						</el-form-item>
-						<el-form-item label="让步数量">
-							<el-input-number v-model="qeForm.concessionQty" :min="0" :controls="false" style="width: 180px" />
-						</el-form-item>
-						<el-form-item label="报废数量">
-							<el-input-number v-model="qeForm.scrapQty" :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="submitting" @click="onSubmitQe">提交 QE 处置</el-button>
-						</el-form-item>
-					</el-form>
-				</div>
-
-				<!-- 已提交 QE 处置(只读展示)-->
-				<el-descriptions v-if="flowState?.hasDisposition" title="QE 处置结论(仅展示)" :column="2" border size="small" class="mt12">
-					<el-descriptions-item label="处置类型">{{ dash(flowState?.dispositionType) }}</el-descriptions-item>
-					<el-descriptions-item label="处置意见">
-						<span class="pre">{{ dash(flowState?.dispositionOpinion) }}</span>
-					</el-descriptions-item>
-				</el-descriptions>
-			</template>
-			<el-empty v-else-if="!loading" description="暂无详情数据" />
-		</div>
-	</el-drawer>
-</template>
-
-<script setup lang="ts" name="FqcInspBillDetailDrawer">
-import { computed, reactive, watch } from 'vue';
-import { ElMessage, ElMessageBox } from 'element-plus';
-import type { FqcDetail, FqcDetailLine } from '../../api/fqcInspBill';
-import type { FqcFlowState, FqcSubmitResultPayload, FqcQeDispositionPayload } from '../../api/fqcInspBillFlow';
-
-const props = defineProps<{
-	modelValue: boolean;
-	detail: FqcDetail | null;
-	loading?: boolean;
-	flowState?: FqcFlowState | null;
-	submitting?: boolean;
-}>();
-const emit = defineEmits<{
-	(e: 'update:modelValue', v: boolean): void;
-	(e: 'closed'): void;
-	(e: 'submit-result', payload: FqcSubmitResultPayload): void;
-	(e: 'approve'): void;
-	(e: 'reject', comment: string): void;
-	(e: 'qe-submit', payload: FqcQeDispositionPayload): void;
-}>();
-
-const innerVisible = computed({
-	get: () => props.modelValue,
-	set: (v: boolean) => emit('update:modelValue', v),
-});
-
-// 检验员录入表单(仅 canSubmitResult 时可见)
-const resultForm = reactive<{ pd: number; hgsl: number; bhgsl: number; clfs: number | null; comment: string }>({
-	pd: 0,
-	hgsl: 0,
-	bhgsl: 0,
-	clfs: null,
-	comment: '',
-});
-
-// QE 处置表单(仅 canSubmitDisposition 时可见)
-const qeForm = reactive<{
-	dispositionType: string;
-	dispositionQty: number | null;
-	reworkQty: number | null;
-	concessionQty: number | null;
-	scrapQty: number | null;
-	dispositionOpinion: string;
-	responsibilityDept: string;
-	responsibleUser: string;
-}>({
-	dispositionType: 'REWORK',
-	dispositionQty: null,
-	reworkQty: null,
-	concessionQty: null,
-	scrapQty: null,
-	dispositionOpinion: '',
-	responsibilityDept: '',
-	responsibleUser: '',
-});
-
-// 每次打开 drawer 重置录入表单
-watch(
-	() => props.modelValue,
-	(v) => {
-		if (v) {
-			resultForm.pd = 0;
-			resultForm.hgsl = 0;
-			resultForm.bhgsl = 0;
-			resultForm.clfs = null;
-			resultForm.comment = '';
-			qeForm.dispositionType = 'REWORK';
-			qeForm.dispositionQty = null;
-			qeForm.reworkQty = null;
-			qeForm.concessionQty = null;
-			qeForm.scrapQty = null;
-			qeForm.dispositionOpinion = '';
-			qeForm.responsibilityDept = '';
-			qeForm.responsibleUser = '';
-		}
-	}
-);
-
-function onSubmitQe() {
-	if (!['REWORK', 'CONCESSION', 'SCRAP', 'OTHER'].includes(qeForm.dispositionType)) return ElMessage.error('请选择处置类型');
-	if (!qeForm.dispositionOpinion || !qeForm.dispositionOpinion.trim()) return ElMessage.error('处置意见必填');
-	for (const q of [qeForm.dispositionQty, qeForm.reworkQty, qeForm.concessionQty, qeForm.scrapQty]) {
-		if (q !== null && q !== undefined && q < 0) return ElMessage.error('处置数量不能为负');
-	}
-	const id = props.detail?.id;
-	if (!id) return ElMessage.error('检验单 id 缺失');
-	emit('qe-submit', {
-		id,
-		dispositionType: qeForm.dispositionType,
-		dispositionQty: qeForm.dispositionQty,
-		reworkQty: qeForm.reworkQty,
-		concessionQty: qeForm.concessionQty,
-		scrapQty: qeForm.scrapQty,
-		dispositionOpinion: qeForm.dispositionOpinion.trim(),
-		responsibilityDept: qeForm.responsibilityDept || undefined,
-		responsibleUser: qeForm.responsibleUser || 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.hgsl < 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,
-		hgsl: resultForm.hgsl,
-		bhgsl: resultForm.bhgsl,
-		clfs: resultForm.pd === 1 ? resultForm.clfs : null,
-		comment: resultForm.comment,
-	});
-}
-
-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 {
-		/* 用户取消 */
-	}
-}
-
-// 仅渲染非空样品,保留样品序号(j1→样品1)
-function nonEmptySamples(row: FqcDetailLine) {
-	const arr = row?.samples || [];
-	const out: { idx: number; val: string }[] = [];
-	for (let i = 0; i < arr.length; i++) {
-		const v = arr[i];
-		if (v !== null && v !== undefined && String(v).trim() !== '') out.push({ idx: i + 1, val: String(v) });
-	}
-	return out;
-}
-
-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;
-}
-.items-title {
-	font-weight: bold;
-	font-size: 14px;
-	margin-bottom: 8px;
-}
-.samples-wrap {
-	padding: 6px 12px;
-	display: flex;
-	flex-wrap: wrap;
-	align-items: center;
-	gap: 6px;
-}
-.samples-label {
-	color: var(--el-text-color-secondary);
-	font-size: 12px;
-}
-.sample-tag {
-	margin: 0;
-}
-.samples-empty {
-	color: var(--el-text-color-secondary);
-	font-size: 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;
-}
-</style>

+ 12 - 253
Web/src/views/aidop/s7/fqc/fqcInspDetail.vue

@@ -1,264 +1,23 @@
 <template>
-	<div class="fqc-insp-detail">
-		<el-page-header @back="goBack">
-			<template #content>
-				<span>检验单录入 {{ head?.billNo || '' }}</span>
-				<el-tag v-if="head?.billStatus" size="small" class="ml8">{{ head.billStatus }}</el-tag>
-			</template>
-		</el-page-header>
-
-		<el-descriptions v-if="head" :column="3" border size="small" class="mt12">
-			<el-descriptions-item label="检验单号">{{ head.billNo }}</el-descriptions-item>
-			<el-descriptions-item label="来源报检">{{ head.sourceBillNo }}</el-descriptions-item>
-			<el-descriptions-item label="检规">{{ head.specNo }} / {{ head.specVersion }}</el-descriptions-item>
-			<el-descriptions-item label="产品名称">{{ head.materialName }}</el-descriptions-item>
-			<el-descriptions-item label="物料编码">{{ head.materialCode }}</el-descriptions-item>
-			<el-descriptions-item label="规格型号">{{ head.spec }}</el-descriptions-item>
-			<el-descriptions-item label="生产批号">{{ head.productionBatchNo }}</el-descriptions-item>
-			<el-descriptions-item label="生产指令单">{{ head.productionOrderNo }}</el-descriptions-item>
-			<el-descriptions-item label="指令单数量">{{ head.orderQty ?? '' }}</el-descriptions-item>
-			<el-descriptions-item label="报检数量">{{ head.applyQty ?? '' }}</el-descriptions-item>
-			<el-descriptions-item label="检验优先级">{{ head.priority }}</el-descriptions-item>
-			<el-descriptions-item label="击发编号">{{ head.fireNo }}</el-descriptions-item>
-			<el-descriptions-item label="申请人">{{ head.applicant }}</el-descriptions-item>
-			<el-descriptions-item label="申请时间">{{ fmtDateTime(head.applyTime) }}</el-descriptions-item>
-			<el-descriptions-item label="检验负责人">{{ head.ownerName }}</el-descriptions-item>
-			<el-descriptions-item label="备注" :span="3">{{ head.remark }}</el-descriptions-item>
-		</el-descriptions>
-
-		<div class="toolbar mt12">
-			<el-button type="warning" :loading="judging" @click="onAutoJudge">自动判定</el-button>
-			<el-button :loading="saving" @click="onSave">保存</el-button>
-			<el-button type="primary" @click="openSubmit">提交</el-button>
-			<span class="tip">数值型展开录样本值,自动判定按快照上下限判;非数值型录合格/不合格数,无规则由人工确认判定。</span>
-		</div>
-
-		<el-table :data="lines" v-loading="loading" border stripe size="small" row-key="id" class="mt12">
-			<el-table-column type="expand">
-				<template #default="{ row }">
-					<div class="expand" v-if="row.lrlx === 'NUMERIC'">
-						<span class="lbl">样本值(上限 {{ row.sx || '-' }} / 下限 {{ row.xx || '-' }}):</span>
-						<el-input v-for="(s, i) in sampleSlots(row)" :key="i" v-model="row.samples[i]" size="small" class="sample" :placeholder="'样本' + (i + 1)" />
-						<el-button link type="primary" size="small" @click="addSample(row)">+样本</el-button>
-					</div>
-					<div class="expand" v-else-if="row.lrlx === 'NON_NUMERIC'">非数值型项目:请在行内填「合格数 / 不合格数」,无需逐样本录入。</div>
-					<div class="expand muted" v-else>请先在「类型」列选择 数值型 或 非数值型,再录入。</div>
-				</template>
-			</el-table-column>
-			<el-table-column prop="xh" label="序号" width="56" />
-			<el-table-column prop="jyxm" label="检验项目" min-width="130" show-overflow-tooltip />
-			<el-table-column label="类型" width="110">
-				<template #default="{ row }">
-					<el-select v-model="row.lrlx" size="small" placeholder="选择类型" style="width: 96px">
-						<el-option label="数值型" value="NUMERIC" />
-						<el-option label="非数值型" value="NON_NUMERIC" />
-					</el-select>
-				</template>
-			</el-table-column>
-			<el-table-column prop="jsyq" label="技术要求" min-width="140" show-overflow-tooltip />
-			<el-table-column label="合格数" width="90">
-				<template #default="{ row }">
-					<el-input-number v-if="row.lrlx === 'NON_NUMERIC'" v-model="row.nonNumericalTypeOK" :controls="false" :min="0" size="small" style="width: 78px" />
-					<span v-else class="muted">—</span>
-				</template>
-			</el-table-column>
-			<el-table-column label="不合格数" width="90">
-				<template #default="{ row }">
-					<el-input-number v-if="row.lrlx === 'NON_NUMERIC'" v-model="row.nonNumericalTypeNG" :controls="false" :min="0" size="small" style="width: 78px" />
-					<span v-else class="muted">—</span>
-				</template>
-			</el-table-column>
-			<el-table-column label="判定" width="120">
-				<template #default="{ row }">
-					<el-select v-model="row.pd" size="small" placeholder="待判定" clearable style="width: 100px">
-						<el-option label="合格" value="合格" />
-						<el-option label="不合格" value="不合格" />
-					</el-select>
-				</template>
-			</el-table-column>
-			<template #empty><el-empty description="该检验单无明细" /></template>
-		</el-table>
-
-		<!-- 提交(整单判定 + 合格/不合格数) -->
-		<el-dialog v-model="submitVisible" title="提交检验结果" width="440px">
-			<el-form label-width="110px">
-				<el-form-item label="整单判定">
-					<el-select v-model="submitForm.pd" style="width: 180px">
-						<el-option :value="0" label="合格" />
-						<el-option :value="1" label="不合格" />
-					</el-select>
-				</el-form-item>
-				<el-form-item label="合格数量"><el-input-number v-model="submitForm.hgsl" :controls="false" :min="0" style="width: 180px" /></el-form-item>
-				<el-form-item label="不合格数量"><el-input-number v-model="submitForm.bhgsl" :controls="false" :min="0" :disabled="submitForm.pd === 0" style="width: 180px" /></el-form-item>
-				<el-form-item label="处理方式" v-if="submitForm.pd === 1">
-					<el-select v-model="submitForm.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>
-			<template #footer>
-				<el-button @click="submitVisible = false">取消</el-button>
-				<el-button type="primary" :loading="submitting" @click="onSubmit">确认提交</el-button>
-			</template>
-		</el-dialog>
-	</div>
+	<div v-loading="true" class="fqc-insp-detail-redirect" element-loading-text="正在跳转…" style="min-height: 200px" />
 </template>
 
 <script setup lang="ts" name="aidopS7FqcInspDetail">
-import { onMounted, reactive, ref } from 'vue';
+// Phase 1J 起:FQC 检验单详情统一到 /aidop/s7/fqc/result-detail。
+// 本组件退化为兼容重定向壳:旧路由 /aidop/s7/fqc/insp-detail?id=X → /aidop/s7/fqc/result-detail?id=X。
+// 不再承载任何业务实现(录入/审核/处置均在 fqcInspResultDetail.vue)。
+import { onMounted } from 'vue';
 import { useRoute, useRouter } from 'vue-router';
-import { ElMessage } from 'element-plus';
-import mittBus from '/@/utils/mitt';
-import { fetchInspectionDetail, saveInspectionDetail, autoJudgeInspection, type FqcInspHead, type FqcInspLine } from '../api/fqcInspection';
-import { submitFqcResult } from '../api/fqcInspBillFlow';
 
 const route = useRoute();
 const router = useRouter();
-const inspBillId = ref<number>(Number(route.query.id) || 0);
-
-const head = ref<FqcInspHead | null>(null);
-const lines = ref<FqcInspLine[]>([]);
-const loading = ref(false);
-const saving = ref(false);
-const judging = ref(false);
-
-function fmtDateTime(v?: string | null) {
-	if (!v) return '';
-	const s = String(v);
-	return s.length > 19 ? s.slice(0, 19).replace('T', ' ') : s.replace('T', ' ');
-}
 
-function sampleSlots(row: FqcInspLine): number {
-	if (!row.samples) row.samples = [];
-	// 至少 5 个空槽,或已录入数+1
-	const filled = row.samples.filter((s) => s != null && String(s).trim() !== '').length;
-	const n = Math.max(5, filled + 1);
-	while (row.samples.length < n) row.samples.push(null);
-	return row.samples.length;
-}
-function addSample(row: FqcInspLine) {
-	if (!row.samples) row.samples = [];
-	row.samples.push(null);
-}
-
-async function load() {
-	if (!inspBillId.value) {
-		ElMessage.error('缺少检验单 id');
-		return;
-	}
-	loading.value = true;
-	try {
-		const d = await fetchInspectionDetail(inspBillId.value);
-		head.value = d.head;
-		lines.value = (d.lines || []).map((l) => ({ ...l, samples: l.samples ? [...l.samples] : [] }));
-	} catch (e: any) {
-		ElMessage.error(e?.message || '加载检验单失败');
-	} finally {
-		loading.value = false;
+onMounted(() => {
+	const id = route.query.id;
+	if (id !== undefined && id !== null && String(id) !== '') {
+		router.replace({ path: '/aidop/s7/fqc/result-detail', query: { id: String(id) } });
+	} else {
+		router.replace('/aidop/s7/fqc/result-list');
 	}
-}
-
-async function onSave() {
-	saving.value = true;
-	try {
-		await saveInspectionDetail({
-			inspBillId: inspBillId.value,
-			lines: lines.value.map((l) => ({ id: l.id, lrlx: l.lrlx ?? null, samples: l.samples, nonNumericalTypeOK: l.nonNumericalTypeOK ?? null, nonNumericalTypeNG: l.nonNumericalTypeNG ?? null, ybl: l.ybl ?? null, pd: l.pd ?? null })),
-		});
-		ElMessage.success('已保存');
-	} catch (e: any) {
-		ElMessage.error(e?.message || '保存失败');
-	} finally {
-		saving.value = false;
-	}
-}
-
-async function onAutoJudge() {
-	judging.value = true;
-	try {
-		await onSave(); // 先存样本再判
-		const res = await autoJudgeInspection(inspBillId.value);
-		ElMessage.success(res.message);
-		await load();
-	} catch (e: any) {
-		ElMessage.error(e?.message || '自动判定失败');
-	} finally {
-		judging.value = false;
-	}
-}
-
-// 提交
-const submitVisible = ref(false);
-const submitting = ref(false);
-const submitForm = reactive<{ pd: number; hgsl: number; bhgsl: number; clfs: number | null }>({ pd: 0, hgsl: 0, bhgsl: 0, clfs: null });
-function openSubmit() {
-	submitForm.pd = 0;
-	submitForm.hgsl = 0;
-	submitForm.bhgsl = 0;
-	submitForm.clfs = null;
-	submitVisible.value = true;
-}
-async function onSubmit() {
-	if (submitForm.pd === 0 && submitForm.bhgsl > 0) {
-		ElMessage.error('合格时不合格数量应为 0');
-		return;
-	}
-	if (submitForm.pd === 1 && submitForm.bhgsl <= 0) {
-		ElMessage.error('不合格时不合格数量需大于 0');
-		return;
-	}
-	submitting.value = true;
-	try {
-		await onSave();
-		await submitFqcResult({ id: inspBillId.value, pd: submitForm.pd, hgsl: submitForm.hgsl, bhgsl: submitForm.bhgsl, clfs: submitForm.pd === 1 ? submitForm.clfs : null });
-		ElMessage.success('已提交,转检验主管审核');
-		submitVisible.value = false;
-		goBack();
-	} catch (e: any) {
-		ElMessage.error(e?.message || '提交失败');
-	} finally {
-		submitting.value = false;
-	}
-}
-
-function goBack() {
-	router.push({ path: '/aidop/s7/fqc/apply' });
-	// 关闭当前 tab(复用框架 tagsView 关闭机制)
-	mittBus.emit('onCurrentContextmenuClick', { contextMenuClickId: 1, ...route });
-}
-
-onMounted(load);
+});
 </script>
-
-<style scoped lang="scss">
-.fqc-insp-detail {
-	padding: 12px;
-}
-.mt12 {
-	margin-top: 12px;
-}
-.ml8 {
-	margin-left: 8px;
-}
-.toolbar .tip {
-	color: #909399;
-	font-size: 12px;
-	margin-left: 12px;
-}
-.expand {
-	padding: 8px 16px;
-	.lbl {
-		margin-right: 8px;
-		color: #606266;
-	}
-	.sample {
-		width: 90px;
-		margin-right: 6px;
-	}
-}
-.muted {
-	color: #c0c4cc;
-}
-</style>

+ 592 - 0
Web/src/views/aidop/s7/fqc/fqcInspResultDetail.vue

@@ -0,0 +1,592 @@
+<template>
+	<AidopDemoShell :title="pageTitle" :subtitle="detail?.billNo || ''">
+		<div class="detail-topbar">
+			<el-button @click="goBack">← 返回</el-button>
+			<span class="bill-no">{{ detail?.billNo || (billId ? '加载中…' : '') }}</span>
+			<el-tag v-if="flowState?.flowStatus" type="info" size="small">{{ flowState.flowStatus }}</el-tag>
+			<el-tag v-if="flowState?.currentNodeName" size="small">{{ flowState.currentNodeName }}</el-tag>
+		</div>
+
+		<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="检验时间">{{ displayInspectTime }}</el-descriptions-item>
+					<el-descriptions-item label="来源单据编号">{{ dash(detail.sourceBillNo) }}</el-descriptions-item>
+					<el-descriptions-item label="检验人">{{ displayInspector }}</el-descriptions-item>
+					<el-descriptions-item label="检验数量">{{ dashNum(detail.inspectQty) }}</el-descriptions-item>
+					<!-- 判定/合格数量/不合格数量:检验员(N1)节点尚未产生,隐藏;主管/QE/只读节点展示 -->
+					<el-descriptions-item v-if="!flowState?.canSubmitResult" label="判定">{{ dash(detail.judgment) }}</el-descriptions-item>
+					<el-descriptions-item v-if="!flowState?.canSubmitResult" label="合格数量">{{ dashNum(detail.qualifiedQty) }}</el-descriptions-item>
+					<el-descriptions-item v-if="!flowState?.canSubmitResult" label="不合格数量">{{ dashNum(detail.unqualifiedQty) }}</el-descriptions-item>
+				</el-descriptions>
+
+				<el-descriptions title="成品信息" :column="2" border size="small" class="mt8">
+					<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.productionBatchNo) }}</el-descriptions-item>
+				</el-descriptions>
+
+				<el-descriptions title="生产信息" :column="2" border size="small" class="mt8">
+					<el-descriptions-item label="生产指令单">{{ dash(detail.productionOrderNo) }}</el-descriptions-item>
+					<el-descriptions-item label="指令单数量">{{ dashNum(detail.orderQty) }}</el-descriptions-item>
+					<el-descriptions-item label="SAP工单号">{{ dash(detail.sapWorkOrderNo) }}</el-descriptions-item>
+				</el-descriptions>
+
+				<el-descriptions title="检规信息" :column="2" border size="small" class="mt8">
+					<el-descriptions-item label="击发编号">{{ dash(detail.fireNo) }}</el-descriptions-item>
+					<el-descriptions-item label="检规编号">{{ dash(detail.inspectSpecNo) }}</el-descriptions-item>
+					<el-descriptions-item label="检规版本">{{ dash(detail.inspectSpecVersion) }}</el-descriptions-item>
+				</el-descriptions>
+
+				<div class="items-title mt8">品检项目明细</div>
+
+				<!-- N1 检验员录入:老风格逐项录入(类型/样本/合格数/自动判定/保存)-->
+				<template v-if="flowState?.canSubmitResult">
+					<div class="insp-toolbar">
+						<el-button type="warning" :loading="judging" @click="onAutoJudge">自动判定</el-button>
+						<el-button :loading="saving" @click="onSaveLines">保存</el-button>
+						<span class="insp-tip">数值型展开录样本值,自动判定按快照上下限判;非数值型录合格/不合格数,无规则由人工确认判定。</span>
+					</div>
+					<el-table :data="inspLines" v-loading="inspLoading" border stripe size="small" row-key="id" class="mt8">
+						<el-table-column type="expand">
+							<template #default="{ row }">
+								<div class="expand" v-if="row.lrlx === 'NUMERIC'">
+									<span class="lbl">样本值(上限 {{ row.sx || '-' }} / 下限 {{ row.xx || '-' }}):</span>
+									<el-input v-for="(s, i) in sampleSlots(row)" :key="i" v-model="row.samples[i]" size="small" class="sample" :placeholder="'样本' + (i + 1)" />
+									<el-button link type="primary" size="small" @click="addSample(row)">+样本</el-button>
+								</div>
+								<div class="expand" v-else-if="row.lrlx === 'NON_NUMERIC'">非数值型项目:请在行内填「合格数 / 不合格数」,无需逐样本录入。</div>
+								<div class="expand muted" v-else>请先在「类型」列选择 数值型 或 非数值型,再录入。</div>
+							</template>
+						</el-table-column>
+						<el-table-column prop="xh" label="序号" width="56" />
+						<el-table-column prop="jyxm" label="检验项目" min-width="130" class-name="wrap-cell" />
+						<el-table-column label="类型" width="110">
+							<template #default="{ row }">
+								<el-select v-model="row.lrlx" size="small" placeholder="选择类型" style="width: 96px">
+									<el-option label="数值型" value="NUMERIC" />
+									<el-option label="非数值型" value="NON_NUMERIC" />
+								</el-select>
+							</template>
+						</el-table-column>
+						<el-table-column prop="jsbz" label="接收标准" min-width="150" class-name="wrap-cell" />
+						<el-table-column prop="jsyq" label="技术要求" min-width="150" class-name="wrap-cell" />
+						<el-table-column prop="cyfa" label="抽样方案" min-width="110" class-name="wrap-cell" />
+						<el-table-column label="合格数" width="90">
+							<template #default="{ row }">
+								<el-input-number v-if="row.lrlx === 'NON_NUMERIC'" v-model="row.nonNumericalTypeOK" :controls="false" :min="0" size="small" style="width: 78px" />
+								<span v-else class="muted">—</span>
+							</template>
+						</el-table-column>
+						<el-table-column label="不合格数" width="90">
+							<template #default="{ row }">
+								<el-input-number v-if="row.lrlx === 'NON_NUMERIC'" v-model="row.nonNumericalTypeNG" :controls="false" :min="0" size="small" style="width: 78px" />
+								<span v-else class="muted">—</span>
+							</template>
+						</el-table-column>
+						<el-table-column label="判定" width="120">
+							<template #default="{ row }">
+								<el-select v-model="row.pd" size="small" placeholder="待判定" clearable style="width: 100px">
+									<el-option label="合格" value="合格" />
+									<el-option label="不合格" value="不合格" />
+								</el-select>
+							</template>
+						</el-table-column>
+						<template #empty><el-empty description="该检验单无明细" :image-size="60" /></template>
+					</el-table>
+				</template>
+
+				<!-- 非 N1(审核/处置/只读):只读明细,文本自动换行 -->
+				<el-table v-else :data="detail.lines || []" border stripe size="small" row-key="id">
+					<el-table-column type="expand">
+						<template #default="{ row }">
+							<div class="samples-wrap">
+								<template v-if="nonEmptySamples(row).length">
+									<span class="samples-label">样品值:</span>
+									<el-tag v-for="s in nonEmptySamples(row)" :key="s.idx" class="sample-tag" type="info" disable-transitions>样品{{ s.idx }}: {{ s.val }}</el-tag>
+								</template>
+								<span v-else class="samples-empty">无样品数据</span>
+							</div>
+						</template>
+					</el-table-column>
+					<el-table-column prop="seqNo" label="序号" min-width="70" resizable />
+					<el-table-column prop="inspectionItem" label="检验项目" min-width="140" class-name="wrap-cell" resizable />
+					<el-table-column prop="acceptStandard" label="接收标准" min-width="150" class-name="wrap-cell" resizable />
+					<el-table-column prop="techRequirement" label="技术要求" min-width="150" class-name="wrap-cell" resizable />
+					<el-table-column prop="samplingPlan" label="抽样方案" min-width="120" class-name="wrap-cell" resizable />
+					<el-table-column prop="sampleQty" label="样本量" min-width="90" resizable />
+					<el-table-column prop="upperLimit" label="上限" min-width="90" resizable />
+					<el-table-column prop="lowerLimit" label="下限" min-width="90" resizable />
+					<el-table-column prop="sizeSpecification" label="规格标准" min-width="120" class-name="wrap-cell" resizable />
+					<el-table-column prop="judgment" label="判定" min-width="90" resizable />
+					<el-table-column prop="nonNumericOk" label="非数值OK" min-width="100" resizable />
+					<el-table-column prop="nonNumericNg" label="非数值NG" min-width="100" resizable />
+					<el-table-column prop="remark" label="备注" min-width="140" class-name="wrap-cell" resizable />
+					<template #empty>
+						<el-empty description="暂无明细" :image-size="60" />
+					</template>
+				</el-table>
+
+				<el-descriptions title="流程信息" :column="2" border size="small" class="mt8">
+					<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>
+
+				<!-- N1 检验员提交(先存逐项明细再提交转主管)-->
+				<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.hgsl" :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 :loading="saving" @click="onSaveLines">保存明细</el-button>
+							<el-button type="primary" :loading="submitting" @click="onSubmitResult">提交检验结果</el-button>
+						</el-form-item>
+					</el-form>
+				</div>
+
+				<!-- N2 检验主管审核 -->
+				<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>
+
+				<!-- N3 QE 处置 -->
+				<div v-if="flowState?.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-number v-model="qeForm.reworkQty" :min="0" :controls="false" style="width: 180px" /></el-form-item>
+						<el-form-item label="让步数量"><el-input-number v-model="qeForm.concessionQty" :min="0" :controls="false" style="width: 180px" /></el-form-item>
+						<el-form-item label="报废数量"><el-input-number v-model="qeForm.scrapQty" :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="submitting" @click="onSubmitQe">提交 QE 处置</el-button>
+						</el-form-item>
+					</el-form>
+				</div>
+
+				<!-- 已提交 QE 处置(只读展示)-->
+				<el-descriptions v-if="flowState?.hasDisposition" title="QE 处置结论(仅展示)" :column="2" border size="small" class="mt12">
+					<el-descriptions-item label="处置类型">{{ dash(flowState?.dispositionType) }}</el-descriptions-item>
+					<el-descriptions-item label="处置意见"><span class="pre">{{ dash(flowState?.dispositionOpinion) }}</span></el-descriptions-item>
+				</el-descriptions>
+			</template>
+			<el-empty v-else-if="!loading" :description="emptyText" />
+		</div>
+	</AidopDemoShell>
+</template>
+
+<script setup lang="ts" name="aidopS7FqcResultDetail">
+import { computed, onMounted, reactive, ref } from 'vue';
+import { useRoute, useRouter } from 'vue-router';
+import { ElMessage, ElMessageBox } from 'element-plus';
+import AidopDemoShell from '/@/views/aidop/components/AidopDemoShell.vue';
+import { fetchFqcDetail, type FqcDetail, type FqcDetailLine } from '../api/fqcInspBill';
+import { fetchInspectionDetail, saveInspectionDetail, autoJudgeInspection, type FqcInspLine, type FqcInspHead } from '../api/fqcInspection';
+import {
+	fetchFqcFlowState,
+	submitFqcResult,
+	fqcSupervisorApprove,
+	fqcSupervisorReject,
+	submitFqcQeDisposition,
+	type FqcFlowState,
+} from '../api/fqcInspBillFlow';
+
+const route = useRoute();
+const router = useRouter();
+const pageTitle = computed(() => (route.meta?.title as string) || 'FQC检验单详情');
+
+const billId = computed(() => Number(route.query.id) || 0);
+const loading = ref(false);
+const submitting = ref(false);
+const detail = ref<FqcDetail | null>(null);
+const flowState = ref<FqcFlowState | null>(null);
+const emptyText = ref('暂无详情数据');
+
+// N1 逐项录入(老风格):活单明细 qms_qcpp_inspbillst + 活单头(取负责人/检验开始时间)
+const inspLines = ref<FqcInspLine[]>([]);
+const inspHead = ref<FqcInspHead | null>(null);
+const inspLoading = ref(false);
+
+// 检验人/检验时间:N1 录入节点取活单头(负责人 + 检验开始时间);否则取只读详情(提交后写入的实际检验人/完成时间)
+const displayInspector = computed(() => {
+	if (flowState.value?.canSubmitResult && inspHead.value?.ownerName) return inspHead.value.ownerName;
+	return dash(detail.value?.inspector);
+});
+const displayInspectTime = computed(() => {
+	if (flowState.value?.canSubmitResult && inspHead.value?.inspectStartTime) return fmtDateTime(inspHead.value.inspectStartTime);
+	return fmtDateTime(detail.value?.inspectTime);
+});
+const saving = ref(false);
+const judging = ref(false);
+
+// 检验员聚合提交表单
+const resultForm = reactive<{ pd: number; hgsl: number; bhgsl: number; clfs: number | null; comment: string }>({ pd: 0, hgsl: 0, bhgsl: 0, clfs: null, comment: '' });
+// QE 处置表单
+const qeForm = reactive<{
+	dispositionType: string;
+	dispositionQty: number | null;
+	reworkQty: number | null;
+	concessionQty: number | null;
+	scrapQty: number | null;
+	dispositionOpinion: string;
+	responsibilityDept: string;
+	responsibleUser: string;
+}>({ dispositionType: 'REWORK', dispositionQty: null, reworkQty: null, concessionQty: null, scrapQty: null, dispositionOpinion: '', responsibilityDept: '', responsibleUser: '' });
+
+function resetForms() {
+	resultForm.pd = 0;
+	resultForm.hgsl = 0;
+	resultForm.bhgsl = 0;
+	resultForm.clfs = null;
+	resultForm.comment = '';
+	qeForm.dispositionType = 'REWORK';
+	qeForm.dispositionQty = null;
+	qeForm.reworkQty = null;
+	qeForm.concessionQty = null;
+	qeForm.scrapQty = null;
+	qeForm.dispositionOpinion = '';
+	qeForm.responsibilityDept = '';
+	qeForm.responsibleUser = '';
+}
+
+// 数值型样本槽位:至少 5 个空槽或已录入数+1
+function sampleSlots(row: FqcInspLine): number {
+	if (!row.samples) row.samples = [];
+	const filled = row.samples.filter((s) => s != null && String(s).trim() !== '').length;
+	const n = Math.max(5, filled + 1);
+	while (row.samples.length < n) row.samples.push(null);
+	return row.samples.length;
+}
+function addSample(row: FqcInspLine) {
+	if (!row.samples) row.samples = [];
+	row.samples.push(null);
+}
+
+async function loadInspLines() {
+	inspLoading.value = true;
+	try {
+		const d = await fetchInspectionDetail(billId.value);
+		inspHead.value = d.head;
+		inspLines.value = (d.lines || []).map((l) => ({ ...l, samples: l.samples ? [...l.samples] : [] }));
+	} catch (e: any) {
+		inspLines.value = [];
+		inspHead.value = null;
+		ElMessage.error(e?.message || '加载检验单录入明细失败');
+	} finally {
+		inspLoading.value = false;
+	}
+}
+
+async function loadDetailAndState() {
+	const id = billId.value;
+	if (!id || id <= 0) {
+		detail.value = null;
+		flowState.value = null;
+		emptyText.value = '缺少检验单 id,无法加载';
+		return;
+	}
+	loading.value = true;
+	try {
+		const [d, s] = await Promise.all([fetchFqcDetail(id), fetchFqcFlowState(id)]);
+		detail.value = d;
+		flowState.value = s;
+		if (!d) emptyText.value = '检验单不存在或无权访问';
+		resetForms();
+		// N1 录入模式:另拉活单逐项明细(含快照上下限/已录样本),供老风格录入
+		if (s?.canSubmitResult) await loadInspLines();
+		else {
+			inspLines.value = [];
+			inspHead.value = null;
+		}
+	} catch (e: any) {
+		detail.value = null;
+		flowState.value = null;
+		emptyText.value = e?.message || '加载FQC检验单详情失败';
+		ElMessage.error(e?.message || '加载FQC检验单详情失败');
+	} finally {
+		loading.value = false;
+	}
+}
+
+// 逐项保存(草稿,不起流)
+async function onSaveLines() {
+	saving.value = true;
+	try {
+		await saveInspectionDetail({
+			inspBillId: billId.value,
+			lines: inspLines.value.map((l) => ({ id: l.id, lrlx: l.lrlx ?? null, samples: l.samples, nonNumericalTypeOK: l.nonNumericalTypeOK ?? null, nonNumericalTypeNG: l.nonNumericalTypeNG ?? null, ybl: l.ybl ?? null, pd: l.pd ?? null })),
+		});
+		ElMessage.success('已保存');
+	} catch (e: any) {
+		ElMessage.error(e?.message || '保存失败');
+	} finally {
+		saving.value = false;
+	}
+}
+
+// 自动判定:先存样本再判(数值型按上下限,非数值型/无规则留人工)
+async function onAutoJudge() {
+	judging.value = true;
+	try {
+		await onSaveLines();
+		const res = await autoJudgeInspection(billId.value);
+		ElMessage.success(res.message);
+		await loadInspLines();
+	} catch (e: any) {
+		ElMessage.error(e?.message || '自动判定失败');
+	} finally {
+		judging.value = false;
+	}
+}
+
+async function onSubmitResult() {
+	if (resultForm.pd !== 0 && resultForm.pd !== 1) return ElMessage.error('请选择判定');
+	if (resultForm.hgsl < 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('判定不合格时请选择处理方式');
+	submitting.value = true;
+	try {
+		await onSaveLines(); // 提交前先存逐项明细(与老风格一致)
+		await submitFqcResult({ id: billId.value, pd: resultForm.pd, hgsl: resultForm.hgsl, bhgsl: resultForm.bhgsl, clfs: resultForm.pd === 1 ? resultForm.clfs : null, comment: resultForm.comment });
+		ElMessage.success('已提交,转检验主管审核');
+		await loadDetailAndState();
+	} catch (e: any) {
+		ElMessage.error(e?.message || '提交检验结果失败');
+	} finally {
+		submitting.value = false;
+	}
+}
+
+function onPdChange() {
+	if (resultForm.pd === 0) {
+		resultForm.bhgsl = 0;
+		resultForm.clfs = null;
+	}
+}
+
+async function onApprove() {
+	submitting.value = true;
+	try {
+		await fqcSupervisorApprove(billId.value);
+		ElMessage.success('已通过');
+		await loadDetailAndState();
+	} catch (e: any) {
+		ElMessage.error(e?.message || '审核通过失败');
+	} finally {
+		submitting.value = false;
+	}
+}
+
+async function onReject() {
+	let comment = '';
+	try {
+		const { value } = await ElMessageBox.prompt('请填写退回意见', '退回', {
+			confirmButtonText: '确定退回',
+			cancelButtonText: '取消',
+			inputType: 'textarea',
+			inputValidator: (v: string) => (v && v.trim() ? true : '退回意见不能为空'),
+		});
+		comment = value.trim();
+	} catch {
+		return;
+	}
+	submitting.value = true;
+	try {
+		await fqcSupervisorReject(billId.value, comment);
+		ElMessage.success('已退回');
+		await loadDetailAndState();
+	} catch (e: any) {
+		ElMessage.error(e?.message || '退回失败');
+	} finally {
+		submitting.value = false;
+	}
+}
+
+async function onSubmitQe() {
+	if (!['REWORK', 'CONCESSION', 'SCRAP', 'OTHER'].includes(qeForm.dispositionType)) return ElMessage.error('请选择处置类型');
+	if (!qeForm.dispositionOpinion || !qeForm.dispositionOpinion.trim()) return ElMessage.error('处置意见必填');
+	for (const q of [qeForm.dispositionQty, qeForm.reworkQty, qeForm.concessionQty, qeForm.scrapQty]) {
+		if (q !== null && q !== undefined && q < 0) return ElMessage.error('处置数量不能为负');
+	}
+	submitting.value = true;
+	try {
+		await submitFqcQeDisposition({
+			id: billId.value,
+			dispositionType: qeForm.dispositionType,
+			dispositionQty: qeForm.dispositionQty,
+			reworkQty: qeForm.reworkQty,
+			concessionQty: qeForm.concessionQty,
+			scrapQty: qeForm.scrapQty,
+			dispositionOpinion: qeForm.dispositionOpinion.trim(),
+			responsibilityDept: qeForm.responsibilityDept || undefined,
+			responsibleUser: qeForm.responsibleUser || undefined,
+		});
+		ElMessage.success('QE 处置已提交');
+		await loadDetailAndState();
+	} catch (e: any) {
+		ElMessage.error(e?.message || '提交 QE 处置失败');
+	} finally {
+		submitting.value = false;
+	}
+}
+
+function nonEmptySamples(row: FqcDetailLine) {
+	const arr = row?.samples || [];
+	const out: { idx: number; val: string }[] = [];
+	for (let i = 0; i < arr.length; i++) {
+		const v = arr[i];
+		if (v !== null && v !== undefined && String(v).trim() !== '') out.push({ idx: i + 1, val: String(v) });
+	}
+	return out;
+}
+
+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 goBack() {
+	if (window.history.length > 1) router.back();
+	else router.push('/aidop/s7/fqc/result-list');
+}
+
+onMounted(loadDetailAndState);
+</script>
+
+<style scoped lang="scss">
+.detail-topbar {
+	display: flex;
+	align-items: center;
+	gap: 10px;
+	margin-bottom: 12px;
+	.bill-no {
+		font-weight: bold;
+		font-size: 15px;
+	}
+}
+.detail-body {
+	min-height: 120px;
+}
+.mt8 {
+	margin-top: 8px;
+}
+.mt12 {
+	margin-top: 12px;
+}
+.items-title {
+	font-weight: bold;
+	font-size: 14px;
+	margin-bottom: 8px;
+}
+.insp-toolbar {
+	display: flex;
+	align-items: center;
+	gap: 10px;
+	flex-wrap: wrap;
+	.insp-tip {
+		color: var(--el-text-color-secondary);
+		font-size: 12px;
+	}
+}
+.expand {
+	padding: 6px 12px;
+	display: flex;
+	flex-wrap: wrap;
+	align-items: center;
+	gap: 6px;
+	.lbl {
+		color: var(--el-text-color-secondary);
+		font-size: 12px;
+	}
+	.sample {
+		width: 96px;
+	}
+}
+.muted {
+	color: var(--el-text-color-secondary);
+}
+.samples-wrap {
+	padding: 6px 12px;
+	display: flex;
+	flex-wrap: wrap;
+	align-items: center;
+	gap: 6px;
+}
+.samples-label {
+	color: var(--el-text-color-secondary);
+	font-size: 12px;
+}
+.sample-tag {
+	margin: 0;
+}
+.samples-empty {
+	color: var(--el-text-color-secondary);
+	font-size: 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;
+}
+// 文本列自动换行(替代 show-overflow-tooltip 的省略号截断)
+:deep(.wrap-cell .cell) {
+	white-space: pre-wrap;
+	word-break: break-word;
+	line-height: 1.4;
+}
+</style>

+ 8 - 118
Web/src/views/aidop/s7/fqc/fqcInspectionResultList.vue

@@ -60,7 +60,7 @@
 			<el-table-column prop="unqualifiedQty" label="不合格数量" min-width="110" resizable />
 			<el-table-column label="操作" min-width="90" fixed="right">
 				<template #default="{ row }">
-					<el-button link type="primary" @click="openDetail(row)">详情</el-button>
+					<el-button link type="primary" @click="goDetail(row)">详情</el-button>
 				</template>
 			</el-table-column>
 			<template #empty>
@@ -79,40 +79,18 @@
 				@size-change="loadList"
 			/>
 		</div>
-
-		<FqcInspBillDetailDrawer
-			v-model="detailVisible"
-			:detail="detail"
-			:loading="detailLoading"
-			:flow-state="flowState"
-			:submitting="flowSubmitting"
-			@submit-result="onFlowSubmit"
-			@approve="onFlowApprove"
-			@reject="onFlowReject"
-			@qe-submit="onFlowQe"
-		/>
 	</AidopDemoShell>
 </template>
 
 <script setup lang="ts" name="aidopS7FqcResultList">
 import { computed, onActivated, onMounted, reactive, ref } from 'vue';
-import { useRoute } from 'vue-router';
+import { useRoute, useRouter } from 'vue-router';
 import { ElMessage } from 'element-plus';
 import AidopDemoShell from '/@/views/aidop/components/AidopDemoShell.vue';
-import FqcInspBillDetailDrawer from './components/FqcInspBillDetailDrawer.vue';
-import { fetchFqcResultList, fetchFqcDetail, type FqcResultRow, type FqcDetail } from '../api/fqcInspBill';
-import {
-	fetchFqcFlowState,
-	submitFqcResult,
-	fqcSupervisorApprove,
-	fqcSupervisorReject,
-	submitFqcQeDisposition,
-	type FqcFlowState,
-	type FqcSubmitResultPayload,
-	type FqcQeDispositionPayload,
-} from '../api/fqcInspBillFlow';
+import { fetchFqcResultList, type FqcResultRow } from '../api/fqcInspBill';
 
 const route = useRoute();
+const router = useRouter();
 const pageTitle = computed(() => (route.meta?.title as string) || 'FQC检验单列表');
 
 const query = reactive({
@@ -133,93 +111,10 @@ const loading = ref(false);
 const rows = ref<FqcResultRow[]>([]);
 const total = ref(0);
 
-const detailVisible = ref(false);
-const detail = ref<FqcDetail | null>(null);
-const detailLoading = ref(false);
-const flowState = ref<FqcFlowState | null>(null);
-const flowSubmitting = ref(false);
-const currentDetailId = ref<number | null>(null);
-
-async function loadDetailAndState(id: number) {
-	detailLoading.value = true;
-	try {
-		const [d, s] = await Promise.all([fetchFqcDetail(id), fetchFqcFlowState(id)]);
-		detail.value = d;
-		flowState.value = s;
-	} catch (e: any) {
-		detail.value = null;
-		flowState.value = null;
-		ElMessage.error(e?.message || '加载FQC检验单详情失败');
-	} finally {
-		detailLoading.value = false;
-	}
-}
-
-async function openDetail(row: FqcResultRow) {
+// 详情统一跳独立页面 result-detail(录入/审核/处置/只读由该页按 flowState 决定)
+function goDetail(row: FqcResultRow) {
 	if (!row?.id) return;
-	currentDetailId.value = row.id;
-	detail.value = null;
-	flowState.value = null;
-	detailVisible.value = true;
-	await loadDetailAndState(row.id);
-}
-
-async function onFlowSubmit(payload: FqcSubmitResultPayload) {
-	flowSubmitting.value = true;
-	try {
-		await submitFqcResult(payload);
-		ElMessage.success('检验结果已提交');
-		if (currentDetailId.value) await loadDetailAndState(currentDetailId.value);
-		loadList();
-	} catch (e: any) {
-		ElMessage.error(e?.message || '提交检验结果失败');
-	} finally {
-		flowSubmitting.value = false;
-	}
-}
-
-async function onFlowApprove() {
-	if (!currentDetailId.value) return;
-	flowSubmitting.value = true;
-	try {
-		await fqcSupervisorApprove(currentDetailId.value);
-		ElMessage.success('已通过');
-		await loadDetailAndState(currentDetailId.value);
-		loadList();
-	} catch (e: any) {
-		ElMessage.error(e?.message || '审核通过失败');
-	} finally {
-		flowSubmitting.value = false;
-	}
-}
-
-async function onFlowReject(comment: string) {
-	if (!currentDetailId.value) return;
-	flowSubmitting.value = true;
-	try {
-		await fqcSupervisorReject(currentDetailId.value, comment);
-		ElMessage.success('已退回');
-		await loadDetailAndState(currentDetailId.value);
-		loadList();
-	} catch (e: any) {
-		ElMessage.error(e?.message || '退回失败');
-	} finally {
-		flowSubmitting.value = false;
-	}
-}
-
-async function onFlowQe(payload: FqcQeDispositionPayload) {
-	flowSubmitting.value = true;
-	try {
-		await submitFqcQeDisposition(payload);
-		ElMessage.success('QE 处置已提交');
-		if (currentDetailId.value) await loadDetailAndState(currentDetailId.value);
-		loadList();
-	} catch (e: any) {
-		ElMessage.error(e?.message || '提交 QE 处置失败');
-	} finally {
-		flowSubmitting.value = false;
-	}
+	router.push({ path: '/aidop/s7/fqc/result-detail', query: { id: String(row.id) } });
 }
 
 function fmtDateTime(v?: string | null) {
@@ -273,12 +168,7 @@ function resetQuery() {
 	loadList();
 }
 
-onMounted(async () => {
-	await loadList();
-	// 审批中心 FQC 待办「去审核/去处置」跳入:带 openBillId 时自动打开该检验单详情抽屉(主管审核/QE处置由 flowState 决定)
-	const openId = Number(route.query.openBillId);
-	if (openId > 0) openDetail({ id: openId } as FqcResultRow);
-});
+onMounted(() => loadList());
 onActivated(() => loadList());
 </script>
 

+ 40 - 113
Web/src/views/aidop/s7/fqc/fqcInspectionTaskList.vue

@@ -36,7 +36,7 @@
 					<el-button :disabled="!selectedEntryIds.length" @click="openPriority">优先级调整</el-button>
 				</div>
 
-				<el-table :data="entryRows" row-key="id" v-loading="entryLoading" border stripe @selection-change="onEntrySelection">
+				<el-table class="sticky-head-table" :data="entryRows" row-key="id" v-loading="entryLoading" border stripe @selection-change="onEntrySelection">
 					<el-table-column type="selection" width="48" />
 					<el-table-column prop="applyBillNo" label="报检单号" min-width="160" show-overflow-tooltip resizable />
 					<el-table-column prop="productionOrderNo" label="生产指令单" min-width="130" show-overflow-tooltip resizable />
@@ -107,7 +107,7 @@
 					<el-table-column prop="qualifiedQty" label="合格数量" min-width="100" align="right" resizable />
 					<el-table-column prop="unqualifiedQty" label="不合格数量" min-width="110" align="right" resizable />
 					<el-table-column label="操作" width="90" fixed="right">
-						<template #default="{ row }"><el-button link type="primary" @click="openDetail(row)">查看</el-button></template>
+						<template #default="{ row }"><el-button link type="primary" @click="goDetail(row)">查看</el-button></template>
 					</el-table-column>
 					<template #empty><el-empty description="暂无检验单" /></template>
 				</el-table>
@@ -118,18 +118,6 @@
 			</el-tab-pane>
 		</el-tabs>
 
-		<FqcInspBillDetailDrawer
-			v-model="detailVisible"
-			:detail="detail"
-			:loading="detailLoading"
-			:flow-state="flowState"
-			:submitting="flowSubmitting"
-			@submit-result="onFlowSubmit"
-			@approve="onFlowApprove"
-			@reject="onFlowReject"
-			@qe-submit="onFlowQe"
-		/>
-
 		<FqcInspectorPickerDialog v-model="assignVisible" @confirm="onAssignConfirm" />
 
 		<el-dialog v-model="priorityVisible" title="优先级调整" width="420px">
@@ -155,9 +143,8 @@ import { useRoute, useRouter } from 'vue-router';
 import { ElMessage } from 'element-plus';
 import AidopDemoShell from '/@/views/aidop/components/AidopDemoShell.vue';
 import { useUserInfo } from '/@/stores/userInfo';
-import FqcInspBillDetailDrawer from './components/FqcInspBillDetailDrawer.vue';
 import FqcInspectorPickerDialog from './components/FqcInspectorPickerDialog.vue';
-import { fetchFqcResultList, fetchFqcDetail, type FqcResultRow, type FqcDetail } from '../api/fqcInspBill';
+import { fetchFqcResultList, type FqcResultRow } from '../api/fqcInspBill';
 import {
 	fetchFqcTaskEntryList,
 	claimFqcTaskEntries,
@@ -166,17 +153,6 @@ import {
 	generateFqcInspBillByTask,
 	type FqcTaskEntryRow,
 } from '../api/fqcTaskEntry';
-import {
-	fetchFqcFlowState,
-	submitFqcResult,
-	fqcSupervisorApprove,
-	fqcSupervisorReject,
-	submitFqcQeDisposition,
-	type FqcFlowState,
-	type FqcSubmitResultPayload,
-	type FqcQeDispositionPayload,
-} from '../api/fqcInspBillFlow';
-
 const route = useRoute();
 const router = useRouter();
 const pageTitle = computed(() => (route.meta?.title as string) || 'FQC检验任务列表');
@@ -295,7 +271,7 @@ async function onGenerate(row: FqcTaskEntryRow) {
 		const res = await generateFqcInspBillByTask(row.id);
 		ElMessage.success(`已生成检验单 ${res.inspBillNo}`);
 		await loadEntryList();
-		if (res.inspBillId) router.push({ path: '/aidop/s7/fqc/insp-detail', query: { id: String(res.inspBillId) } });
+		if (res.inspBillId) router.push({ path: '/aidop/s7/fqc/result-detail', query: { id: String(res.inspBillId) } });
 	} catch (e: any) {
 		ElMessage.error(e?.message || '生成检验单失败');
 	} finally {
@@ -310,7 +286,7 @@ function openInspection(row: FqcTaskEntryRow) {
 		ElMessage.error('检验负责人与当前账号不一致,无法进入录入检验');
 		return;
 	}
-	router.push({ path: '/aidop/s7/fqc/insp-detail', query: { id: String(row.inspBillId) } });
+	router.push({ path: '/aidop/s7/fqc/result-detail', query: { id: String(row.inspBillId) } });
 }
 
 // ── 检验单 ──
@@ -346,91 +322,10 @@ function resetBillQuery() {
 	loadBillList();
 }
 
-// ── 检验单详情 + 流程 ──
-const detailVisible = ref(false);
-const detail = ref<FqcDetail | null>(null);
-const detailLoading = ref(false);
-const flowState = ref<FqcFlowState | null>(null);
-const flowSubmitting = ref(false);
-const currentDetailId = ref<number | null>(null);
-
-async function loadDetailAndState(id: number) {
-	detailLoading.value = true;
-	try {
-		const [d, s] = await Promise.all([fetchFqcDetail(id), fetchFqcFlowState(id)]);
-		detail.value = d;
-		flowState.value = s;
-	} catch (e: any) {
-		detail.value = null;
-		flowState.value = null;
-		ElMessage.error(e?.message || '加载FQC检验单详情失败');
-	} finally {
-		detailLoading.value = false;
-	}
-}
-async function openDetail(row: FqcResultRow) {
+// ── 检验单详情 ──:统一跳独立页面 result-detail(录入/审核/处置/只读由该页按 flowState 决定)
+function goDetail(row: FqcResultRow) {
 	if (!row?.id) return;
-	currentDetailId.value = row.id;
-	detail.value = null;
-	flowState.value = null;
-	detailVisible.value = true;
-	await loadDetailAndState(row.id);
-}
-async function onFlowSubmit(payload: FqcSubmitResultPayload) {
-	flowSubmitting.value = true;
-	try {
-		await submitFqcResult(payload);
-		ElMessage.success('检验结果已提交');
-		if (currentDetailId.value) await loadDetailAndState(currentDetailId.value);
-		loadBillList();
-		loadEntryList();
-	} catch (e: any) {
-		ElMessage.error(e?.message || '提交检验结果失败');
-	} finally {
-		flowSubmitting.value = false;
-	}
-}
-async function onFlowApprove() {
-	if (!currentDetailId.value) return;
-	flowSubmitting.value = true;
-	try {
-		await fqcSupervisorApprove(currentDetailId.value);
-		ElMessage.success('已通过');
-		await loadDetailAndState(currentDetailId.value);
-		loadBillList();
-	} catch (e: any) {
-		ElMessage.error(e?.message || '审核通过失败');
-	} finally {
-		flowSubmitting.value = false;
-	}
-}
-async function onFlowReject(comment: string) {
-	if (!currentDetailId.value) return;
-	flowSubmitting.value = true;
-	try {
-		await fqcSupervisorReject(currentDetailId.value, comment);
-		ElMessage.success('已退回');
-		await loadDetailAndState(currentDetailId.value);
-		loadBillList();
-		loadEntryList();
-	} catch (e: any) {
-		ElMessage.error(e?.message || '退回失败');
-	} finally {
-		flowSubmitting.value = false;
-	}
-}
-async function onFlowQe(payload: FqcQeDispositionPayload) {
-	flowSubmitting.value = true;
-	try {
-		await submitFqcQeDisposition(payload);
-		ElMessage.success('QE 处置已提交');
-		if (currentDetailId.value) await loadDetailAndState(currentDetailId.value);
-		loadBillList();
-	} catch (e: any) {
-		ElMessage.error(e?.message || '提交 QE 处置失败');
-	} finally {
-		flowSubmitting.value = false;
-	}
+	router.push({ path: '/aidop/s7/fqc/result-detail', query: { id: String(row.id) } });
 }
 
 function onTabChange(name: string | number) {
@@ -460,4 +355,36 @@ onActivated(() => {
 	display: flex;
 	justify-content: flex-end;
 }
+
+/* ──────────────────────────────────────────────────────────────
+ * 待检任务列表:页面级滚动 + 表头吸顶
+ * 真实滚动容器 = .layout-parent(height:0; overflow:auto),
+ * 故 sticky 的偏移基准是 .layout-parent 的 scrollport,top:0 即内容区顶部。
+ *
+ * 以下三条 overflow 覆盖仅用于解除 sticky 阻断链,全部经 scoped
+ * data-v 收敛在本页 shell 内,不外溢到其他页面:
+ *   .aidop-demo-shell__card(= .el-card)  overflow:hidden
+ *   .el-card__body                        overflow:auto
+ *   .el-tabs__content                     overflow:hidden
+ *   .sticky-head-table(= .el-table)       overflow:hidden
+ * 注意:.el-table__header-wrapper 自身的 overflow:hidden 必须保留,
+ * Element Plus 靠 headerWrapper.scrollLeft 同步横向滚动,改成 visible 会失同步。
+ * ────────────────────────────────────────────────────────────── */
+:deep(.aidop-demo-shell__card),
+:deep(.aidop-demo-shell__card > .el-card__body),
+:deep(.el-tabs__content) {
+	overflow: visible;
+}
+
+.sticky-head-table {
+	overflow: visible;
+}
+
+.sticky-head-table :deep(.el-table__header-wrapper) {
+	position: sticky;
+	top: 0;
+	/* 高于固定列(z-index:2)与滚动条(z-index:3) */
+	z-index: 5;
+	background-color: var(--el-table-header-bg-color);
+}
 </style>

+ 3 - 2
Web/src/views/approvalFlow/center/components/PendingList.vue

@@ -94,8 +94,9 @@ async function goToException(bizId: number | string) {
 // N1 去录入 → 检验单录入页(受 jyfzr 负责人门保护);N2/N3 去审核/去处置 → 结果列表自动打开详情抽屉,
 // 由 flowState 决定显示 主管通过/退回(supervisor-*) 或 QE处置(qe-submit-disposition)。业务动作全走 FQC 专用 API。
 function goToFqc(action: 'inspect' | 'review' | 'disposition', bizId: number | string) {
-	if (action === 'inspect') router.push(`/aidop/s7/fqc/insp-detail?id=${bizId}`);
-	else router.push(`/aidop/s7/fqc/result-list?openBillId=${bizId}`);
+	// N1/N2/N3 统一进同一详情页 result-detail;页面按 flowState 决定录入/审核/处置/只读(action 仅用于按钮文案)
+	void action;
+	router.push(`/aidop/s7/fqc/result-detail?id=${bizId}`);
 }
 
 function goToImprovement(bizId: number | string) {

+ 6 - 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.381</AssemblyVersion>
-    <FileVersion>1.0.381</FileVersion>
-    <Version>1.0.381</Version>
+    <AssemblyVersion>1.0.382</AssemblyVersion>
+    <FileVersion>1.0.382</FileVersion>
+    <Version>1.0.382</Version>
   </PropertyGroup>
 
   <ItemGroup>
@@ -481,6 +481,9 @@
     <None Update="UpdateScripts\1.0.380.verify.sql">
       <CopyToOutputDirectory>Always</CopyToOutputDirectory>
     </None>
+    <None Update="UpdateScripts\1.0.382.sql">
+      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+    </None>
     <None Update="UpdateScripts\1.0.364.verify.sql">
       <CopyToOutputDirectory>Always</CopyToOutputDirectory>
     </None>

+ 45 - 0
server/Admin.NET.Web.Entry/UpdateScripts/1.0.382.sql

@@ -0,0 +1,45 @@
+-- 1.0.382:Phase 1J S7 FQC 检验单详情统一页 result-detail 隐藏菜单 + 角色授权
+-- 背景:抽屉 + 旧录入页收敛为单一路由页 /aidop/s7/fqc/result-detail(组件 fqcInspResultDetail)。
+--       任务列表/结果列表/审批中心 N1-N3 均跳此页;页面按 flowState 决定录入/审核/处置/只读。
+-- 幂等:菜单按 Id NOT EXISTS 插入;授权按 (RoleId,MenuId) NOT EXISTS 插入,SysRoleMenu.Id = MAX(Id)+每行固定 rn(确定唯一,避免 @变量在 MySQL8 求值顺序未定义导致重复 PK)。
+-- 授权口径:并集 = 旧 insp-detail(1329017010007) ∪ result-list(1329017010002) 的 14 个既有角色
+--            + S5 IQC / S6 IPQC / S7 FQC 共 9 个检验角色(保证仅绑检验角色的用户也能进详情页)。
+-- 方言:MySQL(主库 aidopdev)。SysMenu / SysRoleMenu 为框架表 PascalCase。
+
+-- ① 隐藏菜单:FQC检验单详情(Id=1329017010008,Pid=成品质量管理目录 1322000000023)
+INSERT INTO SysMenu (Id, Pid, Type, Title, Name, Path, Component, Icon, IsIframe, IsHide, IsKeepAlive, IsAffix, OrderNo, Status, CreateTime)
+SELECT 1329017010008, 1322000000023, 2, 'FQC检验单详情', 'aidopS7FqcResultDetail',
+       '/aidop/s7/fqc/result-detail', '/aidop/s7/fqc/fqcInspResultDetail', 'ele-Document',
+       0, 1, 1, 0, 45, 1, NOW()
+WHERE NOT EXISTS (SELECT 1 FROM SysMenu WHERE Id = 1329017010008);
+
+-- ② 角色授权(并集 14 + 检验角色 9;仅补缺,Id = MAX+固定行号 rn)
+INSERT INTO SysRoleMenu (Id, RoleId, MenuId)
+SELECT base.m + t.rn, t.RoleId, 1329017010008
+FROM (SELECT COALESCE(MAX(Id), 0) AS m FROM SysRoleMenu) base
+CROSS JOIN (
+    SELECT 1300000000101 AS RoleId, 1 AS rn
+    UNION ALL SELECT 1300000000106, 2
+    UNION ALL SELECT 1300000000777, 3
+    UNION ALL SELECT 1300000000888, 4
+    UNION ALL SELECT 789818613628997, 5
+    UNION ALL SELECT 795859517808709, 6
+    UNION ALL SELECT 795859651027013, 7
+    UNION ALL SELECT 795860051161157, 8
+    UNION ALL SELECT 798583831580741, 9
+    UNION ALL SELECT 829309961850949, 10
+    UNION ALL SELECT 837196242882629, 11
+    UNION ALL SELECT 838258976469061, 12
+    UNION ALL SELECT 838258980438085, 13
+    UNION ALL SELECT 838259499909189, 14
+    UNION ALL SELECT 1329915010001, 15
+    UNION ALL SELECT 1329915010002, 16
+    UNION ALL SELECT 1329915010003, 17
+    UNION ALL SELECT 1329915020001, 18
+    UNION ALL SELECT 1329915020002, 19
+    UNION ALL SELECT 1329915020003, 20
+    UNION ALL SELECT 1329916010001, 21
+    UNION ALL SELECT 1329916010002, 22
+    UNION ALL SELECT 1329916010003, 23
+) t
+WHERE NOT EXISTS (SELECT 1 FROM SysRoleMenu x WHERE x.RoleId = t.RoleId AND x.MenuId = 1329017010008);

+ 2 - 1
server/Plugins/Admin.NET.Plugin.AiDOP/FinishedWarehouse/FqcInspectionService.cs

@@ -45,7 +45,7 @@ public class FqcInspectionService : IDynamicApiController, ITransient
                      IFNULL(i.pd,'') AS Pd, i.hgsl AS QualifiedQty, i.bhgsl AS UnqualifiedQty, i.FBILLSTATUS AS BillStatus,
                      i.sczldsl AS OrderQty,
                      q.wlbm AS MaterialCode, q.sl AS ApplyQty, q.yxj AS Priority, q.jfbh AS FireNo,
-                     q.FAPPLYUSER AS Applicant, q.FAPPLYTIME AS ApplyTime, q.jyfzr AS OwnerId, q.FCOMMENT AS Remark
+                     q.FAPPLYUSER AS Applicant, q.FAPPLYTIME AS ApplyTime, q.jykssj AS InspectStartTime, q.jyfzr AS OwnerId, q.FCOMMENT AS Remark
               FROM qms_qcpp_inspbill i
               LEFT JOIN qms_fqcbj q ON q.FBILLNO = i.lydjbh AND q.id = i.hid AND q.tenant_id = i.tenant_id
               WHERE i.id=@id AND i.tenant_id=@t LIMIT 1",
@@ -246,6 +246,7 @@ public class FqcInspHeadRow
     public string? FireNo { get; set; }
     public string? Applicant { get; set; }
     public string? ApplyTime { get; set; }
+    public string? InspectStartTime { get; set; }
     public string? OwnerId { get; set; }
     public string? OwnerName { get; set; }
     public string? Remark { get; set; }

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

@@ -625,7 +625,9 @@ public class SysMenuSeedData : ISqlSugarEntitySeedData<SysMenu>
         // 成品报检(Phase 1C):引用完工工单创建报检
         yield return new SysMenu { Id = fqcBase + 6, Pid = fqcDirId, Title = "成品报检", Path = "/aidop/s7/fqc/apply", Name = "aidopS7FqcApply", Component = "/aidop/s7/fqc/fqcApplyList", Icon = "ele-DocumentAdd", Type = MenuTypeEnum.Menu, CreateTime = ct, OrderNo = 5, Remark = "S7 成品报检(引用完工工单创建)" };
         // 检验单录入(Phase 1E):从报检「录入检验」进入的独立隐藏路由 tab(不在侧栏显示)
-        yield return new SysMenu { Id = fqcBase + 7, Pid = fqcDirId, Title = "检验单录入", Path = "/aidop/s7/fqc/insp-detail", Name = "aidopS7FqcInspDetail", Component = "/aidop/s7/fqc/fqcInspDetail", Icon = "ele-EditPen", Type = MenuTypeEnum.Menu, IsHide = true, CreateTime = ct, OrderNo = 40, Remark = "S7 检验单录入(隐藏路由,列表跳转)" };
+        yield return new SysMenu { Id = fqcBase + 7, Pid = fqcDirId, Title = "检验单录入", Path = "/aidop/s7/fqc/insp-detail", Name = "aidopS7FqcInspDetail", Component = "/aidop/s7/fqc/fqcInspDetail", Icon = "ele-EditPen", Type = MenuTypeEnum.Menu, IsHide = true, CreateTime = ct, OrderNo = 40, Remark = "S7 检验单录入(Phase 1J 起为兼容重定向壳→result-detail)" };
+        // Phase 1J:FQC 检验单详情统一页(录入/审核/处置/只读由 flowState 决定)。隐藏路由,任务列表/结果列表/审批中心 N1-N3 均跳此。
+        yield return new SysMenu { Id = fqcBase + 8, Pid = fqcDirId, Title = "FQC检验单详情", Path = "/aidop/s7/fqc/result-detail", Name = "aidopS7FqcResultDetail", Component = "/aidop/s7/fqc/fqcInspResultDetail", Icon = "ele-Document", Type = MenuTypeEnum.Menu, IsHide = true, CreateTime = ct, OrderNo = 45, Remark = "S7 FQC检验单详情统一页(隐藏路由,列表/审批中心跳转)" };
 
         // 生产入库管理 (1322000000024) 下 1 个三级
         const long productionReceiptDirId = 1322000000024L;