优化
This commit is contained in:
@@ -66,9 +66,9 @@ describe('platform container loading API', () => {
|
|||||||
it('normalizes every long id in candidates and plans', async () => {
|
it('normalizes every long id in candidates and plans', async () => {
|
||||||
vi.mocked(http.get)
|
vi.mocked(http.get)
|
||||||
.mockResolvedValueOnce({ code: '200', data: container })
|
.mockResolvedValueOnce({ code: '200', data: container })
|
||||||
.mockResolvedValueOnce({ code: '200', data: [{ accountId: 1, inboundId: 2, orderId: 4, inboundCode: 'IN-1', warehouseId: 5, destinationWarehouseId: 6, channelCode: 'NY_EXX', channelName: '纽约EXX海铁', goodsSummary: '服装、鞋类', availablePackageCount: '7', unitGrossWeightKg: '12.5', unitNetWeightKg: '11.8', averageUnitVolumeCbm: '0.187', availableVolumeCbm: '1.309', coLoadGroupId: 9007199254740999n, groupNo: 'CLG-9007199254740999', memberCount: '3', groupTotalBoxes: '21', locked: false, blockReason: null }] })
|
.mockResolvedValueOnce({ code: '200', data: [{ accountId: 1, inboundId: 2, orderId: 4, inboundCode: 'IN-1', orderStatusSnapshot: 'WAIT_SPLIT', expectedArrivalAt: '2026-09-10T09:30:00', warehouseId: 5, destinationWarehouseId: 6, channelCode: 'NY_EXX', channelName: '纽约EXX海铁', goodsSummary: '服装、鞋类', availablePackageCount: '7', unitGrossWeightKg: '12.5', unitNetWeightKg: '11.8', averageUnitVolumeCbm: '0.187', availableVolumeCbm: '1.309', coLoadGroupId: 9007199254740999n, groupNo: 'CLG-9007199254740999', memberCount: '3', groupTotalBoxes: '21', locked: false, blockReason: null }] })
|
||||||
expect((await getContainer('9')).id).toBe(String(container.id))
|
expect((await getContainer('9')).id).toBe(String(container.id))
|
||||||
expect(await listSmartLoadingCandidates('7', '5')).toMatchObject([{ accountId: '1', inboundId: '2', inboundCode: 'IN-1', destinationWarehouseId: '6', channelCode: 'NY_EXX', channelName: '纽约EXX海铁', goodsSummary: '服装、鞋类', availablePackageCount: 7, unitGrossWeightKg: 12.5, unitNetWeightKg: 11.8, averageUnitVolumeCbm: 0.187, availableVolumeCbm: 1.309, coLoadGroupId: '9007199254740999', groupNo: 'CLG-9007199254740999', memberCount: 3, groupTotalBoxes: 21, locked: false, blockReason: null }])
|
expect(await listSmartLoadingCandidates('7', '5')).toMatchObject([{ accountId: '1', inboundId: '2', inboundCode: 'IN-1', orderStatusSnapshot: 'WAIT_SPLIT', expectedArrivalAt: '2026-09-10T09:30:00', destinationWarehouseId: '6', channelCode: 'NY_EXX', channelName: '纽约EXX海铁', goodsSummary: '服装、鞋类', availablePackageCount: 7, unitGrossWeightKg: 12.5, unitNetWeightKg: 11.8, averageUnitVolumeCbm: 0.187, availableVolumeCbm: 1.309, coLoadGroupId: '9007199254740999', groupNo: 'CLG-9007199254740999', memberCount: 3, groupTotalBoxes: 21, locked: false, blockReason: null }])
|
||||||
expect(http.get).toHaveBeenLastCalledWith('/platform/v1/first-leg/smart-loading/inbound-candidates', { params: { bookingId: '7', warehouseId: '5' } })
|
expect(http.get).toHaveBeenLastCalledWith('/platform/v1/first-leg/smart-loading/inbound-candidates', { params: { bookingId: '7', warehouseId: '5' } })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -60,6 +60,15 @@ const plan = {
|
|||||||
pdaTaskId: null, pdaTaskStatus: null, pdaTaskVersion: null,
|
pdaTaskId: null, pdaTaskStatus: null, pdaTaskVersion: null,
|
||||||
items: [{ id: '101', loadingPlanId: '99', accountId: '1', inboundId: '2', inboundDetailId: '3', orderId: '4', orderNo: 'ORD-1', inboundCode: 'IN-1', customerName: '示例企业', destinationWarehouseId: '9', destinationWarehouseCode: 'LAX9', destinationWarehouseName: 'LAX9', skuCode: 'SKU-1', productNameZh: '服装', packageCount: 6, productQuantity: 6, grossWeightKg: 60, volumeCbm: 1.2, loadSequence: 1, version: 1, pickedPackageCount: 0, minPackageCount: 1, canAdjust: true, canRemove: true }],
|
items: [{ id: '101', loadingPlanId: '99', accountId: '1', inboundId: '2', inboundDetailId: '3', orderId: '4', orderNo: 'ORD-1', inboundCode: 'IN-1', customerName: '示例企业', destinationWarehouseId: '9', destinationWarehouseCode: 'LAX9', destinationWarehouseName: 'LAX9', skuCode: 'SKU-1', productNameZh: '服装', packageCount: 6, productQuantity: 6, grossWeightKg: 60, volumeCbm: 1.2, loadSequence: 1, version: 1, pickedPackageCount: 0, minPackageCount: 1, canAdjust: true, canRemove: true }],
|
||||||
}
|
}
|
||||||
|
const candidate = {
|
||||||
|
accountId: '1', inboundId: '5', orderId: '7', orderNo: 'ORD-2', inboundCode: 'IN-2',
|
||||||
|
orderStatusSnapshot: 'WAIT_SPLIT', expectedArrivalAt: '2026-09-10T09:30:00',
|
||||||
|
warehouseId: '8', channelCode: 'ZBYHK', channelName: '美森正班海卡包税', destinationWarehouseId: '9',
|
||||||
|
destinationWarehouseCode: 'LAX9', destinationWarehouseName: 'LAX9', customerName: '另一企业', goodsSummary: '鞋类',
|
||||||
|
availablePackageCount: 12, unitGrossWeightKg: 10, unitNetWeightKg: 9, averageUnitVolumeCbm: 0.2,
|
||||||
|
availableVolumeCbm: 2.4, coLoadGroupId: null, groupNo: null, memberCount: null, groupTotalBoxes: null,
|
||||||
|
locked: null, blockReason: null,
|
||||||
|
}
|
||||||
|
|
||||||
describe('GlobalSmartLoadingView', () => {
|
describe('GlobalSmartLoadingView', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -67,7 +76,7 @@ describe('GlobalSmartLoadingView', () => {
|
|||||||
vi.clearAllMocks()
|
vi.clearAllMocks()
|
||||||
vi.mocked(listContainers).mockResolvedValue({ records: [container], page: 1, size: 100, total: 1, pages: 1 })
|
vi.mocked(listContainers).mockResolvedValue({ records: [container], page: 1, size: 100, total: 1, pages: 1 })
|
||||||
vi.mocked(getSmartLoadingPlanByBooking).mockResolvedValue(plan)
|
vi.mocked(getSmartLoadingPlanByBooking).mockResolvedValue(plan)
|
||||||
vi.mocked(listSmartLoadingCandidates).mockResolvedValue([{ accountId: '1', inboundId: '5', orderId: '7', orderNo: 'ORD-2', inboundCode: 'IN-2', warehouseId: '8', channelCode: 'ZBYHK', channelName: '美森正班海卡包税', destinationWarehouseId: '9', destinationWarehouseCode: 'LAX9', destinationWarehouseName: 'LAX9', customerName: '另一企业', goodsSummary: '鞋类', availablePackageCount: 12, unitGrossWeightKg: 10, unitNetWeightKg: 9, averageUnitVolumeCbm: 0.2, availableVolumeCbm: 2.4, coLoadGroupId: null, groupNo: null, memberCount: null, groupTotalBoxes: null, locked: null, blockReason: null }])
|
vi.mocked(listSmartLoadingCandidates).mockResolvedValue([candidate])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('loads real containers, candidates and the selected plan into the three-column workbench', async () => {
|
it('loads real containers, candidates and the selected plan into the three-column workbench', async () => {
|
||||||
@@ -199,6 +208,47 @@ describe('GlobalSmartLoadingView', () => {
|
|||||||
expect(source).toContain(': candidate.availablePackageCount')
|
expect(source).toContain(': candidate.availablePackageCount')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('filters candidates by expected arrival range and order status', async () => {
|
||||||
|
vi.mocked(listSmartLoadingCandidates).mockResolvedValue([
|
||||||
|
{ ...candidate, inboundId: '51', inboundCode: 'IN-MATCH', orderStatusSnapshot: 'WAIT_SPLIT', expectedArrivalAt: '2026-09-11T08:00:00' },
|
||||||
|
{ ...candidate, inboundId: '52', inboundCode: 'IN-WRONG-STATUS', orderStatusSnapshot: 'LOADING', expectedArrivalAt: '2026-09-11T08:00:00' },
|
||||||
|
{ ...candidate, inboundId: '53', inboundCode: 'IN-WRONG-DATE', orderStatusSnapshot: 'WAIT_SPLIT', expectedArrivalAt: '2026-09-20T08:00:00' },
|
||||||
|
])
|
||||||
|
const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } })
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
wrapper.getComponent(QueryToolbar).vm.$emit('search', {
|
||||||
|
customerId: '', inboundCode: '', orderStatus: 'WAIT_SPLIT', expectedArrivalRange: ['2026-09-10', '2026-09-12'],
|
||||||
|
})
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
const candidates = wrapper.get('.candidate-pane').text()
|
||||||
|
expect(candidates).toContain('IN-MATCH')
|
||||||
|
expect(candidates).not.toContain('IN-WRONG-STATUS')
|
||||||
|
expect(candidates).not.toContain('IN-WRONG-DATE')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('paginates large inbound candidate results with twenty rows by default', async () => {
|
||||||
|
vi.mocked(listSmartLoadingCandidates).mockResolvedValue(Array.from({ length: 25 }, (_, index) => ({
|
||||||
|
...candidate,
|
||||||
|
inboundId: String(100 + index),
|
||||||
|
orderId: String(200 + index),
|
||||||
|
inboundCode: `IN-${String(index + 1).padStart(3, '0')}`,
|
||||||
|
})))
|
||||||
|
const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } })
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
const pane = wrapper.get('.candidate-pane')
|
||||||
|
expect(pane.text()).toContain('IN-020')
|
||||||
|
expect(pane.text()).not.toContain('IN-021')
|
||||||
|
expect(pane.text()).toContain('共 25 条 · 第 1 / 2 页')
|
||||||
|
|
||||||
|
await pane.findAll('button').find((button) => button.attributes('aria-label') === '下一页')!.trigger('click')
|
||||||
|
await flushPromises()
|
||||||
|
expect(pane.text()).toContain('IN-021')
|
||||||
|
expect(pane.text()).not.toContain('IN-001')
|
||||||
|
})
|
||||||
|
|
||||||
it('aggregates a two-order candidate group and calls only whole-group assignment', async () => {
|
it('aggregates a two-order candidate group and calls only whole-group assignment', async () => {
|
||||||
vi.mocked(listSmartLoadingCandidates).mockResolvedValue([
|
vi.mocked(listSmartLoadingCandidates).mockResolvedValue([
|
||||||
{ accountId: '1', inboundId: '201', orderId: '301', orderNo: 'ORD-201', inboundCode: 'NYEXX2608290051', warehouseId: '8', channelCode: 'ZBYHK', channelName: '美森正班海卡包税', destinationWarehouseId: '9', destinationWarehouseCode: 'LAX9', destinationWarehouseName: 'LAX9', customerName: '客户一', goodsSummary: '货物一', availablePackageCount: 4, unitGrossWeightKg: 10, unitNetWeightKg: 9, averageUnitVolumeCbm: 0.2, availableVolumeCbm: 0.8, coLoadGroupId: '2093539779795795969', groupNo: 'CLG-2093539779795795969', memberCount: 3, groupTotalBoxes: 12, locked: false, blockReason: null },
|
{ accountId: '1', inboundId: '201', orderId: '301', orderNo: 'ORD-201', inboundCode: 'NYEXX2608290051', warehouseId: '8', channelCode: 'ZBYHK', channelName: '美森正班海卡包税', destinationWarehouseId: '9', destinationWarehouseCode: 'LAX9', destinationWarehouseName: 'LAX9', customerName: '客户一', goodsSummary: '货物一', availablePackageCount: 4, unitGrossWeightKg: 10, unitNetWeightKg: 9, averageUnitVolumeCbm: 0.2, availableVolumeCbm: 0.8, coLoadGroupId: '2093539779795795969', groupNo: 'CLG-2093539779795795969', memberCount: 3, groupTotalBoxes: 12, locked: false, blockReason: null },
|
||||||
@@ -260,13 +310,13 @@ describe('GlobalSmartLoadingView', () => {
|
|||||||
expect(wrapper.findAll('button[aria-label="列设置"]')).toHaveLength(0)
|
expect(wrapper.findAll('button[aria-label="列设置"]')).toHaveLength(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('keeps only customer and inbound code filters', async () => {
|
it('keeps the confirmed candidate filters without unrelated customs fields', async () => {
|
||||||
const source = readFileSync(resolve(process.cwd(), 'src/modules/container-loading/pages/GlobalSmartLoadingView.vue'), 'utf8')
|
const source = readFileSync(resolve(process.cwd(), 'src/modules/container-loading/pages/GlobalSmartLoadingView.vue'), 'utf8')
|
||||||
const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } })
|
const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } })
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
|
|
||||||
const fields = wrapper.findComponent(QueryToolbar).props('fields') as Array<{ key: string; options?: Array<{ label: string; value: string }> }>
|
const fields = wrapper.findComponent(QueryToolbar).props('fields') as Array<{ key: string; options?: Array<{ label: string; value: string }> }>
|
||||||
expect(fields.map((field) => field.key)).toEqual(['customerId', 'inboundCode'])
|
expect(fields.map((field) => field.key)).toEqual(['customerId', 'inboundCode', 'expectedArrivalRange', 'orderStatus'])
|
||||||
expect(source).not.toContain('目的仓')
|
expect(source).not.toContain('目的仓')
|
||||||
|
|
||||||
expect(wrapper.text()).not.toContain('税率')
|
expect(wrapper.text()).not.toContain('税率')
|
||||||
@@ -529,6 +579,9 @@ describe('GlobalSmartLoadingView', () => {
|
|||||||
expect(source).toContain('.container-pane { max-height: min(58vh, 460px); }')
|
expect(source).toContain('.container-pane { max-height: min(58vh, 460px); }')
|
||||||
expect(source).toContain('grid-template-columns: minmax(390px, 1.1fr) minmax(320px, .9fr) minmax(460px, 1.35fr)')
|
expect(source).toContain('grid-template-columns: minmax(390px, 1.1fr) minmax(320px, .9fr) minmax(460px, 1.35fr)')
|
||||||
expect(source).toContain('min-height: 112px')
|
expect(source).toContain('min-height: 112px')
|
||||||
|
expect(source).toContain("label: '预计到货时间'")
|
||||||
|
expect(source).toContain("label: '订单状态'")
|
||||||
|
expect(source).toContain(':page-sizes="[20, 50, 100]"')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('uses overflow-aware channel tabs with a compact right-aligned summary', () => {
|
it('uses overflow-aware channel tabs with a compact right-aligned summary', () => {
|
||||||
|
|||||||
@@ -54,8 +54,10 @@ const selectedCandidate = ref<SmartLoadingCandidateRow | null>(null)
|
|||||||
const assignForm = reactive({ packageCount: 1 })
|
const assignForm = reactive({ packageCount: 1 })
|
||||||
const quantityDrafts = reactive<Record<string, number>>({})
|
const quantityDrafts = reactive<Record<string, number>>({})
|
||||||
const planGroupsByInboundId = ref(new Map<string, SmartLoadingPlanCoLoadGroup>())
|
const planGroupsByInboundId = ref(new Map<string, SmartLoadingPlanCoLoadGroup>())
|
||||||
const query = ref<QueryModel>({ customerId: '', inboundCode: '' })
|
const query = ref<QueryModel>({ customerId: '', inboundCode: '', expectedArrivalRange: [], orderStatus: '' })
|
||||||
const appliedQuery = ref<QueryModel>({ ...query.value })
|
const appliedQuery = ref<QueryModel>({ ...query.value })
|
||||||
|
const candidatePage = ref(1)
|
||||||
|
const candidatePageSize = ref(20)
|
||||||
let selectionRequest = 0
|
let selectionRequest = 0
|
||||||
|
|
||||||
const canAssign = computed(() => access.can('platform:firstleg:smart-loading:assign'))
|
const canAssign = computed(() => access.can('platform:firstleg:smart-loading:assign'))
|
||||||
@@ -171,9 +173,23 @@ const volumePercent = computed(() => volumeLimit.value ? totalVolume.value / vol
|
|||||||
const weightPercent = computed(() => weightLimit.value ? totalWeight.value / weightLimit.value * 100 : 0)
|
const weightPercent = computed(() => weightLimit.value ? totalWeight.value / weightLimit.value * 100 : 0)
|
||||||
const displayedVolumePercent = computed(() => percentage(volumePercent.value))
|
const displayedVolumePercent = computed(() => percentage(volumePercent.value))
|
||||||
const displayedWeightPercent = computed(() => percentage(weightPercent.value))
|
const displayedWeightPercent = computed(() => percentage(weightPercent.value))
|
||||||
|
const candidateOrderStatusLabels: Record<string, string> = {
|
||||||
|
DRAFT: '草稿', SUBMITTED: '待审核', PENDING_REVIEW: '待审核', APPROVED: '已审核', ACCEPTED: '已受理',
|
||||||
|
REJECTED: '已驳回', PROCESSING: '处理中', WAIT_MEASURE: '待测量数据', WAIT_SPLIT: '待分柜',
|
||||||
|
LOADING: '装柜中', LOADED: '装柜完成', COMPLETED: '已完成', DELIVERED: '派送完成', CANCELLED: '已取消',
|
||||||
|
}
|
||||||
|
function candidateOrderStatusLabel(status: string) {
|
||||||
|
return candidateOrderStatusLabels[status] || '未知状态'
|
||||||
|
}
|
||||||
|
const candidateOrderStatusOptions = computed(() => [...new Set(candidates.value
|
||||||
|
.map((candidate) => candidate.orderStatusSnapshot?.trim().toUpperCase() || '')
|
||||||
|
.filter(Boolean))]
|
||||||
|
.map((status) => ({ value: status, label: candidateOrderStatusLabel(status) })))
|
||||||
const queryFields = computed<QueryField[]>(() => [
|
const queryFields = computed<QueryField[]>(() => [
|
||||||
{ key: 'customerId', type: 'select', label: '客户', placeholder: '全部客户', width: 220, options: customers.value.filter((customer) => customer.status === 'ACTIVE').map((customer) => ({ label: customer.tenantName, value: String(customer.id) })), props: { filterable: true, loading: customersLoading.value } },
|
{ key: 'customerId', type: 'select', label: '客户', placeholder: '全部客户', width: 220, options: customers.value.filter((customer) => customer.status === 'ACTIVE').map((customer) => ({ label: customer.tenantName, value: String(customer.id) })), props: { filterable: true, loading: customersLoading.value } },
|
||||||
{ key: 'inboundCode', type: 'input', label: '入仓码', placeholder: '请输入入仓码', width: 240 },
|
{ key: 'inboundCode', type: 'input', label: '入仓码', placeholder: '请输入入仓码', width: 240 },
|
||||||
|
{ key: 'expectedArrivalRange', type: 'daterange', label: '预计到货时间', placeholder: '请选择预计到货时间范围', width: 300 },
|
||||||
|
{ key: 'orderStatus', type: 'select', label: '订单状态', placeholder: '全部订单状态', width: 200, options: candidateOrderStatusOptions.value },
|
||||||
])
|
])
|
||||||
|
|
||||||
const filteredContainers = computed(() => containers.value.filter((item) => (
|
const filteredContainers = computed(() => containers.value.filter((item) => (
|
||||||
@@ -184,13 +200,27 @@ const filteredContainers = computed(() => containers.value.filter((item) => (
|
|||||||
const filteredCandidates = computed(() => {
|
const filteredCandidates = computed(() => {
|
||||||
const customerId = String(appliedQuery.value.customerId || '')
|
const customerId = String(appliedQuery.value.customerId || '')
|
||||||
const inboundCode = String(appliedQuery.value.inboundCode || '').trim().toLowerCase()
|
const inboundCode = String(appliedQuery.value.inboundCode || '').trim().toLowerCase()
|
||||||
|
const orderStatus = String(appliedQuery.value.orderStatus || '').trim().toUpperCase()
|
||||||
|
const arrivalRange = Array.isArray(appliedQuery.value.expectedArrivalRange)
|
||||||
|
? appliedQuery.value.expectedArrivalRange.map((value) => String(value || '').slice(0, 10))
|
||||||
|
: []
|
||||||
const channelCode = selectedContainer.value?.channelCode || activeChannelCode.value
|
const channelCode = selectedContainer.value?.channelCode || activeChannelCode.value
|
||||||
return displayedCandidates.value.filter((item) => {
|
return displayedCandidates.value.filter((item) => {
|
||||||
if (!channelCode || item.channelCode !== channelCode) return false
|
if (!channelCode || item.channelCode !== channelCode) return false
|
||||||
if (customerId && !item.sourceCandidates.some((member) => member.accountId === customerId)) return false
|
if (customerId && !item.sourceCandidates.some((member) => member.accountId === customerId)) return false
|
||||||
return !inboundCode || item.memberInboundCodes.some((code) => code.toLowerCase().includes(inboundCode))
|
if (inboundCode && !item.memberInboundCodes.some((code) => code.toLowerCase().includes(inboundCode))) return false
|
||||||
|
if (orderStatus && !item.sourceCandidates.some((member) => member.orderStatusSnapshot?.trim().toUpperCase() === orderStatus)) return false
|
||||||
|
if (arrivalRange.length === 2 && !item.sourceCandidates.some((member) => {
|
||||||
|
const arrivalDate = member.expectedArrivalAt?.slice(0, 10) || ''
|
||||||
|
return arrivalDate && arrivalDate >= arrivalRange[0] && arrivalDate <= arrivalRange[1]
|
||||||
|
})) return false
|
||||||
|
return true
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
const pagedCandidates = computed(() => {
|
||||||
|
const start = (candidatePage.value - 1) * candidatePageSize.value
|
||||||
|
return filteredCandidates.value.slice(start, start + candidatePageSize.value)
|
||||||
|
})
|
||||||
|
|
||||||
function preferredContainerId() {
|
function preferredContainerId() {
|
||||||
const visible = filteredContainers.value
|
const visible = filteredContainers.value
|
||||||
@@ -273,6 +303,7 @@ async function loadSelectedData(preserved: Record<string, number> = {}) {
|
|||||||
clearSelectionData()
|
clearSelectionData()
|
||||||
if (!container) return
|
if (!container) return
|
||||||
if (container.planStatus === 'COMPLETED') return
|
if (container.planStatus === 'COMPLETED') return
|
||||||
|
candidatePage.value = 1
|
||||||
selectionLoading.value = true
|
selectionLoading.value = true
|
||||||
try {
|
try {
|
||||||
const [candidateResult, planResult] = await Promise.all([
|
const [candidateResult, planResult] = await Promise.all([
|
||||||
@@ -351,6 +382,7 @@ async function selectChannel(channelCode: string) {
|
|||||||
}
|
}
|
||||||
async function applyFilters(model: QueryModel) {
|
async function applyFilters(model: QueryModel) {
|
||||||
appliedQuery.value = { ...model }
|
appliedQuery.value = { ...model }
|
||||||
|
candidatePage.value = 1
|
||||||
if (!filteredContainers.value.some((item) => item.id === selectedContainerId.value)) {
|
if (!filteredContainers.value.some((item) => item.id === selectedContainerId.value)) {
|
||||||
selectedContainerId.value = preferredContainerId()
|
selectedContainerId.value = preferredContainerId()
|
||||||
await loadSelectedData()
|
await loadSelectedData()
|
||||||
@@ -548,9 +580,13 @@ onMounted(() => { void load(); void loadCustomers() })
|
|||||||
<section class="dispatch-pane candidate-pane">
|
<section class="dispatch-pane candidate-pane">
|
||||||
<header class="pane-header"><div><strong>候选装柜明细</strong><small>{{ activeChannel?.name || '未选择渠道' }} · {{ selectedContainer?.warehouseName || '请先选择柜次' }}</small></div></header>
|
<header class="pane-header"><div><strong>候选装柜明细</strong><small>{{ activeChannel?.name || '未选择渠道' }} · {{ selectedContainer?.warehouseName || '请先选择柜次' }}</small></div></header>
|
||||||
<DataTable
|
<DataTable
|
||||||
:data="filteredCandidates"
|
v-model:page="candidatePage"
|
||||||
|
v-model:page-size="candidatePageSize"
|
||||||
|
:data="pagedCandidates"
|
||||||
:columns="candidateColumns"
|
:columns="candidateColumns"
|
||||||
:loading="loading || selectionLoading || mutating"
|
:loading="loading || selectionLoading || mutating"
|
||||||
|
:total="filteredCandidates.length"
|
||||||
|
:page-sizes="[20, 50, 100]"
|
||||||
stripe
|
stripe
|
||||||
preference-key="global-smart-loading-candidates"
|
preference-key="global-smart-loading-candidates"
|
||||||
column-settings-mode="hide"
|
column-settings-mode="hide"
|
||||||
|
|||||||
@@ -235,6 +235,8 @@ export interface SmartLoadingCandidate {
|
|||||||
orderId: string
|
orderId: string
|
||||||
orderNo: string | null
|
orderNo: string | null
|
||||||
inboundCode: string | null
|
inboundCode: string | null
|
||||||
|
orderStatusSnapshot?: string | null
|
||||||
|
expectedArrivalAt?: string | null
|
||||||
warehouseId: string
|
warehouseId: string
|
||||||
channelCode: string | null
|
channelCode: string | null
|
||||||
channelName: string | null
|
channelName: string | null
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { ApiResponseError, http, unwrapApiResponse, type ApiEnvelope } from '@/s
|
|||||||
export type ExportTaskStatus = 'PENDING' | 'RUNNING' | 'SUCCEEDED' | 'FAILED' | 'EXPIRED'
|
export type ExportTaskStatus = 'PENDING' | 'RUNNING' | 'SUCCEEDED' | 'FAILED' | 'EXPIRED'
|
||||||
export type KnownExportTaskType =
|
export type KnownExportTaskType =
|
||||||
| 'CUSTOMER_LIST'
|
| 'CUSTOMER_LIST'
|
||||||
|
| 'DOMESTIC_WAREHOUSE_ORDER_LIST'
|
||||||
| 'FINANCE_CUSTOMER_ACCOUNT_LIST'
|
| 'FINANCE_CUSTOMER_ACCOUNT_LIST'
|
||||||
| 'FINANCE_CUSTOMER_ACCOUNT_LEDGER'
|
| 'FINANCE_CUSTOMER_ACCOUNT_LEDGER'
|
||||||
| 'FINANCE_FEE_SOURCE_LIST'
|
| 'FINANCE_FEE_SOURCE_LIST'
|
||||||
@@ -56,7 +57,7 @@ export interface CreatePlatformExportTaskRequest {
|
|||||||
exportType: ExportTaskType
|
exportType: ExportTaskType
|
||||||
format: 'XLSX'
|
format: 'XLSX'
|
||||||
taskName: string
|
taskName: string
|
||||||
parameters: Record<string, string | number | boolean | null | undefined>
|
parameters: Record<string, string | number | boolean | null | undefined | string[]>
|
||||||
}
|
}
|
||||||
|
|
||||||
const basePath = '/platform/v1/files/export-tasks'
|
const basePath = '/platform/v1/files/export-tasks'
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ type DownloadFilters = QueryModel & {
|
|||||||
|
|
||||||
const exportTypeMeta: Record<string, { label: string; retryPermission: string }> = {
|
const exportTypeMeta: Record<string, { label: string; retryPermission: string }> = {
|
||||||
CUSTOMER_LIST: { label: '客户列表', retryPermission: 'platform:customer:export' },
|
CUSTOMER_LIST: { label: '客户列表', retryPermission: 'platform:customer:export' },
|
||||||
|
DOMESTIC_WAREHOUSE_ORDER_LIST: { label: '国内仓订单列表', retryPermission: 'platform:order:export' },
|
||||||
FINANCE_CUSTOMER_ACCOUNT_LIST: { label: '客户账户', retryPermission: 'platform:finance:customer-account:export' },
|
FINANCE_CUSTOMER_ACCOUNT_LIST: { label: '客户账户', retryPermission: 'platform:finance:customer-account:export' },
|
||||||
FINANCE_CUSTOMER_ACCOUNT_LEDGER: { label: '客户账户流水', retryPermission: 'platform:finance:customer-account:export' },
|
FINANCE_CUSTOMER_ACCOUNT_LEDGER: { label: '客户账户流水', retryPermission: 'platform:finance:customer-account:export' },
|
||||||
FINANCE_FEE_SOURCE_LIST: { label: '客户应收应付费用来源', retryPermission: 'platform:finance:customer-arap:export' },
|
FINANCE_FEE_SOURCE_LIST: { label: '客户应收应付费用来源', retryPermission: 'platform:finance:customer-arap:export' },
|
||||||
|
|||||||
@@ -152,7 +152,7 @@ describe('InboundFeePanel', () => {
|
|||||||
expect(wrapper.findAll('.calculation-flow > div > span').map((node) => node.text())).toEqual([
|
expect(wrapper.findAll('.calculation-flow > div > span').map((node) => node.text())).toEqual([
|
||||||
'实际重量',
|
'实际重量',
|
||||||
'实际体积',
|
'实际体积',
|
||||||
'折算量',
|
'体积重量',
|
||||||
'取大后的计费量',
|
'取大后的计费量',
|
||||||
])
|
])
|
||||||
expect(wrapper.findAll('.fee-detail-grid h4').map((node) => node.text())).toEqual([
|
expect(wrapper.findAll('.fee-detail-grid h4').map((node) => node.text())).toEqual([
|
||||||
|
|||||||
@@ -376,13 +376,13 @@ defineExpose({ reload: loadFee })
|
|||||||
|
|
||||||
<section class="calculation-section" aria-labelledby="calculation-title">
|
<section class="calculation-section" aria-labelledby="calculation-title">
|
||||||
<div class="section-title">
|
<div class="section-title">
|
||||||
<div><h4 id="calculation-title">计费依据</h4><p v-if="!compact">实际量与折算量取大后计费,金额以服务端快照为准。</p></div>
|
<div><h4 id="calculation-title">计费依据</h4><p v-if="!compact">实际重量与体积重量取大后计费,金额以服务端快照为准。</p></div>
|
||||||
<el-tag effect="plain">{{ taxModeLabel(snapshot.taxMode) }}</el-tag>
|
<el-tag effect="plain">{{ taxModeLabel(snapshot.taxMode) }}</el-tag>
|
||||||
</div>
|
</div>
|
||||||
<div class="calculation-flow">
|
<div class="calculation-flow">
|
||||||
<div><span>实际重量</span><strong>{{ formatNumber(snapshot.actualWeightKg, 3) }} KG</strong></div>
|
<div><span>实际重量</span><strong>{{ formatNumber(snapshot.actualWeightKg, 3) }} KG</strong></div>
|
||||||
<div><span>实际体积</span><strong>{{ formatNumber(snapshot.actualVolumeCbm, 4) }} CBM</strong></div>
|
<div><span>实际体积</span><strong>{{ formatNumber(snapshot.actualVolumeCbm, 4) }} CBM</strong></div>
|
||||||
<div><span>折算量</span><strong>{{ formatNumber(snapshot.convertedQuantity, snapshot.roundingScale) }} {{ chargeUnitLabel }}</strong><small>系数 {{ formatNumber(snapshot.conversionFactor, 4) }}</small></div>
|
<div><span>体积重量</span><strong>{{ formatNumber(snapshot.convertedQuantity, snapshot.roundingScale) }} {{ chargeUnitLabel }}</strong><small>系数 {{ formatNumber(snapshot.conversionFactor, 4) }}</small></div>
|
||||||
<div class="is-chargeable"><span>取大后的计费量</span><strong>{{ formatNumber(snapshot.chargeableQuantity, snapshot.roundingScale) }} {{ chargeUnitLabel }}</strong></div>
|
<div class="is-chargeable"><span>取大后的计费量</span><strong>{{ formatNumber(snapshot.chargeableQuantity, snapshot.roundingScale) }} {{ chargeUnitLabel }}</strong></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="amount-equation" aria-label="费用计算公式">
|
<div class="amount-equation" aria-label="费用计算公式">
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { flushPromises, shallowMount } from '@vue/test-utils'
|
import { flushPromises, shallowMount } from '@vue/test-utils'
|
||||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
import { listPlatformUnifiedOrders } from '@/modules/unified-orders/api/unifiedOrders'
|
import { listPlatformUnifiedOrders } from '@/modules/unified-orders/api/unifiedOrders'
|
||||||
|
import { createPlatformExportTask } from '@/modules/downloads'
|
||||||
|
import { showExportTaskCreatedDialog } from '@/shared/components/exportTaskCreatedDialog'
|
||||||
import UnifiedOrdersView from './UnifiedOrdersView.vue'
|
import UnifiedOrdersView from './UnifiedOrdersView.vue'
|
||||||
|
|
||||||
vi.mock('vue-router', () => ({
|
vi.mock('vue-router', () => ({
|
||||||
@@ -11,6 +13,8 @@ vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({
|
|||||||
vi.mock('@/modules/iam', () => ({ listPlatformTenants: vi.fn().mockResolvedValue({ records: [], page: 1, size: 200, total: 0, pages: 0 }) }))
|
vi.mock('@/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/warehouses', () => ({ listAllPlatformWarehouses: vi.fn().mockResolvedValue([]) }))
|
||||||
vi.mock('@/modules/inbounds', () => ({ downloadWeightImage: vi.fn(), getInboundMeasurement: vi.fn() }))
|
vi.mock('@/modules/inbounds', () => ({ downloadWeightImage: vi.fn(), getInboundMeasurement: vi.fn() }))
|
||||||
|
vi.mock('@/modules/downloads', () => ({ createPlatformExportTask: vi.fn() }))
|
||||||
|
vi.mock('@/shared/components/exportTaskCreatedDialog', () => ({ showExportTaskCreatedDialog: vi.fn() }))
|
||||||
vi.mock('@/modules/unified-orders/workflows', () => ({ UnifiedOrderWorkflowPanel: { template: '<div />' } }))
|
vi.mock('@/modules/unified-orders/workflows', () => ({ UnifiedOrderWorkflowPanel: { template: '<div />' } }))
|
||||||
vi.mock('@/modules/unified-orders/api/coLoadGroups', () => ({
|
vi.mock('@/modules/unified-orders/api/coLoadGroups', () => ({
|
||||||
cancelPlatformCoLoadGroup: vi.fn(),
|
cancelPlatformCoLoadGroup: vi.fn(),
|
||||||
@@ -45,7 +49,7 @@ function mountView() {
|
|||||||
AdminPage: { template: '<main><slot /></main>' },
|
AdminPage: { template: '<main><slot /></main>' },
|
||||||
ListPageLayout: { template: '<section><slot name="toolbar" /><slot name="state" /><slot /></section>' },
|
ListPageLayout: { template: '<section><slot name="toolbar" /><slot name="state" /><slot /></section>' },
|
||||||
QueryToolbar: { name: 'QueryToolbar', props: ['modelValue'], emits: ['update:modelValue', 'search', 'reset'], template: '<div data-testid="query-toolbar" />' },
|
QueryToolbar: { name: 'QueryToolbar', props: ['modelValue'], emits: ['update:modelValue', 'search', 'reset'], template: '<div data-testid="query-toolbar" />' },
|
||||||
DataTable: { name: 'DataTable', props: ['page', 'pageSize'], emits: ['update:page', 'update:pageSize'], template: '<div data-testid="data-table" />' },
|
DataTable: { name: 'DataTable', props: ['page', 'pageSize'], emits: ['update:page', 'update:pageSize', 'selection-change'], template: '<div data-testid="data-table"><slot name="toolbar-actions" /></div>' },
|
||||||
'el-button': { template: '<button><slot /></button>' },
|
'el-button': { template: '<button><slot /></button>' },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -55,6 +59,8 @@ function mountView() {
|
|||||||
describe('UnifiedOrdersView list request coordination', () => {
|
describe('UnifiedOrdersView list request coordination', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.mocked(listPlatformUnifiedOrders).mockReset().mockResolvedValue(emptyPage())
|
vi.mocked(listPlatformUnifiedOrders).mockReset().mockResolvedValue(emptyPage())
|
||||||
|
vi.mocked(createPlatformExportTask).mockReset().mockResolvedValue({ taskName: '国内仓订单', taskNo: 'EXP-1' } as never)
|
||||||
|
vi.mocked(showExportTaskCreatedDialog).mockReset().mockResolvedValue(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('loads once initially and once for query, reset, status tab and pagination', async () => {
|
it('loads once initially and once for query, reset, status tab and pagination', async () => {
|
||||||
@@ -135,4 +141,48 @@ describe('UnifiedOrdersView list request coordination', () => {
|
|||||||
await flushPromises()
|
await flushPromises()
|
||||||
expect(listPlatformUnifiedOrders).toHaveBeenLastCalledWith(expect.objectContaining({ fullContainer: undefined }))
|
expect(listPlatformUnifiedOrders).toHaveBeenLastCalledWith(expect.objectContaining({ fullContainer: undefined }))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('exports the selected business order numbers without loading other pages', async () => {
|
||||||
|
const wrapper = mountView()
|
||||||
|
await flushPromises()
|
||||||
|
wrapper.getComponent({ name: 'DataTable' }).vm.$emit('selection-change', [
|
||||||
|
{ orderId: '1', orderNo: 'ORDER-1', tenantId: '8' },
|
||||||
|
{ orderId: '2', orderNo: 'ORDER-2', tenantId: '8' },
|
||||||
|
])
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
|
||||||
|
await wrapper.findAll('button').find((button) => button.text().includes('导出'))!.trigger('click')
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
expect(createPlatformExportTask).toHaveBeenCalledWith({
|
||||||
|
exportType: 'DOMESTIC_WAREHOUSE_ORDER_LIST', format: 'XLSX', taskName: '国内仓订单-已选2票',
|
||||||
|
parameters: { selectedOrderNos: ['ORDER-1', 'ORDER-2'] },
|
||||||
|
})
|
||||||
|
expect(listPlatformUnifiedOrders).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('exports every result matching the applied filters instead of the current page', async () => {
|
||||||
|
const wrapper = mountView()
|
||||||
|
await flushPromises()
|
||||||
|
const toolbar = wrapper.getComponent({ name: 'QueryToolbar' })
|
||||||
|
toolbar.vm.$emit('update:modelValue', {
|
||||||
|
...toolbar.props('modelValue'), inboundCode: ' IN-8 ', loaded: 'false', createdRange: ['2026-08-01', '2026-08-31'],
|
||||||
|
})
|
||||||
|
toolbar.vm.$emit('search')
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
await wrapper.findAll('button').find((button) => button.text() === '导出')!.trigger('click')
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
expect(createPlatformExportTask).toHaveBeenCalledWith(expect.objectContaining({
|
||||||
|
exportType: 'DOMESTIC_WAREHOUSE_ORDER_LIST', taskName: '国内仓订单-当前筛选',
|
||||||
|
parameters: expect.objectContaining({
|
||||||
|
inboundCode: 'IN-8', loaded: false,
|
||||||
|
createdFrom: '2026-08-01T00:00:00', createdTo: '2026-08-31T23:59:59',
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
expect(createPlatformExportTask).not.toHaveBeenCalledWith(expect.objectContaining({
|
||||||
|
parameters: expect.objectContaining({ page: expect.anything(), size: expect.anything() }),
|
||||||
|
}))
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -117,7 +117,8 @@ describe('UnifiedOrdersView contract', () => {
|
|||||||
expect(columnSource).not.toContain("label: '配载状态'")
|
expect(columnSource).not.toContain("label: '配载状态'")
|
||||||
expect(columnSource).not.toContain("key: 'recommendedPalletType'")
|
expect(columnSource).not.toContain("key: 'recommendedPalletType'")
|
||||||
expect(columnSource).toContain("key: 'requestedPickupAt', prop: 'requestedPickupAt', label: '预约提货时间'")
|
expect(columnSource).toContain("key: 'requestedPickupAt', prop: 'requestedPickupAt', label: '预约提货时间'")
|
||||||
expect(columnSource.match(/defaultVisible: false/g)).toHaveLength(14)
|
expect(columnSource.match(/defaultVisible: false/g)).toHaveLength(13)
|
||||||
|
expect(columnSource).toContain("key: 'expectedArrivalAt', prop: 'expectedArrivalAt', label: '预计到货时间', minWidth: 160, slotName: 'field' }")
|
||||||
expect(columnSource).toContain("key: 'effectiveSailingAt', prop: 'effectiveSailingAt', label: '开船时间', minWidth: 180, slotName: 'field' }")
|
expect(columnSource).toContain("key: 'effectiveSailingAt', prop: 'effectiveSailingAt', label: '开船时间', minWidth: 180, slotName: 'field' }")
|
||||||
expect(columnSource).toContain("key: 'effectivePortArrivalAt', prop: 'effectivePortArrivalAt', label: '到港时间', minWidth: 180, slotName: 'field' }")
|
expect(columnSource).toContain("key: 'effectivePortArrivalAt', prop: 'effectivePortArrivalAt', label: '到港时间', minWidth: 180, slotName: 'field' }")
|
||||||
expect(source).toContain('preference-key="unified-orders-table-v2"')
|
expect(source).toContain('preference-key="unified-orders-table-v2"')
|
||||||
@@ -162,6 +163,15 @@ describe('UnifiedOrdersView contract', () => {
|
|||||||
expect(source).toContain('<SmartLoadingDialog v-model="smartLoadingOpen" />')
|
expect(source).toContain('<SmartLoadingDialog v-model="smartLoadingOpen" />')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('uses one asynchronous export entry for either selected orders or the applied filter snapshot', () => {
|
||||||
|
expect(source).toContain("exportType: 'DOMESTIC_WAREHOUSE_ORDER_LIST'")
|
||||||
|
expect(source).toContain("parameters: selectedOrderNos.length")
|
||||||
|
expect(source).toContain('? { selectedOrderNos }')
|
||||||
|
expect(source).toContain(': exportFilterParameters()')
|
||||||
|
expect(source).toContain("can('platform:order:export') && can('platform:download:list')")
|
||||||
|
expect(source).not.toContain('page: page.value,\n size: pageSize.value,\n })\n const task = await createPlatformExportTask')
|
||||||
|
})
|
||||||
|
|
||||||
it('states only the domestic warehouse and channel qualification without local destination or split blocking', () => {
|
it('states only the domestic warehouse and channel qualification without local destination or split blocking', () => {
|
||||||
const createSource = source.slice(source.indexOf('function openCoLoadCreate'), source.indexOf('async function batchApproveOrders'))
|
const createSource = source.slice(source.indexOf('function openCoLoadCreate'), source.indexOf('async function batchApproveOrders'))
|
||||||
const dialogSource = source.slice(source.indexOf('<AppDialog v-model="coLoadCreateOpen"'), source.indexOf('<AppDrawer v-model="relationOpen"'))
|
const dialogSource = source.slice(source.indexOf('<AppDialog v-model="coLoadCreateOpen"'), source.indexOf('<AppDrawer v-model="relationOpen"'))
|
||||||
@@ -352,7 +362,7 @@ describe('UnifiedOrdersView contract', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('renders order detail as a routed full page instead of a drawer', () => {
|
it('renders order detail as a routed full page instead of a drawer', () => {
|
||||||
expect(source).toContain('<template v-if="orderId">')
|
expect(source).toContain('<template v-if="isDetailRoute">')
|
||||||
expect(source).toContain('class="order-detail-header"')
|
expect(source).toContain('class="order-detail-header"')
|
||||||
expect(source).toContain('aria-label="订单详情模块"')
|
expect(source).toContain('aria-label="订单详情模块"')
|
||||||
expect(source).toContain("{ key: 'basic', label: '基本信息'")
|
expect(source).toContain("{ key: 'basic', label: '基本信息'")
|
||||||
@@ -401,6 +411,20 @@ describe('UnifiedOrdersView contract', () => {
|
|||||||
expect(routeSource).not.toContain('unified-order-workflow')
|
expect(routeSource).not.toContain('unified-order-workflow')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('supports the measurement-review deep link with an inbound-code lookup and recoverable return path', () => {
|
||||||
|
expect(routeSource).toContain("name: 'unified-order-detail-by-inbound-code'")
|
||||||
|
expect(routeSource.indexOf("path: 'orders/inbound/:inboundCode'")).toBeLessThan(routeSource.indexOf("path: 'orders/:orderId'"))
|
||||||
|
expect(source).toContain("route.query.tab === 'measurement'")
|
||||||
|
expect(source).toContain("route.query.from === 'measurement-review'")
|
||||||
|
expect(source).toContain('getPlatformInboundCodeAssociation(inboundCode)')
|
||||||
|
expect(source).toContain("activeDetailTab.value = 'measurement'")
|
||||||
|
expect(source).toContain('await loadMeasurementDetails()')
|
||||||
|
expect(source).toContain("router.push({ name: 'inbound-measurement-reviews' })")
|
||||||
|
expect(source).toContain('router.back()')
|
||||||
|
expect(source).toContain('可能无权查看或目标已不存在')
|
||||||
|
expect(source).toContain('返回测量审核列表')
|
||||||
|
})
|
||||||
|
|
||||||
it('keeps detail tabs compact on one horizontally scrollable row', () => {
|
it('keeps detail tabs compact on one horizontally scrollable row', () => {
|
||||||
expect(source).toContain('.detail-tabs { display: flex; overflow-x: auto; overflow-y: hidden;')
|
expect(source).toContain('.detail-tabs { display: flex; overflow-x: auto; overflow-y: hidden;')
|
||||||
expect(source).toContain('min-width: max-content; min-height: 56px; flex: 0 0 auto;')
|
expect(source).toContain('min-width: max-content; min-height: 56px; flex: 0 0 auto;')
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch, type Component } from 'vue'
|
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch, type Component } from 'vue'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { ArrowLeft, Box, ChevronDown, ChevronUp, CircleCheck, Eye, GitBranch, History, Link2, ListChecks, MoreHorizontal, PackageOpen, Ruler, SlidersHorizontal, StickyNote, WalletCards } from '@lucide/vue'
|
import { ArrowLeft, Box, ChevronDown, ChevronUp, CircleCheck, Download, Eye, GitBranch, History, Link2, ListChecks, MoreHorizontal, PackageOpen, Ruler, SlidersHorizontal, StickyNote, WalletCards } from '@lucide/vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import {
|
import {
|
||||||
getPlatformUnifiedOrder,
|
getPlatformUnifiedOrder,
|
||||||
@@ -22,8 +22,10 @@ import InboundFeeDetailManager from '@/modules/unified-orders/components/Inbound
|
|||||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||||
import { listPlatformTenants, type PlatformTenantResource } from '@/modules/iam'
|
import { listPlatformTenants, type PlatformTenantResource } from '@/modules/iam'
|
||||||
import { listAllPlatformWarehouses, type PlatformWarehouseSummaryResource } from '@/modules/warehouses'
|
import { listAllPlatformWarehouses, type PlatformWarehouseSummaryResource } from '@/modules/warehouses'
|
||||||
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
|
import { getApiErrorDetails, getApiErrorMessage, type ApiErrorDetails } from '@/shared/api/http'
|
||||||
import { getLoadingAssociationSummary, getOverseasInboundCodeRelationship, type LoadingAssociationSummary, type OverseasInboundCodeRelationship } from '@/shared/api/inboundCodeRelations'
|
import { createPlatformExportTask } from '@/modules/downloads'
|
||||||
|
import { showExportTaskCreatedDialog } from '@/shared/components/exportTaskCreatedDialog'
|
||||||
|
import { getLoadingAssociationSummary, getOverseasInboundCodeRelationship, getPlatformInboundCodeAssociation, type LoadingAssociationSummary, type OverseasInboundCodeRelationship } from '@/shared/api/inboundCodeRelations'
|
||||||
import AdminPage from '@/shared/components/AdminPage.vue'
|
import AdminPage from '@/shared/components/AdminPage.vue'
|
||||||
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
||||||
import AppDialog from '@/shared/components/AppDialog.vue'
|
import AppDialog from '@/shared/components/AppDialog.vue'
|
||||||
@@ -123,6 +125,7 @@ const approvalOpen = ref(false)
|
|||||||
const approvalLoadingId = ref('')
|
const approvalLoadingId = ref('')
|
||||||
const approvalSaving = ref(false)
|
const approvalSaving = ref(false)
|
||||||
const batchApprovalSaving = ref(false)
|
const batchApprovalSaving = ref(false)
|
||||||
|
const exportSubmitting = ref(false)
|
||||||
const selectedApprovalOrders = ref<UnifiedOrderResource[]>([])
|
const selectedApprovalOrders = ref<UnifiedOrderResource[]>([])
|
||||||
const selectedOrders = ref<UnifiedOrderResource[]>([])
|
const selectedOrders = ref<UnifiedOrderResource[]>([])
|
||||||
const coLoadCreateOpen = ref(false)
|
const coLoadCreateOpen = ref(false)
|
||||||
@@ -161,8 +164,10 @@ const overseasRelationship = ref<OverseasInboundCodeRelationship | null>(null)
|
|||||||
const loadingAssociationError = ref<ApiErrorDetails | null>(null)
|
const loadingAssociationError = ref<ApiErrorDetails | null>(null)
|
||||||
const overseasRelationshipError = ref<ApiErrorDetails | null>(null)
|
const overseasRelationshipError = ref<ApiErrorDetails | null>(null)
|
||||||
const subsequentFulfillmentLoading = ref(false)
|
const subsequentFulfillmentLoading = ref(false)
|
||||||
|
const resolvedDetailTarget = ref<{ orderId: string; tenantId: string } | null>(null)
|
||||||
let detailLoadingKey = ''
|
let detailLoadingKey = ''
|
||||||
let detailRequestSerial = 0
|
let detailRequestSerial = 0
|
||||||
|
let detailAssociationRequestSerial = 0
|
||||||
let measurementRequestSerial = 0
|
let measurementRequestSerial = 0
|
||||||
let measurementPreviewRequestSerial = 0
|
let measurementPreviewRequestSerial = 0
|
||||||
let subsequentFulfillmentRequestSerial = 0
|
let subsequentFulfillmentRequestSerial = 0
|
||||||
@@ -170,9 +175,15 @@ const showAdvancedFilters = ref(false)
|
|||||||
const filters = ref<FilterModel>(emptyFilters())
|
const filters = ref<FilterModel>(emptyFilters())
|
||||||
const appliedFilters = ref<FilterModel>(emptyFilters())
|
const appliedFilters = ref<FilterModel>(emptyFilters())
|
||||||
const selectedStatus = ref<UnifiedOrderMainStatus | ''>('')
|
const selectedStatus = ref<UnifiedOrderMainStatus | ''>('')
|
||||||
const orderId = computed(() => typeof route.params.orderId === 'string' ? route.params.orderId : '')
|
const businessInboundCode = computed(() => route.name === 'unified-order-detail-by-inbound-code' && typeof route.params.inboundCode === 'string'
|
||||||
const detailTenantId = computed(() => typeof route.query.tenantId === 'string' ? route.query.tenantId : '')
|
? route.params.inboundCode.trim()
|
||||||
|
: '')
|
||||||
|
const orderId = computed(() => typeof route.params.orderId === 'string' ? route.params.orderId : resolvedDetailTarget.value?.orderId || '')
|
||||||
|
const detailTenantId = computed(() => typeof route.query.tenantId === 'string' ? route.query.tenantId : resolvedDetailTarget.value?.tenantId || '')
|
||||||
const detailRouteKey = computed(() => orderId.value ? `${detailTenantId.value}:${orderId.value}` : '')
|
const detailRouteKey = computed(() => orderId.value ? `${detailTenantId.value}:${orderId.value}` : '')
|
||||||
|
const isDetailRoute = computed(() => Boolean(orderId.value || businessInboundCode.value))
|
||||||
|
const requestedDetailTab = computed(() => route.query.tab === 'measurement' ? 'measurement' : 'basic')
|
||||||
|
const fromMeasurementReview = computed(() => route.query.from === 'measurement-review')
|
||||||
function normalizedIdentifier(value: unknown) {
|
function normalizedIdentifier(value: unknown) {
|
||||||
const identifier = String(value ?? '').trim()
|
const identifier = String(value ?? '').trim()
|
||||||
return identifier && identifier !== 'null' && identifier !== 'undefined' ? identifier : ''
|
return identifier && identifier !== 'null' && identifier !== 'undefined' ? identifier : ''
|
||||||
@@ -261,9 +272,9 @@ const queryFields = computed(() => showAdvancedFilters.value
|
|||||||
|
|
||||||
const columns = computed<DataTableColumn[]>(() => [
|
const columns = computed<DataTableColumn[]>(() => [
|
||||||
{ key: 'actions', label: '操作', width: 300, fixed: 'left', slotName: 'actions' },
|
{ key: 'actions', label: '操作', width: 300, fixed: 'left', slotName: 'actions' },
|
||||||
...(can('platform:order:approve') || can('platform:firstleg:smart-loading:assign') ? [{
|
...(can('platform:order:approve') || can('platform:firstleg:smart-loading:assign') || can('platform:order:export') ? [{
|
||||||
key: 'selection', type: 'selection', width: 48, fixed: 'left',
|
key: 'selection', type: 'selection', width: 48, fixed: 'left',
|
||||||
selectable: () => !batchApprovalSaving.value && !coLoadSaving.value,
|
selectable: () => !batchApprovalSaving.value && !coLoadSaving.value && !exportSubmitting.value,
|
||||||
} satisfies DataTableColumn] : []),
|
} satisfies DataTableColumn] : []),
|
||||||
{ key: 'inboundCode', prop: 'inboundCode', label: '入仓码', minWidth: 260, slotName: 'inboundCode' },
|
{ key: 'inboundCode', prop: 'inboundCode', label: '入仓码', minWidth: 260, slotName: 'inboundCode' },
|
||||||
{ key: 'mainStatus', prop: 'mainStatus', label: '订单主状态', minWidth: 130, slotName: 'mainStatus' },
|
{ key: 'mainStatus', prop: 'mainStatus', label: '订单主状态', minWidth: 130, slotName: 'mainStatus' },
|
||||||
@@ -282,7 +293,7 @@ const columns = computed<DataTableColumn[]>(() => [
|
|||||||
{ key: 'chargeableWeightKg', prop: 'chargeableWeightKg', label: '计费毛重', minWidth: 120, slotName: 'field' },
|
{ key: 'chargeableWeightKg', prop: 'chargeableWeightKg', label: '计费毛重', minWidth: 120, slotName: 'field' },
|
||||||
{ key: 'chargeableVolumeCbm', prop: 'chargeableVolumeCbm', label: '计费体积(m³)', minWidth: 130, slotName: 'field' },
|
{ key: 'chargeableVolumeCbm', prop: 'chargeableVolumeCbm', label: '计费体积(m³)', minWidth: 130, slotName: 'field' },
|
||||||
{ key: 'expectedShipDate', prop: 'expectedShipDate', label: '预计出运日期', minWidth: 150, slotName: 'field' },
|
{ key: 'expectedShipDate', prop: 'expectedShipDate', label: '预计出运日期', minWidth: 150, slotName: 'field' },
|
||||||
{ key: 'expectedArrivalAt', prop: 'expectedArrivalAt', label: '预计到达时间', minWidth: 160, slotName: 'field', defaultVisible: false },
|
{ key: 'expectedArrivalAt', prop: 'expectedArrivalAt', label: '预计到货时间', minWidth: 160, slotName: 'field' },
|
||||||
{ key: 'feeReviewStatus', prop: 'feeReviewStatus', label: '费用审核状态', minWidth: 140, slotName: 'field', defaultVisible: false },
|
{ key: 'feeReviewStatus', prop: 'feeReviewStatus', label: '费用审核状态', minWidth: 140, slotName: 'field', defaultVisible: false },
|
||||||
{ key: 'skuNos', prop: 'skuNos', label: 'SKU 编码', minWidth: 180, slotName: 'field', defaultVisible: false },
|
{ key: 'skuNos', prop: 'skuNos', label: 'SKU 编码', minWidth: 180, slotName: 'field', defaultVisible: false },
|
||||||
{ key: 'totalQuantity', prop: 'totalQuantity', label: '货物总数量', minWidth: 120, slotName: 'field', defaultVisible: false },
|
{ key: 'totalQuantity', prop: 'totalQuantity', label: '货物总数量', minWidth: 120, slotName: 'field', defaultVisible: false },
|
||||||
@@ -1039,6 +1050,88 @@ function closeDetail() {
|
|||||||
void router.push({ name: 'unified-orders' })
|
void router.push({ name: 'unified-orders' })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function returnToMeasurementReviews() {
|
||||||
|
if (window.history.length > 1) router.back()
|
||||||
|
else void router.push({ name: 'inbound-measurement-reviews' })
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resolveBusinessDetailTarget() {
|
||||||
|
const inboundCode = businessInboundCode.value
|
||||||
|
const serial = ++detailAssociationRequestSerial
|
||||||
|
resolvedDetailTarget.value = null
|
||||||
|
detail.value = null
|
||||||
|
detailError.value = null
|
||||||
|
if (!inboundCode) {
|
||||||
|
detailError.value = { message: '入仓码缺失,无法定位对应订单' }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
detailLoading.value = true
|
||||||
|
try {
|
||||||
|
const association = await getPlatformInboundCodeAssociation(inboundCode)
|
||||||
|
if (serial !== detailAssociationRequestSerial || inboundCode !== businessInboundCode.value) return
|
||||||
|
if (!association.orderId || !association.customerId) {
|
||||||
|
detailError.value = { message: `未找到入仓码 ${inboundCode} 对应的订单,请返回测量审核列表重新打开` }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resolvedDetailTarget.value = { orderId: String(association.orderId), tenantId: String(association.customerId) }
|
||||||
|
} catch (error) {
|
||||||
|
if (serial !== detailAssociationRequestSerial || inboundCode !== businessInboundCode.value) return
|
||||||
|
detailError.value = getApiErrorDetails(error, `入仓码 ${inboundCode} 的订单关联加载失败,可能无权查看或目标已不存在`)
|
||||||
|
} finally {
|
||||||
|
if (serial === detailAssociationRequestSerial && !resolvedDetailTarget.value) detailLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function exportFilterParameters() {
|
||||||
|
const activeFilters = appliedFilters.value
|
||||||
|
const range = activeFilters.createdRange
|
||||||
|
return {
|
||||||
|
tenantId: activeFilters.tenantId || undefined,
|
||||||
|
mainStatus: selectedStatus.value || undefined,
|
||||||
|
receivingWarehouseId: activeFilters.receivingWarehouseId || undefined,
|
||||||
|
destinationWarehouseId: activeFilters.destinationWarehouseId || undefined,
|
||||||
|
inboundCode: trimmed(activeFilters.inboundCode),
|
||||||
|
expectedArrivalDate: activeFilters.expectedArrivalDate || undefined,
|
||||||
|
feeReviewStatus: activeFilters.feeReviewStatus || undefined,
|
||||||
|
channelName: trimmed(activeFilters.channelName),
|
||||||
|
containerNo: trimmed(activeFilters.containerNo),
|
||||||
|
cabinetNo: trimmed(activeFilters.cabinetNo),
|
||||||
|
sailingDate: activeFilters.sailingDate || undefined,
|
||||||
|
loaded: optionalBoolean(activeFilters.loaded),
|
||||||
|
fullContainer: optionalBoolean(activeFilters.fullContainer),
|
||||||
|
measurementReviewStatus: activeFilters.measurementReviewStatus || undefined,
|
||||||
|
fbaCode: trimmed(activeFilters.fbaCode),
|
||||||
|
createdFrom: range.length === 2 ? `${range[0]}T00:00:00` : undefined,
|
||||||
|
createdTo: range.length === 2 ? `${range[1]}T23:59:59` : undefined,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function exportOrders() {
|
||||||
|
if (exportSubmitting.value) return
|
||||||
|
const selectedOrderNos = selectedOrders.value.map((row) => row.orderNo?.trim() || '')
|
||||||
|
if (selectedOrderNos.some((orderNo) => !orderNo)) {
|
||||||
|
ElMessage.error('所选订单缺少订单号,请刷新列表后重试')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
exportSubmitting.value = true
|
||||||
|
try {
|
||||||
|
const task = await createPlatformExportTask({
|
||||||
|
exportType: 'DOMESTIC_WAREHOUSE_ORDER_LIST',
|
||||||
|
format: 'XLSX',
|
||||||
|
taskName: selectedOrderNos.length ? `国内仓订单-已选${selectedOrderNos.length}票` : '国内仓订单-当前筛选',
|
||||||
|
parameters: selectedOrderNos.length
|
||||||
|
? { selectedOrderNos }
|
||||||
|
: exportFilterParameters(),
|
||||||
|
})
|
||||||
|
const openDownloadCenter = await showExportTaskCreatedDialog({ taskName: task.taskName, taskNo: task.taskNo })
|
||||||
|
if (openDownloadCenter) await router.push({ name: 'download-center' })
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(getApiErrorMessage(error, '订单导出任务创建失败'))
|
||||||
|
} finally {
|
||||||
|
exportSubmitting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function loadDetail(force = false) {
|
async function loadDetail(force = false) {
|
||||||
if (!orderId.value) return
|
if (!orderId.value) return
|
||||||
const tenantId = detailTenantId.value
|
const tenantId = detailTenantId.value
|
||||||
@@ -1064,6 +1157,10 @@ async function loadDetail(force = false) {
|
|||||||
if (serial !== detailRequestSerial || requestKey !== detailRouteKey.value) return
|
if (serial !== detailRequestSerial || requestKey !== detailRouteKey.value) return
|
||||||
detail.value = response
|
detail.value = response
|
||||||
await loadCargoImagePreviews(response, serial)
|
await loadCargoImagePreviews(response, serial)
|
||||||
|
if (requestedDetailTab.value === 'measurement') {
|
||||||
|
activeDetailTab.value = 'measurement'
|
||||||
|
await loadMeasurementDetails()
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (serial !== detailRequestSerial || requestKey !== detailRouteKey.value) return
|
if (serial !== detailRequestSerial || requestKey !== detailRouteKey.value) return
|
||||||
detail.value = null
|
detail.value = null
|
||||||
@@ -1075,13 +1172,18 @@ async function loadDetail(force = false) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
watch([page, pageSize], () => { void loadOrders() })
|
watch([page, pageSize], () => { void loadOrders() })
|
||||||
|
watch(businessInboundCode, (value) => {
|
||||||
|
detailAssociationRequestSerial += 1
|
||||||
|
resolvedDetailTarget.value = null
|
||||||
|
if (value) void resolveBusinessDetailTarget()
|
||||||
|
}, { immediate: true })
|
||||||
watch(detailRouteKey, (value) => {
|
watch(detailRouteKey, (value) => {
|
||||||
if (value) {
|
if (value) {
|
||||||
activeDetailTab.value = 'basic'
|
activeDetailTab.value = requestedDetailTab.value
|
||||||
detail.value = null
|
detail.value = null
|
||||||
detailError.value = null
|
detailError.value = null
|
||||||
void loadDetail()
|
void loadDetail()
|
||||||
} else {
|
} else if (!businessInboundCode.value) {
|
||||||
detailRequestSerial += 1
|
detailRequestSerial += 1
|
||||||
subsequentFulfillmentRequestSerial += 1
|
subsequentFulfillmentRequestSerial += 1
|
||||||
detail.value = null
|
detail.value = null
|
||||||
@@ -1095,9 +1197,10 @@ watch(measurementPreviewOpen, (open) => {
|
|||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
void loadCustomers()
|
void loadCustomers()
|
||||||
void loadWarehouses()
|
void loadWarehouses()
|
||||||
if (!orderId.value) void loadOrders()
|
if (!isDetailRoute.value) void loadOrders()
|
||||||
})
|
})
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
|
detailAssociationRequestSerial += 1
|
||||||
measurementRequestSerial += 1
|
measurementRequestSerial += 1
|
||||||
releaseMeasurementPreview()
|
releaseMeasurementPreview()
|
||||||
releaseCargoImagePreviews()
|
releaseCargoImagePreviews()
|
||||||
@@ -1107,11 +1210,11 @@ onBeforeUnmount(() => {
|
|||||||
<template>
|
<template>
|
||||||
<AdminPage>
|
<AdminPage>
|
||||||
<section class="business-page unified-orders-page">
|
<section class="business-page unified-orders-page">
|
||||||
<template v-if="orderId">
|
<template v-if="isDetailRoute">
|
||||||
<DetailPageLayout ariaLabel="统一订单详情">
|
<DetailPageLayout ariaLabel="统一订单详情">
|
||||||
<header class="order-detail-header">
|
<header class="order-detail-header">
|
||||||
<div class="order-detail-heading">
|
<div class="order-detail-heading">
|
||||||
<el-button link class="back-button" @click="closeDetail"><ArrowLeft :size="16" />返回列表</el-button>
|
<el-button link class="back-button" @click="fromMeasurementReview ? returnToMeasurementReviews() : closeDetail()"><ArrowLeft :size="16" />{{ fromMeasurementReview ? '返回测量审核列表' : '返回列表' }}</el-button>
|
||||||
<span class="order-mark" aria-hidden="true"><Box :size="19" /></span>
|
<span class="order-mark" aria-hidden="true"><Box :size="19" /></span>
|
||||||
<div class="order-title">
|
<div class="order-title">
|
||||||
<h1>{{ detail?.summary.inboundCode || '入仓码暂缺' }}</h1>
|
<h1>{{ detail?.summary.inboundCode || '入仓码暂缺' }}</h1>
|
||||||
@@ -1123,8 +1226,9 @@ onBeforeUnmount(() => {
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<LoadErrorState v-if="detailError" class="detail-load-error" title="订单详情暂时无法加载" :message="detailError.message" :loading="detailLoading" @retry="loadDetail">
|
<LoadErrorState v-if="detailError" class="detail-load-error" title="订单详情暂时无法加载" :message="detailError.message" :loading="detailLoading" @retry="businessInboundCode ? resolveBusinessDetailTarget() : loadDetail(true)">
|
||||||
<template #details><ApiErrorMeta :details="detailError" /></template>
|
<template #details><ApiErrorMeta :details="detailError" /></template>
|
||||||
|
<template v-if="fromMeasurementReview" #actions><el-button type="primary" plain :loading="detailLoading" @click="businessInboundCode ? resolveBusinessDetailTarget() : loadDetail(true)">重新加载</el-button><el-button @click="returnToMeasurementReviews">返回测量审核列表</el-button></template>
|
||||||
</LoadErrorState>
|
</LoadErrorState>
|
||||||
|
|
||||||
<div v-else v-loading="detailLoading" class="order-detail-content">
|
<div v-else v-loading="detailLoading" class="order-detail-content">
|
||||||
@@ -1304,6 +1408,7 @@ onBeforeUnmount(() => {
|
|||||||
<el-alert v-if="!can('platform:firstleg:inbound:measure:view')" title="当前账号缺少测量查看权限" type="warning" :closable="false" show-icon />
|
<el-alert v-if="!can('platform:firstleg:inbound:measure:view')" title="当前账号缺少测量查看权限" type="warning" :closable="false" show-icon />
|
||||||
<LoadErrorState v-else-if="measurementError" title="测量数据暂时无法加载" :message="measurementError.message" :loading="measurementLoading" @retry="loadMeasurementDetails">
|
<LoadErrorState v-else-if="measurementError" title="测量数据暂时无法加载" :message="measurementError.message" :loading="measurementLoading" @retry="loadMeasurementDetails">
|
||||||
<template #details><ApiErrorMeta :details="measurementError" /></template>
|
<template #details><ApiErrorMeta :details="measurementError" /></template>
|
||||||
|
<template v-if="fromMeasurementReview" #actions><el-button type="primary" plain :loading="measurementLoading" @click="loadMeasurementDetails">重新加载</el-button><el-button @click="returnToMeasurementReviews">返回测量审核列表</el-button></template>
|
||||||
</LoadErrorState>
|
</LoadErrorState>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<section class="measurement-section">
|
<section class="measurement-section">
|
||||||
@@ -1428,6 +1533,7 @@ onBeforeUnmount(() => {
|
|||||||
<div v-if="coLoadError" class="coload-error" role="alert"><span>{{ coLoadError.message }}</span><ApiErrorMeta :details="coLoadError" /></div>
|
<div v-if="coLoadError" class="coload-error" role="alert"><span>{{ coLoadError.message }}</span><ApiErrorMeta :details="coLoadError" /></div>
|
||||||
<DataTable v-model:page="page" v-model:page-size="pageSize" :data="records" :columns="columns" preference-key="unified-orders-table-v2" :loading="loading" :total="total" :page-sizes="[20, 50, 100]" row-key="orderId" border stripe empty-text="暂无符合条件的订单" aria-label="统一订单列表" @selection-change="handleOrderSelectionChange">
|
<DataTable v-model:page="page" v-model:page-size="pageSize" :data="records" :columns="columns" preference-key="unified-orders-table-v2" :loading="loading" :total="total" :page-sizes="[20, 50, 100]" row-key="orderId" border stripe empty-text="暂无符合条件的订单" aria-label="统一订单列表" @selection-change="handleOrderSelectionChange">
|
||||||
<template #toolbar-actions>
|
<template #toolbar-actions>
|
||||||
|
<el-button v-if="can('platform:order:export') && can('platform:download:list')" :loading="exportSubmitting" :disabled="loading || exportSubmitting" @click="exportOrders"><Download :size="16" />导出<span v-if="selectedOrders.length">(已选 {{ selectedOrders.length }})</span></el-button>
|
||||||
<el-button v-if="can('platform:order:approve')" type="success" :loading="batchApprovalSaving" :disabled="loading || !selectedApprovalOrders.length" @click="batchApproveOrders"><CircleCheck :size="16" />批量审核通过<span v-if="selectedApprovalOrders.length">({{ selectedApprovalOrders.length }})</span></el-button>
|
<el-button v-if="can('platform:order:approve')" type="success" :loading="batchApprovalSaving" :disabled="loading || !selectedApprovalOrders.length" @click="batchApproveOrders"><CircleCheck :size="16" />批量审核通过<span v-if="selectedApprovalOrders.length">({{ selectedApprovalOrders.length }})</span></el-button>
|
||||||
<div v-if="can('platform:firstleg:smart-loading:assign')" class="coload-toolbar-action">
|
<div v-if="can('platform:firstleg:smart-loading:assign')" class="coload-toolbar-action">
|
||||||
<el-button type="primary" plain :loading="coLoadSaving" :disabled="loading || !canCreateCoLoadGroup" aria-label="合柜报关" @click="openCoLoadCreate"><Link2 :size="16" />合柜报关<span v-if="selectedOrders.length">({{ selectedOrders.length }})</span></el-button>
|
<el-button type="primary" plain :loading="coLoadSaving" :disabled="loading || !canCreateCoLoadGroup" aria-label="合柜报关" @click="openCoLoadCreate"><Link2 :size="16" />合柜报关<span v-if="selectedOrders.length">({{ selectedOrders.length }})</span></el-button>
|
||||||
|
|||||||
@@ -12,6 +12,17 @@ export const unifiedOrderRoutes = [
|
|||||||
permissionAnyOf: ['platform:firstleg:inbound:list'],
|
permissionAnyOf: ['platform:firstleg:inbound:list'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'orders/inbound/:inboundCode',
|
||||||
|
name: 'unified-order-detail-by-inbound-code',
|
||||||
|
component: () => import('./pages/UnifiedOrdersView.vue'),
|
||||||
|
meta: {
|
||||||
|
title: '订单详情',
|
||||||
|
description: '按入仓码查看订单测量数据和履约详情',
|
||||||
|
permission: 'platform:order:view',
|
||||||
|
permissionAnyOf: ['platform:order:list', 'platform:firstleg:inbound:view'],
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'orders/:orderId',
|
path: 'orders/:orderId',
|
||||||
name: 'unified-order-detail',
|
name: 'unified-order-detail',
|
||||||
|
|||||||
Reference in New Issue
Block a user