|
@@ -0,0 +1,500 @@
|
|
|
|
|
+<template>
|
|
|
|
|
+ <div class="item-panel">
|
|
|
|
|
+ <div class="panel-head">
|
|
|
|
|
+ <span class="panel-title">检验项目明细</span>
|
|
|
|
|
+ <el-tag v-if="canEdit" type="success" size="small" effect="plain">可录入</el-tag>
|
|
|
|
|
+ <el-tag v-else type="info" size="small" effect="plain">只读(当前节点不允许修改检验明细)</el-tag>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- LEGACY MODE:历史单 / 未命中检规 / 命中检规但无明细,三者同一语义 -->
|
|
|
|
|
+ <el-alert
|
|
|
|
|
+ v-if="itemCount === 0"
|
|
|
|
|
+ class="mt12"
|
|
|
|
|
+ type="info"
|
|
|
|
|
+ :closable="false"
|
|
|
|
|
+ show-icon
|
|
|
|
|
+ title="该检验单没有项目实例"
|
|
|
|
|
+ description="历史单、未命中检规、命中检规但检规无明细,均为此状态。此类检验单按整单结论录入(见上方「检验员录入检验结果」),不做项目级录入。"
|
|
|
|
|
+ />
|
|
|
|
|
+
|
|
|
|
|
+ <template v-else>
|
|
|
|
|
+ <div class="summary mt12">
|
|
|
|
|
+ <el-tag size="small" type="info" effect="plain">项目数 {{ summaryVal.itemCount }}</el-tag>
|
|
|
|
|
+ <el-tag size="small" type="info" effect="plain">已判定 {{ summaryVal.judgedCount }}</el-tag>
|
|
|
|
|
+ <el-tag size="small" type="success" effect="plain">合格 {{ summaryVal.passCount }}</el-tag>
|
|
|
|
|
+ <el-tag size="small" type="danger" effect="plain">不合格 {{ summaryVal.failCount }}</el-tag>
|
|
|
|
|
+ <el-tag size="small" type="warning" effect="plain">无效 {{ summaryVal.invalidCount }}</el-tag>
|
|
|
|
|
+ <el-tag size="small" effect="plain">未录 {{ summaryVal.unjudgedCount }}</el-tag>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <el-table ref="tableRef" :data="lines" row-key="id" border stripe size="small" class="mt12">
|
|
|
|
|
+ <el-table-column type="expand">
|
|
|
|
|
+ <template #default="{ row }">
|
|
|
|
|
+ <div class="expand-body">
|
|
|
|
|
+ <el-descriptions :column="2" border size="small" title="检规快照(生成时冻结,不回查检规)">
|
|
|
|
|
+ <el-descriptions-item label="检验标准" :span="2">
|
|
|
|
|
+ <span class="pre">{{ snap(row.inspectionStandard) }}</span>
|
|
|
|
|
+ </el-descriptions-item>
|
|
|
|
|
+ <el-descriptions-item label="检验方法·工具" :span="2">
|
|
|
|
|
+ <span class="pre">{{ snap(row.methodTool) }}</span>
|
|
|
|
|
+ </el-descriptions-item>
|
|
|
|
|
+ <el-descriptions-item label="下限">{{ snap(row.lowerLimit) }}</el-descriptions-item>
|
|
|
|
|
+ <el-descriptions-item label="上限">{{ snap(row.upperLimit) }}</el-descriptions-item>
|
|
|
|
|
+ <el-descriptions-item label="抽样方案">{{ snap(row.samplingScheme) }}</el-descriptions-item>
|
|
|
|
|
+ <el-descriptions-item label="图片分类">{{ snap(row.imageCategory) }}</el-descriptions-item>
|
|
|
|
|
+ <el-descriptions-item label="检规备注" :span="2">
|
|
|
|
|
+ <span class="pre">{{ snap(row.specRemark) }}</span>
|
|
|
|
|
+ </el-descriptions-item>
|
|
|
|
|
+ </el-descriptions>
|
|
|
|
|
+
|
|
|
|
|
+ <div v-if="drafts[String(row.id)]" class="editor mt12">
|
|
|
|
|
+ <el-form label-width="104px" size="small" @submit.prevent>
|
|
|
|
|
+ <el-form-item label="录入类型">
|
|
|
|
|
+ <el-select
|
|
|
|
|
+ v-model="drafts[String(row.id)].resultType"
|
|
|
|
|
+ :disabled="!canEdit"
|
|
|
|
|
+ placeholder="请选择"
|
|
|
|
|
+ style="width: 150px"
|
|
|
|
|
+ @change="(v: any) => onTypeChange(row, v)"
|
|
|
|
|
+ >
|
|
|
|
|
+ <el-option value="NUMERIC" label="数值型" />
|
|
|
|
|
+ <el-option value="NON_NUMERIC" label="非数值型" />
|
|
|
|
|
+ </el-select>
|
|
|
|
|
+ <el-tag v-if="isTypeSwitching(row)" class="ml8" type="warning" size="small" effect="plain">
|
|
|
|
|
+ 保存后将清空该项目原有测量事实
|
|
|
|
|
+ </el-tag>
|
|
|
|
|
+ <span v-else-if="!drafts[String(row.id)].resultType" class="hint ml8">未选择录入类型,无法录入测量结果</span>
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- NUMERIC:测量真源 = 逐样本实测值 -->
|
|
|
|
|
+ <template v-if="drafts[String(row.id)].resultType === 'NUMERIC'">
|
|
|
|
|
+ <el-form-item label="样本实测值">
|
|
|
|
|
+ <div class="sample-wrap">
|
|
|
|
|
+ <el-table :data="drafts[String(row.id)].samples" border size="small" class="sample-table">
|
|
|
|
|
+ <el-table-column label="样本" width="64" align="center">
|
|
|
|
|
+ <template #default="{ row: s }">{{ s.sampleIndex }}</template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column label="实测值" min-width="130">
|
|
|
|
|
+ <template #default="{ row: s }">
|
|
|
|
|
+ <el-input v-if="canEdit" v-model="s.sampleValueRaw" size="small" placeholder="录入实测值" />
|
|
|
|
|
+ <span v-else>{{ snap(s.sampleValueRaw) }}</span>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column label="判定(已保存)" width="110" align="center">
|
|
|
|
|
+ <template #default="{ row: s }">
|
|
|
|
|
+ <el-tag :type="judgeTagType(savedSampleJudgement(row, s.sampleIndex))" size="small" effect="plain">
|
|
|
|
|
+ {{ judgeText(savedSampleJudgement(row, s.sampleIndex)) }}
|
|
|
|
|
+ </el-tag>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column label="备注" min-width="120">
|
|
|
|
|
+ <template #default="{ row: s }">
|
|
|
|
|
+ <el-input v-if="canEdit" v-model="s.remark" size="small" />
|
|
|
|
|
+ <span v-else>{{ snap(s.remark) }}</span>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column v-if="canEdit" label="操作" width="64" align="center">
|
|
|
|
|
+ <template #default="{ $index }">
|
|
|
|
|
+ <el-button type="danger" link @click="removeSample(row.id, $index)">删除</el-button>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <template #empty>
|
|
|
|
|
+ <span class="hint">暂无样本,判定为「未录」</span>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table>
|
|
|
|
|
+ <div v-if="canEdit" class="mt8">
|
|
|
|
|
+ <el-button size="small" @click="addSample(row.id)">新增样本</el-button>
|
|
|
|
|
+ <span class="hint ml8">样本值为该项目的完整集合;判定由后端按快照上下限重算,保存后刷新</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ </template>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- NON_NUMERIC:测量真源 = 项目级合格/不合格数 -->
|
|
|
|
|
+ <template v-else-if="drafts[String(row.id)].resultType === 'NON_NUMERIC'">
|
|
|
|
|
+ <el-form-item label="合格数">
|
|
|
|
|
+ <el-input-number
|
|
|
|
|
+ v-model="drafts[String(row.id)].qualifiedQty"
|
|
|
|
|
+ :min="0"
|
|
|
|
|
+ :controls="false"
|
|
|
|
|
+ :value-on-clear="null"
|
|
|
|
|
+ :disabled="!canEdit"
|
|
|
|
|
+ style="width: 150px"
|
|
|
|
|
+ />
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item label="不合格数">
|
|
|
|
|
+ <el-input-number
|
|
|
|
|
+ v-model="drafts[String(row.id)].unqualifiedQty"
|
|
|
|
|
+ :min="0"
|
|
|
|
|
+ :controls="false"
|
|
|
|
|
+ :value-on-clear="null"
|
|
|
|
|
+ :disabled="!canEdit"
|
|
|
|
|
+ style="width: 150px"
|
|
|
|
|
+ />
|
|
|
|
|
+ <span class="hint ml8">合格数与不合格数均需录入才会形成判定</span>
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ </template>
|
|
|
|
|
+
|
|
|
|
|
+ <el-form-item label="人工判定">
|
|
|
|
|
+ <el-select v-model="drafts[String(row.id)].manualJudgement" :disabled="!canEdit" style="width: 150px">
|
|
|
|
|
+ <el-option value="AUTO" label="自动判定" />
|
|
|
|
|
+ <el-option value="PASS" label="人工判合格" />
|
|
|
|
|
+ <el-option value="FAIL" label="人工判不合格" />
|
|
|
|
|
+ </el-select>
|
|
|
|
|
+ <span class="hint ml8">人工判定仅合格/不合格;「无效」为系统派生结论,不可人工设置</span>
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+
|
|
|
|
|
+ <el-form-item label="结果备注">
|
|
|
|
|
+ <el-input
|
|
|
|
|
+ v-model="drafts[String(row.id)].resultRemark"
|
|
|
|
|
+ type="textarea"
|
|
|
|
|
+ :rows="2"
|
|
|
|
|
+ :disabled="!canEdit"
|
|
|
|
|
+ style="width: 360px"
|
|
|
|
|
+ />
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+
|
|
|
|
|
+ <el-form-item label="最近录入">
|
|
|
|
|
+ <span class="hint">
|
|
|
|
|
+ {{ dash(row.lastResultBy) }} · {{ fmtDateTime(row.lastResultAt) }}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+
|
|
|
|
|
+ <el-form-item v-if="canEdit">
|
|
|
|
|
+ <el-button type="primary" :loading="isSaving(row)" @click="onSaveItem(row)">保存本项目</el-button>
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ </el-form>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+
|
|
|
|
|
+ <el-table-column label="序号" width="62" align="center">
|
|
|
|
|
+ <template #default="{ row }">{{ dash(row.seq) }}</template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column label="检验项目" min-width="120" show-overflow-tooltip>
|
|
|
|
|
+ <template #default="{ row }">{{ snap(row.inspectionItem) }}</template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column label="检验标准" min-width="120" show-overflow-tooltip>
|
|
|
|
|
+ <template #default="{ row }">{{ snap(row.inspectionStandard) }}</template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column label="检验方法·工具" min-width="130" show-overflow-tooltip>
|
|
|
|
|
+ <template #default="{ row }">{{ snap(row.methodTool) }}</template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column label="限值" width="120" align="center">
|
|
|
|
|
+ <template #default="{ row }">{{ limitText(row) }}</template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column label="录入类型" width="86" align="center">
|
|
|
|
|
+ <template #default="{ row }">{{ resultTypeText(row.resultType) }}</template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column label="判定" width="104" align="center">
|
|
|
|
|
+ <template #default="{ row }">
|
|
|
|
|
+ <el-tag :type="judgeTagType(row.itemJudgement)" size="small" effect="plain">{{ judgeText(row.itemJudgement) }}</el-tag>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column label="来源" width="62" align="center">
|
|
|
|
|
+ <template #default="{ row }">{{ sourceText(row.judgementSource) }}</template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column label="结果·测量" min-width="130">
|
|
|
|
|
+ <template #default="{ row }">{{ measureText(row) }}</template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column label="结果备注" min-width="104" show-overflow-tooltip>
|
|
|
|
|
+ <template #default="{ row }">{{ snap(row.resultRemark) }}</template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column label="操作" width="78" align="center" fixed="right">
|
|
|
|
|
+ <template #default="{ row }">
|
|
|
|
|
+ <el-button type="primary" link @click="toggleExpand(row)">{{ canEdit ? '录入' : '查看' }}</el-button>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ </el-table>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </div>
|
|
|
|
|
+</template>
|
|
|
|
|
+
|
|
|
|
|
+<script setup lang="ts" name="IqcInspBillItemPanel">
|
|
|
|
|
+import { computed, ref, watch } from 'vue';
|
|
|
|
|
+import { ElMessage, ElMessageBox } from 'element-plus';
|
|
|
|
|
+import type { IqcInspBillItemLine, IqcInspBillLineSummary, IqcItemResultType } from '../../api/iqcInspBill';
|
|
|
|
|
+import type { IqcSaveDetailItemPayload, IqcSaveDetailPayload } from '../../api/iqcInspection';
|
|
|
|
|
+
|
|
|
|
|
+// 项目级检验结果录入面板(嵌在来料检验单详情抽屉内,不单独路由)。
|
|
|
|
|
+//
|
|
|
|
|
+// 【授权】canEdit 由后端 detail 给出,是**唯一** UI 编辑 authority;
|
|
|
|
|
+// 本组件不按 currentNodeCode 自行推断是否可编辑。
|
|
|
|
|
+// 【判定】一律由后端依据「快照冻结的上下限 + 实录测量事实」计算;
|
|
|
|
|
+// 本组件只展示后端已保存的判定,**不做任何前端复算/预判**。
|
|
|
|
|
+// id 类一律 number | string:后端 long 序列化为 JSON 字符串(见 api/iqcInspBill.ts 注释)
|
|
|
|
|
+const props = defineProps<{
|
|
|
|
|
+ billId?: number | string | null;
|
|
|
|
|
+ lines?: IqcInspBillItemLine[];
|
|
|
|
|
+ summary?: IqcInspBillLineSummary | null;
|
|
|
|
|
+ canEdit?: boolean;
|
|
|
|
|
+ savingItemId?: number | string | null;
|
|
|
|
|
+}>();
|
|
|
|
|
+const emit = defineEmits<{
|
|
|
|
|
+ (e: 'save', payload: IqcSaveDetailPayload, itemId: number | string): void;
|
|
|
|
|
+}>();
|
|
|
|
|
+
|
|
|
|
|
+const tableRef = ref<any>(null);
|
|
|
|
|
+
|
|
|
|
|
+interface SampleDraft {
|
|
|
|
|
+ sampleIndex: number;
|
|
|
|
|
+ sampleValueRaw: string;
|
|
|
|
|
+ remark: string;
|
|
|
|
|
+}
|
|
|
|
|
+interface ItemDraft {
|
|
|
|
|
+ resultType: IqcItemResultType | null;
|
|
|
|
|
+ qualifiedQty: number | null;
|
|
|
|
|
+ unqualifiedQty: number | null;
|
|
|
|
|
+ resultRemark: string;
|
|
|
|
|
+ /** AUTO = 不提交 manualJudgement,由后端按测量真源重算 */
|
|
|
|
|
+ manualJudgement: 'AUTO' | 'PASS' | 'FAIL';
|
|
|
|
|
+ samples: SampleDraft[];
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 以 String(id) 作 key:id 可能是字符串形态的 long
|
|
|
|
|
+const drafts = ref<Record<string, ItemDraft>>({});
|
|
|
|
|
+
|
|
|
|
|
+const lines = computed<IqcInspBillItemLine[]>(() => props.lines ?? []);
|
|
|
|
|
+const itemCount = computed(() => props.summary?.itemCount ?? lines.value.length);
|
|
|
|
|
+const canEdit = computed(() => props.canEdit === true);
|
|
|
|
|
+const summaryVal = computed(() => ({
|
|
|
|
|
+ itemCount: props.summary?.itemCount ?? 0,
|
|
|
|
|
+ judgedCount: props.summary?.judgedCount ?? 0,
|
|
|
|
|
+ passCount: props.summary?.passCount ?? 0,
|
|
|
|
|
+ failCount: props.summary?.failCount ?? 0,
|
|
|
|
|
+ invalidCount: props.summary?.invalidCount ?? 0,
|
|
|
|
|
+ unjudgedCount: props.summary?.unjudgedCount ?? 0,
|
|
|
|
|
+}));
|
|
|
|
|
+
|
|
|
|
|
+// 每次 detail 重新拉取(打开 / 保存后 / 流程动作后)都按后端最新值重建草稿,
|
|
|
|
|
+// 避免旧的本地编辑残留覆盖服务端事实。
|
|
|
|
|
+watch(
|
|
|
|
|
+ () => props.lines,
|
|
|
|
|
+ (list) => {
|
|
|
|
|
+ const next: Record<string, ItemDraft> = {};
|
|
|
|
|
+ for (const l of list ?? []) {
|
|
|
|
|
+ next[String(l.id)] = {
|
|
|
|
|
+ resultType: (l.resultType as IqcItemResultType) ?? null,
|
|
|
|
|
+ qualifiedQty: l.qualifiedQty ?? null,
|
|
|
|
|
+ unqualifiedQty: l.unqualifiedQty ?? null,
|
|
|
|
|
+ resultRemark: l.resultRemark ?? '',
|
|
|
|
|
+ manualJudgement:
|
|
|
|
|
+ l.judgementSource === 'MANUAL' && (l.itemJudgement === 'PASS' || l.itemJudgement === 'FAIL')
|
|
|
|
|
+ ? (l.itemJudgement as 'PASS' | 'FAIL')
|
|
|
|
|
+ : 'AUTO',
|
|
|
|
|
+ samples: (l.samples ?? []).map((s) => ({
|
|
|
|
|
+ sampleIndex: s.sampleIndex ?? 0,
|
|
|
|
|
+ sampleValueRaw: s.sampleValueRaw ?? '',
|
|
|
|
|
+ remark: s.remark ?? '',
|
|
|
|
|
+ })),
|
|
|
|
|
+ };
|
|
|
|
|
+ }
|
|
|
|
|
+ drafts.value = next;
|
|
|
|
|
+ },
|
|
|
|
|
+ { immediate: true }
|
|
|
|
|
+);
|
|
|
|
|
+
|
|
|
|
|
+/** 单项保存中(id 可能是字符串形态的 long,统一按字符串比较) */
|
|
|
|
|
+function isSaving(row: IqcInspBillItemLine) {
|
|
|
|
|
+ const s = props.savingItemId;
|
|
|
|
|
+ return s !== null && s !== undefined && String(s) === String(row.id);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function toggleExpand(row: IqcInspBillItemLine) {
|
|
|
|
|
+ tableRef.value?.toggleRowExpansion(row);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/** 草稿类型与库中现值不同 ⇒ 保存时后端会清空该项目原有测量事实 */
|
|
|
|
|
+function isTypeSwitching(row: IqcInspBillItemLine) {
|
|
|
|
|
+ const d = drafts.value[String(row.id)];
|
|
|
|
|
+ if (!d || !d.resultType) return false;
|
|
|
|
|
+ return d.resultType !== (row.resultType ?? null);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/** 该项目在**服务端**是否已存在测量事实(样本行 或 计数真源) */
|
|
|
|
|
+function hasSavedFacts(row: IqcInspBillItemLine) {
|
|
|
|
|
+ if ((row.samples?.length ?? 0) > 0) return true;
|
|
|
|
|
+ if (row.qualifiedQty !== null && row.qualifiedQty !== undefined) return true;
|
|
|
|
|
+ return row.unqualifiedQty !== null && row.unqualifiedQty !== undefined;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function onTypeChange(row: IqcInspBillItemLine, next: IqcItemResultType) {
|
|
|
|
|
+ const d = drafts.value[String(row.id)];
|
|
|
|
|
+ if (!d) return;
|
|
|
|
|
+ const dbType = row.resultType ?? null;
|
|
|
|
|
+ if (next === dbType) return; // 切回库中现值,无破坏性
|
|
|
|
|
+ if (!hasSavedFacts(row)) return; // 尚无已保存的测量事实,无需确认
|
|
|
|
|
+ try {
|
|
|
|
|
+ await ElMessageBox.confirm(
|
|
|
|
|
+ dbType === 'NUMERIC'
|
|
|
|
|
+ ? '该项目已录入样本实测值。切换为「非数值型」并保存后,这些样本将被全部清空,判定随之重算。是否继续?'
|
|
|
|
|
+ : '该项目已录入合格/不合格数。切换为「数值型」并保存后,这些计数将被清空,判定随之重算。是否继续?',
|
|
|
|
|
+ '确认切换录入类型',
|
|
|
|
|
+ { type: 'warning', confirmButtonText: '继续切换', cancelButtonText: '取消' }
|
|
|
|
|
+ );
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ d.resultType = dbType; // 取消 → 回退到库中现值
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function addSample(itemId: number | string) {
|
|
|
|
|
+ const d = drafts.value[String(itemId)];
|
|
|
|
|
+ if (!d) return;
|
|
|
|
|
+ const maxIdx = d.samples.reduce((m, s) => (s.sampleIndex > m ? s.sampleIndex : m), 0);
|
|
|
|
|
+ d.samples.push({ sampleIndex: maxIdx + 1, sampleValueRaw: '', remark: '' });
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function removeSample(itemId: number | string, index: number) {
|
|
|
|
|
+ const d = drafts.value[String(itemId)];
|
|
|
|
|
+ if (!d) return;
|
|
|
|
|
+ d.samples.splice(index, 1);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function onSaveItem(row: IqcInspBillItemLine) {
|
|
|
|
|
+ const billId = props.billId;
|
|
|
|
|
+ if (!billId) return ElMessage.error('检验单 id 缺失');
|
|
|
|
|
+ const d = drafts.value[String(row.id)];
|
|
|
|
|
+ if (!d) return ElMessage.error('检验项目数据缺失,请重新打开详情');
|
|
|
|
|
+
|
|
|
|
|
+ const item: IqcSaveDetailItemPayload = { itemId: row.id };
|
|
|
|
|
+ if (d.resultType) item.resultType = d.resultType;
|
|
|
|
|
+ item.resultRemark = d.resultRemark && d.resultRemark.trim() ? d.resultRemark.trim() : null;
|
|
|
|
|
+ if (d.manualJudgement !== 'AUTO') item.manualJudgement = d.manualJudgement;
|
|
|
|
|
+
|
|
|
|
|
+ if (d.resultType === 'NUMERIC') {
|
|
|
|
|
+ const seen = new Set<number>();
|
|
|
|
|
+ for (const s of d.samples) {
|
|
|
|
|
+ if (!s.sampleValueRaw || !s.sampleValueRaw.trim())
|
|
|
|
|
+ return ElMessage.error(`样本 ${s.sampleIndex} 未填实测值(如需删除请点该行「删除」)`);
|
|
|
|
|
+ if (s.sampleIndex < 1) return ElMessage.error('样本序号必须大于 0');
|
|
|
|
|
+ if (seen.has(s.sampleIndex)) return ElMessage.error(`样本序号 ${s.sampleIndex} 重复`);
|
|
|
|
|
+ seen.add(s.sampleIndex);
|
|
|
|
|
+ }
|
|
|
|
|
+ item.samples = d.samples.map((s) => ({
|
|
|
|
|
+ sampleIndex: s.sampleIndex,
|
|
|
|
|
+ sampleValueRaw: s.sampleValueRaw.trim(),
|
|
|
|
|
+ remark: s.remark && s.remark.trim() ? s.remark.trim() : null,
|
|
|
|
|
+ }));
|
|
|
|
|
+ } else if (d.resultType === 'NON_NUMERIC') {
|
|
|
|
|
+ item.qualifiedQty = d.qualifiedQty;
|
|
|
|
|
+ item.unqualifiedQty = d.unqualifiedQty;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ emit('save', { billId, items: [item] }, row.id);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ───────────────────────── 展示辅助(只格式化,不判定) ─────────────────────────
|
|
|
|
|
+
|
|
|
|
|
+/** 取**已保存**的样本判定(后端唯一写入方)。草稿里改过的值在保存前不重算,刻意保持 stale 并在列头标注 */
|
|
|
|
|
+function savedSampleJudgement(row: IqcInspBillItemLine, sampleIndex: number) {
|
|
|
|
|
+ return row.samples?.find((s) => s.sampleIndex === sampleIndex)?.judgement ?? null;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function judgeText(j?: string | null) {
|
|
|
|
|
+ if (j === 'PASS') return '合格';
|
|
|
|
|
+ if (j === 'FAIL') return '不合格';
|
|
|
|
|
+ if (j === 'INVALID') return '无效';
|
|
|
|
|
+ return '未录';
|
|
|
|
|
+}
|
|
|
|
|
+function judgeTagType(j?: string | null) {
|
|
|
|
|
+ if (j === 'PASS') return 'success';
|
|
|
|
|
+ if (j === 'FAIL') return 'danger';
|
|
|
|
|
+ if (j === 'INVALID') return 'warning';
|
|
|
|
|
+ return 'info';
|
|
|
|
|
+}
|
|
|
|
|
+function sourceText(s?: string | null) {
|
|
|
|
|
+ if (s === 'MANUAL') return '人工';
|
|
|
|
|
+ if (s === 'AUTO') return '自动';
|
|
|
|
|
+ return '-';
|
|
|
|
|
+}
|
|
|
|
|
+function resultTypeText(t?: string | null) {
|
|
|
|
|
+ if (t === 'NUMERIC') return '数值型';
|
|
|
|
|
+ if (t === 'NON_NUMERIC') return '非数值型';
|
|
|
|
|
+ return '未选';
|
|
|
|
|
+}
|
|
|
|
|
+function limitText(row: IqcInspBillItemLine) {
|
|
|
|
|
+ const lo = snap(row.lowerLimit);
|
|
|
|
|
+ const up = snap(row.upperLimit);
|
|
|
|
|
+ if (lo === '-' && up === '-') return '-';
|
|
|
|
|
+ return `${lo} ~ ${up}`;
|
|
|
|
|
+}
|
|
|
|
|
+function measureText(row: IqcInspBillItemLine) {
|
|
|
|
|
+ if (row.resultType === 'NUMERIC') {
|
|
|
|
|
+ const n = row.samples?.length ?? 0;
|
|
|
|
|
+ return n === 0 ? '样本 0 条' : `样本 ${n} 条`;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (row.resultType === 'NON_NUMERIC') {
|
|
|
|
|
+ return `合格 ${dashNum(row.qualifiedQty)} / 不合格 ${dashNum(row.unqualifiedQty)}`;
|
|
|
|
|
+ }
|
|
|
|
|
+ return '-';
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+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);
|
|
|
|
|
+}
|
|
|
|
|
+/** 快照/文本字段展示:空值与脏写入的 'null'/'undefined' 字面量一律显示 '-' */
|
|
|
|
|
+function snap(v: unknown) {
|
|
|
|
|
+ if (v === null || v === undefined) return '-';
|
|
|
|
|
+ const s = String(v).trim();
|
|
|
|
|
+ if (s === '' || s.toLowerCase() === 'null' || s.toLowerCase() === 'undefined') return '-';
|
|
|
|
|
+ return s;
|
|
|
|
|
+}
|
|
|
|
|
+function fmtDateTime(v?: string | null) {
|
|
|
|
|
+ if (!v) return '-';
|
|
|
|
|
+ return String(v).replace('T', ' ').slice(0, 19);
|
|
|
|
|
+}
|
|
|
|
|
+</script>
|
|
|
|
|
+
|
|
|
|
|
+<style scoped lang="scss">
|
|
|
|
|
+.item-panel {
|
|
|
|
|
+ padding: 12px;
|
|
|
|
|
+ border: 1px solid var(--el-border-color-lighter);
|
|
|
|
|
+ border-radius: 4px;
|
|
|
|
|
+}
|
|
|
|
|
+.panel-head {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 10px;
|
|
|
|
|
+}
|
|
|
|
|
+.panel-title {
|
|
|
|
|
+ font-weight: bold;
|
|
|
|
|
+}
|
|
|
|
|
+.summary {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-wrap: wrap;
|
|
|
|
|
+ gap: 8px;
|
|
|
|
|
+}
|
|
|
|
|
+.expand-body {
|
|
|
|
|
+ padding: 8px 12px 4px;
|
|
|
|
|
+ background: var(--el-fill-color-lighter);
|
|
|
|
|
+}
|
|
|
|
|
+.sample-wrap {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+}
|
|
|
|
|
+.sample-table {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+}
|
|
|
|
|
+.hint {
|
|
|
|
|
+ color: var(--el-text-color-secondary);
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+}
|
|
|
|
|
+.pre {
|
|
|
|
|
+ white-space: pre-wrap;
|
|
|
|
|
+ word-break: break-word;
|
|
|
|
|
+}
|
|
|
|
|
+.mt8 {
|
|
|
|
|
+ margin-top: 8px;
|
|
|
|
|
+}
|
|
|
|
|
+.mt12 {
|
|
|
|
|
+ margin-top: 12px;
|
|
|
|
|
+}
|
|
|
|
|
+.ml8 {
|
|
|
|
|
+ margin-left: 8px;
|
|
|
|
|
+}
|
|
|
|
|
+</style>
|