This commit is contained in:
2026-11-06 13:33:14 +08:00
parent 110d52fd5b
commit e10e079565
8 changed files with 61 additions and 12 deletions
@@ -25,7 +25,8 @@ describe('InboundTaskList measurement scope', () => {
expect(toolbar).toContain('</h1></slot>') expect(toolbar).toContain('</h1></slot>')
expect(filters).toContain('<QueryToolbar') expect(filters).toContain('<QueryToolbar')
expect(filters).not.toContain('name="status-tabs"') expect(filters).not.toContain('name="status-tabs"')
expect(source).toContain('<template v-if="isReview && access.can(actionPermission)" #toolbar-actions>') expect(source).toContain('<template #toolbar-actions>')
expect(source).toContain('v-if="isReview && access.can(actionPermission)" type="success"')
}) })
it('supports selecting only pending measurement reviews for batch approval', () => { it('supports selecting only pending measurement reviews for batch approval', () => {
@@ -39,7 +39,7 @@ const props = defineProps<{
title?: string title?: string
taskLoader?: (pagination: { page: number; size: number }) => Promise<InboundPageResult> taskLoader?: (pagination: { page: number; size: number }) => Promise<InboundPageResult>
}>() }>()
const emit = defineEmits<{ changed: [] }>() const emit = defineEmits<{ changed: []; selectionChange: [rows: InboundOrderSummary[]] }>()
const router = useRouter() const router = useRouter()
const access = useAccessPreviewStore() const access = useAccessPreviewStore()
const reviewPanelRef = ref<InstanceType<typeof MeasurementReviewPanel> | null>(null) const reviewPanelRef = ref<InstanceType<typeof MeasurementReviewPanel> | null>(null)
@@ -169,7 +169,7 @@ async function loadOptions() {
} }
async function loadTasks() { async function loadTasks() {
selectedReviewRows.value = [] handleReviewSelectionChange([])
const serial = ++requestSerial const serial = ++requestSerial
loading.value = true loading.value = true
loadError.value = null loadError.value = null
@@ -270,6 +270,7 @@ async function handleTaskChanged() {
function handleReviewSelectionChange(rows: InboundOrderSummary[]) { function handleReviewSelectionChange(rows: InboundOrderSummary[]) {
selectedReviewRows.value = rows.filter((row) => row.measureReviewStatus === 'PENDING') selectedReviewRows.value = rows.filter((row) => row.measureReviewStatus === 'PENDING')
emit('selectionChange', selectedReviewRows.value)
} }
async function batchApproveMeasurements() { async function batchApproveMeasurements() {
@@ -399,8 +400,9 @@ onBeforeUnmount(() => { disposed = true; requestSerial += 1; optionsRequestSeria
</template></template> </template></template>
<template v-if="isReview && access.can(actionPermission)" #toolbar-actions> <template #toolbar-actions>
<el-button type="success" :loading="batchReviewLoading" :disabled="loading || !selectedReviewRows.length" @click="batchApproveMeasurements"><CircleCheckBig :size="16" />批量审核通过<span v-if="selectedReviewRows.length">({{ selectedReviewRows.length }})</span></el-button> <el-button v-if="isReview && access.can(actionPermission)" type="success" :loading="batchReviewLoading" :disabled="loading || !selectedReviewRows.length" @click="batchApproveMeasurements"><CircleCheckBig :size="16" />批量审核通过<span v-if="selectedReviewRows.length">({{ selectedReviewRows.length }})</span></el-button>
<slot name="toolbar-actions" :loading="loading || batchReviewLoading" />
</template> </template>
<template #pagination-left>当前显示 {{ result.records.length }} 条 · 共 {{ result.total }} 条</template> <template #pagination-left>当前显示 {{ result.records.length }} 条 · 共 {{ result.total }} 条</template>
<template #actions="{ row }"><div class="row-actions density-actions admin-text-actions"> <template #actions="{ row }"><div class="row-actions density-actions admin-text-actions">
@@ -27,7 +27,7 @@ const detail = ref<LocalLooseboardDetail | null>(null), loading = ref(false)
const error = ref<ApiErrorDetails | null>(null) const error = ref<ApiErrorDetails | null>(null)
let requestSequence = 0 let requestSequence = 0
const dateTime = (value: string | null | undefined) => value ? value.replace('T', ' ').slice(0, 16) : '—' const dateTime = (value: string | null | undefined) => value ? value.replace('T', ' ').slice(0, 16) : '—'
const statusLabels: Record<string, string> = { PENDING_ACCEPTANCE: '待接单', DISPATCHED: '已派车', ...poolOrderStatusLabels } const statusLabels: Record<string, string> = { FUNDING_PENDING: '资金处理中', FUNDING_FAILED: '资金冻结失败', PENDING_ACCEPTANCE: '待接单', DISPATCHED: '已派车', ...poolOrderStatusLabels }
const statusLabel = (value: string | null) => value ? statusLabels[value] || value : '—' const statusLabel = (value: string | null) => value ? statusLabels[value] || value : '—'
const fields = computed(() => detail.value ? [ const fields = computed(() => detail.value ? [
['客户编号', detail.value.customerId], ['商品名称', detail.value.productName], ['预计托数', detail.value.estimatedPalletCount], ['客户编号', detail.value.customerId], ['商品名称', detail.value.productName], ['预计托数', detail.value.estimatedPalletCount],
@@ -42,7 +42,7 @@ const page = ref(1); const pageSize = ref(20); const total = ref(0)
const arrangementOpen = ref(false), arrangement = ref<FleetArrangement | null>(null) const arrangementOpen = ref(false), arrangement = ref<FleetArrangement | null>(null)
function showArrangement(row: LocalTransportOrder | LocalLooseboardOrder) { arrangement.value = row.fleetArrangement; arrangementOpen.value = true } function showArrangement(row: LocalTransportOrder | LocalLooseboardOrder) { arrangement.value = row.fleetArrangement; arrangementOpen.value = true }
const filters = ref<QueryModel>({ orderNo: String(route.query.orderNo || ''), status: '', routeKeyword: '' }); const applied = ref({ ...filters.value }) const filters = ref<QueryModel>({ orderNo: String(route.query.orderNo || ''), status: '', routeKeyword: '' }); const applied = ref({ ...filters.value })
const looseboardStatusLabels: Record<LocalLooseboardStatus, string> = { PENDING_ACCEPTANCE: '待接单', WAITING_INBOUND: '待入仓', RECEIVING: '收货中', WAITING_CONSOLIDATION: '待合托', PENDING_DISPATCH: '待派车', DISPATCHED: '已派车', IN_TRANSIT: '派送中', COMPLETED: '已完成', CANCELLED: '已取消', ...poolOrderStatusLabels } const looseboardStatusLabels: Record<LocalLooseboardStatus, string> = { FUNDING_PENDING: '资金处理中', FUNDING_FAILED: '资金冻结失败', PENDING_ACCEPTANCE: '待接单', WAITING_INBOUND: '待入仓', RECEIVING: '收货中', WAITING_CONSOLIDATION: '待合托', PENDING_DISPATCH: '待派车', DISPATCHED: '已派车', IN_TRANSIT: '派送中', COMPLETED: '已完成', CANCELLED: '已取消', ...poolOrderStatusLabels }
const transportTypeLabels: Record<LocalTransportType, string> = { PORT_PICKUP: '港口提柜', WAREHOUSE_TO_WAREHOUSE: '仓到仓', WAREHOUSE_TO_DOOR: '仓到门' } const transportTypeLabels: Record<LocalTransportType, string> = { PORT_PICKUP: '港口提柜', WAREHOUSE_TO_WAREHOUSE: '仓到仓', WAREHOUSE_TO_DOOR: '仓到门' }
const statusLabels = computed<Record<string, string>>(() => isLooseboard.value ? looseboardStatusLabels : truckStatusLabels) const statusLabels = computed<Record<string, string>>(() => isLooseboard.value ? looseboardStatusLabels : truckStatusLabels)
const statusTones: Record<LocalTransportStatus | LocalLooseboardStatus, 'warning' | 'primary' | 'success' | 'danger' | 'info'> = { const statusTones: Record<LocalTransportStatus | LocalLooseboardStatus, 'warning' | 'primary' | 'success' | 'danger' | 'info'> = {
@@ -75,7 +75,7 @@ export function transportDemandRules(kind: TransportDemandKind, form: TransportD
if (field === 'packageCount' && kind === 'truck') continue if (field === 'packageCount' && kind === 'truck') continue
rules[field] = [{ required: true, type: 'number', min: 0, message: `${label}须为不小于 0 的数字`, trigger: 'change' }] rules[field] = [{ required: true, type: 'number', min: 0, message: `${label}须为不小于 0 的数字`, trigger: 'change' }]
} }
rules.budgetAmount = [{ required: kind === 'truck', type: 'number', min: kind === 'truck' ? 0.01 : 0, message: '请输入大于 0 的预算总价', trigger: 'change' }] rules.budgetAmount = [{ required: true, type: 'number', min: kind === 'truck' ? 0.01 : 0, message: kind === 'truck' ? '请输入大于 0 的预算总价' : '请输入预算总价,不能小于 0', trigger: 'change' }]
const pickupLabel = kind === 'truck' ? '取货时间' : '取货时间', deliveryLabel = kind === 'truck' ? '送达时间' : '送货时间' const pickupLabel = kind === 'truck' ? '取货时间' : '取货时间', deliveryLabel = kind === 'truck' ? '送达时间' : '送货时间'
rules.pickupAt = [{ required: true, message: `请选择${pickupLabel}`, trigger: 'change' }] rules.pickupAt = [{ required: true, message: `请选择${pickupLabel}`, trigger: 'change' }]
rules.deliveryAt = [{ required: true, message: `请选择${deliveryLabel}`, trigger: 'change' }, { rules.deliveryAt = [{ required: true, message: `请选择${deliveryLabel}`, trigger: 'change' }, {
@@ -110,6 +110,7 @@ export function transportDemandError(kind: TransportDemandKind, form: TransportD
if (!Number.isInteger(form.packageCount) || kind === 'looseboard' && !Number.isInteger(form.estimatedPalletCount)) return '件数和预计托数须为整数' if (!Number.isInteger(form.packageCount) || kind === 'looseboard' && !Number.isInteger(form.estimatedPalletCount)) return '件数和预计托数须为整数'
if (kind === 'truck' && (!Number.isSafeInteger(form.estimatedPalletCount) || form.estimatedPalletCount <= 0)) return '请输入大于 0 的托数' if (kind === 'truck' && (!Number.isSafeInteger(form.estimatedPalletCount) || form.estimatedPalletCount <= 0)) return '请输入大于 0 的托数'
if (kind === 'truck' && (form.budgetAmount == null || !Number.isFinite(form.budgetAmount) || form.budgetAmount <= 0)) return '请输入大于 0 的预算总价' if (kind === 'truck' && (form.budgetAmount == null || !Number.isFinite(form.budgetAmount) || form.budgetAmount <= 0)) return '请输入大于 0 的预算总价'
if (kind === 'looseboard' && form.budgetAmount == null) return '请输入预算总价'
if (form.budgetAmount != null && (!Number.isFinite(form.budgetAmount) || form.budgetAmount < 0)) return '预算总价不能小于 0' if (form.budgetAmount != null && (!Number.isFinite(form.budgetAmount) || form.budgetAmount < 0)) return '预算总价不能小于 0'
if (!['USD', 'CNY'].includes(form.currency)) return '请选择预算币种' if (!['USD', 'CNY'].includes(form.currency)) return '请选择预算币种'
return undefined return undefined
+1 -1
View File
@@ -66,7 +66,7 @@ export interface UpdateLocalTransportOrderRequest extends Omit<CreateLocalTransp
} }
export type PalletRequired = 'YES' | 'NO' export type PalletRequired = 'YES' | 'NO'
export type LocalLooseboardStatus = 'PENDING_ACCEPTANCE' | 'WAITING_INBOUND' | 'RECEIVING' | 'WAITING_CONSOLIDATION' | 'PENDING_DISPATCH' | 'DISPATCHED' | 'IN_TRANSIT' | 'COMPLETED' | 'CANCELLED' export type LocalLooseboardStatus = 'FUNDING_PENDING' | 'FUNDING_FAILED' | 'PENDING_ACCEPTANCE' | 'WAITING_INBOUND' | 'RECEIVING' | 'WAITING_CONSOLIDATION' | 'PENDING_DISPATCH' | 'DISPATCHED' | 'IN_TRANSIT' | 'COMPLETED' | 'CANCELLED'
export interface LocalLooseboardQuery { export interface LocalLooseboardQuery {
page?: number; size?: number; customerId?: string; orderNo?: string; routeKeyword?: string page?: number; size?: number; customerId?: string; orderNo?: string; routeKeyword?: string
status?: LocalLooseboardStatus | ''; requestedFrom?: string; requestedTo?: string status?: LocalLooseboardStatus | ''; requestedFrom?: string; requestedTo?: string
@@ -23,7 +23,7 @@ vi.mock('@/modules/inbounds', async () => {
name: 'InboundTaskList', props: ['mode', 'title', 'taskLoader'], name: 'InboundTaskList', props: ['mode', 'title', 'taskLoader'],
methods: { reload() { return this.taskLoader({ page: 1, size: 20 }) } }, methods: { reload() { return this.taskLoader({ page: 1, size: 20 }) } },
mounted() { void this.reload() }, mounted() { void this.reload() },
template: '<section><slot name="filters" :loading="false" /><slot name="status-tabs" /></section>', template: '<section><slot name="filters" :loading="false" /><slot name="status-tabs" /><slot name="toolbar-actions" :loading="false" /></section>',
}), }),
} }
}) })
@@ -77,6 +77,32 @@ function mountView() {
} }
describe('UnifiedOrdersView list request coordination', () => { describe('UnifiedOrdersView list request coordination', () => {
it('shares fulfillment actions with measurement reviews and uses only that tab selection', async () => {
const wrapper = mountView()
await flushPromises()
const reviewPage = { ...emptyPage(), records: [
{ orderId: 'review-1', inboundId: 'inbound-1', tenantId: '8', mainStatus: 'WAIT_MEASURE', receivingWarehouseId: '9', channelCode: 'CHANNEL' },
{ orderId: 'review-2', inboundId: 'inbound-2', tenantId: '8', mainStatus: 'WAIT_MEASURE', receivingWarehouseId: '9', channelCode: 'CHANNEL' },
], total: 2, pages: 1 } as Awaited<ReturnType<typeof listPlatformUnifiedOrders>>
vi.mocked(listPlatformUnifiedOrders).mockResolvedValueOnce(reviewPage)
await wrapper.findAll('[role="tab"]').find(tab => tab.text().startsWith('测量数据审核'))!.trigger('click')
await flushPromises()
const action = (label: string) => wrapper.findAll('button').find(button => button.text().startsWith(label))!
expect(action('合柜报关').attributes('disabled')).toBeDefined()
expect(action('打印箱唛').attributes('disabled')).toBeDefined()
expect(action('智能装柜').exists()).toBe(true)
const reviewList = wrapper.getComponent({ name: 'InboundTaskList' })
reviewList.vm.$emit('selection-change', [{ id: 'inbound-1' }, { id: 'inbound-2' }])
await flushPromises()
expect(action('合柜报关').attributes('disabled')).toBeUndefined()
expect(action('打印箱唛').text()).toContain('2')
expect(action('打印箱唛').attributes('disabled')).toBeUndefined()
reviewList.vm.$emit('selection-change', [])
await flushPromises()
expect(action('打印箱唛').attributes('disabled')).toBeDefined()
wrapper.unmount()
})
beforeEach(() => { beforeEach(() => {
vi.mocked(getPlatformUnifiedOrderStatusCounts).mockReset().mockResolvedValue({ counts: {} }) vi.mocked(getPlatformUnifiedOrderStatusCounts).mockReset().mockResolvedValue({ counts: {} })
vi.mocked(listPlatformUnifiedOrders).mockReset().mockResolvedValue(emptyPage()) vi.mocked(listPlatformUnifiedOrders).mockReset().mockResolvedValue(emptyPage())
@@ -747,7 +747,7 @@ function selectStatus(status: UnifiedOrderMainStatus | '' | 'MEASUREMENT_REVIEW'
if (selectedTab.value === status) return if (selectedTab.value === status) return
ordersRequestSerial += 1 ordersRequestSerial += 1
measurementReviewSelected.value = status === 'MEASUREMENT_REVIEW' measurementReviewSelected.value = status === 'MEASUREMENT_REVIEW'
if (measurementReviewSelected.value) selectedOrders.value = [] handleOrderSelectionChange([])
if (status === 'MEASUREMENT_REVIEW') { if (status === 'MEASUREMENT_REVIEW') {
return return
} }
@@ -1670,11 +1670,23 @@ function tabCount(value: UnifiedOrderMainStatus | '' | 'MEASUREMENT_REVIEW') {
return value ? statusCounts.value[value] : undefined return value ? statusCounts.value[value] : undefined
} }
const measurementReviewOrders = ref<UnifiedOrderResource[]>([])
let measurementReviewRequestSerial = 0
/** 保留审核列表对应的原始订单,工具栏操作复用同一客户、入库单和业务资格。 */
async function loadMeasurementReviewTasks(pagination: { page: number; size: number }) { async function loadMeasurementReviewTasks(pagination: { page: number; size: number }) {
const serial = ++measurementReviewRequestSerial
const result = await listPlatformUnifiedOrders({ ...orderCountFilters(), mainStatus: 'MEASUREMENT_REVIEW', ...pagination }) const result = await listPlatformUnifiedOrders({ ...orderCountFilters(), mainStatus: 'MEASUREMENT_REVIEW', ...pagination })
if (serial === measurementReviewRequestSerial) measurementReviewOrders.value = result.records
return { ...result, records: result.records.map(toMeasurementReviewTask) } return { ...result, records: result.records.map(toMeasurementReviewTask) }
} }
/** 审核列表勾选映射回原始订单,避免混用其他页签的勾选数据。 */
function handleMeasurementReviewSelection(rows: Array<{ id: string }>) {
const ids = new Set(rows.map(row => String(row.id)))
handleOrderSelectionChange(measurementReviewOrders.value.filter(row => ids.has(String(row.inboundId))))
}
async function loadOrders(refreshCounts = true) { async function loadOrders(refreshCounts = true) {
const serial = ++ordersRequestSerial const serial = ++ordersRequestSerial
if (refreshCounts) void loadStatusCounts() if (refreshCounts) void loadStatusCounts()
@@ -2862,7 +2874,14 @@ function orderDialogTitle(action: string, inboundCode?: string | null) {
<template v-else> <template v-else>
<div v-if="measurementReviewSelected" id="unified-orders-results" class="orders-status-toolbar" role="tabpanel"> <div v-if="measurementReviewSelected" id="unified-orders-results" class="orders-status-toolbar" role="tabpanel">
<InboundTaskList ref="measurementReviewList" :task-loader="loadMeasurementReviewTasks" mode="review" title="订单列表" class="is-embedded" @changed="loadStatusCounts"> <InboundTaskList ref="measurementReviewList" :task-loader="loadMeasurementReviewTasks" mode="review" title="订单列表" class="is-embedded" @changed="loadStatusCounts" @selection-change="handleMeasurementReviewSelection">
<template #toolbar-actions="{ loading: reviewLoading }">
<div v-if="can('platform:firstleg:smart-loading:assign')" class="coload-toolbar-action">
<el-button type="primary" plain :loading="coLoadSaving" :disabled="reviewLoading || !canCreateCoLoadGroup" aria-label="合柜报关" @click="openCoLoadCreate"><Link2 :size="16" />合柜报关<span v-if="selectedOrders.length">({{ selectedOrders.length }})</span></el-button>
</div>
<el-button v-if="can('platform:firstleg:inbound:view')" :loading="printingCartonLabels" :disabled="reviewLoading || printingCartonLabels || !selectedCartonLabelOrders.length" @click="printSelectedCartonLabels"><Printer :size="16" />打印箱唛<span v-if="selectedCartonLabelOrders.length">({{ selectedCartonLabelOrders.length }})</span></el-button>
<el-button v-if="can('platform:firstleg:smart-loading:list')" type="primary" @click="smartLoadingOpen = true"><PackageOpen :size="16" />智能装柜</el-button>
</template>
<template #status-tabs> <template #status-tabs>
<div ref="statusTabList" class="status-tabs density-status-tabs workbench-list-tabs" role="tablist" aria-label="订单主状态" :aria-busy="statusCountsLoading"> <div ref="statusTabList" class="status-tabs density-status-tabs workbench-list-tabs" role="tablist" aria-label="订单主状态" :aria-busy="statusCountsLoading">
<button <button