- 将运费应用到已选中的 {{ 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) {