fix: 子表保存改为就地同步,避免整批换 id 打断外键。

协议样本、生产产出和盘点明细按行更新并保留原 id;补齐存储证明质检预填、离职人员显示名,以及样本采集通知模板字段。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zlwuandCursor committed 2026-10-28 10:51:53 +08:00
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 || ''
}
}
+61 -16
View File
@@ -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,
@@ -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;
}