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

feat(s6): close process inspection result experience

Phase 1E 展示收口:结果详情 + 状态展示 + 审核历史 + QE处置展示 + Approval Center deep-link + 列表状态。
纯展示/聚合层,不改 1A–1D 业务真源(快照/结果/判定/OverallResult/处置/flow 均不动)。

- 详情审核面板:displayStatus(业务中文) + 检验结果文案(合格/不合格) + QE处置 + 审核历史(复用 ApprovalFlowLog,不造第二套)
- 质量结果与处置分离展示:FAIL+返工 仍显示"不合格",不塌缩成合格
- CLOSED/审核中:录入只读,隐藏保存/完成/提交
- 生产指令列表新增 检验状态/检验结果/审核状态 列 + 状态化按钮文案(去重子查询,无行膨胀,租户约束)
- Approval Center PendingList 加 S6_PROCESS_INSPECTION deep-link(billId=bizId=bill.id,刷新可恢复;不按 WorkOrder 猜单据)
- 后端拥有业务中文语义(displayStatus/节点/动作/处置/质量),前端不猜;deep-link 租户 gate 不放松
- 契约测试 6 例

版本:server 1.0.461 / Web 2.4.360
YY968XX 2 дней назад
Родитель
Сommit
f219817d70

+ 1 - 1
Web/package.json

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

+ 32 - 0
Web/src/views/aidop/s6/api/productionInstruction.ts

@@ -38,6 +38,14 @@ export interface ProductionInstructionRow {
 	status?: string;
 	/** 工单类型 work_order_type */
 	workOrderType?: string;
+	/** Phase 1E 列表状态:检验单 id(null=未生成) */
+	billId?: number | null;
+	/** 检验结果状态 NOT_STARTED/IN_PROGRESS/COMPLETED */
+	inspectionResultStatus?: string | null;
+	/** 整单质量结果 PASS/FAIL/... */
+	overallResult?: string | null;
+	/** 审核流状态原始值 0/1/2/3/4/5(null=未发起) */
+	flowStatusRaw?: number | null;
 }
 
 export interface ProductionInstructionQuery {
@@ -176,6 +184,30 @@ export interface ReviewState {
 	canSupervisorReturn?: boolean;
 	canSubmitDisposition?: boolean;
 	message?: string;
+	// Phase 1E 展示
+	qualityText?: string;
+	displayStatus?: string;
+	disposition?: DispositionView | null;
+	history?: ReviewHistoryItem[];
+}
+
+export interface DispositionView {
+	dispositionType?: string;
+	dispositionTypeText?: string;
+	dispositionQty?: number | null;
+	dispositionOpinion?: string;
+	responsibilityDept?: string;
+	responsibleUser?: string;
+	operatorName?: string;
+	submittedAt?: string;
+}
+
+export interface ReviewHistoryItem {
+	time?: string;
+	operatorName?: string;
+	nodeName?: string;
+	actionText?: string;
+	comment?: string;
 }
 
 export interface QeDispositionInput {

+ 124 - 8
Web/src/views/aidop/s6/production-record/productionInstructionList.vue

@@ -59,9 +59,21 @@
 					<span v-else>—</span>
 				</template>
 			</el-table-column>
+			<el-table-column label="检验状态" min-width="100" align="center" resizable>
+				<template #default="{ row }">{{ piInspText(row) }}</template>
+			</el-table-column>
+			<el-table-column label="检验结果" min-width="90" align="center" resizable>
+				<template #default="{ row }">
+					<el-tag v-if="row.overallResult" :type="piResultTag(row)" size="small" disable-transitions>{{ piResultText(row) }}</el-tag>
+					<span v-else>—</span>
+				</template>
+			</el-table-column>
+			<el-table-column label="审核状态" min-width="100" align="center" resizable>
+				<template #default="{ row }">{{ piFlowText(row) }}</template>
+			</el-table-column>
 			<el-table-column label="操作" width="130" align="center" fixed="right">
 				<template #default="{ row }">
-					<el-button link type="primary" :loading="inspecting === row.workOrderNo" @click="onProcessInspect(row)">过程检验</el-button>
+					<el-button link type="primary" :loading="inspecting === row.workOrderNo" @click="onProcessInspect(row)">{{ piActionText(row) }}</el-button>
 				</template>
 			</el-table-column>
 			<template #empty>
@@ -120,12 +132,12 @@
 						<template #default="{ row }">
 							<template v-if="row.resultType === 'NUMERIC'">
 								<span v-for="i in (row.sampleCount || 1)" :key="i" class="sample-cell">
-									<el-input v-model="entry[key(row.id, i)]" size="small" style="width: 74px" :placeholder="'样' + i" />
+									<el-input v-model="entry[key(row.id, i)]" size="small" style="width: 74px" :placeholder="'样' + i" :disabled="reviewLocked" />
 									<el-tag v-if="sampleJudge(row, i)" size="small" :type="judgeTag(sampleJudge(row, i))" disable-transitions>{{ shortJ(sampleJudge(row, i)) }}</el-tag>
 								</span>
 							</template>
 							<template v-else>
-								<el-select v-model="entry[key(row.id, 1)]" size="small" style="width: 100px" placeholder="OK / NG" clearable>
+								<el-select v-model="entry[key(row.id, 1)]" size="small" style="width: 100px" placeholder="OK / NG" clearable :disabled="reviewLocked">
 									<el-option label="OK" value="OK" />
 									<el-option label="NG" value="NG" />
 								</el-select>
@@ -140,19 +152,36 @@
 					</el-table-column>
 				</el-table>
 
-				<div class="drawer-foot">
+				<div class="drawer-foot" v-if="!reviewLocked">
 					<span class="foot-hint">判定以后端为准</span>
 					<el-button :loading="saving" @click="onSave">保存</el-button>
 					<el-button type="primary" :loading="completing" @click="onComplete">完成检验</el-button>
 				</div>
+				<div class="drawer-foot" v-else>
+					<span class="foot-hint">已提交审核,检验结果只读</span>
+				</div>
 
 				<div class="review-panel" v-if="detail">
 					<div class="snap-title">审核流程 · 复用 ApprovalFlow(质量结果只读,审批不改写)</div>
 					<div class="review-row">
-						审核状态:<b>{{ review?.flowStatus || '—' }}</b>
-						<span v-if="review?.currentNodeName"> 当前节点:{{ review.currentNodeName }}</span>
-						<span v-if="review?.message" class="review-msg"> {{ review.message }}</span>
-						<el-tag v-if="review?.hasDisposition" type="warning" size="small" style="margin-left: 8px">已处置</el-tag>
+						检验结果:<el-tag :type="judgeTag(detail?.overallResult)" size="small" disable-transitions>{{ review?.qualityText || detail?.overallResult || '—' }}</el-tag>
+						<span class="rv-sep">审核状态:</span><b>{{ review?.displayStatus || '—' }}</b>
+						<span v-if="review?.currentNodeName" class="rv-sep">当前节点:{{ review.currentNodeName }}</span>
+					</div>
+					<div v-if="review?.disposition" class="review-disp">
+						质量处置:<el-tag type="warning" size="small" disable-transitions>{{ review.disposition.dispositionTypeText || review.disposition.dispositionType }}</el-tag>
+						<span v-if="review.disposition.dispositionOpinion" class="rv-sep">意见:{{ review.disposition.dispositionOpinion }}</span>
+						<span v-if="review.disposition.operatorName" class="rv-sep">处置人:{{ review.disposition.operatorName }}</span>
+						<span v-if="review.disposition.submittedAt" class="rv-sep">{{ fmtDate(review.disposition.submittedAt) }}</span>
+					</div>
+					<div v-if="review?.history && review.history.length" class="review-history">
+						<div class="hist-title">审核历史</div>
+						<el-timeline>
+							<el-timeline-item v-for="(h, i) in review.history" :key="i" :timestamp="fmtDateTime(h.time)" placement="top" size="small">
+								<span class="hist-node">{{ h.nodeName }}</span> · {{ h.actionText }} <span class="hist-op">{{ h.operatorName }}</span>
+								<span v-if="h.comment" class="rv-sep">意见:{{ h.comment }}</span>
+							</el-timeline-item>
+						</el-timeline>
 					</div>
 					<div class="review-actions">
 						<el-button v-if="review?.canSubmitReview" type="primary" :loading="reviewing" @click="onSubmitReview">提交审核</el-button>
@@ -178,6 +207,7 @@
 
 <script setup lang="ts">
 import { reactive, ref, computed } from 'vue';
+import { useRoute } from 'vue-router';
 import { ElMessage, ElMessageBox } from 'element-plus';
 import AidopDemoShell from '/@/views/aidop/components/AidopDemoShell.vue';
 import {
@@ -219,6 +249,17 @@ const reviewing = ref(false);
 const dispType = ref('');
 const dispOpinion = ref('');
 
+// 已提交审核后检验结果只读(§14 CLOSED / §15 IN_REVIEW):流程一旦发起即锁录入。
+const reviewLocked = computed(() => {
+	const fs = review.value?.flowStatus;
+	return !!fs && fs !== 'NotStarted';
+});
+
+function fmtDateTime(v?: string) {
+	if (!v) return '—';
+	return String(v).replace('T', ' ').slice(0, 19);
+}
+
 async function loadReview(billId: number) {
 	try {
 		review.value = await fetchReviewState(billId);
@@ -316,6 +357,38 @@ function fmtNum(v?: number) {
 	return Number(v).toLocaleString('en-US', { maximumFractionDigits: 2 });
 }
 
+// ── Phase 1E 列表状态映射(业务中文;确定性映射,非语义猜测)──
+function piInspText(row: ProductionInstructionRow) {
+	if (!row.billId) return '未生成';
+	return row.inspectionResultStatus === 'COMPLETED' ? '已完成' : row.inspectionResultStatus === 'IN_PROGRESS' ? '检验中' : '待录入';
+}
+function piResultText(row: ProductionInstructionRow) {
+	return row.overallResult === 'PASS' ? '合格' : row.overallResult === 'FAIL' ? '不合格' : '—';
+}
+function piResultTag(row: ProductionInstructionRow): 'success' | 'danger' | 'info' {
+	return row.overallResult === 'PASS' ? 'success' : row.overallResult === 'FAIL' ? 'danger' : 'info';
+}
+function piFlowText(row: ProductionInstructionRow) {
+	if (!row.billId) return '—';
+	const f = row.flowStatusRaw;
+	if (f === 1) return '审核中';
+	if (f === 2) return '已关闭';
+	if (f === 3) return '已驳回';
+	if (f === 4) return '已撤回';
+	if (f === 5) return '已终止';
+	return row.inspectionResultStatus === 'COMPLETED' ? '待提交审核' : '—';
+}
+function piActionText(row: ProductionInstructionRow) {
+	if (!row.billId) return '过程检验';
+	const f = row.flowStatusRaw;
+	if (f === 2) return '查看结果';
+	if (f === 1) return '查看审核';
+	if (f === 3 || f === 4) return '重新提交';
+	if (row.inspectionResultStatus === 'COMPLETED') return '查看/提交';
+	if (row.inspectionResultStatus === 'IN_PROGRESS') return '继续检验';
+	return '过程检验';
+}
+
 async function loadList() {
 	loading.value = true;
 	try {
@@ -508,7 +581,11 @@ async function onComplete() {
 	}
 }
 
+const route = useRoute();
 loadList();
+// Deep-link(Approval Center / URL 刷新):?billId=X 直接打开对应单据详情,不依赖列表内存 state。
+const deepBillId = Number(route.query.billId);
+if (deepBillId > 0) openDetail(deepBillId);
 </script>
 
 <style scoped>
@@ -548,4 +625,43 @@ loadList();
 	font-size: 12px;
 	margin-right: auto;
 }
+.review-panel {
+	margin-top: 16px;
+	padding-top: 12px;
+	border-top: 1px solid #ebeef5;
+}
+.review-row,
+.review-disp {
+	display: flex;
+	align-items: center;
+	flex-wrap: wrap;
+	gap: 4px;
+	margin: 6px 0;
+	font-size: 13px;
+}
+.rv-sep {
+	margin-left: 12px;
+	color: #606266;
+}
+.review-actions {
+	display: flex;
+	align-items: center;
+	flex-wrap: wrap;
+	gap: 8px;
+	margin-top: 10px;
+}
+.review-history {
+	margin-top: 12px;
+}
+.hist-title {
+	font-weight: 600;
+	font-size: 13px;
+	margin-bottom: 8px;
+}
+.hist-node {
+	font-weight: 600;
+}
+.hist-op {
+	color: #409eff;
+}
 </style>

+ 13 - 0
Web/src/views/approvalFlow/center/components/PendingList.vue

@@ -39,6 +39,13 @@
 						<el-text v-else type="info" size="small">请进入 FQC 业务模块处理</el-text>
 						<el-button size="small" text @click="emit('timeline', row)">详情</el-button>
 					</template>
+					<template v-else-if="row.bizType === 'S6_PROCESS_INSPECTION'">
+						<!-- S6 过程检验(Phase1C) 任务:deep-link 到专用业务详情(billId=bizId),不走通用同意/拒绝旁路 -->
+						<el-button v-if="row.nodeId === 'N2_SUP_REVIEW'" size="small" type="primary" text @click="goToS6(row.bizId)">去审核</el-button>
+						<el-button v-else-if="row.nodeId === 'N3_QE_DISPOSITION'" size="small" type="primary" text @click="goToS6(row.bizId)">去处置</el-button>
+						<el-button v-else size="small" type="primary" text @click="goToS6(row.bizId)">查看</el-button>
+						<el-button size="small" text @click="emit('timeline', row)">详情</el-button>
+					</template>
 					<template v-else>
 						<el-button v-if="row.bizType === 'ORDER_CHANGE_REVIEW'" size="small" type="primary" text @click="emit('viewBiz', row)">查看变更单</el-button>
 						<el-button v-if="row.bizType === 'MATERIAL_SHORTAGE'" size="small" type="primary" text @click="emit('viewBiz', row)">查看欠料信息</el-button>
@@ -94,6 +101,12 @@ async function goToException(bizId: number | string) {
 // FQC(S7_FQC_INSPBILL) 待办按节点跳专用业务入口,不调 ApprovalFlow 通用 Approve/Reject:
 // N1 去录入 → 检验单录入页(受 jyfzr 负责人门保护);N2/N3 去审核/去处置 → 结果列表自动打开详情抽屉,
 // 由 flowState 决定显示 主管通过/退回(supervisor-*) 或 QE处置(qe-submit-disposition)。业务动作全走 FQC 专用 API。
+// S6 过程检验(Phase1C) deep-link:billId=bizId=ado_s6_process_inspection_bill.id,刷新可恢复;不按 WorkOrder/qms_gcjyd 猜单据。
+function goToS6(bizId: number | string) {
+	if (!bizId) return;
+	router.push(`/aidop/s6/production-record/order-list?billId=${bizId}`);
+}
+
 function goToFqc(action: 'inspect' | 'review' | 'disposition', bizId: number | string) {
 	// N1/N2/N3 统一进同一详情页 result-detail;页面按 flowState 决定录入/审核/处置/只读(action 仅用于按钮文案)
 	void action;

+ 3 - 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.460</AssemblyVersion>
-    <FileVersion>1.0.460</FileVersion>
-    <Version>1.0.460</Version>
+    <AssemblyVersion>1.0.461</AssemblyVersion>
+    <FileVersion>1.0.461</FileVersion>
+    <Version>1.0.461</Version>
   </PropertyGroup>
 
   <ItemGroup>

+ 94 - 0
server/Plugins/Admin.NET.Plugin.AiDOP.Tests/S6/Manufacturing/S6ProcessInspectionPresentationTests.cs

@@ -0,0 +1,94 @@
+using Xunit;
+
+namespace Admin.NET.Plugin.AiDOP.Tests.S6.Manufacturing;
+
+/// <summary>
+/// S6 Phase 1E 展示收口契约:displayStatus/质量文案/history/disposition 展示、列表状态列、Approval Center deep-link。
+/// 纯展示层,不改业务真源;后端拥有业务中文语义,前端不猜。
+/// </summary>
+public class S6ProcessInspectionPresentationTests
+{
+    private static string Svc() => File.ReadAllText(FindFile("server", "Plugins", "Admin.NET.Plugin.AiDOP", "Manufacturing", "S6ProcessInspectionReviewService.cs"));
+    private static string Dto() => File.ReadAllText(FindFile("server", "Plugins", "Admin.NET.Plugin.AiDOP", "Manufacturing", "Dto", "S6ProcessInspectionReviewDto.cs"));
+    private static string ListSvc() => File.ReadAllText(FindFile("server", "Plugins", "Admin.NET.Plugin.AiDOP", "Manufacturing", "S6ProductionInstructionService.cs"));
+    private static string PendingList() => File.ReadAllText(FindFile("Web", "src", "views", "approvalFlow", "center", "components", "PendingList.vue"));
+
+    [Fact]
+    public void ReviewState_Has_DisplayFields()
+    {
+        var d = Dto();
+        Assert.Contains("DisplayStatus", d);
+        Assert.Contains("QualityText", d);
+        Assert.Contains("S6DispositionView", d);
+        Assert.Contains("List<S6ReviewHistoryItem> History", d);
+    }
+
+    [Fact]
+    public void Service_Maps_BusinessChinese_NotRawCodes()
+    {
+        var s = Svc();
+        // 节点/动作/质量/处置 → 业务中文映射
+        Assert.Contains("\"提交审核\"", s);
+        Assert.Contains("\"检验主管审核\"", s);
+        Assert.Contains("\"质量处置\"", s);
+        Assert.Contains("\"合格\"", s);
+        Assert.Contains("\"不合格\"", s);
+        Assert.Contains("\"已关闭\"", s);
+    }
+
+    [Fact]
+    public void History_ReusesApprovalFlowLog_NoSecondHistory()
+    {
+        var s = Svc();
+        Assert.Contains("FROM ApprovalFlowLog", s);
+        Assert.Contains("LoadHistoryAsync", s);
+    }
+
+    [Fact]
+    public void Disposition_DisplayOnly_DoesNotOverwriteQuality()
+    {
+        var s = Svc();
+        Assert.Contains("LoadDispositionAsync", s);
+        Assert.DoesNotContain("SET overall_result", s);
+    }
+
+    [Fact]
+    public void List_Has_StatusColumns_NoRowMultiplication()
+    {
+        var l = ListSvc();
+        Assert.Contains("BillId", l);
+        Assert.Contains("OverallResult", l);
+        Assert.Contains("FlowStatusRaw", l);
+        // 去重子查询避免一工单多单据造成行膨胀
+        Assert.Contains("MAX(id) AS bill_id", l);
+        Assert.Contains("GROUP BY work_order_no", l);
+        // 租户约束
+        Assert.Contains("WHERE tenant_id=@TenantId GROUP BY work_order_no", l);
+    }
+
+    [Fact]
+    public void ApprovalCenter_DeepLink_S6_ByBillId_NotWorkOrder()
+    {
+        var p = PendingList();
+        Assert.Contains("S6_PROCESS_INSPECTION", p);
+        Assert.Contains("goToS6", p);
+        // S6 deep-link 用 billId=bizId(=bill.id),不按 WorkOrder 猜单据
+        Assert.Contains("order-list?billId=", p);
+        // goToS6 内不引 qms_gcjyd(限定在 S6 函数体内检查,不误伤其它 bizType 的历史 case)
+        var s6fn = p.Substring(p.IndexOf("function goToS6"));
+        s6fn = s6fn.Substring(0, s6fn.IndexOf('}') + 1);
+        Assert.DoesNotContain("qms_gcjyd", s6fn);
+    }
+
+    private static string FindFile(params string[] parts)
+    {
+        var dir = new DirectoryInfo(AppContext.BaseDirectory);
+        while (dir != null)
+        {
+            var candidate = Path.Combine(new[] { dir.FullName }.Concat(parts).ToArray());
+            if (File.Exists(candidate)) return candidate;
+            dir = dir.Parent;
+        }
+        throw new FileNotFoundException(string.Join("/", parts));
+    }
+}

+ 33 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Manufacturing/Dto/S6ProcessInspectionReviewDto.cs

@@ -24,6 +24,39 @@ public class S6ReviewStateOutput
     public bool CanSupervisorReturn { get; set; }
     public bool CanSubmitDisposition { get; set; }
     public string? Message { get; set; }
+
+    // ── Phase 1E 展示收口(后端拥有业务中文,前端不猜语义)──
+    /// <summary>检验结果业务文案:合格 / 不合格 / 检验中 / —。</summary>
+    public string? QualityText { get; set; }
+    /// <summary>统一展示状态(业务中文):检验中 / 待提交审核 / 主管审核中 / QE质量处置中 / 已关闭 / …。</summary>
+    public string? DisplayStatus { get; set; }
+    /// <summary>QE 质量处置(存在则展示;不改写质量结果)。</summary>
+    public S6DispositionView? Disposition { get; set; }
+    /// <summary>审核历史(正式 ApprovalFlow 时间线,按时间升序)。</summary>
+    public List<S6ReviewHistoryItem> History { get; set; } = new();
+}
+
+/// <summary>QE 质量处置展示(只读)。</summary>
+public class S6DispositionView
+{
+    public string? DispositionType { get; set; }
+    public string? DispositionTypeText { get; set; }
+    public decimal? DispositionQty { get; set; }
+    public string? DispositionOpinion { get; set; }
+    public string? ResponsibilityDept { get; set; }
+    public string? ResponsibleUser { get; set; }
+    public string? OperatorName { get; set; }
+    public DateTime? SubmittedAt { get; set; }
+}
+
+/// <summary>审核历史条目(源自 ApprovalFlowLog;action/node 已转业务中文)。</summary>
+public class S6ReviewHistoryItem
+{
+    public DateTime? Time { get; set; }
+    public string? OperatorName { get; set; }
+    public string? NodeName { get; set; }
+    public string? ActionText { get; set; }
+    public string? Comment { get; set; }
 }
 
 /// <summary>提交审核入参:仅单据 id;租户/发起人后端自解析。</summary>

+ 10 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Manufacturing/Dto/S6ProductionInstructionDto.cs

@@ -82,4 +82,14 @@ public class S6ProductionInstructionRow
 
     /// <summary>工单类型(work_order_type ← WorkOrdMaster.WoTyped)</summary>
     public string? WorkOrderType { get; set; }
+
+    // ── Phase 1E 列表状态(LEFT JOIN process_inspection_bill/result/flow;无单据则为空)──
+    /// <summary>过程检验单 id(null=尚未生成检验单)。</summary>
+    public long? BillId { get; set; }
+    /// <summary>检验结果状态(NOT_STARTED/IN_PROGRESS/COMPLETED)。</summary>
+    public string? InspectionResultStatus { get; set; }
+    /// <summary>整单质量结果(PASS/FAIL/PENDING/INVALID)。</summary>
+    public string? OverallResult { get; set; }
+    /// <summary>审核流实例状态原始值(0草稿/1运行/2通过/3驳回/4撤回/5终止;null=未发起)。</summary>
+    public int? FlowStatusRaw { get; set; }
 }

+ 103 - 8
server/Plugins/Admin.NET.Plugin.AiDOP/Manufacturing/S6ProcessInspectionReviewService.cs

@@ -244,38 +244,50 @@ public class S6ProcessInspectionReviewService : IDynamicApiController, ITransien
             WHERE b.id=@id AND b.tenant_id=@tid LIMIT 1
             """,
             new List<SugarParameter> { new("@id", billId), new("@tid", tid) });
+        var completed = string.Equals(bill?.ResultStatus, S6ProcessInspectionFlowConst.StatusCompleted, StringComparison.OrdinalIgnoreCase);
         output.OverallResult = bill?.OverallResult;
         output.ResultStatus = bill?.ResultStatus;
+        output.QualityText = QualityTextOf(bill?.OverallResult);
         output.DispositionRequired = string.Equals(bill?.OverallResult, S6ProcessInspectionFlowConst.ResultFail, StringComparison.OrdinalIgnoreCase) ? 1 : 0;
-        output.HasDisposition = await _db.Queryable<AdoS6IpqcQualityDisposition>().ClearFilter()
-            .AnyAsync(x => x.InspectionId == billId && x.TenantId == tid && x.Status == 1);
+        output.Disposition = await LoadDispositionAsync(billId, tid);
+        output.HasDisposition = output.Disposition != null;
 
         var inst = await GetLatestInstanceAsync(billId);
         if (inst == null)
         {
             output.FlowStatus = "NotStarted";
-            output.CanSubmitReview = string.Equals(bill?.ResultStatus, S6ProcessInspectionFlowConst.StatusCompleted, StringComparison.OrdinalIgnoreCase);
-            output.Message = output.CanSubmitReview ? "检验完成,待提交审核" : "检验未完成,暂不可提交审核";
+            output.CanSubmitReview = completed;
+            output.DisplayStatus = completed ? "待提交审核" : "检验中";
+            output.Message = completed ? "检验完成,待提交审核" : "检验未完成,暂不可提交审核";
             return output;
         }
 
         output.InstanceId = inst.Id;
         output.FlowStatus = inst.Status.ToString();
+        output.History = await LoadHistoryAsync(inst.Id, tid);
+
         if (inst.Status != FlowInstanceStatusEnum.Running)
         {
             output.IsCompleted = true;
+            output.DisplayStatus = inst.Status == FlowInstanceStatusEnum.Approved ? "已关闭"
+                : inst.Status == FlowInstanceStatusEnum.Rejected ? "已驳回"
+                : inst.Status == FlowInstanceStatusEnum.Cancelled ? "已撤回" : "已结束";
             output.Message = inst.Status == FlowInstanceStatusEnum.Approved ? "审核已闭环(CLOSED)" : $"流程已结束({inst.Status})";
-            // 已结束且非通过(驳回/撤回)→ 允许重新提交
-            output.CanSubmitReview = inst.Status is FlowInstanceStatusEnum.Rejected or FlowInstanceStatusEnum.Cancelled
-                && string.Equals(bill?.ResultStatus, S6ProcessInspectionFlowConst.StatusCompleted, StringComparison.OrdinalIgnoreCase);
+            output.CanSubmitReview = inst.Status is FlowInstanceStatusEnum.Rejected or FlowInstanceStatusEnum.Cancelled && completed;
             return output;
         }
 
         output.CurrentNodeCode = inst.CurrentNodeId;
+        output.DisplayStatus = inst.CurrentNodeId switch
+        {
+            S6ProcessInspectionFlowConst.NodeSupervisor => "主管审核中",
+            S6ProcessInspectionFlowConst.NodeDisposition => "QE质量处置中",
+            _ => "审核中",
+        };
         var myTask = await GetMyPendingTaskAsync(inst.Id);
         if (myTask != null)
         {
-            output.CurrentNodeName = myTask.NodeName;
+            output.CurrentNodeName = NodeNameOf(myTask.NodeId) ?? myTask.NodeName;
             if (myTask.NodeId == S6ProcessInspectionFlowConst.NodeSupervisor)
             {
                 output.CurrentAssigneeRole = S6ProcessInspectionFlowConst.RoleSupervisorCode;
@@ -296,6 +308,7 @@ public class S6ProcessInspectionReviewService : IDynamicApiController, ITransien
         }
         else
         {
+            output.CurrentNodeName = NodeNameOf(inst.CurrentNodeId);
             output.CurrentAssigneeRole = inst.CurrentNodeId switch
             {
                 S6ProcessInspectionFlowConst.NodeDisposition => S6ProcessInspectionFlowConst.RoleQualityEngineerCode,
@@ -306,4 +319,86 @@ public class S6ProcessInspectionReviewService : IDynamicApiController, ITransien
         }
         return output;
     }
+
+    // ── Phase 1E 展示辅助(业务中文映射;后端拥有语义)──
+    private static string QualityTextOf(string? overallResult) => (overallResult ?? "").ToUpperInvariant() switch
+    {
+        "PASS" => "合格",
+        "FAIL" => "不合格",
+        "PENDING" or "IN_PROGRESS" => "检验中",
+        "INVALID" => "无效",
+        _ => "—",
+    };
+
+    private static string? NodeNameOf(string? nodeId) => nodeId switch
+    {
+        S6ProcessInspectionFlowConst.NodeSubmit => "提交审核",
+        S6ProcessInspectionFlowConst.NodeSupervisor => "检验主管审核",
+        S6ProcessInspectionFlowConst.NodeDisposition => "质量处置",
+        _ => null,
+    };
+
+    private static string DispositionTypeTextOf(string? t) => (t ?? "").ToUpperInvariant() switch
+    {
+        "REWORK" => "返工",
+        "CONCESSION" => "让步接收",
+        "SCRAP" => "报废",
+        "OTHER" => "其它",
+        _ => t ?? "",
+    };
+
+    private static string ActionTextOf(int action) => action switch
+    {
+        1 => "提交",
+        2 => "通过",
+        3 => "驳回",
+        5 => "撤回",
+        6 => "退回",
+        9 => "取消",
+        _ => "操作",
+    };
+
+    private async Task<S6DispositionView?> LoadDispositionAsync(long billId, long tid)
+    {
+        var d = await _db.Queryable<AdoS6IpqcQualityDisposition>().ClearFilter()
+            .Where(x => x.InspectionId == billId && x.TenantId == tid && x.Status == 1)
+            .OrderByDescending(x => x.Id).FirstAsync();
+        if (d == null) return null;
+        return new S6DispositionView
+        {
+            DispositionType = d.DispositionType,
+            DispositionTypeText = DispositionTypeTextOf(d.DispositionType),
+            DispositionQty = d.DispositionQty,
+            DispositionOpinion = d.DispositionOpinion,
+            ResponsibilityDept = d.ResponsibilityDept,
+            ResponsibleUser = d.ResponsibleUser,
+            OperatorName = d.SubmittedByName,
+            SubmittedAt = d.SubmittedAt,
+        };
+    }
+
+    private sealed class FlowLogRow
+    {
+        public int Action { get; set; }
+        public string? NodeId { get; set; }
+        public string? OperatorName { get; set; }
+        public string? Comment { get; set; }
+        public DateTime? CreateTime { get; set; }
+    }
+
+    private async Task<List<S6ReviewHistoryItem>> LoadHistoryAsync(long instanceId, long tid)
+    {
+        // 复用正式 ApprovalFlowLog(不造第二套 history)。租户隔离由实例归属保证(instanceId 已按 BizType+BizId 定位)。
+        var rows = await _db.Ado.SqlQueryAsync<FlowLogRow>(
+            "SELECT Action AS Action, NodeId AS NodeId, OperatorName AS OperatorName, Comment AS Comment, CreateTime AS CreateTime FROM ApprovalFlowLog WHERE InstanceId=@iid ORDER BY Id",
+            new List<SugarParameter> { new("@iid", instanceId) });
+        return rows.Select(r => new S6ReviewHistoryItem
+        {
+            Time = r.CreateTime,
+            OperatorName = r.OperatorName,
+            NodeName = NodeNameOf(r.NodeId) ?? r.NodeId,
+            ActionText = ActionTextOf(r.Action),
+            Comment = r.Comment,
+        }).ToList();
+    }
 }

+ 7 - 1
server/Plugins/Admin.NET.Plugin.AiDOP/Manufacturing/S6ProductionInstructionService.cs

@@ -114,8 +114,14 @@ public class S6ProductionInstructionService : IDynamicApiController, ITransient
             : await _db.Ado.SqlQueryAsync<S6ProductionInstructionRow>(
                 $"""
                 SELECT
-                {SelectColumns}
+                {SelectColumns},
+                pb.bill_id       AS BillId,
+                r.status         AS InspectionResultStatus,
+                r.overall_result AS OverallResult,
+                (SELECT i.Status FROM ApprovalFlowInstance i WHERE i.BizType='S6_PROCESS_INSPECTION' AND i.BizId=pb.bill_id ORDER BY i.Id DESC LIMIT 1) AS FlowStatusRaw
                 FROM mdp_std_work_order_schedule m
+                LEFT JOIN (SELECT work_order_no, MAX(id) AS bill_id FROM ado_s6_process_inspection_bill WHERE tenant_id=@TenantId GROUP BY work_order_no) pb ON pb.work_order_no = m.work_order
+                LEFT JOIN ado_s6_process_inspection_result r ON r.bill_id = pb.bill_id AND r.tenant_id = @TenantId
                 WHERE {whereSql}
                 ORDER BY m.order_date DESC, m.work_order DESC
                 LIMIT {pageSize} OFFSET {offset}