feat(admin): support indivisible coload groups
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { http } from '@/shared/api/http'
|
||||
import { batchQueryPlatformCoLoadGroups, cancelPlatformCoLoadGroup, createPlatformCoLoadGroup } from './coLoadGroups'
|
||||
|
||||
const group = {
|
||||
groupId: 2093000000000000001n, groupNo: 'CLG-2093000000000000001', status: 'ACTIVE',
|
||||
warehouseId: 2093000000000000002n, channelCode: 'NY-EXX', destinationType: 'WAREHOUSE',
|
||||
destinationWarehouseId: 2093000000000000003n, destinationCode: 'ONT8', lockedPlanId: null,
|
||||
version: 0, fullPackageCount: 18, createdAt: '2026-08-29T10:00:00', updatedAt: '2026-08-29T10:00:00',
|
||||
members: [
|
||||
{ tenantId: 2093000000000000004n, orderId: 2093000000000000005n, inboundId: 2093000000000000006n, fullPackageCount: 8 },
|
||||
{ tenantId: 2093000000000000007n, orderId: 2093000000000000008n, inboundId: 2093000000000000009n, fullPackageCount: 10 },
|
||||
],
|
||||
}
|
||||
|
||||
describe('co-load group API', () => {
|
||||
beforeEach(() => vi.restoreAllMocks())
|
||||
|
||||
it('creates one group from two stable order ids with idempotency', async () => {
|
||||
const post = vi.spyOn(http, 'post').mockResolvedValue({ code: '200', message: 'ok', data: group })
|
||||
const orderIds = ['2093000000000000005', '2093000000000000008']
|
||||
const result = await createPlatformCoLoadGroup(orderIds)
|
||||
expect(post).toHaveBeenCalledWith('/platform/v1/first-leg/coload-groups', { orderIds }, { headers: { 'Idempotency-Key': expect.any(String) } })
|
||||
expect(result.groupId).toBe('2093000000000000001')
|
||||
expect(result.members.map((member) => member.orderId)).toEqual(orderIds)
|
||||
expect(result.members.map((member) => member.tenantId)).toEqual(['2093000000000000004', '2093000000000000007'])
|
||||
})
|
||||
|
||||
it('cancels only the whole group with optimistic version and an optional reason', async () => {
|
||||
const post = vi.spyOn(http, 'post').mockResolvedValue({ code: '200', message: 'ok', data: { ...group, status: 'CANCELLED', version: 1 } })
|
||||
await cancelPlatformCoLoadGroup('2093000000000000001', 0, '路线调整')
|
||||
expect(post).toHaveBeenCalledWith(
|
||||
'/platform/v1/first-leg/coload-groups/2093000000000000001/cancel',
|
||||
{ expectedVersion: 0, reason: '路线调整' },
|
||||
{ headers: { 'Idempotency-Key': expect.any(String) } },
|
||||
)
|
||||
})
|
||||
|
||||
it('batch-enriches three list rows and normalizes every long id without numeric coercion', async () => {
|
||||
const third = { ...group.members[0], orderId: 2093000000000000010n, inboundId: 2093000000000000011n }
|
||||
const threeMemberGroup = { ...group, fullPackageCount: 23, members: [...group.members, third] }
|
||||
const post = vi.spyOn(http, 'post').mockResolvedValue({ code: '200', message: 'ok', data: threeMemberGroup.members.map((member) => ({ orderId: member.orderId, inboundId: member.inboundId, group: threeMemberGroup })) })
|
||||
const orderIds = ['2093000000000000005', '2093000000000000008', '2093000000000000010']
|
||||
const result = await batchQueryPlatformCoLoadGroups(orderIds)
|
||||
expect(post).toHaveBeenCalledWith('/platform/v1/first-leg/coload-groups/batch-query', { orderIds })
|
||||
expect(result).toHaveLength(3)
|
||||
expect(result[2]).toMatchObject({ orderId: '2093000000000000010', inboundId: '2093000000000000011', group: { groupId: '2093000000000000001', fullPackageCount: 23 } })
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,100 @@
|
||||
import { http, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
|
||||
|
||||
export type CoLoadGroupStatus = 'ACTIVE' | 'LOCKED' | 'CONSUMED' | 'CANCELLED'
|
||||
|
||||
export interface CoLoadGroupMember {
|
||||
tenantId: string
|
||||
orderId: string
|
||||
inboundId: string
|
||||
fullPackageCount: number
|
||||
}
|
||||
|
||||
export interface CoLoadGroup {
|
||||
groupId: string
|
||||
groupNo: string
|
||||
status: CoLoadGroupStatus
|
||||
warehouseId: string
|
||||
channelCode: string
|
||||
destinationType: string
|
||||
destinationWarehouseId: string | null
|
||||
destinationCode: string | null
|
||||
lockedPlanId: string | null
|
||||
version: number
|
||||
members: CoLoadGroupMember[]
|
||||
fullPackageCount: number
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
export interface OrderCoLoadGroupInfo {
|
||||
orderId: string
|
||||
inboundId: string
|
||||
group: CoLoadGroup
|
||||
}
|
||||
|
||||
type RawId = string | number
|
||||
type RawCoLoadGroup = Omit<CoLoadGroup, 'groupId' | 'warehouseId' | 'destinationWarehouseId' | 'lockedPlanId' | 'members'> & {
|
||||
groupId: RawId
|
||||
warehouseId: RawId
|
||||
destinationWarehouseId: RawId | null
|
||||
lockedPlanId: RawId | null
|
||||
members: Array<Omit<CoLoadGroupMember, 'tenantId' | 'orderId' | 'inboundId'> & { tenantId: RawId; orderId: RawId; inboundId: RawId }>
|
||||
}
|
||||
type RawOrderCoLoadGroupInfo = Omit<OrderCoLoadGroupInfo, 'orderId' | 'inboundId' | 'group'> & {
|
||||
orderId: RawId
|
||||
inboundId: RawId
|
||||
group: RawCoLoadGroup
|
||||
}
|
||||
|
||||
const basePath = '/platform/v1/first-leg/coload-groups'
|
||||
const optionalId = (value: RawId | null) => value == null ? null : String(value)
|
||||
const operationId = (prefix: string) => `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`
|
||||
|
||||
function normalizeGroup(raw: RawCoLoadGroup): CoLoadGroup {
|
||||
return {
|
||||
...raw,
|
||||
groupId: String(raw.groupId),
|
||||
warehouseId: String(raw.warehouseId),
|
||||
destinationWarehouseId: optionalId(raw.destinationWarehouseId),
|
||||
lockedPlanId: optionalId(raw.lockedPlanId),
|
||||
version: Number(raw.version),
|
||||
fullPackageCount: Number(raw.fullPackageCount),
|
||||
members: (raw.members || []).map((member) => ({
|
||||
...member,
|
||||
tenantId: String(member.tenantId),
|
||||
orderId: String(member.orderId),
|
||||
inboundId: String(member.inboundId),
|
||||
fullPackageCount: Number(member.fullPackageCount),
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
export async function createPlatformCoLoadGroup(orderIds: string[]) {
|
||||
const response = await http.post<ApiEnvelope<RawCoLoadGroup>, ApiEnvelope<RawCoLoadGroup>>(
|
||||
basePath,
|
||||
{ orderIds },
|
||||
{ headers: { 'Idempotency-Key': operationId('admin-coload-create') } },
|
||||
)
|
||||
return normalizeGroup(unwrapApiResponse(response))
|
||||
}
|
||||
|
||||
export async function cancelPlatformCoLoadGroup(groupId: string, expectedVersion: number, reason?: string | null) {
|
||||
const response = await http.post<ApiEnvelope<RawCoLoadGroup>, ApiEnvelope<RawCoLoadGroup>>(
|
||||
`${basePath}/${encodeURIComponent(groupId)}/cancel`,
|
||||
{ expectedVersion, reason: reason?.trim() || null },
|
||||
{ headers: { 'Idempotency-Key': operationId('admin-coload-cancel') } },
|
||||
)
|
||||
return normalizeGroup(unwrapApiResponse(response))
|
||||
}
|
||||
|
||||
export async function batchQueryPlatformCoLoadGroups(orderIds: string[]) {
|
||||
const response = await http.post<ApiEnvelope<RawOrderCoLoadGroupInfo[]>, ApiEnvelope<RawOrderCoLoadGroupInfo[]>>(
|
||||
`${basePath}/batch-query`,
|
||||
{ orderIds },
|
||||
)
|
||||
return unwrapApiResponse(response).map((item) => ({
|
||||
orderId: String(item.orderId),
|
||||
inboundId: String(item.inboundId),
|
||||
group: normalizeGroup(item.group),
|
||||
}))
|
||||
}
|
||||
@@ -4,6 +4,15 @@ export {
|
||||
type UnifiedOrderFeeDetailResource,
|
||||
type UnifiedOrderResource,
|
||||
} from './api/unifiedOrders'
|
||||
export {
|
||||
batchQueryPlatformCoLoadGroups,
|
||||
cancelPlatformCoLoadGroup,
|
||||
createPlatformCoLoadGroup,
|
||||
type CoLoadGroup,
|
||||
type CoLoadGroupMember,
|
||||
type CoLoadGroupStatus,
|
||||
type OrderCoLoadGroupInfo,
|
||||
} from './api/coLoadGroups'
|
||||
|
||||
export {
|
||||
unifiedOrderFulfillmentRoleLabel,
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
import { flushPromises, shallowMount } from '@vue/test-utils'
|
||||
import { ElMessageBox } from 'element-plus'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { ApiResponseError } from '@/shared/api/http'
|
||||
import { listPlatformUnifiedOrders } from '@/modules/unified-orders/api/unifiedOrders'
|
||||
import { batchQueryPlatformCoLoadGroups, cancelPlatformCoLoadGroup, createPlatformCoLoadGroup } from '@/modules/unified-orders/api/coLoadGroups'
|
||||
import UnifiedOrdersView from './UnifiedOrdersView.vue'
|
||||
|
||||
vi.mock('vue-router', () => ({ useRoute: () => ({ params: {}, query: {} }), useRouter: () => ({ push: vi.fn() }) }))
|
||||
vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can: () => true }) }))
|
||||
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/inbounds', () => ({ downloadWeightImage: vi.fn(), getInboundMeasurement: vi.fn() }))
|
||||
vi.mock('@/modules/unified-orders/workflows', () => ({ UnifiedOrderWorkflowPanel: { template: '<div />' } }))
|
||||
vi.mock('@/modules/unified-orders/api/unifiedOrders', () => ({
|
||||
addPlatformUnifiedOrderNote: vi.fn(), approvePlatformUnifiedOrder: vi.fn(), batchApprovePlatformUnifiedOrders: vi.fn(),
|
||||
getPlatformUnifiedOrder: vi.fn(), listPlatformUnifiedOrderFeeDetails: vi.fn(), listPlatformUnifiedOrders: vi.fn(),
|
||||
unifiedOrderStatusOptions: [{ value: 'WAIT_SPLIT', label: '待分柜' }],
|
||||
}))
|
||||
vi.mock('@/modules/unified-orders/api/coLoadGroups', () => ({
|
||||
batchQueryPlatformCoLoadGroups: vi.fn(), cancelPlatformCoLoadGroup: vi.fn(), createPlatformCoLoadGroup: vi.fn(),
|
||||
}))
|
||||
|
||||
const rows = ['5001', '5002'].map((orderId, index) => ({
|
||||
orderId, tenantId: `600${index}`, orderNo: `ORD-${index + 1}`, mainStatus: 'WAIT_SPLIT' as const, nextAction: null,
|
||||
relation: null, internalOrderStatus: 'WAIT_SPLIT', customerId: `600${index}`, customerName: `客户${index + 1}`,
|
||||
channelName: 'NY-EXX', receivingWarehouseId: '8', receivingWarehouseName: '义乌仓', destinationWarehouseId: '9', destinationWarehouseName: 'ONT8',
|
||||
inboundId: `700${index}`, inboundVersion: 1, inboundCode: `IN-${index + 1}`, expectedShipDate: null, totalPackageCount: 5,
|
||||
locationCodes: null, actualGrossWeightKg: 10, actualVolumeCbm: 1, measurement: null, preload: null, loading: null, delivery: null, transport: null,
|
||||
createdAt: null, updatedAt: null, expectedArrivalAt: null, feeReviewStatus: null, requestedPickupAt: null, totalPalletCount: null,
|
||||
chargeableVolumeCbm: null, volumetricWeightKg: null, totalNetWeightKg: null, measurementCompletedAt: null, inboundCompletedAt: null,
|
||||
expressCompany: null, expressTrackingNo: null, skuNos: null, totalQuantity: null, shipmentId: null, referenceId: null,
|
||||
}))
|
||||
const group = {
|
||||
groupId: '9001', groupNo: 'CLG-9001', status: 'ACTIVE' as const, warehouseId: '8', channelCode: 'NY-EXX', destinationType: 'WAREHOUSE',
|
||||
destinationWarehouseId: '9', destinationCode: 'ONT8', lockedPlanId: null, version: 0, fullPackageCount: 10,
|
||||
members: rows.map((row) => ({ tenantId: String(row.tenantId), orderId: String(row.orderId), inboundId: String(row.inboundId), fullPackageCount: 5 })),
|
||||
createdAt: '2026-08-29T10:00:00', updatedAt: '2026-08-29T10:00:00',
|
||||
}
|
||||
|
||||
function mountView() {
|
||||
return shallowMount(UnifiedOrdersView, {
|
||||
global: { directives: { loading: () => undefined }, stubs: {
|
||||
AdminPage: { template: '<main><slot /></main>' },
|
||||
ListPageLayout: { template: '<section><slot name="toolbar" /><slot name="state" /><slot /></section>' },
|
||||
QueryToolbar: { template: '<div />' },
|
||||
DataTable: { name: 'DataTable', props: ['data'], emits: ['selection-change'], template: '<section><slot name="toolbar-actions" /><button data-testid="select" @click="$emit(\'selection-change\', data)">勾选两票</button><template v-if="data?.length"><slot name="actions" :row="data[0]" /><slot name="coLoadGroup" :row="data[0]" /></template></section>' },
|
||||
AppDialog: { name: 'AppDialog', props: ['modelValue', 'confirmText'], emits: ['confirm', 'update:modelValue'], template: '<section v-if="modelValue"><slot /><button data-testid="dialog-confirm" @click="$emit(\'confirm\')">{{ confirmText }}</button></section>' },
|
||||
ApiErrorMeta: { props: ['details'], template: '<div>{{ details.code }} {{ details.requestId }} {{ details.traceId }}</div>' },
|
||||
'el-button': { template: '<button @click="$emit(\'click\')"><slot /></button>' },
|
||||
'el-tag': { template: '<span><slot /></span>' },
|
||||
'el-alert': { props: ['title'], template: '<div>{{ title }}</div>' },
|
||||
} },
|
||||
})
|
||||
}
|
||||
|
||||
describe('UnifiedOrdersView co-load group behavior', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
vi.mocked(listPlatformUnifiedOrders).mockResolvedValue({ records: rows, page: 1, size: 20, total: 2, pages: 1 })
|
||||
vi.mocked(batchQueryPlatformCoLoadGroups).mockResolvedValue([])
|
||||
})
|
||||
|
||||
it('keeps both selected orders and backend tracing evidence when create validation fails', async () => {
|
||||
vi.mocked(createPlatformCoLoadGroup).mockRejectedValue(new ApiResponseError('成员路线不一致', 'A0409', 409, undefined, 'request-coload', 'trace-coload'))
|
||||
const wrapper = mountView()
|
||||
await flushPromises()
|
||||
await wrapper.get('[data-testid="select"]').trigger('click')
|
||||
await wrapper.findAll('button').find((button) => button.text().includes('设为同柜不可拆组'))!.trigger('click')
|
||||
await wrapper.get('[data-testid="dialog-confirm"]').trigger('click')
|
||||
await flushPromises()
|
||||
expect(createPlatformCoLoadGroup).toHaveBeenCalledWith(['5001', '5002'])
|
||||
expect(wrapper.text()).toContain('设为同柜不可拆组(2)')
|
||||
expect(wrapper.text()).toContain('成员路线不一致')
|
||||
expect(wrapper.text()).toContain('A0409 request-coload trace-coload')
|
||||
expect(listPlatformUnifiedOrders).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('shows group number and member count and cancels only the whole ACTIVE group', async () => {
|
||||
vi.mocked(batchQueryPlatformCoLoadGroups).mockResolvedValue(rows.map((row) => ({ orderId: row.orderId, inboundId: String(row.inboundId), group })))
|
||||
vi.mocked(cancelPlatformCoLoadGroup).mockResolvedValue({ ...group, status: 'CANCELLED', version: 1 })
|
||||
vi.spyOn(ElMessageBox, 'prompt').mockResolvedValue({ value: '计划调整', action: 'confirm' } as never)
|
||||
const wrapper = mountView()
|
||||
await flushPromises()
|
||||
expect(wrapper.text()).toContain('CLG-9001')
|
||||
expect(wrapper.text()).toContain('2 票 · 未锁定')
|
||||
await wrapper.findAll('button').find((button) => button.text().includes('取消同柜组'))!.trigger('click')
|
||||
await flushPromises()
|
||||
expect(cancelPlatformCoLoadGroup).toHaveBeenCalledWith('9001', 0, '计划调整')
|
||||
expect(ElMessageBox.prompt).toHaveBeenCalledWith(expect.stringContaining('不支持单独移除成员'), '取消同柜不可拆组', expect.any(Object))
|
||||
})
|
||||
})
|
||||
@@ -12,6 +12,11 @@ vi.mock('@/modules/iam', () => ({ listPlatformTenants: vi.fn().mockResolvedValue
|
||||
vi.mock('@/modules/warehouses', () => ({ listAllPlatformWarehouses: vi.fn().mockResolvedValue([]) }))
|
||||
vi.mock('@/modules/inbounds', () => ({ downloadWeightImage: vi.fn(), getInboundMeasurement: vi.fn() }))
|
||||
vi.mock('@/modules/unified-orders/workflows', () => ({ UnifiedOrderWorkflowPanel: { template: '<div />' } }))
|
||||
vi.mock('@/modules/unified-orders/api/coLoadGroups', () => ({
|
||||
batchQueryPlatformCoLoadGroups: vi.fn().mockResolvedValue([]),
|
||||
cancelPlatformCoLoadGroup: vi.fn(),
|
||||
createPlatformCoLoadGroup: vi.fn(),
|
||||
}))
|
||||
vi.mock('@/modules/unified-orders/api/unifiedOrders', () => ({
|
||||
addPlatformUnifiedOrderNote: vi.fn(),
|
||||
approvePlatformUnifiedOrder: vi.fn(),
|
||||
|
||||
@@ -85,7 +85,7 @@ describe('UnifiedOrdersView contract', () => {
|
||||
expect(source).toContain("{ key: 'actions', label: '操作', width: 300, fixed: 'left'")
|
||||
expect(source).toContain("key: 'selection', type: 'selection'")
|
||||
expect(source).not.toContain('批量审核接口待接入')
|
||||
expect(source).toContain('@selection-change="handleApprovalSelectionChange"')
|
||||
expect(source).toContain('@selection-change="handleOrderSelectionChange"')
|
||||
expect(source).toContain('batchApprovePlatformUnifiedOrders')
|
||||
expect(source).toContain('批量审核通过')
|
||||
expect(source).toContain('title="订单列表暂时无法加载"')
|
||||
@@ -95,7 +95,7 @@ describe('UnifiedOrdersView contract', () => {
|
||||
expect(source).toContain('v-model:page-size="pageSize"')
|
||||
const columnSource = source.slice(source.indexOf('const columns ='), source.indexOf('function emptyFilters'))
|
||||
const expectedColumnKeys = [
|
||||
'actions', 'orderNo', 'inboundCode', 'mainStatus', 'customerName', 'receivingWarehouseName',
|
||||
'actions', 'orderNo', 'inboundCode', 'coLoadGroup', 'mainStatus', 'customerName', 'receivingWarehouseName',
|
||||
'destinationWarehouseName', 'channelName', 'totalPackageCount', 'totalPalletCount', 'requestedPickupAt', 'locationCodes', 'measurementReviewStatus', 'actualGrossWeightKg',
|
||||
'actualVolumeCbm', 'totalNetWeightKg', 'chargeableWeightKg', 'chargeableVolumeCbm', 'expectedShipDate',
|
||||
'expectedArrivalAt', 'feeReviewStatus', 'skuNos', 'totalQuantity', 'expressCompany', 'expressTrackingNo',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch, type Component } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { ArrowLeft, Box, ChevronDown, ChevronUp, CircleCheck, Eye, GitBranch, History, ListChecks, MoreHorizontal, PackageOpen, Ruler, SlidersHorizontal, StickyNote, WalletCards } from '@lucide/vue'
|
||||
import { ArrowLeft, Box, ChevronDown, ChevronUp, CircleCheck, Eye, GitBranch, History, Link2, ListChecks, MoreHorizontal, PackageOpen, Ruler, SlidersHorizontal, StickyNote, Unlink, WalletCards } from '@lucide/vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import {
|
||||
getPlatformUnifiedOrder,
|
||||
@@ -15,6 +15,12 @@ import {
|
||||
type UnifiedOrderMainStatus,
|
||||
type UnifiedOrderResource,
|
||||
} from '@/modules/unified-orders/api/unifiedOrders'
|
||||
import {
|
||||
batchQueryPlatformCoLoadGroups,
|
||||
cancelPlatformCoLoadGroup,
|
||||
createPlatformCoLoadGroup,
|
||||
type CoLoadGroup,
|
||||
} from '@/modules/unified-orders/api/coLoadGroups'
|
||||
import OrderFulfillmentTreePanel from '@/modules/unified-orders/components/OrderFulfillmentTreePanel.vue'
|
||||
import InboundFeeDetailManager from '@/modules/unified-orders/components/InboundFeeDetailManager.vue'
|
||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
@@ -121,6 +127,11 @@ const approvalLoadingId = ref('')
|
||||
const approvalSaving = ref(false)
|
||||
const batchApprovalSaving = ref(false)
|
||||
const selectedApprovalOrders = ref<UnifiedOrderResource[]>([])
|
||||
const selectedOrders = ref<UnifiedOrderResource[]>([])
|
||||
const coLoadGroupsByOrderId = ref(new Map<string, CoLoadGroup>())
|
||||
const coLoadCreateOpen = ref(false)
|
||||
const coLoadSaving = ref(false)
|
||||
const coLoadError = ref<ApiErrorDetails | null>(null)
|
||||
const approvalOrder = ref<UnifiedOrderResource | null>(null)
|
||||
const noteOpen = ref(false)
|
||||
const noteSaving = ref(false)
|
||||
@@ -241,12 +252,13 @@ const queryFields = computed(() => showAdvancedFilters.value
|
||||
|
||||
const columns = computed<DataTableColumn[]>(() => [
|
||||
{ key: 'actions', label: '操作', width: 300, fixed: 'left', slotName: 'actions' },
|
||||
...(can('platform:order:approve') ? [{
|
||||
...(can('platform:order:approve') || can('platform:firstleg:smart-loading:assign') ? [{
|
||||
key: 'selection', type: 'selection', width: 48, fixed: 'left',
|
||||
selectable: (row: UnifiedOrderResource) => canApprove(row) && !batchApprovalSaving.value,
|
||||
selectable: () => !batchApprovalSaving.value && !coLoadSaving.value,
|
||||
} satisfies DataTableColumn] : []),
|
||||
{ key: 'orderNo', prop: 'orderNo', label: '订单号', minWidth: 180, slotName: 'field' },
|
||||
{ key: 'inboundCode', prop: 'inboundCode', label: '入仓码', minWidth: 190, slotName: 'inboundCode' },
|
||||
{ key: 'coLoadGroup', label: '同柜组', minWidth: 190, slotName: 'coLoadGroup' },
|
||||
{ key: 'mainStatus', prop: 'mainStatus', label: '订单主状态', minWidth: 130, slotName: 'mainStatus' },
|
||||
{ key: 'customerName', prop: 'customerName', label: '客户', minWidth: 150, slotName: 'field' },
|
||||
{ key: 'receivingWarehouseName', prop: 'receivingWarehouseName', label: '平台收货仓', minWidth: 150, slotName: 'field' },
|
||||
@@ -694,8 +706,11 @@ async function loadWarehouses() {
|
||||
|
||||
async function loadOrders() {
|
||||
selectedApprovalOrders.value = []
|
||||
selectedOrders.value = []
|
||||
coLoadGroupsByOrderId.value = new Map()
|
||||
loading.value = true
|
||||
loadError.value = null
|
||||
coLoadError.value = null
|
||||
try {
|
||||
const activeFilters = appliedFilters.value
|
||||
const range = activeFilters.createdRange
|
||||
@@ -722,6 +737,14 @@ async function loadOrders() {
|
||||
})
|
||||
records.value = result.records
|
||||
total.value = result.total
|
||||
if (can('platform:firstleg:smart-loading:assign') && result.records.length) {
|
||||
try {
|
||||
const groups = await batchQueryPlatformCoLoadGroups(result.records.map((row) => String(row.orderId)))
|
||||
coLoadGroupsByOrderId.value = new Map(groups.map((item) => [item.orderId, item.group]))
|
||||
} catch (error) {
|
||||
coLoadError.value = getApiErrorDetails(error, '同柜不可拆组信息加载失败')
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
records.value = []
|
||||
total.value = 0
|
||||
@@ -759,10 +782,62 @@ function openMeasurement(row: UnifiedOrderResource) {
|
||||
measurementDialogOpen.value = true
|
||||
}
|
||||
|
||||
function handleApprovalSelectionChange(rows: UnifiedOrderResource[]) {
|
||||
function handleOrderSelectionChange(rows: UnifiedOrderResource[]) {
|
||||
selectedOrders.value = rows
|
||||
selectedApprovalOrders.value = rows.filter(canApprove)
|
||||
}
|
||||
|
||||
function coLoadGroup(row: UnifiedOrderResource) {
|
||||
return coLoadGroupsByOrderId.value.get(String(row.orderId)) || null
|
||||
}
|
||||
|
||||
function openCoLoadCreate() {
|
||||
if (selectedOrders.value.length < 2) return
|
||||
coLoadError.value = null
|
||||
coLoadCreateOpen.value = true
|
||||
}
|
||||
|
||||
async function createCoLoadGroup() {
|
||||
if (selectedOrders.value.length < 2 || coLoadSaving.value) return
|
||||
coLoadSaving.value = true
|
||||
coLoadError.value = null
|
||||
try {
|
||||
const group = await createPlatformCoLoadGroup(selectedOrders.value.map((row) => String(row.orderId)))
|
||||
coLoadCreateOpen.value = false
|
||||
ElMessage.success(`已创建同柜不可拆组 ${group.groupNo},共 ${group.members.length} 票`)
|
||||
await loadOrders()
|
||||
} catch (error) {
|
||||
coLoadError.value = getApiErrorDetails(error, '创建同柜不可拆组失败')
|
||||
} finally {
|
||||
coLoadSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function cancelCoLoadGroup(row: UnifiedOrderResource) {
|
||||
const group = coLoadGroup(row)
|
||||
if (!group || group.status !== 'ACTIVE' || coLoadSaving.value) return
|
||||
let reason = ''
|
||||
try {
|
||||
const result = await ElMessageBox.prompt(
|
||||
`将整组取消 ${group.groupNo}(${group.members.length} 票)。不支持单独移除成员;如需调整,请取消后重新建组。`,
|
||||
'取消同柜不可拆组',
|
||||
{ confirmButtonText: '确认整组取消', cancelButtonText: '返回', inputPlaceholder: '取消原因(选填)', inputType: 'textarea' },
|
||||
)
|
||||
reason = result.value
|
||||
} catch { return }
|
||||
coLoadSaving.value = true
|
||||
coLoadError.value = null
|
||||
try {
|
||||
await cancelPlatformCoLoadGroup(group.groupId, group.version, reason)
|
||||
ElMessage.success(`同柜不可拆组 ${group.groupNo} 已整组取消`)
|
||||
await loadOrders()
|
||||
} catch (error) {
|
||||
coLoadError.value = getApiErrorDetails(error, '取消同柜不可拆组失败')
|
||||
} finally {
|
||||
coLoadSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function batchApproveOrders() {
|
||||
const selected = [...selectedApprovalOrders.value]
|
||||
if (!selected.length || batchApprovalSaving.value) return
|
||||
@@ -816,17 +891,19 @@ function clearMeasurementOrder() {
|
||||
measurementOrder.value = null
|
||||
}
|
||||
|
||||
type UnifiedOrderRowAction = 'note' | 'approve' | 'measurement'
|
||||
type UnifiedOrderRowAction = 'note' | 'approve' | 'measurement' | 'cancelCoLoad'
|
||||
const unifiedOrderRowActionIcons: Record<UnifiedOrderRowAction, Component> = {
|
||||
note: StickyNote,
|
||||
approve: CircleCheck,
|
||||
measurement: Ruler,
|
||||
cancelCoLoad: Unlink,
|
||||
}
|
||||
function unifiedOrderSecondaryActions(row: UnifiedOrderResource): UnifiedOrderRowAction[] {
|
||||
return [
|
||||
...(can('platform:order:note') ? ['note' as const] : []),
|
||||
...(canApprove(row) ? ['approve' as const] : []),
|
||||
...(row.mainStatus === 'WAIT_MEASURE' && row.inboundId && can('platform:firstleg:inbound:measure:view') ? ['measurement' as const] : []),
|
||||
...(can('platform:firstleg:smart-loading:assign') && coLoadGroup(row)?.status === 'ACTIVE' ? ['cancelCoLoad' as const] : []),
|
||||
]
|
||||
}
|
||||
function directUnifiedOrderActions(row: UnifiedOrderResource) {
|
||||
@@ -841,15 +918,17 @@ function moreUnifiedOrderActionsLabel(row: UnifiedOrderResource) {
|
||||
function unifiedOrderActionLabel(action: UnifiedOrderRowAction) {
|
||||
if (action === 'note') return '添加备注'
|
||||
if (action === 'approve') return '审核通过'
|
||||
if (action === 'cancelCoLoad') return '取消同柜组'
|
||||
return '测量作业'
|
||||
}
|
||||
function unifiedOrderActionType(action: UnifiedOrderRowAction) {
|
||||
return action === 'approve' ? 'success' : 'primary'
|
||||
return action === 'approve' ? 'success' : action === 'cancelCoLoad' ? 'danger' : 'primary'
|
||||
}
|
||||
function runMoreAction(command: string | number | object, row: UnifiedOrderResource) {
|
||||
if (command === 'note') openNoteDialog(row)
|
||||
if (command === 'approve') openApproval(row)
|
||||
if (command === 'measurement') openMeasurement(row)
|
||||
if (command === 'cancelCoLoad') void cancelCoLoadGroup(row)
|
||||
}
|
||||
|
||||
function closeDetail() {
|
||||
@@ -1223,9 +1302,11 @@ onBeforeUnmount(() => {
|
||||
|
||||
<template #default>
|
||||
<div v-if="!loadError" id="unified-orders-results" class="surface-panel table-panel table-card" role="tabpanel">
|
||||
<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="handleApprovalSelectionChange">
|
||||
<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">
|
||||
<template #toolbar-actions>
|
||||
<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:firstleg:smart-loading:assign')" type="primary" plain :loading="coLoadSaving" :disabled="loading || selectedOrders.length < 2" @click="openCoLoadCreate"><Link2 :size="16" />设为同柜不可拆组<span v-if="selectedOrders.length">({{ selectedOrders.length }})</span></el-button>
|
||||
<el-button v-if="can('platform:firstleg:smart-loading:list')" type="primary" @click="smartLoadingOpen = true"><PackageOpen :size="16" />智能装柜</el-button>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
@@ -1255,6 +1336,7 @@ onBeforeUnmount(() => {
|
||||
><GitBranch :size="14" />{{ orderRelationLabel(row as UnifiedOrderResource) }}</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<template #coLoadGroup="{ row }"><div v-if="coLoadGroup(row as UnifiedOrderResource)" class="coload-cell"><el-tag size="small" :type="coLoadGroup(row as UnifiedOrderResource)?.status === 'ACTIVE' ? 'warning' : 'info'">{{ coLoadGroup(row as UnifiedOrderResource)?.groupNo }}</el-tag><span>{{ coLoadGroup(row as UnifiedOrderResource)?.members.length }} 票 · {{ coLoadGroup(row as UnifiedOrderResource)?.status === 'ACTIVE' ? '未锁定' : coLoadGroup(row as UnifiedOrderResource)?.status === 'LOCKED' ? '已锁定' : '已消费' }}</span></div><span v-else>—</span></template>
|
||||
<template #orderStatus="{ row }"><StatusBadge :status="orderStatusLabel((row as UnifiedOrderResource).internalOrderStatus || (row as UnifiedOrderResource).mainStatus)" /></template>
|
||||
<template #measurementReviewStatus="{ row }"><StatusBadge :status="measurementStatusLabel((row as UnifiedOrderResource).measurement?.reviewStatus)" /></template>
|
||||
<template #field="{ row, column }"><LineClampText v-if="['customerRemark', 'latestInternalNote'].includes(String(column.property))" :content="displayField(row as UnifiedOrderResource, column.property)" :lines="2" /><template v-else>{{ displayField(row as UnifiedOrderResource, column.property) }}</template></template>
|
||||
@@ -1272,6 +1354,11 @@ onBeforeUnmount(() => {
|
||||
</template>
|
||||
</ListPageLayout>
|
||||
<SmartLoadingDialog v-model="smartLoadingOpen" />
|
||||
<AppDialog v-model="coLoadCreateOpen" title="设为同柜不可拆组" confirm-text="确认建组" :confirm-loading="coLoadSaving" :confirm-disabled="selectedOrders.length < 2" @confirm="createCoLoadGroup">
|
||||
<el-alert type="warning" :closable="false" show-icon :title="`所选 ${selectedOrders.length} 票必须全量进入同一柜,不能部分装柜、拆子单或单独移除成员。`" />
|
||||
<div class="coload-selection"><span v-for="row in selectedOrders" :key="String(row.orderId)">{{ row.orderNo || row.inboundCode || row.orderId }}</span></div>
|
||||
<div v-if="coLoadError" class="coload-error" role="alert"><span>{{ coLoadError.message }}</span><ApiErrorMeta :details="coLoadError" /></div>
|
||||
</AppDialog>
|
||||
</template>
|
||||
|
||||
<AppDrawer v-model="relationOpen" title="父子订单关联" size="min(860px, 94vw)" :show-footer="false" destroy-on-close>
|
||||
@@ -1337,6 +1424,12 @@ onBeforeUnmount(() => {
|
||||
.status-tab.is-active::after { background: var(--cc-primary); }
|
||||
.orders-query-toolbar { border: 0; }
|
||||
.table-card { overflow: hidden; }
|
||||
.coload-error { margin: 8px 12px; padding: 8px 10px; border: 1px solid var(--el-color-danger-light-5); border-radius: 4px; background: var(--el-color-danger-light-9); color: var(--el-color-danger); font-size: 12px; }
|
||||
.coload-error :deep(.api-error-meta) { margin-top: 5px; }
|
||||
.coload-cell { display: grid; width: fit-content; gap: 3px; }
|
||||
.coload-cell span { color: var(--cc-muted); font-size: 11px; }
|
||||
.coload-selection { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 12px; }
|
||||
.coload-selection > span { padding: 4px 7px; border: 1px solid var(--cc-border); border-radius: 4px; background: var(--cc-surface-muted); font-size: 12px; }
|
||||
.table-card :deep(.el-table) { --el-table-border-color: #e5e7eb; --el-table-header-bg-color: #f9fafb; --el-table-row-hover-bg-color: #f3f7fb; --el-table-tr-bg-color: #fff; color: #374151; font-size: 12px; }
|
||||
.table-card :deep(.el-table th.el-table__cell) { height: 42px; padding: 0; background: #f9fafb; color: #374151; font-size: 12px; font-weight: 600; }
|
||||
.table-card :deep(.el-table td.el-table__cell) { min-height: 42px; padding: 9px 0; }
|
||||
|
||||
Reference in New Issue
Block a user