fix: 子表保存改为就地同步,避免整批换 id 打断外键。
协议样本、生产产出和盘点明细按行更新并保留原 id;补齐存储证明质检预填、离职人员显示名,以及样本采集通知模板字段。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
d66e55af65
commit
5e563a6669
139 files changed
+59986
-49048
No files matched your search
@@ -11,6 +11,7 @@ export type SampleStorageCertQcDetail = {
|
||||
qcWorkOrderId?: string
|
||||
filledFlag: boolean
|
||||
payload: Record<string, unknown>
|
||||
prefill?: Record<string, unknown>
|
||||
}
|
||||
|
||||
export function fetchSampleStorageCertQc(sampleCode: string) {
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<div class="val">{{ identity.labFlowCode || '—' }}</div>
|
||||
</div>
|
||||
<div class="ctx-item">
|
||||
<label>样本编码</label>
|
||||
<label>{{ codeFieldLabel }}</label>
|
||||
<div class="val">{{ identity.sampleCode || '—' }}</div>
|
||||
</div>
|
||||
<div class="ctx-item">
|
||||
@@ -55,21 +55,39 @@
|
||||
}"
|
||||
>
|
||||
<template v-if="mode === 'view'">
|
||||
<div class="readonly-box">{{ displayValue(field.key) }}</div>
|
||||
<div
|
||||
class="readonly-box"
|
||||
:class="{ 'is-positive': field.control === 'select_yny' && isPositiveQual(form[field.key]) }"
|
||||
>
|
||||
{{ displayValue(field.key) }}
|
||||
</div>
|
||||
</template>
|
||||
<el-select
|
||||
v-else-if="field.control === 'select_yny'"
|
||||
v-else-if="field.control === 'select_yny' || field.control === 'select_result'"
|
||||
v-model="form[field.key]"
|
||||
clearable
|
||||
placeholder="请选择"
|
||||
:class="{
|
||||
'is-positive': field.control === 'select_yny' && isPositiveQual(form[field.key])
|
||||
}"
|
||||
:filterable="field.control === 'select_yny' && !isPerinatalQualField(typeKey, field.control)"
|
||||
:allow-create="field.control === 'select_yny' && !isPerinatalQualField(typeKey, field.control)"
|
||||
:placeholder="
|
||||
field.control === 'select_yny' && !isPerinatalQualField(typeKey, field.control)
|
||||
? '请选择或输入'
|
||||
: '请选择'
|
||||
"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="opt in YNY_OPTIONS"
|
||||
v-for="opt in qualOptionsFor(typeKey, field.control)"
|
||||
:key="opt.value"
|
||||
:label="opt.label"
|
||||
:value="opt.value"
|
||||
/>
|
||||
>
|
||||
<span :class="{ 'qual-option--positive': isPositiveQual(opt.value) }">{{
|
||||
opt.label
|
||||
}}</span>
|
||||
</el-option>
|
||||
</el-select>
|
||||
<el-input
|
||||
v-else-if="field.control === 'textarea'"
|
||||
@@ -122,7 +140,10 @@
|
||||
} from '@/api/sample-storage-cert-qc'
|
||||
import {
|
||||
STORAGE_CERT_QC_GROUPS,
|
||||
YNY_OPTIONS,
|
||||
isPerinatalQualField,
|
||||
mergeStorageCertPrefill,
|
||||
normalizeQualValue,
|
||||
qualOptionsFor,
|
||||
resolveStorageCertQcTypeKey,
|
||||
type StorageCertQcTypeKey
|
||||
} from '@/utils/storage-cert-qc-fields'
|
||||
@@ -156,6 +177,11 @@
|
||||
props.mode === 'edit' ? '填写储存证质检信息' : '查看储存证的质检信息'
|
||||
)
|
||||
|
||||
const codeFieldLabel = computed(() => {
|
||||
const key = typeKey.value || resolveStorageCertQcTypeKey(props.sampleTypeName)
|
||||
return key === 'perinatal_hsc' || key === 'uc_msc' ? '协议编码' : '样本编码'
|
||||
})
|
||||
|
||||
const groups = computed(() => (typeKey.value ? STORAGE_CERT_QC_GROUPS[typeKey.value] || [] : []))
|
||||
|
||||
function displayValue(key: string) {
|
||||
@@ -164,13 +190,19 @@
|
||||
return String(v)
|
||||
}
|
||||
|
||||
function isPositiveQual(value: unknown) {
|
||||
const text = String(value ?? '').trim()
|
||||
return text === '阳性' || text === '阳'
|
||||
}
|
||||
|
||||
function resetFormKeys(key: StorageCertQcTypeKey | null, payload: Record<string, unknown>) {
|
||||
Object.keys(form).forEach((k) => delete form[k])
|
||||
if (!key) return
|
||||
for (const group of STORAGE_CERT_QC_GROUPS[key] || []) {
|
||||
for (const field of group.fields) {
|
||||
const raw = payload[field.key]
|
||||
form[field.key] = raw == null ? '' : String(raw)
|
||||
const text = raw == null ? '' : String(raw)
|
||||
form[field.key] = normalizeQualValue(key, field.control, text)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -185,7 +217,11 @@
|
||||
(detail.sampleTypeKey as StorageCertQcTypeKey | null) ||
|
||||
resolveStorageCertQcTypeKey(identity.sampleTypeName)
|
||||
typeKey.value = resolved
|
||||
resetFormKeys(resolved, (detail.payload || {}) as Record<string, unknown>)
|
||||
const payload =
|
||||
props.mode === 'edit'
|
||||
? mergeStorageCertPrefill(detail.payload, detail.prefill)
|
||||
: ((detail.payload || {}) as Record<string, unknown>)
|
||||
resetFormKeys(resolved, payload)
|
||||
}
|
||||
|
||||
async function loadDetail() {
|
||||
@@ -330,6 +366,24 @@
|
||||
background: #f8fafc;
|
||||
border: 1px solid var(--el-border-color-lighter, #e2e8f0);
|
||||
border-radius: 8px;
|
||||
|
||||
&.is-positive {
|
||||
color: #dc2626;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.is-positive {
|
||||
:deep(.el-select__selected-item) {
|
||||
color: #dc2626 !important;
|
||||
-webkit-text-fill-color: #dc2626 !important;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.qual-option--positive {
|
||||
color: #dc2626;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, it } from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { createEmptyStepOutputRow } from './production-step-output'
|
||||
import { createEmptyStepOutputRow, toStepOutputReplaceItem } from './production-step-output'
|
||||
|
||||
describe('createEmptyStepOutputRow', () => {
|
||||
it('defaults barcode to lab storage code', () => {
|
||||
@@ -20,4 +20,14 @@ describe('createEmptyStepOutputRow', () => {
|
||||
assert.equal(createEmptyStepOutputRow(null).outputBarcode, '')
|
||||
assert.equal(createEmptyStepOutputRow(undefined).outputBarcode, '')
|
||||
})
|
||||
|
||||
it('assigns a stable id and keeps it when building the save payload', () => {
|
||||
const row = createEmptyStepOutputRow('LAB-001')
|
||||
assert.match(row.id, /^[0-9A-F]{32}$/)
|
||||
const first = toStepOutputReplaceItem(row)
|
||||
const second = toStepOutputReplaceItem(row)
|
||||
assert.equal(first.id, row.id)
|
||||
assert.equal(second.id, row.id)
|
||||
assert.equal(first.output_barcode, 'LAB-001')
|
||||
})
|
||||
})
|
||||
@@ -1,9 +1,36 @@
|
||||
/** 与后端 generate_uuid 一致:32 位大写、无连字符。子表行从创建起就带稳定 ID。 */
|
||||
export function createStableRowId() {
|
||||
const uuid =
|
||||
typeof globalThis.crypto?.randomUUID === 'function'
|
||||
? globalThis.crypto.randomUUID()
|
||||
: `${Date.now().toString(16)}${Math.random().toString(16).slice(2)}`
|
||||
return uuid.replace(/-/g, '').toUpperCase()
|
||||
}
|
||||
|
||||
export function createStepOutputId() {
|
||||
return createStableRowId()
|
||||
}
|
||||
|
||||
/** 工艺步骤「产出物」新增行:条码默认实验室入库编码。 */
|
||||
export function createEmptyStepOutputRow(labStorageCode?: string | null) {
|
||||
return {
|
||||
id: createStepOutputId(),
|
||||
outputTypeId: '',
|
||||
outputName: '',
|
||||
outputBarcode: String(labStorageCode || '').trim(),
|
||||
remark: ''
|
||||
}
|
||||
}
|
||||
|
||||
/** 保存步骤产出物:带上原行 ID,避免后端整表换 ID。 */
|
||||
export function toStepOutputReplaceItem(row: Record<string, any>) {
|
||||
const id = String(row.id || '').trim() || createStepOutputId()
|
||||
row.id = id
|
||||
return {
|
||||
id,
|
||||
output_type_id: row.outputTypeId,
|
||||
output_name: row.outputName,
|
||||
output_barcode: row.outputBarcode,
|
||||
remark: row.remark || ''
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
|
||||
export type StorageCertQcTypeKey = 'perinatal_hsc' | 'uc_msc'
|
||||
|
||||
export type StorageCertQcControl = 'text' | 'textarea' | 'select_yny'
|
||||
export type StorageCertQcControl = 'text' | 'textarea' | 'select_yny' | 'select_result'
|
||||
|
||||
export interface StorageCertQcField {
|
||||
key: string
|
||||
@@ -53,19 +53,13 @@ export const STORAGE_CERT_QC_GROUPS: Record<StorageCertQcTypeKey, StorageCertQcG
|
||||
{
|
||||
title: '采集与运输',
|
||||
variant: 'alt',
|
||||
fields: [
|
||||
{ key: 'collection_time', label: '造血干细胞采集时间' },
|
||||
{ key: 'transport_temp_c', label: '运输温度 /℃' }
|
||||
]
|
||||
fields: [{ key: 'collection_time', label: '造血干细胞采集时间' }]
|
||||
},
|
||||
{
|
||||
title: '造血干细胞检测报告',
|
||||
variant: 'primary',
|
||||
fields: [
|
||||
{ key: 'receive_time', label: '造血干细胞接收时间' },
|
||||
{ key: 'process_time', label: '处理时间' },
|
||||
{ key: 'cryo_time', label: '冻存时间' },
|
||||
{ key: 'weight_g', label: '造血干细胞重量/g' },
|
||||
{ key: 'cryo_volume_ml', label: '造血干细胞冻存体积/mL' },
|
||||
{ key: 'tnc_1e8', label: '有核细胞数 TNC / *10E8' },
|
||||
{ key: 'cd34_count_1e6', label: 'CD34+ 细胞计数 / *10E6' },
|
||||
@@ -91,17 +85,16 @@ export const STORAGE_CERT_QC_GROUPS: Record<StorageCertQcTypeKey, StorageCertQcG
|
||||
{ key: 'hla_typing', label: 'HLA 分型', control: 'textarea', span: 3 },
|
||||
{ key: 'storage_date', label: '储存日期' },
|
||||
{ key: 'storage_years', label: '储存年限' },
|
||||
{ key: 'result_comment', label: '检测结果评论' },
|
||||
{ key: 'result_comment', label: '检测结果评论', control: 'select_result' },
|
||||
{ key: 'report_issue_date', label: '报告发出日期' }
|
||||
]
|
||||
}
|
||||
],
|
||||
uc_msc: [
|
||||
{
|
||||
title: '采集与运输 / 邮寄',
|
||||
title: '采集与运输',
|
||||
variant: 'alt',
|
||||
fields: [
|
||||
{ key: 'mail_flag', label: '是否邮寄' },
|
||||
{ key: 'collection_time', label: '脐带采集时间' },
|
||||
{ key: 'transport_temp_c', label: '运输温度 /℃' }
|
||||
]
|
||||
@@ -111,8 +104,6 @@ export const STORAGE_CERT_QC_GROUPS: Record<StorageCertQcTypeKey, StorageCertQcG
|
||||
variant: 'primary',
|
||||
fields: [
|
||||
{ key: 'receive_time', label: '脐带接收时间' },
|
||||
{ key: 'process_time', label: '处理时间' },
|
||||
{ key: 'cryo_time', label: '冻存时间' },
|
||||
{ key: 'storage_tube_count', label: '储存总量(管)' },
|
||||
{ key: 'available_cell_type', label: '可提供细胞类型' },
|
||||
{ key: 'available_cell_count_1e7', label: '可提供细胞数量 / *10E7' },
|
||||
@@ -134,7 +125,7 @@ export const STORAGE_CERT_QC_GROUPS: Record<StorageCertQcTypeKey, StorageCertQcG
|
||||
title: '储存证收尾信息',
|
||||
variant: 'meta',
|
||||
fields: [
|
||||
{ key: 'result_judgment', label: '检测结果评判' },
|
||||
{ key: 'result_judgment', label: '检测结果评判', control: 'select_result' },
|
||||
{ key: 'report_issue_date', label: '报告发出日期' },
|
||||
{ key: 'storage_date_p2', label: '储存日期 (P2冻存日期)' },
|
||||
{ key: 'freeze_method', label: '冷冻方式' },
|
||||
@@ -147,6 +138,60 @@ export const STORAGE_CERT_QC_GROUPS: Record<StorageCertQcTypeKey, StorageCertQcG
|
||||
|
||||
export const YNY_OPTIONS = [
|
||||
{ label: '阴', value: '阴' },
|
||||
{ label: '阳', value: '阳' },
|
||||
{ label: '/', value: '/' }
|
||||
{ label: '阳', value: '阳' }
|
||||
]
|
||||
|
||||
/** 储存证阴阳性:与质检参数值一致,用阴性 / 阳性。 */
|
||||
export const NP_OPTIONS = [
|
||||
{ label: '阴性', value: '阴性' },
|
||||
{ label: '阳性', value: '阳性' }
|
||||
]
|
||||
|
||||
export function isPerinatalQualField(
|
||||
typeKey: string | null | undefined,
|
||||
control?: StorageCertQcControl
|
||||
) {
|
||||
return (
|
||||
(typeKey === 'perinatal_hsc' || typeKey === 'uc_msc') && control === 'select_yny'
|
||||
)
|
||||
}
|
||||
|
||||
export function qualOptionsFor(
|
||||
typeKey: string | null | undefined,
|
||||
control?: StorageCertQcControl
|
||||
) {
|
||||
if (control === 'select_result') return RESULT_OPTIONS
|
||||
if (isPerinatalQualField(typeKey, control)) return NP_OPTIONS
|
||||
return YNY_OPTIONS
|
||||
}
|
||||
|
||||
export function normalizeQualValue(
|
||||
typeKey: string | null | undefined,
|
||||
control: StorageCertQcControl | undefined,
|
||||
raw: string
|
||||
) {
|
||||
if (!isPerinatalQualField(typeKey, control)) return raw
|
||||
const text = raw.trim()
|
||||
if (text === '阴' || text === '阴性') return '阴性'
|
||||
if (text === '阳' || text === '阳性') return '阳性'
|
||||
return raw
|
||||
}
|
||||
|
||||
export const RESULT_OPTIONS = [
|
||||
{ label: '合格', value: '合格' },
|
||||
{ label: '不合格', value: '不合格' }
|
||||
]
|
||||
|
||||
/** 已存档的非空值优先,只把预填补进空格。 */
|
||||
export function mergeStorageCertPrefill(
|
||||
payload: Record<string, unknown> | null | undefined,
|
||||
prefill: Record<string, unknown> | null | undefined
|
||||
): Record<string, unknown> {
|
||||
const out: Record<string, unknown> = { ...(payload || {}) }
|
||||
for (const [key, value] of Object.entries(prefill || {})) {
|
||||
if (value == null || String(value).trim() === '') continue
|
||||
const current = out[key]
|
||||
if (current == null || String(current).trim() === '') out[key] = value
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -1572,6 +1572,7 @@
|
||||
await fetchSaveMaterialStockCheckUnusedLines(
|
||||
unusedCheckId.value,
|
||||
items.map((l) => ({
|
||||
id: l.id || undefined,
|
||||
usageId: l.usageId || undefined,
|
||||
batchId: l.batchId || undefined,
|
||||
materialName: l.materialName.trim(),
|
||||
|
||||
@@ -1158,6 +1158,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { formatDisplayOrDash } from '@/utils/datetime-display'
|
||||
import { createStableRowId } from '@/utils/production-step-output'
|
||||
import { computed } from 'vue'
|
||||
import {
|
||||
Plus,
|
||||
@@ -2179,6 +2180,7 @@
|
||||
if (!selectedStep.value) return
|
||||
if (!selectedStep.value.reagents) selectedStep.value.reagents = []
|
||||
selectedStep.value.reagents.push({
|
||||
id: createStableRowId(),
|
||||
reagentId: '',
|
||||
reagentCode: '',
|
||||
reagentName: '',
|
||||
@@ -2215,6 +2217,7 @@
|
||||
await fetchUpdateStepReagents({
|
||||
stepId: selectedStep.value.id,
|
||||
items: reagents.map((r: Record<string, any>, idx: number) => ({
|
||||
id: r.id || '',
|
||||
reagentCode: r.reagentCode,
|
||||
reagentName: r.reagentName,
|
||||
spec: r.spec,
|
||||
@@ -2235,6 +2238,7 @@
|
||||
if (!selectedStep.value) return
|
||||
if (!selectedStep.value.consumables) selectedStep.value.consumables = []
|
||||
selectedStep.value.consumables.push({
|
||||
id: createStableRowId(),
|
||||
consumableId: '',
|
||||
consumableCode: '',
|
||||
consumableName: '',
|
||||
@@ -2271,6 +2275,7 @@
|
||||
await fetchUpdateStepConsumables({
|
||||
stepId: selectedStep.value.id,
|
||||
items: consumables.map((c: Record<string, any>, idx: number) => ({
|
||||
id: c.id || '',
|
||||
consumableCode: c.consumableCode,
|
||||
consumableName: c.consumableName,
|
||||
specModel: c.specModel,
|
||||
@@ -2291,6 +2296,7 @@
|
||||
if (!selectedStep.value) return
|
||||
if (!selectedStep.value.outputs) selectedStep.value.outputs = []
|
||||
selectedStep.value.outputs.push({
|
||||
id: createStableRowId(),
|
||||
outputTypeId: '',
|
||||
outputName: '',
|
||||
outputBarcode: '',
|
||||
@@ -2310,6 +2316,7 @@
|
||||
await fetchUpdateStepOutputs({
|
||||
stepId: selectedStep.value.id,
|
||||
items: (selectedStep.value.outputs || []).map((o: Record<string, any>, idx: number) => ({
|
||||
id: o.id || '',
|
||||
outputTypeId: o.outputTypeId,
|
||||
outputName: o.outputName,
|
||||
outputBarcode: o.outputBarcode,
|
||||
@@ -2333,6 +2340,7 @@
|
||||
if (!selectedStep.value) return
|
||||
if (!selectedStep.value.dataCollections) selectedStep.value.dataCollections = []
|
||||
selectedStep.value.dataCollections.push({
|
||||
id: createStableRowId(),
|
||||
collectionName: '',
|
||||
unit: '',
|
||||
orderNum: 0,
|
||||
@@ -2353,6 +2361,7 @@
|
||||
stepId: selectedStep.value.id,
|
||||
items: (selectedStep.value.dataCollections || []).map(
|
||||
(d: Record<string, any>, idx: number) => ({
|
||||
id: d.id || '',
|
||||
collectionName: d.collectionName,
|
||||
unit: d.unit,
|
||||
orderNum: idx,
|
||||
|
||||
@@ -1033,6 +1033,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { formatDisplayOrDash } from '@/utils/datetime-display'
|
||||
import { createStableRowId } from '@/utils/production-step-output'
|
||||
import { Plus, Close, Check, Rank, RefreshLeft, Search, Delete, Edit, Upload } from '@element-plus/icons-vue'
|
||||
import Sortable from 'sortablejs'
|
||||
import type { SortableEvent } from 'sortablejs'
|
||||
@@ -1373,6 +1374,7 @@
|
||||
function appendReagentFromMaster(r: any) {
|
||||
if (isReagentDuplicate(r.id, r.reagentCode)) return false
|
||||
reagentList.value.push({
|
||||
id: createStableRowId(),
|
||||
reagentId: r.id,
|
||||
reagentCode: r.reagentCode || '',
|
||||
reagentName: r.reagentName || '',
|
||||
@@ -1391,6 +1393,7 @@
|
||||
) {
|
||||
if (isConsumableDuplicate(c.id || c.consumableId, c.consumableCode)) return false
|
||||
consumableList.value.push({
|
||||
id: createStableRowId(),
|
||||
consumableId: c.consumableId || c.id,
|
||||
consumableCode: c.consumableCode || '',
|
||||
consumableName: c.consumableName || '',
|
||||
@@ -1482,7 +1485,7 @@
|
||||
sopContent: formData.sopContent || undefined,
|
||||
remark: formData.remark?.trim() || undefined,
|
||||
reagents: reagentList.value.map((r) => ({
|
||||
reagentId: r.reagentId || r.id,
|
||||
id: r.id || '',
|
||||
reagentCode: r.reagentCode,
|
||||
reagentName: r.reagentName,
|
||||
spec: r.spec,
|
||||
@@ -1492,7 +1495,7 @@
|
||||
remark: r.remark || ''
|
||||
})),
|
||||
consumables: consumableList.value.map((c) => ({
|
||||
consumableId: c.consumableId || c.id,
|
||||
id: c.id || '',
|
||||
consumableCode: c.consumableCode,
|
||||
consumableName: c.consumableName,
|
||||
specModel: c.specModel,
|
||||
@@ -1502,12 +1505,14 @@
|
||||
remark: c.remark || ''
|
||||
})),
|
||||
outputs: outputList.value.map((o) => ({
|
||||
id: o.id || '',
|
||||
outputTypeId: o.outputTypeId,
|
||||
outputName: o.outputName,
|
||||
outputBarcode: o.outputBarcode,
|
||||
remark: o.remark || ''
|
||||
})),
|
||||
dataCollections: dataCollectionList.value.map((d) => ({
|
||||
id: d.id || '',
|
||||
collectionName: d.collectionName,
|
||||
unit: d.unit || undefined,
|
||||
orderNum: d.orderNum ?? 0,
|
||||
@@ -1721,6 +1726,7 @@
|
||||
// =============== 产出物 ===============
|
||||
function addOutputRow() {
|
||||
outputList.value.push({
|
||||
id: createStableRowId(),
|
||||
outputTypeId: '',
|
||||
outputName: '',
|
||||
outputBarcode: '',
|
||||
@@ -1731,6 +1737,7 @@
|
||||
function addDataCollectionRow() {
|
||||
dataCollectionList.value.push(
|
||||
withDataCollectionClientKey({
|
||||
id: createStableRowId(),
|
||||
collectionName: '',
|
||||
unit: '',
|
||||
orderNum: dataCollectionList.value.length + 1,
|
||||
|
||||
+3
@@ -131,6 +131,7 @@
|
||||
import { ref, reactive } from 'vue'
|
||||
import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
|
||||
import { Plus, Close, Check, Delete } from '@element-plus/icons-vue'
|
||||
import { createStableRowId } from '@/utils/production-step-output'
|
||||
import DialogFormBody from '@/components/business/dialog-form-body/index.vue'
|
||||
import {
|
||||
fetchCreateProductionMaterial,
|
||||
@@ -229,6 +230,7 @@
|
||||
|
||||
function addFormulaLine() {
|
||||
formulaLines.value.push({
|
||||
id: createStableRowId(),
|
||||
componentMaterialId: '',
|
||||
componentMaterialType: 'reagent',
|
||||
standardQty: 1,
|
||||
@@ -330,6 +332,7 @@
|
||||
await fetchReplaceProductionMaterialFormula(
|
||||
id,
|
||||
formulaLines.value.map((ln) => ({
|
||||
id: ln.id || '',
|
||||
componentMaterialId: ln.componentMaterialId,
|
||||
componentMaterialType: ln.componentMaterialType,
|
||||
standardQty: ln.standardQty,
|
||||
|
||||
@@ -1600,6 +1600,7 @@
|
||||
await fetchSaveProductionMaterialStockCheckUnusedLines(
|
||||
unusedCheckId.value,
|
||||
items.map((l) => ({
|
||||
id: l.id || undefined,
|
||||
usageId: l.usageId || undefined,
|
||||
batchId: l.batchId || undefined,
|
||||
materialName: l.materialName.trim(),
|
||||
|
||||
@@ -25,6 +25,7 @@ export function createEmptySnapshots(): EditorSnapshots {
|
||||
export function serializeInputs(rows: Record<string, any>[]): string {
|
||||
return JSON.stringify(
|
||||
rows.map((r) => ({
|
||||
id: r.id || '',
|
||||
inputType: r.inputType,
|
||||
inputId: r.inputId,
|
||||
inputCode: r.inputCode,
|
||||
@@ -62,6 +63,7 @@ export function serializeEquipmentIds(ids: string[]): string {
|
||||
|
||||
function normalizeReagents(rows: Record<string, any>[] | undefined) {
|
||||
return (rows || []).map((r) => ({
|
||||
id: r.id || '',
|
||||
reagentCode: r.reagentCode,
|
||||
reagentName: r.reagentName,
|
||||
spec: r.spec,
|
||||
@@ -76,6 +78,7 @@ function normalizeReagents(rows: Record<string, any>[] | undefined) {
|
||||
|
||||
function normalizeConsumables(rows: Record<string, any>[] | undefined) {
|
||||
return (rows || []).map((c) => ({
|
||||
id: c.id || '',
|
||||
consumableCode: c.consumableCode,
|
||||
consumableName: c.consumableName,
|
||||
specModel: c.specModel,
|
||||
@@ -90,6 +93,7 @@ function normalizeConsumables(rows: Record<string, any>[] | undefined) {
|
||||
|
||||
function normalizeConfiguredReagents(rows: Record<string, any>[] | undefined) {
|
||||
return (rows || []).map((r) => ({
|
||||
id: r.id || '',
|
||||
configuredReagentCode: r.configuredReagentCode,
|
||||
configuredReagentName: r.configuredReagentName,
|
||||
spec: r.spec,
|
||||
@@ -103,6 +107,7 @@ function normalizeConfiguredReagents(rows: Record<string, any>[] | undefined) {
|
||||
|
||||
function normalizeStepOutputs(rows: Record<string, any>[] | undefined) {
|
||||
return (rows || []).map((o) => ({
|
||||
id: o.id || '',
|
||||
outputTypeId: o.outputTypeId,
|
||||
outputName: o.outputName,
|
||||
outputBarcode: o.outputBarcode,
|
||||
@@ -112,6 +117,7 @@ function normalizeStepOutputs(rows: Record<string, any>[] | undefined) {
|
||||
|
||||
function normalizeDataCollections(rows: Record<string, any>[] | undefined) {
|
||||
return (rows || []).map((d) => ({
|
||||
id: d.id || '',
|
||||
collectionName: d.collectionName,
|
||||
unit: d.unit,
|
||||
collectionValue: d.collectionValue || '',
|
||||
|
||||
@@ -48,7 +48,11 @@ import type { FinanceAttachmentItem } from '@/components/business/protocol-finan
|
||||
import { financeAttachmentsToUrlString } from '@/components/business/protocol-finance-confirm-fields/finance-attachments'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import { normalizePlannedStartDate, extractProductionTaskSteps } from '@/utils/production-labels'
|
||||
import { createEmptyStepOutputRow } from '@/utils/production-step-output'
|
||||
import {
|
||||
createEmptyStepOutputRow,
|
||||
createStableRowId,
|
||||
toStepOutputReplaceItem
|
||||
} from '@/utils/production-step-output'
|
||||
import { resolveCellProductSpecDisplay } from '@/utils/cell-product-spec'
|
||||
import { countOutputsMissingStorageSlot } from '@/utils/storage-slot-options'
|
||||
import {
|
||||
@@ -1322,6 +1326,7 @@ export function useTaskStepEditor() {
|
||||
if (!selectedStep.value) return
|
||||
if (!selectedStep.value.reagents) selectedStep.value.reagents = []
|
||||
selectedStep.value.reagents.push({
|
||||
id: createStableRowId(),
|
||||
reagentId: '',
|
||||
reagentCode: '',
|
||||
reagentName: '',
|
||||
@@ -1475,6 +1480,7 @@ export function useTaskStepEditor() {
|
||||
if (!validateExecutionReagents()) return
|
||||
try {
|
||||
const items = (selectedStep.value.reagents || []).map((r: Record<string, any>) => ({
|
||||
id: r.id || '',
|
||||
material_code: r.reagentCode,
|
||||
material_name: r.reagentName,
|
||||
spec: r.spec,
|
||||
@@ -1496,6 +1502,7 @@ export function useTaskStepEditor() {
|
||||
if (!selectedStep.value) return
|
||||
if (!selectedStep.value.consumables) selectedStep.value.consumables = []
|
||||
selectedStep.value.consumables.push({
|
||||
id: createStableRowId(),
|
||||
consumableId: '',
|
||||
consumableCode: '',
|
||||
consumableName: '',
|
||||
@@ -1516,6 +1523,7 @@ export function useTaskStepEditor() {
|
||||
if (!validateExecutionConsumables()) return
|
||||
try {
|
||||
const items = (selectedStep.value.consumables || []).map((c: Record<string, any>) => ({
|
||||
id: c.id || '',
|
||||
material_code: c.consumableCode,
|
||||
material_name: c.consumableName,
|
||||
spec_model: c.specModel,
|
||||
@@ -1651,6 +1659,7 @@ export function useTaskStepEditor() {
|
||||
if (!selectedStep.value) return
|
||||
if (!selectedStep.value.configuredReagents) selectedStep.value.configuredReagents = []
|
||||
selectedStep.value.configuredReagents.push({
|
||||
id: createStableRowId(),
|
||||
configuredReagentId: '',
|
||||
configuredReagentCode: '',
|
||||
configuredReagentName: '',
|
||||
@@ -1716,6 +1725,7 @@ export function useTaskStepEditor() {
|
||||
if (!validateExecutionConfiguredReagents()) return
|
||||
try {
|
||||
const items = (selectedStep.value.configuredReagents || []).map((r: Record<string, any>) => ({
|
||||
id: r.id || '',
|
||||
material_code: r.configuredReagentCode,
|
||||
material_name: r.configuredReagentName,
|
||||
spec: r.spec,
|
||||
@@ -1750,18 +1760,40 @@ export function useTaskStepEditor() {
|
||||
}
|
||||
function removeOutputRow(i: number) {
|
||||
selectedStep.value?.outputs?.splice(i, 1)
|
||||
void saveOutputs()
|
||||
}
|
||||
async function saveOutputs() {
|
||||
let outputSaveQueue: Promise<void> = Promise.resolve()
|
||||
|
||||
function saveOutputs() {
|
||||
if (!selectedStep.value || configSuppressSave.value) return
|
||||
const step = selectedStep.value
|
||||
const currentTaskId = taskId.value
|
||||
const scope = operateScope.value
|
||||
const run = outputSaveQueue.then(() => persistStepOutputs(step, currentTaskId, scope))
|
||||
outputSaveQueue = run.then(
|
||||
() => undefined,
|
||||
() => undefined
|
||||
)
|
||||
return run
|
||||
}
|
||||
|
||||
async function persistStepOutputs(
|
||||
step: Record<string, any>,
|
||||
currentTaskId: string,
|
||||
scope: typeof operateScope.value
|
||||
) {
|
||||
try {
|
||||
const items = (selectedStep.value.outputs || []).map((o: Record<string, any>) => ({
|
||||
output_type_id: o.outputTypeId,
|
||||
output_name: o.outputName,
|
||||
output_barcode: o.outputBarcode,
|
||||
remark: o.remark || ''
|
||||
}))
|
||||
await fetchReplaceStepOutputs(taskId.value, selectedStep.value.id, items, operateScope.value)
|
||||
refreshStepSnapshot(selectedStep.value.id)
|
||||
const outputs = step.outputs || []
|
||||
const items = outputs.map((row: Record<string, any>) => toStepOutputReplaceItem(row))
|
||||
const saved = await fetchReplaceStepOutputs(currentTaskId, step.id, items, scope)
|
||||
if (Array.isArray(saved)) {
|
||||
saved.forEach((item: Record<string, any>, index: number) => {
|
||||
const id = String(item?.id || '').trim()
|
||||
const row = outputs[index]
|
||||
if (id && row && !String(row.id || '').trim()) row.id = id
|
||||
})
|
||||
}
|
||||
refreshStepSnapshot(step.id)
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
@@ -1771,6 +1803,7 @@ export function useTaskStepEditor() {
|
||||
if (!selectedStep.value) return
|
||||
if (!selectedStep.value.dataCollections) selectedStep.value.dataCollections = []
|
||||
selectedStep.value.dataCollections.push({
|
||||
id: createStableRowId(),
|
||||
collectionName: '',
|
||||
unit: '',
|
||||
orderNum: 0,
|
||||
@@ -1784,6 +1817,7 @@ export function useTaskStepEditor() {
|
||||
if (!selectedStep.value || configSuppressSave.value) return
|
||||
try {
|
||||
const items = (selectedStep.value.dataCollections || []).map((d: Record<string, any>) => ({
|
||||
id: d.id || '',
|
||||
collection_name: d.collectionName,
|
||||
unit: d.unit,
|
||||
collection_value: d.collectionValue || '',
|
||||
@@ -2000,6 +2034,7 @@ export function useTaskStepEditor() {
|
||||
operateScope.value
|
||||
)
|
||||
const items = (step.reagents || []).map((r: Record<string, any>) => ({
|
||||
id: r.id || '',
|
||||
material_code: r.reagentCode,
|
||||
material_name: r.reagentName,
|
||||
spec: r.spec,
|
||||
@@ -2012,6 +2047,7 @@ export function useTaskStepEditor() {
|
||||
}))
|
||||
await fetchReplaceStepReagents(taskId.value, step.id, items, operateScope.value)
|
||||
const consumableItems = (step.consumables || []).map((c: Record<string, any>) => ({
|
||||
id: c.id || '',
|
||||
material_code: c.consumableCode,
|
||||
material_name: c.consumableName,
|
||||
spec_model: c.specModel,
|
||||
@@ -2029,6 +2065,7 @@ export function useTaskStepEditor() {
|
||||
operateScope.value
|
||||
)
|
||||
const configuredItems = (step.configuredReagents || []).map((r: Record<string, any>) => ({
|
||||
id: r.id || '',
|
||||
material_code: r.configuredReagentCode,
|
||||
material_name: r.configuredReagentName,
|
||||
spec: r.spec,
|
||||
@@ -2044,14 +2081,12 @@ export function useTaskStepEditor() {
|
||||
configuredItems,
|
||||
operateScope.value
|
||||
)
|
||||
const outputItems = (step.outputs || []).map((o: Record<string, any>) => ({
|
||||
output_type_id: o.outputTypeId,
|
||||
output_name: o.outputName,
|
||||
output_barcode: o.outputBarcode,
|
||||
remark: o.remark || ''
|
||||
}))
|
||||
const outputItems = (step.outputs || []).map((o: Record<string, any>) =>
|
||||
toStepOutputReplaceItem(o)
|
||||
)
|
||||
await fetchReplaceStepOutputs(taskId.value, step.id, outputItems, operateScope.value)
|
||||
const dataItems = (step.dataCollections || []).map((d: Record<string, any>) => ({
|
||||
id: d.id || '',
|
||||
collection_name: d.collectionName,
|
||||
unit: d.unit,
|
||||
collection_value: d.collectionValue || '',
|
||||
|
||||
@@ -1385,6 +1385,7 @@
|
||||
await fetchSaveQcMaterialStockCheckUnusedLines(
|
||||
unusedCheckId.value,
|
||||
items.map((l) => ({
|
||||
id: l.id || undefined,
|
||||
usageId: l.usageId || undefined,
|
||||
batchId: l.batchId || undefined,
|
||||
materialName: l.materialName.trim(),
|
||||
|
||||
@@ -30,7 +30,48 @@
|
||||
</el-tag>
|
||||
</div>
|
||||
|
||||
<!-- 检测参数区已隐藏:加载仍拉取、保存原样回传,避免清空历史;详情弹窗仍只读展示 -->
|
||||
<div class="section-title">检测参数</div>
|
||||
<div v-if="!item.params.length" class="company-item-block__empty">该项目暂无配置参数</div>
|
||||
<div v-else class="form-grid-3">
|
||||
<el-form-item
|
||||
v-for="(param, idx) in item.params"
|
||||
:key="param.paramId || param.paramCode || idx"
|
||||
:label="paramLabel(param)"
|
||||
>
|
||||
<el-input
|
||||
v-if="param.paramType === 'text'"
|
||||
v-model="param.paramValue"
|
||||
placeholder="请输入"
|
||||
:disabled="readonly"
|
||||
/>
|
||||
<DecimalInput
|
||||
v-else-if="param.paramType === 'number'"
|
||||
:model-value="toResultNumber(param.paramValue)"
|
||||
:precision="2"
|
||||
:disabled="readonly"
|
||||
style="width: 100%"
|
||||
placeholder="请输入"
|
||||
@update:model-value="(v) => setResultNumber(param, v)"
|
||||
/>
|
||||
<el-select
|
||||
v-else-if="param.paramType === 'select'"
|
||||
v-model="param.paramValue"
|
||||
placeholder="请选择"
|
||||
clearable
|
||||
filterable
|
||||
:disabled="readonly"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="opt in parseQcParamOptions(param.options)"
|
||||
:key="opt"
|
||||
:label="opt"
|
||||
:value="opt"
|
||||
/>
|
||||
</el-select>
|
||||
<el-input v-else v-model="param.paramValue" placeholder="请输入" :disabled="readonly" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div class="section-title">检测结论</div>
|
||||
<div class="form-grid-2 result-conclusion-row">
|
||||
<el-form-item label="任务状态" required>
|
||||
@@ -113,6 +154,7 @@
|
||||
type QcTaskResultItem
|
||||
} from '@/api/qc-task-result'
|
||||
import { taskStatusLabel, taskStatusTagType } from '@/utils/qc-labels'
|
||||
import { parseQcParamOptions } from '@/utils/qc-item-param-options'
|
||||
import {
|
||||
pickSharedRemarkAndAttachments,
|
||||
sortThirdPartyTasksByCompany,
|
||||
@@ -225,6 +267,21 @@
|
||||
return v !== '' && v !== undefined && v !== null
|
||||
}
|
||||
|
||||
function paramLabel(param: QcTaskResultItem) {
|
||||
const unit = (param.paramUnit || '').trim()
|
||||
return unit ? `${param.paramName}(${unit})` : param.paramName
|
||||
}
|
||||
|
||||
function toResultNumber(v: string | undefined) {
|
||||
if (v === '' || v === undefined || v === null) return undefined
|
||||
const n = Number(v)
|
||||
return Number.isFinite(n) ? n : undefined
|
||||
}
|
||||
|
||||
function setResultNumber(param: QcTaskResultItem, v: number | undefined) {
|
||||
param.paramValue = v === undefined || v === null ? '' : String(v)
|
||||
}
|
||||
|
||||
function onAttachmentPreview(file: Record<string, unknown>) {
|
||||
qcPreviewAttachment(file)
|
||||
}
|
||||
@@ -318,6 +375,12 @@
|
||||
ElMessage.warning(`请选择「${item.itemName}」的检测结论`)
|
||||
return
|
||||
}
|
||||
for (const param of item.params) {
|
||||
if (param.requiredFlag === '1' && !hasResultValue(param.paramValue)) {
|
||||
ElMessage.warning(`请填写「${item.itemName}」的参数「${param.paramName}」`)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const thirdPartyAttachments = financeAttachmentsToUrlString(attachments.value)
|
||||
@@ -415,6 +478,12 @@
|
||||
color: var(--mint-text-muted, #64748b);
|
||||
}
|
||||
|
||||
&__empty {
|
||||
margin: 0 0 12px;
|
||||
font-size: 13px;
|
||||
color: var(--mint-text-muted, #64748b);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user