fix: 协议子资源单事务保存,并补齐样本编码/OA/质检等近期能力

锁定缴费校验失败时整单回滚,避免样本半成功落库;前端禁止删除锁定缴费样本,改码走更正编码。同步落地样本更正、采集微信通知、OA提及未读、细胞产品套餐与相关迁移文档。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zlwuandCursor committed 2026-10-26 11:02:25 +08:00
1 parent 19bc66ba45
commit 5dad1bb0e3
142 files changed
+95584 -65717

No files matched your search

@@ -166,12 +166,18 @@
/>
</el-select>
</el-form-item>
<el-form-item label="样本编码">
<el-form-item>
<template #label>
样本编码
<span v-if="isPersistedSampleCodeLocked" class="form-label-tip"
>已落库不可改,请用客户服务「更正编码」</span
>
</template>
<el-input
v-if="autoGenerateSampleCode"
v-if="autoGenerateSampleCode || isPersistedSampleCodeLocked"
:model-value="sampleDraft.sampleCode"
disabled
placeholder="系统自动生成"
:placeholder="autoGenerateSampleCode ? '系统自动生成' : '—'"
/>
<el-select
v-else
@@ -381,6 +387,7 @@
} from '@/components/business/freight-unlink-dialog/index.vue'
import { sampleStatusLabel, sampleStatusTagType } from '@/utils/status-tag-registry'
import { formatMoneyDisplay } from '@/utils/money-format'
import { samplePaymentHasLockedTransfer } from '@/utils/protocol-sample-payment'
import {
buildFreightHeaderPayload,
freightAmountText as formatFreightAmountText,
@@ -424,6 +431,8 @@
siteOptions?: Api.Site.SiteItem[]
expressCompanyOptions?: Api.ExpressCompany.ExpressCompanyItem[]
showPrice?: boolean
/** 按样本缴费方案(用于判断锁定转账,禁止删除对应样本) */
samplePayments?: Array<Record<string, any>>
/** 科研捐赠等:列表与样本弹窗均不展示存储年限及相关采集明细 */
hideCollectDetailFields?: boolean
/** 添加/编辑样本弹窗内隐藏采集量、单位(不影响列表列) */
@@ -440,6 +449,7 @@
{
protocolId: '',
showPrice: false,
samplePayments: () => [],
hideCollectDetailFields: false,
hideDialogCollectAmountUnit: false,
autoGenerateSampleCode: false,
@@ -495,6 +505,12 @@
const canUnlinkFreight = computed(() => selectedSamples.value.some((r) => sampleHasFreight(r)))
/** 已落库样本(有持久化 id)编辑时锁定编码,纠错走客户服务「更正编码」 */
const isPersistedSampleCodeLocked = computed(
() =>
sampleDialogMode.value === 'edit' && Boolean(String(sampleDraft.id || '').trim())
)
function fmtMoney(v: unknown): string {
if (v == null || v === '') return '—'
return '¥' + formatMoneyDisplay(v)
@@ -738,7 +754,7 @@
}
row.storageYears = st.storageYears
}
if (props.autoGenerateSampleCode) return
if (props.autoGenerateSampleCode || isPersistedSampleCodeLocked.value) return
row.sampleCodeId = ''
row.sampleCode = ''
emit('search-sample-code', '', sampleCodeFilterForRow(row))
@@ -1061,6 +1077,7 @@
props.samples.length
)
)
sampleDraft.id = ''
if (props.autoGenerateSampleCode && props.reserveSampleCode) {
try {
sampleDraft.sampleCode = await props.reserveSampleCode()
@@ -1137,7 +1154,27 @@
sampleDialogVisible.value = false
}
function sampleHasLockedPayment(row: Record<string, any>): boolean {
const code = String(row?.sampleCode || '').trim()
const sampleId = String(row?.id || row?.protocolSampleId || '').trim()
return (props.samplePayments || []).some((pay) => {
if (!samplePaymentHasLockedTransfer(pay)) return false
const payCode = String(pay.sampleCode || '').trim()
const paySampleId = String(pay.protocolSampleId || '').trim()
if (code && payCode && code === payCode) return true
if (sampleId && paySampleId && sampleId === paySampleId) return true
return false
})
}
function removeSample(index: number) {
const row = props.samples[index]
if (row && sampleHasLockedPayment(row)) {
ElMessage.warning(
'该样本存在锁定缴费方案,不可删除;请使用客户服务「更正编码」调整样本编码'
)
return
}
const next = [...props.samples]
next.splice(index, 1)
updateSamples(next)
@@ -17,8 +17,8 @@
<div class="val">{{ identity.labFlowCode || '—' }}</div>
</div>
<div class="ctx-item">
<label>协议编码</label>
<div class="val">{{ identity.protocolCode || '—' }}</div>
<label>样本编码</label>
<div class="val">{{ identity.sampleCode || '—' }}</div>
</div>
<div class="ctx-item">
<label>母亲姓名</label>
@@ -32,7 +32,7 @@
<el-empty
v-if="mode === 'view' && !filledFlag && !loading"
description="质检尚未填写"
description="尚未填写"
/>
<template v-else-if="typeKey && groups.length">
@@ -147,7 +147,7 @@
const form = reactive<Record<string, string>>({})
const identity = reactive({
labFlowCode: '',
protocolCode: '',
sampleCode: '',
motherName: '',
sampleTypeName: ''
})
@@ -176,8 +176,8 @@
}
function applyDetail(detail: SampleStorageCertQcDetail) {
identity.labFlowCode = detail.labFlowCode || detail.sampleCode || ''
identity.protocolCode = detail.protocolCode || ''
identity.labFlowCode = detail.labFlowCode || ''
identity.sampleCode = detail.sampleCode || props.sampleCode || ''
identity.motherName = detail.motherName || ''
identity.sampleTypeName = detail.sampleTypeName || props.sampleTypeName || ''
filledFlag.value = !!detail.filledFlag
@@ -199,7 +199,8 @@
typeKey.value = resolveStorageCertQcTypeKey(props.sampleTypeName)
filledFlag.value = false
resetFormKeys(typeKey.value, {})
identity.labFlowCode = code
identity.labFlowCode = ''
identity.sampleCode = code
identity.sampleTypeName = props.sampleTypeName || ''
} finally {
loading.value = false