Merge branch 'release/test' of http://8.163.30.128:10082/html/admin-web into release/test
This commit is contained in:
@@ -21,7 +21,7 @@ export const unifiedOrderStatusOptions: Array<{ value: UnifiedOrderMainStatus; l
|
||||
{ value: 'WEIGHT_CONFIRMED', label: '已确认数据' },
|
||||
{ value: 'WAIT_CUSTOMS_DOCS', label: '待上传资料' },
|
||||
{ value: 'WAIT_PALLETIZE', label: '一件代发打托' },
|
||||
{ value: 'WAIT_SPLIT', label: '待分柜' },
|
||||
{ value: 'WAIT_SPLIT', label: '待装柜' },
|
||||
{ value: 'LOADING', label: '装柜中' },
|
||||
{ value: 'LOADED', label: '装柜完成' },
|
||||
{ value: 'EXPORT_CUSTOMS', label: '出口报关' },
|
||||
@@ -30,7 +30,7 @@ export const unifiedOrderStatusOptions: Array<{ value: UnifiedOrderMainStatus; l
|
||||
{ value: 'WAIT_DISPATCH', label: '待排车' },
|
||||
{ value: 'OUTBOUND_DOCUMENT', label: '出库单' },
|
||||
{ value: 'DELIVERING', label: '派送中' },
|
||||
{ value: 'SIGNED', label: '签收完成' },
|
||||
{ value: 'SIGNED', label: '派送完成' },
|
||||
]
|
||||
|
||||
export interface UnifiedOrderMeasurementSummary {
|
||||
@@ -302,7 +302,7 @@ export interface UnifiedOrderQuery {
|
||||
tenantId?: string
|
||||
keyword?: string
|
||||
orderNo?: string
|
||||
mainStatus?: UnifiedOrderMainStatus
|
||||
mainStatus?: UnifiedOrderMainStatus | 'MEASUREMENT_REVIEW'
|
||||
customerId?: string
|
||||
receivingWarehouseId?: string
|
||||
destinationWarehouseId?: string
|
||||
@@ -694,7 +694,7 @@ export async function listPlatformUnifiedOrderFeeDetails(accountId: string, inbo
|
||||
}
|
||||
|
||||
export function unifiedOrderStatusLabel(status: UnifiedOrderMainStatus) {
|
||||
if (status === 'DELIVERED') return '签收完成'
|
||||
if (status === 'DELIVERED') return '派送完成'
|
||||
if (status === 'COMPLETED') return '已完成'
|
||||
return unifiedOrderStatusOptions.find((item) => item.value === status)?.label || '未知状态'
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import type { UnifiedOrderResource } from './api/unifiedOrders'
|
||||
import { toMeasurementReviewTask } from './measurementReviewTasks'
|
||||
|
||||
describe('measurement review task identity', () => {
|
||||
it('keeps original tenant, inbound identity and optimistic version for approval', () => {
|
||||
const task = toMeasurementReviewTask({
|
||||
orderId: '9007199254740995', tenantId: '9007199254740993', inboundId: '9007199254740994', inboundVersion: 0,
|
||||
inboundCode: 'IN-1', customerId: '9007199254740993', fulfillmentType: 'ONE_CLICK_DROPSHIP',
|
||||
measurement: { reviewStatus: 'PENDING', actualGrossWeightKg: 12, actualVolumeCbm: 0.2, chargeableWeightKg: 13, completedAt: '2026-10-20T08:00:00' },
|
||||
} as UnifiedOrderResource)
|
||||
expect(task).toMatchObject({ id: '9007199254740994', tenantId: '9007199254740993', orderId: '9007199254740995', version: 0, inboundCode: 'IN-1', measureReviewStatus: 'PENDING', fulfillmentType: 'ONE_CLICK_DROPSHIP', actualGrossWeightKg: 12, actualVolumeCbm: 0.2 })
|
||||
})
|
||||
it('rejects an order without an inbound identity instead of approving a different resource', () => {
|
||||
expect(() => toMeasurementReviewTask({ orderId: '1', inboundId: null } as UnifiedOrderResource)).toThrow('缺少入库单标识')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,66 @@
|
||||
import type { InboundOrderSummary } from '@/modules/inbounds'
|
||||
import type { UnifiedOrderResource } from './api/unifiedOrders'
|
||||
|
||||
/** 将统一订单查询的审核待办映射到现有测量审核操作,保留原始客户、入库单与版本。 */
|
||||
export function toMeasurementReviewTask(row: UnifiedOrderResource): InboundOrderSummary {
|
||||
if (row.inboundId == null) throw new Error('测量审核订单缺少入库单标识,请刷新后重试')
|
||||
return {
|
||||
id: String(row.inboundId),
|
||||
tenantId: String(row.tenantId),
|
||||
orderId: String(row.orderId),
|
||||
orderNo: row.orderNo,
|
||||
inboundCode: row.inboundCode,
|
||||
processStatus: 'MEASURE_PENDING',
|
||||
orderStatusSnapshot: row.internalOrderStatus,
|
||||
measureReviewStatus: row.measurement?.reviewStatus ?? null,
|
||||
feeStatus: null,
|
||||
feeReviewStatus: row.feeReviewStatus,
|
||||
notificationStatus: null,
|
||||
version: row.inboundVersion,
|
||||
expectedArrivalAt: row.expectedArrivalAt,
|
||||
customerId: row.customerId == null ? null : String(row.customerId),
|
||||
customerName: row.customerName,
|
||||
companyId: null,
|
||||
companyName: row.companyName ?? null,
|
||||
warehouseId: row.receivingWarehouseId == null ? null : String(row.receivingWarehouseId),
|
||||
warehouseName: row.receivingWarehouseName,
|
||||
deliveryWarehouseId: row.destinationWarehouseId == null ? null : String(row.destinationWarehouseId),
|
||||
deliveryWarehouseName: row.destinationWarehouseName,
|
||||
businessType: row.businessType ?? null,
|
||||
fulfillmentType: row.fulfillmentType ?? null,
|
||||
channelCode: row.channelCode,
|
||||
channelName: row.channelName,
|
||||
taxMode: row.taxMode ?? null,
|
||||
containerMode: null,
|
||||
customsInspection: row.customsInspection ?? null,
|
||||
destinationType: row.destinationType ?? null,
|
||||
destinationWarehouseId: row.destinationWarehouseId == null ? null : String(row.destinationWarehouseId),
|
||||
destinationCode: null,
|
||||
destinationName: row.destinationWarehouseName,
|
||||
holdFlag: row.holdFlag,
|
||||
currentHoldReason: row.currentHoldReason,
|
||||
totalPackageCount: row.totalPackageCount,
|
||||
actualGrossWeightKg: row.measurement?.actualGrossWeightKg ?? row.actualGrossWeightKg,
|
||||
actualNetWeightKg: row.totalNetWeightKg,
|
||||
actualVolumeCbm: row.measurement?.actualVolumeCbm ?? row.actualVolumeCbm,
|
||||
volumeWeightKg: row.volumetricWeightKg,
|
||||
chargeableWeightKg: row.measurement?.chargeableWeightKg ?? null,
|
||||
chargeableVolumeCbm: row.chargeableVolumeCbm,
|
||||
palletCount: row.totalPalletCount,
|
||||
locationCodes: row.locationCodes,
|
||||
measureCompletedAt: row.measurement?.completedAt ?? row.measurementCompletedAt,
|
||||
inboundedAt: row.inboundCompletedAt,
|
||||
expressCompanyCode: null,
|
||||
expressCompanyName: row.expressCompany,
|
||||
expressTrackingNo: row.expressTrackingNo,
|
||||
loadingStatus: row.transport?.loadingStatus ?? null,
|
||||
containerNo: row.transport?.containerNo ?? null,
|
||||
cabinetNo: row.transport?.cabinetNo ?? null,
|
||||
vesselName: row.transport?.vesselName ?? null,
|
||||
voyageNo: row.transport?.voyageNo ?? null,
|
||||
sailingAt: row.transport?.effectiveSailingAt ?? null,
|
||||
portArrivalAt: row.transport?.effectivePortArrivalAt ?? null,
|
||||
createdAt: row.createdAt,
|
||||
updatedAt: row.updatedAt,
|
||||
}
|
||||
}
|
||||
@@ -107,6 +107,56 @@ describe('UnifiedOrdersView downstream fulfillment detail', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps unchanged actual and outer volumes on the same server snapshot with three decimal places', async () => {
|
||||
const wrapper = mountDetail()
|
||||
await flushPromises()
|
||||
const vm = wrapper.vm as unknown as {
|
||||
measurementDetail: Record<string, unknown>
|
||||
measurementSectionModes: { outer: string; actual: string }
|
||||
outerMeasurementRows: Record<string, unknown>[]
|
||||
actualMeasurementRows: Record<string, unknown>[]
|
||||
measurementTotals(rows: Record<string, unknown>[]): { volume: number; weightVolume: number }
|
||||
measurementValue(row: Record<string, unknown>, key: string): string
|
||||
formatMeasurementVolume(value: number | null): string
|
||||
}
|
||||
const items = [
|
||||
{ lengthCm: 60, widthCm: 50, heightCm: 50, packageCount: 10, unitVolumeCbm: 0.15, totalVolumeCbm: 1.5 },
|
||||
{ lengthCm: 61, widthCm: 51, heightCm: 51, packageCount: 9, unitVolumeCbm: 0.159, totalVolumeCbm: 1.431 },
|
||||
{ lengthCm: 50, widthCm: 40, heightCm: 40, packageCount: 1, unitVolumeCbm: 0.08, totalVolumeCbm: 0.08 },
|
||||
].map((row, index) => ({
|
||||
...row, itemNo: index + 1, coefficientMode: 'ACTUAL',
|
||||
calculatedOuterLengthCm: row.lengthCm, calculatedOuterWidthCm: row.widthCm, calculatedOuterHeightCm: row.heightCm,
|
||||
}))
|
||||
vm.measurementDetail = { items }
|
||||
vm.measurementSectionModes.outer = 'ACTUAL'
|
||||
vm.measurementSectionModes.actual = 'ACTUAL'
|
||||
await flushPromises()
|
||||
|
||||
for (const rows of [vm.outerMeasurementRows, vm.actualMeasurementRows]) {
|
||||
expect(rows.map(row => vm.measurementValue(row, 'totalVolumeCbm'))).toEqual(['1.500', '1.431', '0.080'])
|
||||
expect(rows.map(row => vm.measurementValue(row, 'unitVolumeCbm'))).toEqual(['0.150', '0.159', '0.080'])
|
||||
expect(vm.formatMeasurementVolume(vm.measurementTotals(rows).volume)).toBe('3.011')
|
||||
expect(vm.formatMeasurementVolume(vm.measurementTotals(rows).weightVolume)).toBe('3.011')
|
||||
}
|
||||
expect(vm.formatMeasurementVolume(3)).toBe('3.000')
|
||||
expect(vm.formatMeasurementVolume(0)).toBe('0.000')
|
||||
expect(vm.formatMeasurementVolume(null)).toBe('—')
|
||||
|
||||
// 仓库修正的外箱仍与实测尺寸分开,不能把外箱计费快照当成原始实测体积。
|
||||
vm.measurementDetail = { items: [{ ...items[1], outerLengthCm: 62,
|
||||
calculatedOuterLengthCm: 62, unitVolumeCbm: 0.1613, totalVolumeCbm: 1.4517,
|
||||
calculationRuleVersion: 'WAREHOUSE_OUTER_OVERRIDE_V1' }] }
|
||||
await flushPromises()
|
||||
expect(vm.measurementValue(vm.outerMeasurementRows[0]!, 'totalVolumeCbm')).toBe('1.452')
|
||||
expect(vm.measurementValue(vm.actualMeasurementRows[0]!, 'totalVolumeCbm')).toBe('1.428')
|
||||
|
||||
vm.measurementDetail = { items: [{ ...items[1], coefficientMode: 'ADD', calculatedOuterLengthCm: 62,
|
||||
calculatedOuterWidthCm: 52, calculatedOuterHeightCm: 52, unitVolumeCbm: 0.168, totalVolumeCbm: 1.512 }] }
|
||||
await flushPromises()
|
||||
expect(vm.measurementValue(vm.actualMeasurementRows[0]!, 'totalVolumeCbm')).toBe('1.428')
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('downloads one preview for cargo rows sharing the same product image', async () => {
|
||||
vi.mocked(getPlatformUnifiedOrder).mockResolvedValue({
|
||||
...detail,
|
||||
|
||||
@@ -15,10 +15,18 @@ vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({
|
||||
vi.mock('@/modules/iam', async importOriginal => ({
|
||||
...await importOriginal<typeof import('@/modules/iam')>(), listPlatformTenants: vi.fn().mockResolvedValue({ records: [], page: 1, size: 200, total: 0, pages: 0 }) }))
|
||||
vi.mock('@/modules/warehouses', () => ({ listAllPlatformWarehouses: vi.fn().mockResolvedValue([]) }))
|
||||
vi.mock('@/modules/inbounds', () => ({
|
||||
vi.mock('@/modules/inbounds', async () => {
|
||||
const { defineComponent } = await import('vue')
|
||||
return {
|
||||
downloadWeightImage: vi.fn(), getInboundMeasurement: vi.fn(),
|
||||
InboundTaskList: { name: 'InboundTaskList', props: ['mode', 'title'], template: '<section><slot name="status-tabs" /></section>' },
|
||||
}))
|
||||
InboundTaskList: defineComponent({
|
||||
name: 'InboundTaskList', props: ['mode', 'title', 'taskLoader'],
|
||||
methods: { reload() { return this.taskLoader({ page: 1, size: 20 }) } },
|
||||
mounted() { void this.reload() },
|
||||
template: '<section><slot name="filters" :loading="false" /><slot name="status-tabs" /></section>',
|
||||
}),
|
||||
}
|
||||
})
|
||||
vi.mock('@/modules/downloads', () => ({ createPlatformExportTask: vi.fn() }))
|
||||
vi.mock('@/shared/components/exportTaskCreatedDialog', () => ({ showExportTaskCreatedDialog: vi.fn() }))
|
||||
vi.mock('@/modules/unified-orders/workflows', () => ({ UnifiedOrderWorkflowPanel: { template: '<div />' } }))
|
||||
@@ -37,11 +45,11 @@ vi.mock('@/modules/unified-orders/api/unifiedOrders', () => ({
|
||||
unifiedOrderStatusOptions: [
|
||||
{ value: 'PENDING_REVIEW', label: '待审核' },
|
||||
{ value: 'WAIT_MEASURE', label: '待测量数据' },
|
||||
{ value: 'WAIT_SPLIT', label: '待分柜' },
|
||||
{ value: 'WAIT_SPLIT', label: '待装柜' },
|
||||
{ value: 'LOADING', label: '装柜中' },
|
||||
{ value: 'LOADED', label: '装柜完成' },
|
||||
{ value: 'DELIVERING', label: '派送中' },
|
||||
{ value: 'SIGNED', label: '签收完成' },
|
||||
{ value: 'SIGNED', label: '派送完成' },
|
||||
],
|
||||
}))
|
||||
|
||||
@@ -77,6 +85,37 @@ describe('UnifiedOrdersView list request coordination', () => {
|
||||
vi.mocked(showExportTaskCreatedDialog).mockReset().mockResolvedValue(false)
|
||||
})
|
||||
|
||||
it('shares applied filters, advanced fields and reset across measurement review and other tabs', async () => {
|
||||
const wrapper = mountView()
|
||||
await flushPromises()
|
||||
const model = { receivingWarehouseId: '9007199254740993', destinationWarehouseId: '200', inboundCode: ' IN-9 ', tenantId: '300', channelName: '海派', containerNo: 'CONT-1', cabinetNo: 'CAB-1', loaded: 'false', fullContainer: 'true', expectedArrivalDate: '2026-10-20', sailingDate: '2026-11-01', feeReviewStatus: 'APPROVED', measurementReviewStatus: 'PENDING', fbaCode: 'SKU-1', createdRange: ['2026-10-01', '2026-10-20'] }
|
||||
const toolbar = wrapper.getComponent({ name: 'QueryToolbar' })
|
||||
toolbar.vm.$emit('update:modelValue', { ...toolbar.props('modelValue'), ...model })
|
||||
await flushPromises()
|
||||
toolbar.vm.$emit('search')
|
||||
await flushPromises()
|
||||
const ordinaryQuery = vi.mocked(listPlatformUnifiedOrders).mock.lastCall![0]
|
||||
await wrapper.findAll('[role="tab"]').find(tab => tab.text().startsWith('测量数据审核'))!.trigger('click')
|
||||
await flushPromises()
|
||||
expect(listPlatformUnifiedOrders).toHaveBeenLastCalledWith({ ...ordinaryQuery, mainStatus: 'MEASUREMENT_REVIEW' })
|
||||
const reviewToolbar = wrapper.getComponent({ name: 'QueryToolbar' })
|
||||
expect(reviewToolbar.props('modelValue')).toMatchObject(model)
|
||||
expect(reviewToolbar.attributes('preference-key')).toBe('unified-orders-query-v2')
|
||||
expect(reviewToolbar.attributes('available-fields')).toBeDefined()
|
||||
reviewToolbar.vm.$emit('reset')
|
||||
await flushPromises()
|
||||
expect(listPlatformUnifiedOrders).toHaveBeenLastCalledWith(expect.objectContaining({ mainStatus: 'MEASUREMENT_REVIEW', receivingWarehouseId: undefined, inboundCode: undefined, channelName: undefined, tenantId: undefined }))
|
||||
reviewToolbar.vm.$emit('update:modelValue', { ...reviewToolbar.props('modelValue'), inboundCode: 'IN-REVIEW' })
|
||||
await flushPromises()
|
||||
reviewToolbar.vm.$emit('search')
|
||||
await flushPromises()
|
||||
expect(listPlatformUnifiedOrders).toHaveBeenLastCalledWith(expect.objectContaining({ mainStatus: 'MEASUREMENT_REVIEW', inboundCode: 'IN-REVIEW' }))
|
||||
await wrapper.findAll('[role="tab"]').find(tab => tab.text().startsWith('待审核'))!.trigger('click')
|
||||
await flushPromises()
|
||||
expect(wrapper.getComponent({ name: 'QueryToolbar' }).props('modelValue')).toMatchObject({ inboundCode: 'IN-REVIEW' })
|
||||
expect(listPlatformUnifiedOrders).toHaveBeenLastCalledWith(expect.objectContaining({ mainStatus: 'PENDING_REVIEW', inboundCode: 'IN-REVIEW' }))
|
||||
})
|
||||
|
||||
it('shows only supported counts, keeps all text-only and never scopes counts by status or pagination', async () => {
|
||||
vi.mocked(getPlatformUnifiedOrderStatusCounts).mockResolvedValue({ counts: { PENDING_REVIEW: 12, WAIT_MEASURE: 0, SIGNED: 3 } })
|
||||
const wrapper = mountView()
|
||||
@@ -86,7 +125,7 @@ describe('UnifiedOrdersView list request coordination', () => {
|
||||
expect(tabs[1]!.text()).toBe('待审核(12)')
|
||||
expect(tabs[2]!.text()).toBe('待测量数据(0)')
|
||||
expect(tabs[3]!.text()).toBe('测量数据审核')
|
||||
expect(tabs.at(-1)!.text()).toBe('签收完成(3)')
|
||||
expect(tabs.at(-1)!.text()).toBe('派送完成(3)')
|
||||
expect(tabs.map((tab) => tab.text())).not.toContain('派送完成')
|
||||
expect(tabs.map((tab) => tab.text())).not.toContain('已完成')
|
||||
await tabs[1]!.trigger('click')
|
||||
@@ -169,14 +208,14 @@ describe('UnifiedOrdersView list request coordination', () => {
|
||||
expect(wrapper.getComponent({ name: 'InboundTaskList' }).classes()).toContain('is-embedded')
|
||||
expect(wrapper.get('main').classes()).not.toContain('is-embedded')
|
||||
expect(wrapper.get('[aria-selected="true"]').text()).toBe('测量数据审核')
|
||||
expect(listPlatformUnifiedOrders).toHaveBeenCalledTimes(1)
|
||||
expect(listPlatformUnifiedOrders).toHaveBeenCalledTimes(2)
|
||||
|
||||
await wrapper.findAll('[role="tab"]').find((tab) => tab.text() === '待测量数据')!.trigger('click')
|
||||
await flushPromises()
|
||||
expect(wrapper.findComponent({ name: 'InboundTaskList' }).exists()).toBe(false)
|
||||
expect(wrapper.find('.is-embedded').exists()).toBe(false)
|
||||
expect(listPlatformUnifiedOrders).toHaveBeenLastCalledWith(expect.objectContaining({ mainStatus: 'WAIT_MEASURE' }))
|
||||
expect(listPlatformUnifiedOrders).toHaveBeenCalledTimes(2)
|
||||
expect(listPlatformUnifiedOrders).toHaveBeenCalledTimes(3)
|
||||
})
|
||||
|
||||
it('does not apply draft filters to pagination until query is submitted', async () => {
|
||||
|
||||
@@ -62,7 +62,7 @@ describe('UnifiedOrdersView contract', () => {
|
||||
|
||||
it('exposes the product main statuses including dropship palletizing', () => {
|
||||
expect(unifiedOrderStatusOptions.map((item) => item.label)).toEqual([
|
||||
'待审核', '待测量数据', '待客户确认', '已确认数据', '待上传资料', '一件代发打托', '待分柜', '装柜中', '装柜完成', '出口报关', '已开船/待清关', '待拆柜', '待排车', '出库单', '派送中', '签收完成',
|
||||
'待审核', '待测量数据', '待客户确认', '已确认数据', '待上传资料', '一件代发打托', '待装柜', '装柜中', '装柜完成', '出口报关', '已开船/待清关', '待拆柜', '待排车', '出库单', '派送中', '派送完成',
|
||||
])
|
||||
for (const legacyLabel of ['差异处理中', '进入分柜']) {
|
||||
expect(source).not.toContain(legacyLabel)
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
type LegacyOrderImportResult,
|
||||
} from '@/modules/unified-orders/api/unifiedOrders'
|
||||
import { cancelPlatformCoLoadGroup, createPlatformCoLoadGroup, getPlatformCoLoadGroupByInboundCode, type CoLoadBusinessSummary, type CoLoadGroup } from '@/modules/unified-orders/api/coLoadGroups'
|
||||
import { toMeasurementReviewTask } from '../measurementReviewTasks'
|
||||
import OrderFulfillmentTreePanel from '@/modules/unified-orders/components/OrderFulfillmentTreePanel.vue'
|
||||
import InboundFeeDetailManager from '@/modules/unified-orders/components/InboundFeeDetailManager.vue'
|
||||
import OrderDeclarationEditorDialog from '@/modules/unified-orders/components/OrderDeclarationEditorDialog.vue'
|
||||
@@ -130,6 +131,7 @@ const statusCounts = ref<UnifiedOrderStatusCounts['counts']>({})
|
||||
const statusCountsLoading = ref(false)
|
||||
const statusCountsError = ref<ApiErrorDetails | null>(null)
|
||||
let statusCountsRequestSerial = 0
|
||||
let ordersRequestSerial = 0
|
||||
const loadError = ref<ApiErrorDetails | null>(null)
|
||||
const legacyImportOpen = ref(false)
|
||||
const legacyImportFile = ref<File | null>(null)
|
||||
@@ -332,6 +334,7 @@ const showAdvancedFilters = ref(false)
|
||||
const filters = ref<FilterModel>(emptyFilters())
|
||||
const appliedFilters = ref<FilterModel>(emptyFilters())
|
||||
const selectedStatus = ref<UnifiedOrderMainStatus | ''>('')
|
||||
const measurementReviewList = ref<InstanceType<typeof InboundTaskList> | null>(null)
|
||||
const measurementReviewSelected = ref(false)
|
||||
const selectedTab = computed(() => measurementReviewSelected.value ? 'MEASUREMENT_REVIEW' : selectedStatus.value)
|
||||
const statusTabList = ref<HTMLElement | null>(null)
|
||||
@@ -500,10 +503,18 @@ function calculatedMeasurementRow(
|
||||
const strategyDimensions = matchedDimensions.map((value) => roundedUp(value * specificationCoefficient, 3))
|
||||
const [lengthCm, widthCm, heightCm] = strategyDimensions
|
||||
const packageCount = Number(source.packageCount)
|
||||
const unitVolumeCbm = mode === 'ADD' && rule && !hasOuterOverride
|
||||
const savedDimensions = [source.calculatedOuterLengthCm, source.calculatedOuterWidthCm, source.calculatedOuterHeightCm]
|
||||
// 不调整且尺寸与落库快照一致时沿用服务端体积,避免历史三位精度与预览四位取整产生差额。
|
||||
const useSavedVolume = !customerSource && mode === 'ACTUAL'
|
||||
&& measurementCalculationModeForRow(source) === 'ACTUAL'
|
||||
&& savedDimensions.every((value, index) => value != null
|
||||
&& Math.abs(Number(value) - strategyDimensions[index]!) < 0.000001)
|
||||
&& Number(source.unitVolumeCbm) > 0 && Number(source.totalVolumeCbm) > 0
|
||||
const unitVolumeCbm = useSavedVolume ? Number(source.unitVolumeCbm) : mode === 'ADD' && rule && !hasOuterOverride
|
||||
? Math.round((lengthCm * widthCm * heightCm / 1_000_000) * 1000) / 1000
|
||||
: roundedUp(lengthCm * widthCm * heightCm / 1_000_000, 4)
|
||||
const totalVolumeCbm = Number.isFinite(packageCount) ? roundedUp(unitVolumeCbm * packageCount, 4) : null
|
||||
const totalVolumeCbm = useSavedVolume ? Number(source.totalVolumeCbm)
|
||||
: Number.isFinite(packageCount) ? roundedUp(unitVolumeCbm * packageCount, 4) : null
|
||||
const fallbackMeasuredUnitWeight = !customerSource && Number.isFinite(Number(measurementDetail.value?.actualGrossWeightKg))
|
||||
&& Number(measurementDetail.value?.actualPackageCount) > 0
|
||||
? Number(measurementDetail.value?.actualGrossWeightKg) / Number(measurementDetail.value?.actualPackageCount)
|
||||
@@ -517,7 +528,8 @@ function calculatedMeasurementRow(
|
||||
...source,
|
||||
lengthCm, widthCm, heightCm, unitWeightKg, unitVolumeCbm, totalWeightKg, totalVolumeCbm,
|
||||
girthCm: roundedUp(lengthCm + 2 * (widthCm + heightCm), 3),
|
||||
volumeWeightKg: totalVolumeCbm == null ? null : roundedUp(totalVolumeCbm * 167, 3),
|
||||
volumeWeightKg: useSavedVolume && Number(source.volumeWeightKg) > 0 ? Number(source.volumeWeightKg)
|
||||
: totalVolumeCbm == null ? null : roundedUp(totalVolumeCbm * 167, 3),
|
||||
remark: customerSource ? source.cargoRemark : source.remark,
|
||||
}
|
||||
}
|
||||
@@ -723,15 +735,13 @@ function optionalBoolean(value: string | null | undefined) {
|
||||
|
||||
function selectStatus(status: UnifiedOrderMainStatus | '' | 'MEASUREMENT_REVIEW') {
|
||||
if (selectedTab.value === status) return
|
||||
const leavingMeasurementReview = measurementReviewSelected.value
|
||||
ordersRequestSerial += 1
|
||||
measurementReviewSelected.value = status === 'MEASUREMENT_REVIEW'
|
||||
if (measurementReviewSelected.value) selectedOrders.value = []
|
||||
if (status === 'MEASUREMENT_REVIEW') {
|
||||
void loadStatusCounts()
|
||||
return
|
||||
}
|
||||
selectedStatus.value = status
|
||||
if (leavingMeasurementReview) void loadStatusCounts()
|
||||
if (page.value === 1) void loadOrders(false)
|
||||
else page.value = 1
|
||||
}
|
||||
@@ -866,6 +876,11 @@ function formatNumber(value: number | null | undefined, digits = 2) {
|
||||
return value === null || value === undefined ? '—' : value.toLocaleString('zh-CN', { maximumFractionDigits: digits })
|
||||
}
|
||||
|
||||
function formatMeasurementVolume(value: number | null | undefined) {
|
||||
return value == null || !Number.isFinite(value) ? '—'
|
||||
: value.toLocaleString('zh-CN', { minimumFractionDigits: 3, maximumFractionDigits: 3 })
|
||||
}
|
||||
|
||||
function partyValue(parties: Array<Record<string, unknown>>, type: 'SENDER' | 'RECEIVER', key: string) {
|
||||
const party = parties.find((item) => String(item.partyType || '').toUpperCase() === type)
|
||||
const value = party?.[key]
|
||||
@@ -1020,11 +1035,12 @@ function measurementValue(row: Record<string, unknown>, key: string, customerSou
|
||||
if (customerSource && key === 'unitVolumeCbm') {
|
||||
const total = Number(row.volumeCbm)
|
||||
const count = Number(row.packageCount)
|
||||
return Number.isFinite(total) && Number.isFinite(count) && count > 0 ? formatNumber(total / count, 6) : '—'
|
||||
return Number.isFinite(total) && Number.isFinite(count) && count > 0 ? formatMeasurementVolume(total / count) : '—'
|
||||
}
|
||||
if (customerSource && key === 'remark') return String(row.cargoRemark || '—')
|
||||
const value = row[key]
|
||||
if (value === null || value === undefined || value === '') return '—'
|
||||
if (key === 'unitVolumeCbm' || key === 'totalVolumeCbm') return formatMeasurementVolume(Number(value))
|
||||
return typeof value === 'number' ? formatNumber(value, 6) : String(value)
|
||||
}
|
||||
|
||||
@@ -1643,9 +1659,15 @@ function tabCount(value: UnifiedOrderMainStatus | '' | 'MEASUREMENT_REVIEW') {
|
||||
return value ? statusCounts.value[value] : undefined
|
||||
}
|
||||
|
||||
async function loadMeasurementReviewTasks(pagination: { page: number; size: number }) {
|
||||
const result = await listPlatformUnifiedOrders({ ...orderCountFilters(), mainStatus: 'MEASUREMENT_REVIEW', ...pagination })
|
||||
return { ...result, records: result.records.map(toMeasurementReviewTask) }
|
||||
}
|
||||
|
||||
async function loadOrders(refreshCounts = true) {
|
||||
const serial = ++ordersRequestSerial
|
||||
if (refreshCounts) void loadStatusCounts()
|
||||
if (measurementReviewSelected.value) return
|
||||
if (measurementReviewSelected.value) { await measurementReviewList.value?.reload(); return }
|
||||
selectedApprovalOrders.value = []
|
||||
selectedOrders.value = []
|
||||
loading.value = true
|
||||
@@ -1659,14 +1681,16 @@ async function loadOrders(refreshCounts = true) {
|
||||
page: page.value,
|
||||
size: pageSize.value,
|
||||
})
|
||||
if (serial !== ordersRequestSerial) return
|
||||
records.value = result.records
|
||||
total.value = result.total
|
||||
} catch (error) {
|
||||
if (serial !== ordersRequestSerial) return
|
||||
records.value = []
|
||||
total.value = 0
|
||||
loadError.value = getApiErrorDetails(error, '统一订单列表加载失败,请检查 C02 统一查询接口和订单权限')
|
||||
} finally {
|
||||
loading.value = false
|
||||
if (serial === ordersRequestSerial) loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2377,6 +2401,7 @@ onMounted(() => {
|
||||
if (!isDetailRoute.value) void loadOrders()
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
ordersRequestSerial += 1
|
||||
statusCountsRequestSerial += 1
|
||||
detailAssociationRequestSerial += 1
|
||||
measurementRequestSerial += 1
|
||||
@@ -2655,17 +2680,17 @@ function orderDialogTitle(action: string, inboundCode?: string | null) {
|
||||
</LoadErrorState>
|
||||
<template v-else>
|
||||
<section class="measurement-section">
|
||||
<div class="measurement-section-title"><button type="button" class="measurement-section-toggle" :aria-expanded="!measurementCollapsed.customer" @click="toggleMeasurementSection('customer')"><h3>一、客户下单数据</h3><span v-if="measurementCollapsed.customer" class="measurement-section-summary"><span>总件数:{{ measurementTotals(customerMeasurementRows).count ?? '—' }}</span><span>总毛重:{{ formatNumber(measurementTotals(customerMeasurementRows).gross) }}</span><span>总体积:{{ formatNumber(measurementTotals(customerMeasurementRows).volume, 6) }}</span><span>总材积重:{{ formatNumber(measurementTotals(customerMeasurementRows).volumetric) }}</span><span>重量体积:{{ formatNumber(measurementTotals(customerMeasurementRows).weightVolume, 6) }}</span></span><ChevronUp v-if="!measurementCollapsed.customer" :size="18" /><ChevronDown v-else :size="18" /></button><el-select class="measurement-section-mode" :model-value="measurementSectionModes.customer" aria-label="客户下单数据计算方式" @change="updateMeasurementSectionMode('customer', $event)"><el-option label="实测原尺寸(不调整)" value="ACTUAL" /><el-option label="箱规尺寸" value="ADD" /><el-option label="箱规系数" value="MULTIPLY" :disabled="multiplyModeDisabled" /></el-select></div>
|
||||
<div class="measurement-section-title"><button type="button" class="measurement-section-toggle" :aria-expanded="!measurementCollapsed.customer" @click="toggleMeasurementSection('customer')"><h3>一、客户下单数据</h3><span v-if="measurementCollapsed.customer" class="measurement-section-summary"><span>总件数:{{ measurementTotals(customerMeasurementRows).count ?? '—' }}</span><span>总毛重:{{ formatNumber(measurementTotals(customerMeasurementRows).gross) }}</span><span>总体积:{{ formatMeasurementVolume(measurementTotals(customerMeasurementRows).volume) }}</span><span>总材积重:{{ formatNumber(measurementTotals(customerMeasurementRows).volumetric) }}</span><span>重量体积:{{ formatMeasurementVolume(measurementTotals(customerMeasurementRows).weightVolume) }}</span></span><ChevronUp v-if="!measurementCollapsed.customer" :size="18" /><ChevronDown v-else :size="18" /></button><el-select class="measurement-section-mode" :model-value="measurementSectionModes.customer" aria-label="客户下单数据计算方式" @change="updateMeasurementSectionMode('customer', $event)"><el-option label="实测原尺寸(不调整)" value="ACTUAL" /><el-option label="箱规尺寸" value="ADD" /><el-option label="箱规系数" value="MULTIPLY" :disabled="multiplyModeDisabled" /></el-select></div>
|
||||
<div v-if="!measurementCollapsed.customer">
|
||||
<div class="detail-table-wrap"><el-table :data="customerMeasurementRows" border stripe empty-text="暂无客户下单数据"><el-table-column v-for="column in customerMeasurementColumns" :key="column.key" :label="column.label" :prop="column.key" min-width="120"><template #default="{ row }"><LineClampText v-if="column.key === 'remark'" :content="measurementValue(row as Record<string, unknown>, column.key)" /><template v-else>{{ measurementValue(row as Record<string, unknown>, column.key) }}</template></template></el-table-column></el-table></div>
|
||||
<div class="measurement-total"><strong>合计</strong><span>箱数:{{ measurementTotals(customerMeasurementRows).count ?? '—' }}</span><span>总毛重:{{ formatNumber(measurementTotals(customerMeasurementRows).gross) }}</span><span>总体积:{{ formatNumber(measurementTotals(customerMeasurementRows).volume, 6) }}</span><span>总材积重:{{ formatNumber(measurementTotals(customerMeasurementRows).volumetric) }}</span><span>重量体积:{{ formatNumber(measurementTotals(customerMeasurementRows).weightVolume, 6) }}</span></div>
|
||||
<div class="measurement-total"><strong>合计</strong><span>箱数:{{ measurementTotals(customerMeasurementRows).count ?? '—' }}</span><span>总毛重:{{ formatNumber(measurementTotals(customerMeasurementRows).gross) }}</span><span>总体积:{{ formatMeasurementVolume(measurementTotals(customerMeasurementRows).volume) }}</span><span>总材积重:{{ formatNumber(measurementTotals(customerMeasurementRows).volumetric) }}</span><span>重量体积:{{ formatMeasurementVolume(measurementTotals(customerMeasurementRows).weightVolume) }}</span></div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="measurement-section">
|
||||
<div class="measurement-section-title"><button type="button" class="measurement-section-toggle" :aria-expanded="!measurementCollapsed.outer" @click="toggleMeasurementSection('outer')"><h3>二、外箱规数据</h3><span v-if="measurementCollapsed.outer" class="measurement-section-summary"><span>总件数:{{ measurementTotals(outerMeasurementRows).count ?? '—' }}</span><span>总毛重:{{ formatNumber(measurementTotals(outerMeasurementRows).gross) }}</span><span>总体积:{{ formatNumber(measurementTotals(outerMeasurementRows).volume, 6) }}</span><span>总材积重:{{ formatNumber(measurementTotals(outerMeasurementRows).volumetric) }}</span><span>重量体积:{{ formatNumber(measurementTotals(outerMeasurementRows).weightVolume, 6) }}</span></span><ChevronUp v-if="!measurementCollapsed.outer" :size="18" /><ChevronDown v-else :size="18" /></button></div>
|
||||
<div class="measurement-section-title"><button type="button" class="measurement-section-toggle" :aria-expanded="!measurementCollapsed.outer" @click="toggleMeasurementSection('outer')"><h3>二、外箱规数据</h3><span v-if="measurementCollapsed.outer" class="measurement-section-summary"><span>总件数:{{ measurementTotals(outerMeasurementRows).count ?? '—' }}</span><span>总毛重:{{ formatNumber(measurementTotals(outerMeasurementRows).gross) }}</span><span>总体积:{{ formatMeasurementVolume(measurementTotals(outerMeasurementRows).volume) }}</span><span>总材积重:{{ formatNumber(measurementTotals(outerMeasurementRows).volumetric) }}</span><span>重量体积:{{ formatMeasurementVolume(measurementTotals(outerMeasurementRows).weightVolume) }}</span></span><ChevronUp v-if="!measurementCollapsed.outer" :size="18" /><ChevronDown v-else :size="18" /></button></div>
|
||||
<div v-if="!measurementCollapsed.outer">
|
||||
<div v-loading="measurementLoading" class="detail-table-wrap"><el-table :data="outerMeasurementRows" border stripe empty-text="暂无外箱规数据"><el-table-column label="#" prop="itemNo" min-width="70" /><el-table-column v-for="column in outerColumns.filter((item) => item.key !== 'itemNo')" :key="column.key" :label="column.label" :prop="column.key" min-width="150"><template #default="{ row }"><LineClampText v-if="column.key === 'remark'" :content="measurementValue(row as Record<string, unknown>, column.key)" /><template v-else>{{ measurementValue(row as Record<string, unknown>, column.key) }}</template></template></el-table-column></el-table></div>
|
||||
<div class="measurement-total"><strong>合计</strong><span>箱数:{{ measurementTotals(outerMeasurementRows).count ?? '—' }}</span><span>总毛重:{{ formatNumber(measurementTotals(outerMeasurementRows).gross) }}</span><span>总体积:{{ formatNumber(measurementTotals(outerMeasurementRows).volume, 6) }}</span><span>总材积重:{{ formatNumber(measurementTotals(outerMeasurementRows).volumetric) }}</span><span>重量体积:{{ formatNumber(measurementTotals(outerMeasurementRows).weightVolume, 6) }}</span></div>
|
||||
<div class="measurement-total"><strong>合计</strong><span>箱数:{{ measurementTotals(outerMeasurementRows).count ?? '—' }}</span><span>总毛重:{{ formatNumber(measurementTotals(outerMeasurementRows).gross) }}</span><span>总体积:{{ formatMeasurementVolume(measurementTotals(outerMeasurementRows).volume) }}</span><span>总材积重:{{ formatNumber(measurementTotals(outerMeasurementRows).volumetric) }}</span><span>重量体积:{{ formatMeasurementVolume(measurementTotals(outerMeasurementRows).weightVolume) }}</span></div>
|
||||
</div>
|
||||
</section>
|
||||
<div class="measurement-rule-selector">
|
||||
@@ -2678,7 +2703,7 @@ function orderDialogTitle(action: string, inboundCode?: string | null) {
|
||||
<span v-if="!measurementStrategyEditable" class="measurement-rule-selector__hint">{{ measurementStrategyLockedHint }}</span>
|
||||
</div>
|
||||
<section class="measurement-section">
|
||||
<div class="measurement-section-title"><button type="button" class="measurement-section-toggle" :aria-expanded="!measurementCollapsed.actual" @click="toggleMeasurementSection('actual')"><h3>三、实际测量数据</h3><span v-if="measurementCollapsed.actual" class="measurement-section-summary"><span>总件数:{{ measurementTotals(actualMeasurementRows).count ?? '—' }}</span><span>总毛重:{{ formatNumber(measurementTotals(actualMeasurementRows).gross) }}</span><span>总体积:{{ formatNumber(measurementTotals(actualMeasurementRows).volume, 6) }}</span><span>总材积重:{{ formatNumber(measurementTotals(actualMeasurementRows).volumetric) }}</span><span>重量体积:{{ formatNumber(measurementTotals(actualMeasurementRows).weightVolume, 6) }}</span></span><ChevronUp v-if="!measurementCollapsed.actual" :size="18" /><ChevronDown v-else :size="18" /></button><el-select class="measurement-section-mode" :model-value="measurementSectionModes.actual" aria-label="实际测量数据计算方式" @change="updateMeasurementSectionMode('actual', $event)"><el-option label="实测原尺寸(不调整)" value="ACTUAL" /><el-option label="箱规尺寸" value="ADD" /><el-option label="箱规系数" value="MULTIPLY" :disabled="multiplyModeDisabled" /></el-select></div>
|
||||
<div class="measurement-section-title"><button type="button" class="measurement-section-toggle" :aria-expanded="!measurementCollapsed.actual" @click="toggleMeasurementSection('actual')"><h3>三、实际测量数据</h3><span v-if="measurementCollapsed.actual" class="measurement-section-summary"><span>总件数:{{ measurementTotals(actualMeasurementRows).count ?? '—' }}</span><span>总毛重:{{ formatNumber(measurementTotals(actualMeasurementRows).gross) }}</span><span>总体积:{{ formatMeasurementVolume(measurementTotals(actualMeasurementRows).volume) }}</span><span>总材积重:{{ formatNumber(measurementTotals(actualMeasurementRows).volumetric) }}</span><span>重量体积:{{ formatMeasurementVolume(measurementTotals(actualMeasurementRows).weightVolume) }}</span></span><ChevronUp v-if="!measurementCollapsed.actual" :size="18" /><ChevronDown v-else :size="18" /></button><el-select class="measurement-section-mode" :model-value="measurementSectionModes.actual" aria-label="实际测量数据计算方式" @change="updateMeasurementSectionMode('actual', $event)"><el-option label="实测原尺寸(不调整)" value="ACTUAL" /><el-option label="箱规尺寸" value="ADD" /><el-option label="箱规系数" value="MULTIPLY" :disabled="multiplyModeDisabled" /></el-select></div>
|
||||
<div v-if="!measurementCollapsed.actual">
|
||||
<div v-loading="measurementLoading" class="detail-table-wrap">
|
||||
<el-table :data="actualMeasurementRows" row-key="id" border stripe empty-text="暂无实际测量数据">
|
||||
@@ -2716,7 +2741,7 @@ function orderDialogTitle(action: string, inboundCode?: string | null) {
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div class="measurement-total"><strong>合计</strong><span>箱数:{{ measurementTotals(actualMeasurementRows).count ?? '—' }}</span><span>总毛重:{{ formatNumber(measurementTotals(actualMeasurementRows).gross) }}</span><span>总体积:{{ formatNumber(measurementTotals(actualMeasurementRows).volume, 6) }}</span><span>总材积重:{{ formatNumber(measurementTotals(actualMeasurementRows).volumetric) }}</span><span>重量体积:{{ formatNumber(measurementTotals(actualMeasurementRows).weightVolume, 6) }}</span></div>
|
||||
<div class="measurement-total"><strong>合计</strong><span>箱数:{{ measurementTotals(actualMeasurementRows).count ?? '—' }}</span><span>总毛重:{{ formatNumber(measurementTotals(actualMeasurementRows).gross) }}</span><span>总体积:{{ formatMeasurementVolume(measurementTotals(actualMeasurementRows).volume) }}</span><span>总材积重:{{ formatNumber(measurementTotals(actualMeasurementRows).volumetric) }}</span><span>重量体积:{{ formatMeasurementVolume(measurementTotals(actualMeasurementRows).weightVolume) }}</span></div>
|
||||
<section class="weight-evidence" aria-label="称重图片与服务端确认重量">
|
||||
<div class="weight-evidence__heading">
|
||||
<div><h4>称重图片与确认重量</h4></div>
|
||||
@@ -2779,7 +2804,7 @@ function orderDialogTitle(action: string, inboundCode?: string | null) {
|
||||
|
||||
<template v-else>
|
||||
<div v-if="measurementReviewSelected" id="unified-orders-results" class="orders-status-toolbar" role="tabpanel">
|
||||
<InboundTaskList mode="review" title="订单列表" class="is-embedded" @changed="loadStatusCounts">
|
||||
<InboundTaskList ref="measurementReviewList" :task-loader="loadMeasurementReviewTasks" mode="review" title="订单列表" class="is-embedded" @changed="loadStatusCounts">
|
||||
<template #status-tabs>
|
||||
<div ref="statusTabList" class="status-tabs density-status-tabs workbench-list-tabs" role="tablist" aria-label="订单主状态" :aria-busy="statusCountsLoading">
|
||||
<button
|
||||
@@ -2802,6 +2827,18 @@ function orderDialogTitle(action: string, inboundCode?: string | null) {
|
||||
<ApiErrorMeta :details="statusCountsError" />
|
||||
</div>
|
||||
</template>
|
||||
<template #filters="{ loading: reviewLoading }">
|
||||
<div class="orders-query-panel integrated-query-group">
|
||||
<QueryToolbar id="unified-orders-advanced-filters" v-model="filters" class="orders-query-toolbar" ariaLabel="订单列表筛选" :fields="queryFields" :available-fields="allQueryFields" preference-key="unified-orders-query-v2" :loading="reviewLoading" :search-on-reset="false" @search="search" @reset="reset">
|
||||
<template #actions>
|
||||
<el-button :aria-expanded="showAdvancedFilters" aria-controls="unified-orders-advanced-filters" @click="showAdvancedFilters = !showAdvancedFilters">
|
||||
<SlidersHorizontal :size="16" />{{ showAdvancedFilters ? '收起筛选' : '高级筛选' }}
|
||||
<ChevronUp v-if="showAdvancedFilters" :size="15" /><ChevronDown v-else :size="15" />
|
||||
</el-button>
|
||||
</template>
|
||||
</QueryToolbar>
|
||||
</div>
|
||||
</template>
|
||||
</InboundTaskList>
|
||||
</div>
|
||||
<ListPageLayout v-else ariaLabel="统一订单列表">
|
||||
|
||||
Reference in New Issue
Block a user