Ver Fonte

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 há 20 horas atrás
pai
commit
3067b3e406

+ 1 - 1
Web/package.json

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

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

@@ -25,6 +25,7 @@ export interface FqcInspHead {
 	fireNo?: string;
 	fireNo?: string;
 	applicant?: string;
 	applicant?: string;
 	applyTime?: string;
 	applyTime?: string;
+	inspectStartTime?: string;
 	ownerId?: string;
 	ownerId?: string;
 	ownerName?: string;
 	ownerName?: string;
 	remark?: 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>
 <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>
 </template>
 
 
 <script setup lang="ts" name="aidopS7FqcInspDetail">
 <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 { 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 route = useRoute();
 const router = useRouter();
 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>
 </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 prop="unqualifiedQty" label="不合格数量" min-width="110" resizable />
 			<el-table-column label="操作" min-width="90" fixed="right">
 			<el-table-column label="操作" min-width="90" fixed="right">
 				<template #default="{ row }">
 				<template #default="{ row }">
-					<el-button link type="primary" @click="openDetail(row)">详情</el-button>
+					<el-button link type="primary" @click="goDetail(row)">详情</el-button>
 				</template>
 				</template>
 			</el-table-column>
 			</el-table-column>
 			<template #empty>
 			<template #empty>
@@ -79,40 +79,18 @@
 				@size-change="loadList"
 				@size-change="loadList"
 			/>
 			/>
 		</div>
 		</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>
 	</AidopDemoShell>
 </template>
 </template>
 
 
 <script setup lang="ts" name="aidopS7FqcResultList">
 <script setup lang="ts" name="aidopS7FqcResultList">
 import { computed, onActivated, onMounted, reactive, ref } from 'vue';
 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 { ElMessage } from 'element-plus';
 import AidopDemoShell from '/@/views/aidop/components/AidopDemoShell.vue';
 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 route = useRoute();
+const router = useRouter();
 const pageTitle = computed(() => (route.meta?.title as string) || 'FQC检验单列表');
 const pageTitle = computed(() => (route.meta?.title as string) || 'FQC检验单列表');
 
 
 const query = reactive({
 const query = reactive({
@@ -133,93 +111,10 @@ const loading = ref(false);
 const rows = ref<FqcResultRow[]>([]);
 const rows = ref<FqcResultRow[]>([]);
 const total = ref(0);
 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;
 	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) {
 function fmtDateTime(v?: string | null) {
@@ -273,12 +168,7 @@ function resetQuery() {
 	loadList();
 	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());
 onActivated(() => loadList());
 </script>
 </script>
 
 

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

@@ -36,7 +36,7 @@
 					<el-button :disabled="!selectedEntryIds.length" @click="openPriority">优先级调整</el-button>
 					<el-button :disabled="!selectedEntryIds.length" @click="openPriority">优先级调整</el-button>
 				</div>
 				</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 type="selection" width="48" />
 					<el-table-column prop="applyBillNo" label="报检单号" min-width="160" show-overflow-tooltip resizable />
 					<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 />
 					<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="qualifiedQty" label="合格数量" min-width="100" align="right" resizable />
 					<el-table-column prop="unqualifiedQty" label="不合格数量" min-width="110" align="right" resizable />
 					<el-table-column prop="unqualifiedQty" label="不合格数量" min-width="110" align="right" resizable />
 					<el-table-column label="操作" width="90" fixed="right">
 					<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>
 					</el-table-column>
 					<template #empty><el-empty description="暂无检验单" /></template>
 					<template #empty><el-empty description="暂无检验单" /></template>
 				</el-table>
 				</el-table>
@@ -118,18 +118,6 @@
 			</el-tab-pane>
 			</el-tab-pane>
 		</el-tabs>
 		</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" />
 		<FqcInspectorPickerDialog v-model="assignVisible" @confirm="onAssignConfirm" />
 
 
 		<el-dialog v-model="priorityVisible" title="优先级调整" width="420px">
 		<el-dialog v-model="priorityVisible" title="优先级调整" width="420px">
@@ -155,9 +143,8 @@ import { useRoute, useRouter } from 'vue-router';
 import { ElMessage } from 'element-plus';
 import { ElMessage } from 'element-plus';
 import AidopDemoShell from '/@/views/aidop/components/AidopDemoShell.vue';
 import AidopDemoShell from '/@/views/aidop/components/AidopDemoShell.vue';
 import { useUserInfo } from '/@/stores/userInfo';
 import { useUserInfo } from '/@/stores/userInfo';
-import FqcInspBillDetailDrawer from './components/FqcInspBillDetailDrawer.vue';
 import FqcInspectorPickerDialog from './components/FqcInspectorPickerDialog.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 {
 import {
 	fetchFqcTaskEntryList,
 	fetchFqcTaskEntryList,
 	claimFqcTaskEntries,
 	claimFqcTaskEntries,
@@ -166,17 +153,6 @@ import {
 	generateFqcInspBillByTask,
 	generateFqcInspBillByTask,
 	type FqcTaskEntryRow,
 	type FqcTaskEntryRow,
 } from '../api/fqcTaskEntry';
 } from '../api/fqcTaskEntry';
-import {
-	fetchFqcFlowState,
-	submitFqcResult,
-	fqcSupervisorApprove,
-	fqcSupervisorReject,
-	submitFqcQeDisposition,
-	type FqcFlowState,
-	type FqcSubmitResultPayload,
-	type FqcQeDispositionPayload,
-} from '../api/fqcInspBillFlow';
-
 const route = useRoute();
 const route = useRoute();
 const router = useRouter();
 const router = useRouter();
 const pageTitle = computed(() => (route.meta?.title as string) || 'FQC检验任务列表');
 const pageTitle = computed(() => (route.meta?.title as string) || 'FQC检验任务列表');
@@ -295,7 +271,7 @@ async function onGenerate(row: FqcTaskEntryRow) {
 		const res = await generateFqcInspBillByTask(row.id);
 		const res = await generateFqcInspBillByTask(row.id);
 		ElMessage.success(`已生成检验单 ${res.inspBillNo}`);
 		ElMessage.success(`已生成检验单 ${res.inspBillNo}`);
 		await loadEntryList();
 		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) {
 	} catch (e: any) {
 		ElMessage.error(e?.message || '生成检验单失败');
 		ElMessage.error(e?.message || '生成检验单失败');
 	} finally {
 	} finally {
@@ -310,7 +286,7 @@ function openInspection(row: FqcTaskEntryRow) {
 		ElMessage.error('检验负责人与当前账号不一致,无法进入录入检验');
 		ElMessage.error('检验负责人与当前账号不一致,无法进入录入检验');
 		return;
 		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();
 	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;
 	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) {
 function onTabChange(name: string | number) {
@@ -460,4 +355,36 @@ onActivated(() => {
 	display: flex;
 	display: flex;
 	justify-content: flex-end;
 	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>
 </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 去审核/去处置 → 结果列表自动打开详情抽屉,
 // N1 去录入 → 检验单录入页(受 jyfzr 负责人门保护);N2/N3 去审核/去处置 → 结果列表自动打开详情抽屉,
 // 由 flowState 决定显示 主管通过/退回(supervisor-*) 或 QE处置(qe-submit-disposition)。业务动作全走 FQC 专用 API。
 // 由 flowState 决定显示 主管通过/退回(supervisor-*) 或 QE处置(qe-submit-disposition)。业务动作全走 FQC 专用 API。
 function goToFqc(action: 'inspect' | 'review' | 'disposition', bizId: number | string) {
 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) {
 function goToImprovement(bizId: number | string) {

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

@@ -11,9 +11,9 @@
     <GenerateSatelliteAssembliesForCore>true</GenerateSatelliteAssembliesForCore>
     <GenerateSatelliteAssembliesForCore>true</GenerateSatelliteAssembliesForCore>
     <Copyright>Admin.NET</Copyright>
     <Copyright>Admin.NET</Copyright>
     <Description>Admin.NET 通用权限开发平台</Description>
     <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>
   </PropertyGroup>
 
 
   <ItemGroup>
   <ItemGroup>
@@ -481,6 +481,9 @@
     <None Update="UpdateScripts\1.0.380.verify.sql">
     <None Update="UpdateScripts\1.0.380.verify.sql">
       <CopyToOutputDirectory>Always</CopyToOutputDirectory>
       <CopyToOutputDirectory>Always</CopyToOutputDirectory>
     </None>
     </None>
+    <None Update="UpdateScripts\1.0.382.sql">
+      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+    </None>
     <None Update="UpdateScripts\1.0.364.verify.sql">
     <None Update="UpdateScripts\1.0.364.verify.sql">
       <CopyToOutputDirectory>Always</CopyToOutputDirectory>
       <CopyToOutputDirectory>Always</CopyToOutputDirectory>
     </None>
     </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,
                      IFNULL(i.pd,'') AS Pd, i.hgsl AS QualifiedQty, i.bhgsl AS UnqualifiedQty, i.FBILLSTATUS AS BillStatus,
                      i.sczldsl AS OrderQty,
                      i.sczldsl AS OrderQty,
                      q.wlbm AS MaterialCode, q.sl AS ApplyQty, q.yxj AS Priority, q.jfbh AS FireNo,
                      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
               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
               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",
               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? FireNo { get; set; }
     public string? Applicant { get; set; }
     public string? Applicant { get; set; }
     public string? ApplyTime { get; set; }
     public string? ApplyTime { get; set; }
+    public string? InspectStartTime { get; set; }
     public string? OwnerId { get; set; }
     public string? OwnerId { get; set; }
     public string? OwnerName { get; set; }
     public string? OwnerName { get; set; }
     public string? Remark { 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):引用完工工单创建报检
         // 成品报检(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 成品报检(引用完工工单创建)" };
         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(不在侧栏显示)
         // 检验单录入(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 个三级
         // 生产入库管理 (1322000000024) 下 1 个三级
         const long productionReceiptDirId = 1322000000024L;
         const long productionReceiptDirId = 1322000000024L;