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