|
|
@@ -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>
|