优化
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user