feat(web-admin): gate protocol freight config and unify cancel dialog
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
75bae974e4
commit
8c0cbb8ab8
1 file changed
+211
-50
@@ -17,10 +17,10 @@
|
||||
<el-button
|
||||
class="btn-ghost"
|
||||
:disabled="!canUnlinkFreight"
|
||||
@click="unlinkSelectedFreight"
|
||||
@click="openUnlinkFreightDialog"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>
|
||||
取消运费关联
|
||||
取消运费
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
@@ -106,6 +106,9 @@
|
||||
>
|
||||
<template #default="{ row }">{{ freightAmountText(row) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="预计到达" min-width="160" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ row.transport?.etaTime || '—' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
label="操作"
|
||||
min-width="180"
|
||||
@@ -192,11 +195,10 @@
|
||||
@change="onSampleCodeChange(sampleDraft)"
|
||||
>
|
||||
<el-option
|
||||
v-for="sc in sampleCodeOptions"
|
||||
v-for="sc in selectableSampleCodeOptions"
|
||||
:key="sc.id"
|
||||
:label="sampleCodeOptionLabel(sc)"
|
||||
:value="sc.id"
|
||||
:disabled="isSampleCodeOptionDisabled(sc, sampleDraft)"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
@@ -233,16 +235,24 @@
|
||||
<el-option label="已作废" value="yzf" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item class="form-col-span-2">
|
||||
<el-form-item label="预计到达时间">
|
||||
<el-date-picker
|
||||
v-model="sampleDraft.transport.etaTime"
|
||||
type="datetime"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
placeholder="请选择预计到达时间"
|
||||
clearable
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="特殊样本">
|
||||
<el-checkbox v-model="sampleDraft.isSpecial" @change="onSpecialSampleToggle">
|
||||
特殊样本
|
||||
是
|
||||
</el-checkbox>
|
||||
</el-form-item>
|
||||
<el-form-item label="特殊说明" class="form-col-span-2">
|
||||
<el-form-item label="特殊说明">
|
||||
<el-input
|
||||
v-model="sampleDraft.specialRemark"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
:disabled="!sampleDraft.isSpecial"
|
||||
maxlength="256"
|
||||
show-word-limit
|
||||
@@ -276,7 +286,7 @@
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<p class="sample-transport-hint batch-transport-dialog-hint">
|
||||
将运费应用到已选中的 {{ selectedSamples.length }} 个样本(可跨协议共用一笔)。
|
||||
将运费应用到已选中的 {{ freightConfigTargets.length }} 个样本(可跨协议共用一笔)。
|
||||
</p>
|
||||
<el-radio-group v-model="freightMode" class="freight-mode-radio" style="margin-bottom: 12px">
|
||||
<el-radio-button value="create">新建运费</el-radio-button>
|
||||
@@ -345,6 +355,14 @@
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<FreightUnlinkDialog
|
||||
v-model="unlinkDialogVisible"
|
||||
:items="unlinkDialogItems"
|
||||
:default-selected-keys="unlinkDefaultKeys"
|
||||
:submitting="unlinkSubmitting"
|
||||
@confirm="onUnlinkDialogConfirm"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -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<SampleFreight | null>(null)
|
||||
const selectedFreightId = ref('')
|
||||
/** 本次「配置运费」弹窗实际作用的样本(已过滤已配) */
|
||||
const freightConfigTargets = ref<Array<Record<string, any>>>([])
|
||||
|
||||
const unlinkDialogVisible = ref(false)
|
||||
const unlinkDialogItems = ref<FreightUnlinkRow[]>([])
|
||||
const unlinkDefaultKeys = ref<string[]>([])
|
||||
const unlinkSubmitting = ref(false)
|
||||
|
||||
const sampleDialogVisible = ref(false)
|
||||
const sampleDialogMode = ref<'add' | 'edit'>('add')
|
||||
@@ -446,7 +481,7 @@
|
||||
const sampleDraft = reactive<Record<string, any>>({})
|
||||
|
||||
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<string, any>
|
||||
) {
|
||||
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, any>): 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<string, Array<Record<string, any>>>()
|
||||
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<Record<string, any>>) {
|
||||
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<string, FreightUnlinkRow>()
|
||||
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<string, FreightUnlinkRow[]>()
|
||||
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) {
|
||||
|
||||
Reference in new issue
Block a user