diff --git a/web-admin/src/components/business/sample-transport-form-section/index.vue b/web-admin/src/components/business/sample-transport-form-section/index.vue index 0f687b70..c976f220 100644 --- a/web-admin/src/components/business/sample-transport-form-section/index.vue +++ b/web-admin/src/components/business/sample-transport-form-section/index.vue @@ -17,10 +17,10 @@ - 取消运费关联 + 取消运费 @@ -106,6 +106,9 @@ > + + + @@ -233,16 +235,24 @@ - + + + + - 特殊样本 + 是 - +

- 将运费应用到已选中的 {{ selectedSamples.length }} 个样本(可跨协议共用一笔)。 + 将运费应用到已选中的 {{ freightConfigTargets.length }} 个样本(可跨协议共用一笔)。

新建运费 @@ -345,6 +355,14 @@ + + @@ -353,12 +371,22 @@ import AmountInput from '@/components/common/amount-input/index.vue' import SampleCodeWithSpecialDisplay from '@/components/business/sample-code-with-special-display/index.vue' import SampleFreightPickTable from '@/components/business/sample-freight-pick-table/index.vue' + import FreightUnlinkDialog, { + type FreightUnlinkRow + } from '@/components/business/freight-unlink-dialog/index.vue' import { sampleStatusLabel, sampleStatusTagType } from '@/utils/status-tag-registry' import { formatMoneyDisplay } from '@/utils/money-format' + import { + partitionFreightConfigSamples, + freightConfigSkipMessage, + freightConfigAllConfiguredMessage, + sampleHasFreight + } from '@/utils/sample-freight-config-guard' import { fetchDictDataOptions } from '@/api/admin' import { fetchCreateSampleFreight, fetchLinkSampleFreight, + fetchSampleFreightDetail, fetchSampleFreightList, fetchUnlinkSampleFreight, type SampleFreight, @@ -439,6 +467,13 @@ const existingFreightKeyword = ref('') const selectedExistingFreight = ref(null) const selectedFreightId = ref('') + /** 本次「配置运费」弹窗实际作用的样本(已过滤已配) */ + const freightConfigTargets = ref>>([]) + + const unlinkDialogVisible = ref(false) + const unlinkDialogItems = ref([]) + const unlinkDefaultKeys = ref([]) + const unlinkSubmitting = ref(false) const sampleDialogVisible = ref(false) const sampleDialogMode = ref<'add' | 'edit'>('add') @@ -446,7 +481,7 @@ const sampleDraft = reactive>({}) const canUnlinkFreight = computed(() => - selectedSamples.value.some((r) => r.freight?.id || r.freightDraft || r.freightId) + selectedSamples.value.some((r) => sampleHasFreight(r)) ) function fmtMoney(v: unknown): string { @@ -483,12 +518,23 @@ : null const freightDraft = raw.freightDraft && typeof raw.freightDraft === 'object' ? { ...raw.freightDraft } : undefined + const transportRaw = raw.transport && typeof raw.transport === 'object' ? raw.transport : {} return { ...raw, sampleStatus, isSpecial: !!raw.isSpecial, specialRemark: String(raw.specialRemark || '').trim(), _rowKey: raw.id || raw._rowKey || `new-${++sampleRowSeq}`, + transport: { + transportType: transportRaw.transportType || '', + expressCompanyId: transportRaw.expressCompanyId || '', + waybillNo: transportRaw.waybillNo || '', + destSiteId: transportRaw.destSiteId || '', + expressFee: transportRaw.expressFee ?? null, + etaTime: transportRaw.etaTime || '', + carrierName: transportRaw.carrierName || '', + carrierPhone: transportRaw.carrierPhone || '' + }, freight, freightDraft, freightId: freight?.id || raw.freightId || '' @@ -535,18 +581,26 @@ } function sampleCodeOptionLabel(sc: Api.BizSampleCode.SampleCodeItem) { - if (sc.usedInProtocol) return `${sc.sampleCode}(已占用)` + const currentId = String(sampleDraft.sampleCodeId || '') + // 当前编辑行自己的编码仅展示编码本身,避免「已占用」干扰回显 + if (currentId && sc.id === currentId) return sc.sampleCode if (sc.issued) return `${sc.sampleCode}(已下发)` return sc.sampleCode } - function isSampleCodeOptionDisabled( - sc: Api.BizSampleCode.SampleCodeItem, - row: Record - ) { - if (sc.usedInProtocol && String(row.sampleCodeId || '') !== sc.id) return true - return props.samples.some((s) => s !== row && String(s.sampleCodeId || '') === sc.id) - } + /** 已占用 / 本单其它样本已选:不展示;当前编辑行编码始终保留以便回显 */ + const selectableSampleCodeOptions = computed(() => { + const currentId = String(sampleDraft.sampleCodeId || '') + const editingIndex = sampleDialogIndex.value + return props.sampleCodeOptions.filter((sc) => { + if (currentId && sc.id === currentId) return true + if (sc.usedInProtocol) return false + return !props.samples.some( + (s, i) => + (editingIndex < 0 || i !== editingIndex) && String(s.sampleCodeId || '') === sc.id + ) + }) + }) function sampleCodeFilterForRow(row: Record): string | undefined { if (!row.sampleTypeId) return undefined @@ -716,6 +770,15 @@ ElMessage.warning('请先勾选需要配置运费的样本') return } + const { configurable, skipped } = partitionFreightConfigSamples(selectedSamples.value) + if (!configurable.length) { + ElMessage.warning(freightConfigAllConfiguredMessage()) + return + } + if (skipped.length) { + ElMessage.info(freightConfigSkipMessage(skipped.length)) + } + freightConfigTargets.value = configurable freightMode.value = 'create' freightForm.transportMode = '' freightForm.freightAmount = null @@ -733,7 +796,9 @@ } async function confirmFreight() { - const selected = selectedSamples.value + const selected = freightConfigTargets.value.length + ? freightConfigTargets.value + : selectedSamples.value if (!selected.length) return const persisted = selected.filter((r) => r.id) const drafts = selected.filter((r) => !r.id) @@ -805,41 +870,137 @@ } } - async function unlinkSelectedFreight() { - const selected = selectedSamples.value.filter( - (r) => r.freight?.id || r.freightDraft || r.freightId - ) - if (!selected.length) { - ElMessage.warning('请勾选已关联运费的样本') - return - } - try { - await ElMessageBox.confirm('确定取消所选样本的运费关联吗?', '取消运费关联', { - type: 'warning', - confirmButtonText: '确定', - cancelButtonText: '取消' - }) - } catch { - return - } - const byFreight = new Map>>() - for (const row of selected) { - const fid = String(row.freight?.id || row.freightId || '') - if (!fid) continue - if (!byFreight.has(fid)) byFreight.set(fid, []) - byFreight.get(fid)!.push(row) - } - for (const [fid, rows] of byFreight) { - await fetchUnlinkSampleFreight(fid, toFreightItems(rows)) - } - const selectedSet = new Set(selected) + function unlinkRowKey(it: { sampleSource: string; sampleId: string }) { + return `${it.sampleSource}:${it.sampleId}` + } + + function clearLocalFreight(rows: Array>) { updateSamples( props.samples.map((row) => { - if (!selectedSet.has(row)) return row + const hit = rows.some( + (r) => + (r.id && row.id && r.id === row.id) || + (r._rowKey && row._rowKey && r._rowKey === row._rowKey) + ) + if (!hit) return row return { ...row, freight: null, freightId: '', freightDraft: undefined } }) ) - ElMessage.success('已取消运费关联') + } + + async function openUnlinkFreightDialog() { + const seeds = selectedSamples.value.filter((r) => sampleHasFreight(r)) + if (!seeds.length) { + ElMessage.warning('请先选择已配置运费的样本') + return + } + + const draftOnly = seeds.filter((s) => !s.id && s.freightDraft) + const persistedSeeds = seeds.filter((s) => s.id) + + if (!persistedSeeds.length && draftOnly.length) { + try { + await ElMessageBox.confirm( + '确定取消所选样本的运费吗?取消后可再次配置运费。', + '取消运费', + { type: 'warning', confirmButtonText: '确定', cancelButtonText: '取消' } + ) + } catch { + return + } + clearLocalFreight(draftOnly) + ElMessage.success('已取消运费') + return + } + + const freightIds = [ + ...new Set( + persistedSeeds + .map((r) => String(r.freight?.id || r.freightId || '').trim()) + .filter(Boolean) + ) + ] + const byKey = new Map() + for (const fid of freightIds) { + try { + const res = await fetchSampleFreightDetail(fid) + const data = ((res as any)?.data || res) as SampleFreight + for (const it of data.items || []) { + const row: FreightUnlinkRow = { + ...it, + freightId: fid, + rowKey: unlinkRowKey(it) + } + byKey.set(row.rowKey, row) + } + } catch { + /* 单批失败时用种子兜底 */ + } + } + for (const s of persistedSeeds) { + const sampleId = String(s.id) + const key = unlinkRowKey({ sampleSource: props.sampleSource, sampleId }) + if (!byKey.has(key)) { + byKey.set(key, { + sampleSource: props.sampleSource, + sampleId, + protocolId: props.protocolId || undefined, + sampleCode: s.sampleCode, + sampleTypeName: sampleTypeName(s.sampleTypeId), + freightId: String(s.freight?.id || s.freightId || ''), + rowKey: key + }) + } + } + + unlinkDialogItems.value = [...byKey.values()] + unlinkDefaultKeys.value = persistedSeeds.map((s) => + unlinkRowKey({ sampleSource: props.sampleSource, sampleId: String(s.id) }) + ) + if (!unlinkDialogItems.value.length) { + ElMessage.warning('没有可取消的运费样本') + return + } + unlinkDialogVisible.value = true + } + + async function onUnlinkDialogConfirm(rows: FreightUnlinkRow[]) { + unlinkSubmitting.value = true + try { + const byFreight = new Map() + for (const row of rows) { + const fid = String(row.freightId || '').trim() + if (!fid) continue + if (!byFreight.has(fid)) byFreight.set(fid, []) + byFreight.get(fid)!.push(row) + } + for (const [fid, list] of byFreight) { + await fetchUnlinkSampleFreight( + fid, + list.map((x) => ({ + sampleSource: x.sampleSource, + sampleId: x.sampleId, + protocolId: x.protocolId, + sampleCode: x.sampleCode + })) + ) + } + const idSet = new Set(rows.map((r) => r.sampleId)) + updateSamples( + props.samples.map((row) => { + if (!row.id || !idSet.has(String(row.id))) return row + return { ...row, freight: null, freightId: '', freightDraft: undefined } + }) + ) + const draftSeeds = selectedSamples.value.filter((r) => !r.id && r.freightDraft) + if (draftSeeds.length) clearLocalFreight(draftSeeds) + unlinkDialogVisible.value = false + ElMessage.success('已取消运费') + } catch (e: any) { + ElMessage.error(e?.message || '取消运费失败') + } finally { + unlinkSubmitting.value = false + } } function onSpecialSampleToggle(checked: boolean | string | number) {