This commit is contained in:
2026-09-03 11:47:32 +08:00
parent 246cbc115e
commit d8b59ecca5
11 changed files with 314 additions and 119 deletions
@@ -9,6 +9,7 @@ import {
listPlatformUnifiedOrders,
listPlatformUnifiedOrderFeeDetails,
updatePlatformUnifiedOrderDeclaration,
updatePlatformUnifiedOrderPlanning,
} from './unifiedOrders'
vi.mock('@/shared/api/http', () => ({
@@ -72,11 +73,11 @@ describe('unified orders API contract', () => {
it('normalizes C02 row IDs and keeps inboundId for measurement routing', async () => {
vi.mocked(http.get).mockResolvedValue({ data: {
records: [{ orderId: '90001', tenantId: '208', inboundId: '77', inboundVersion: 3, mainStatus: 'WAIT_MEASURE', channelCode: ' NY_EXX ', channelName: '纽约渠道', locationCodes: 'A0-01, B2-03', actualGrossWeightKg: '12.5', actualVolumeCbm: '0.45', totalPalletCount: '3', totalNetWeightKg: '11.2', chargeableVolumeCbm: '0.5', totalQuantity: '48', skuNos: ['SKU-1', ''], requestedPickupAt: '2026-08-18T09:30:00', customerRemark: '客户备注', latestInternalNote: '最新内部备注', relation: { role: 'PARENT', rootOrderId: 90001, parentOrderId: null, splitSequence: null, childCount: '2' }, measurement: null, preload: null, loading: null, delivery: null, transport: null }],
records: [{ orderId: '90001', orderVersion: '7', tenantId: '208', inboundId: '77', inboundVersion: 3, mainStatus: 'WAIT_MEASURE', channelCode: ' NY_EXX ', channelName: '纽约渠道', locationCodes: 'A0-01, B2-03', estimatedVolumeCbm: '0.4', expectedVolumeCbm: '0.45', actualGrossWeightKg: '12.5', actualVolumeCbm: '0.46', totalPalletCount: '3', totalNetWeightKg: '11.2', chargeableVolumeCbm: '0.5', totalQuantity: '48', skuNos: ['SKU-1', ''], requestedPickupAt: '2026-08-18T09:30:00', customerRemark: '客户备注', latestInternalNote: '最新内部备注', relation: { role: 'PARENT', rootOrderId: 90001, parentOrderId: null, splitSequence: null, childCount: '2' }, measurement: null, preload: null, loading: null, delivery: null, transport: null }],
page: 1, size: 20, total: 1, pages: 1,
} })
await expect(listPlatformUnifiedOrders({ page: 1, size: 20 })).resolves.toMatchObject({
records: [{ orderId: '90001', tenantId: '208', inboundId: '77', inboundVersion: 3, channelCode: 'NY_EXX', locationCodes: 'A0-01, B2-03', actualGrossWeightKg: 12.5, actualVolumeCbm: 0.45, totalPalletCount: 3, totalNetWeightKg: 11.2, chargeableVolumeCbm: 0.5, totalQuantity: 48, skuNos: ['SKU-1'], requestedPickupAt: '2026-08-18T09:30:00', customerRemark: '客户备注', latestInternalNote: '最新内部备注', relation: { role: 'PARENT', rootOrderId: '90001', childCount: 2 } }],
records: [{ orderId: '90001', orderVersion: 7, tenantId: '208', inboundId: '77', inboundVersion: 3, channelCode: 'NY_EXX', locationCodes: 'A0-01, B2-03', estimatedVolumeCbm: 0.4, expectedVolumeCbm: 0.45, actualGrossWeightKg: 12.5, actualVolumeCbm: 0.46, totalPalletCount: 3, totalNetWeightKg: 11.2, chargeableVolumeCbm: 0.5, totalQuantity: 48, skuNos: ['SKU-1'], requestedPickupAt: '2026-08-18T09:30:00', customerRemark: '客户备注', latestInternalNote: '最新内部备注', relation: { role: 'PARENT', rootOrderId: '90001', childCount: 2 } }],
})
})
@@ -160,6 +161,23 @@ describe('unified orders API contract', () => {
expect(http.put).toHaveBeenCalledWith('/platform/v1/orders/90001/declaration', request, { params: { tenantId: '208' } })
})
it('updates arrival and operator-estimated volume through the dedicated versioned contract', async () => {
const request = {
expectedVersion: 7,
expectedArrivalAt: '2026-09-10T18:30:00',
estimatedVolumeCbm: 0.625,
}
vi.mocked(http.put).mockResolvedValue({ data: { previousVersion: 7, version: 8, changes: [] } })
await expect(updatePlatformUnifiedOrderPlanning('2089176869176389634', '2089223263733518337', request))
.resolves.toMatchObject({ version: 8 })
expect(http.put).toHaveBeenCalledWith(
'/platform/v1/orders/2089223263733518337/planning',
request,
{ params: { tenantId: '2089176869176389634' } },
)
})
it('loads fee detail rows from the inbound fee-detail contract', async () => {
vi.mocked(http.get).mockResolvedValue({ data: [{ id: '1', inboundId: '88', reviewStatus: 'APPROVED', amount: 100 }] })
await expect(listPlatformUnifiedOrderFeeDetails('208', '88')).resolves.toHaveLength(1)
@@ -83,6 +83,7 @@ export interface UnifiedOrderCoLoadGroupSummary {
export interface UnifiedOrderResource {
orderId: number | string
orderVersion?: number | null
tenantId: number | string
orderNo: string | null
mainStatus: UnifiedOrderMainStatus
@@ -103,6 +104,7 @@ export interface UnifiedOrderResource {
inboundCode: string | null
expectedShipDate: string | null
totalPackageCount: number | null
estimatedVolumeCbm?: number | null
expectedVolumeCbm?: number | null
locationCodes: string | null
actualGrossWeightKg: number | null
@@ -362,6 +364,7 @@ export interface UpdateUnifiedOrderDeclarationRequest {
expectedVersion: number
customerRemark?: string | null
expectedArrivalAt?: string | null
estimatedVolumeCbm?: number | null
channelCode: string
channelName: string
cargoes: UnifiedOrderDeclarationCargo[]
@@ -384,6 +387,12 @@ export interface UpdateUnifiedOrderDeclarationResult {
changes: UnifiedOrderDeclarationChange[]
}
export interface UpdateUnifiedOrderPlanningRequest {
expectedVersion: number
expectedArrivalAt?: string | null
estimatedVolumeCbm?: number | null
}
function optionalText(value: string | null | undefined) {
const normalized = value?.trim()
return normalized || null
@@ -397,6 +406,7 @@ function normalizeRow(raw: UnifiedOrderResource): UnifiedOrderResource {
return {
...raw,
orderId: String(raw.orderId),
orderVersion: nullableInteger(raw.orderVersion),
tenantId: String(raw.tenantId),
customerId: id(raw.customerId),
channelCode: optionalText(raw.channelCode),
@@ -412,6 +422,7 @@ function normalizeRow(raw: UnifiedOrderResource): UnifiedOrderResource {
childCount: integer(raw.relation.childCount, 0),
} : null,
totalPackageCount: nullableInteger(raw.totalPackageCount),
estimatedVolumeCbm: numberValue(raw.estimatedVolumeCbm),
expectedVolumeCbm: numberValue(raw.expectedVolumeCbm),
channelChanged: Boolean(raw.channelChanged),
actualGrossWeightKg: numberValue(raw.actualGrossWeightKg),
@@ -492,6 +503,18 @@ export function updatePlatformUnifiedOrderDeclaration(
).then(unwrapApiResponse)
}
export function updatePlatformUnifiedOrderPlanning(
tenantId: string,
orderId: string,
request: UpdateUnifiedOrderPlanningRequest,
) {
return http.put<ApiEnvelope<UpdateUnifiedOrderDeclarationResult>, ApiEnvelope<UpdateUnifiedOrderDeclarationResult>>(
`${unifiedOrdersApiPath}/${orderId}/planning`,
request,
{ params: { tenantId } },
).then(unwrapApiResponse)
}
export function downloadPlatformProductImage(tenantId: string, fileId: string) {
return http.get<Blob, Blob>(
`/platform/v1/files/product-images/${encodeURIComponent(tenantId)}/${encodeURIComponent(fileId)}`,
@@ -13,7 +13,7 @@ const detail = {
orderId: '90001', tenantId: '208', inboundCode: 'RK-001', mainStatus: 'WAIT_MEASURE',
receivingWarehouseId: '700000000000000001', receivingWarehouseName: '深圳仓', destinationWarehouseName: '洛杉矶中转仓',
channelCode: 'OLD', channelName: '原渠道', channelChanged: false,
expectedArrivalAt: '2026-09-06T10:00:00', customerRemark: '原备注',
expectedArrivalAt: '2026-09-06T10:00:00', estimatedVolumeCbm: 0.625, customerRemark: '原备注',
},
order: {
order: { version: 7, receivingWarehouseId: '700000000000000001', channelCode: 'OLD', channelName: '原渠道', customerRemark: '原备注' },
@@ -68,7 +68,7 @@ describe('OrderDeclarationEditorDialog', () => {
expect(ElMessageBox.confirm).toHaveBeenCalledWith(expect.stringContaining('原箱码继续有效,无需更换已打印箱唛'), '确认变更渠道', expect.any(Object))
expect(ElMessageBox.confirm).toHaveBeenCalledWith(expect.stringContaining('纸面上的旧渠道文字不会自动变化'), '确认变更渠道', expect.any(Object))
expect(updatePlatformUnifiedOrderDeclaration).toHaveBeenCalledWith('208', '90001', expect.objectContaining({ expectedVersion: 7, channelCode: 'NEW', channelName: '新渠道' }))
expect(updatePlatformUnifiedOrderDeclaration).toHaveBeenCalledWith('208', '90001', expect.objectContaining({ expectedVersion: 7, estimatedVolumeCbm: 0.625, channelCode: 'NEW', channelName: '新渠道' }))
expect(ElMessage.success).toHaveBeenCalledWith(expect.stringContaining('订单已标记为“曾变更渠道”'))
expect(wrapper.emitted('updated')).toHaveLength(1)
expect(wrapper.emitted('update:modelValue')).toContainEqual([false])
@@ -36,6 +36,7 @@ const form = reactive({
expectedVersion: 0,
customerRemark: '',
expectedArrivalAt: '',
estimatedVolumeCbm: null as number | null,
channelCode: '',
channelName: '',
cargoes: [] as EditableCargo[],
@@ -67,6 +68,7 @@ function resetForm() {
form.expectedVersion = Number(snapshot?.version ?? 0)
form.customerRemark = snapshot?.customerRemark || props.detail.summary.customerRemark || ''
form.expectedArrivalAt = props.detail.summary.expectedArrivalAt || ''
form.estimatedVolumeCbm = props.detail.summary.estimatedVolumeCbm ?? null
form.channelCode = snapshot?.channelCode || props.detail.summary.channelCode || ''
form.channelName = snapshot?.channelName || props.detail.summary.channelName || ''
originalChannel.code = form.channelCode.trim()
@@ -191,6 +193,7 @@ async function save() {
expectedVersion: form.expectedVersion,
customerRemark: nullable(form.customerRemark),
expectedArrivalAt: nullable(form.expectedArrivalAt),
estimatedVolumeCbm: form.estimatedVolumeCbm,
channelCode: form.channelCode.trim(),
channelName: form.channelName.trim(),
cargoes: form.cargoes.map(({ key: _key, ...cargo }) => ({
@@ -245,10 +248,11 @@ async function save() {
<el-form label-position="top" class="order-fields">
<el-row :gutter="14">
<el-col :span="8"><el-form-item label="渠道名称" required><el-select v-model="form.channelCode" aria-label="渠道名称" filterable clearable :loading="channelsLoading" placeholder="请选择正式渠道" @change="selectChannel"><el-option v-for="channel in channels" :key="channel.channelCode" :label="`${channel.channelName} · ${channel.channelCode}`" :value="channel.channelCode" /></el-select></el-form-item></el-col>
<el-col :span="7"><el-form-item label="渠道名称" required><el-select v-model="form.channelCode" aria-label="渠道名称" filterable clearable :loading="channelsLoading" placeholder="请选择正式渠道" @change="selectChannel"><el-option v-for="channel in channels" :key="channel.channelCode" :label="`${channel.channelName} · ${channel.channelCode}`" :value="channel.channelCode" /></el-select></el-form-item></el-col>
<el-col :span="4"><el-form-item label="渠道编码"><el-input :model-value="form.channelCode" readonly aria-label="渠道编码" /></el-form-item></el-col>
<el-col :span="6"><el-form-item label="预计到货时间"><el-date-picker v-model="form.expectedArrivalAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" format="YYYY-MM-DD HH:mm" clearable /></el-form-item></el-col>
<el-col :span="6"><el-form-item label="客户备注"><el-input v-model="form.customerRemark" maxlength="1000" show-word-limit /></el-form-item></el-col>
<el-col :span="4"><el-form-item label="预估体积(m³)"><el-input-number v-model="form.estimatedVolumeCbm" :min="0.0001" :precision="4" :controls="false" /></el-form-item></el-col>
<el-col :span="5"><el-form-item label="预计到货时间"><el-date-picker v-model="form.expectedArrivalAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" format="YYYY-MM-DD HH:mm" clearable /></el-form-item></el-col>
<el-col :span="4"><el-form-item label="客户备注"><el-input v-model="form.customerRemark" maxlength="1000" show-word-limit /></el-form-item></el-col>
</el-row>
</el-form>
@@ -1,8 +1,9 @@
import { flushPromises, shallowMount } from '@vue/test-utils'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { listPlatformUnifiedOrders } from '@/modules/unified-orders/api/unifiedOrders'
import { listPlatformUnifiedOrders, updatePlatformUnifiedOrderPlanning } from '@/modules/unified-orders/api/unifiedOrders'
import { createPlatformExportTask } from '@/modules/downloads'
import { showExportTaskCreatedDialog } from '@/shared/components/exportTaskCreatedDialog'
import { ApiResponseError } from '@/shared/api/http'
import UnifiedOrdersView from './UnifiedOrdersView.vue'
vi.mock('vue-router', () => ({
@@ -27,6 +28,7 @@ vi.mock('@/modules/unified-orders/api/unifiedOrders', () => ({
getPlatformUnifiedOrder: vi.fn(),
listPlatformUnifiedOrderFeeDetails: vi.fn(),
listPlatformUnifiedOrders: vi.fn(),
updatePlatformUnifiedOrderPlanning: vi.fn(),
unifiedOrderStatusOptions: [
{ value: 'PENDING_REVIEW', label: '待审核' },
{ value: 'WAIT_MEASURE', label: '待测量数据' },
@@ -49,8 +51,11 @@ function mountView() {
AdminPage: { template: '<main><slot /></main>' },
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" />' },
DataTable: { name: 'DataTable', props: ['page', 'pageSize'], emits: ['update:page', 'update:pageSize', 'selection-change'], template: '<div data-testid="data-table"><slot name="toolbar-actions" /></div>' },
DataTable: { name: 'DataTable', props: ['page', 'pageSize', 'data'], emits: ['update:page', 'update:pageSize', 'selection-change'], template: '<div data-testid="data-table"><slot name="toolbar-actions" /><div v-for="row in data" :key="row.orderId" class="test-row"><slot name="estimatedVolume" :row="row" /><slot name="expectedArrival" :row="row" /></div></div>' },
'el-button': { template: '<button><slot /></button>' },
'el-input-number': { name: 'ElInputNumber', props: ['modelValue', 'disabled'], emits: ['update:modelValue', 'change'], template: '<input class="number-input" :value="modelValue" :disabled="disabled" @change="$emit(\'change\')" />' },
'el-date-picker': { name: 'ElDatePicker', props: ['modelValue', 'disabled'], emits: ['update:modelValue', 'change'], template: '<input class="date-input" :value="modelValue" :disabled="disabled" @change="$emit(\'change\')" />' },
ApiErrorMeta: { props: ['details'], template: '<code class="api-meta">{{ details.code }} {{ details.requestId }} {{ details.traceId }}</code>' },
},
},
})
@@ -59,6 +64,7 @@ function mountView() {
describe('UnifiedOrdersView list request coordination', () => {
beforeEach(() => {
vi.mocked(listPlatformUnifiedOrders).mockReset().mockResolvedValue(emptyPage())
vi.mocked(updatePlatformUnifiedOrderPlanning).mockReset()
vi.mocked(createPlatformExportTask).mockReset().mockResolvedValue({ taskName: '国内仓订单', taskNo: 'EXP-1' } as never)
vi.mocked(showExportTaskCreatedDialog).mockReset().mockResolvedValue(false)
})
@@ -185,4 +191,41 @@ describe('UnifiedOrdersView list request coordination', () => {
parameters: expect.objectContaining({ page: expect.anything(), size: expect.anything() }),
}))
})
it('edits arrival and estimated volume with the row version and preserves evidence on failure', async () => {
vi.mocked(listPlatformUnifiedOrders).mockResolvedValue({
records: [{
orderId: '2089223263733518337', orderVersion: 7, tenantId: '2089176869176389634',
inboundCode: 'NYEXX2609030001', expectedArrivalAt: '2026-09-10T08:30:00', estimatedVolumeCbm: 0.25,
}], page: 1, size: 20, total: 1, pages: 1,
} as never)
vi.mocked(updatePlatformUnifiedOrderPlanning)
.mockResolvedValueOnce({ version: 8 } as never)
.mockRejectedValueOnce(new ApiResponseError('订单版本已变化', 'A0409', 200, undefined, 'req-planning', 'trace-planning'))
const wrapper = mountView()
await flushPromises()
const volume = wrapper.getComponent({ name: 'ElInputNumber' })
volume.vm.$emit('update:modelValue', 0.5)
volume.vm.$emit('change', 0.5)
await flushPromises()
expect(updatePlatformUnifiedOrderPlanning).toHaveBeenNthCalledWith(1,
'2089176869176389634', '2089223263733518337', {
expectedVersion: 7, expectedArrivalAt: '2026-09-10T08:30:00', estimatedVolumeCbm: 0.5,
})
const arrival = wrapper.getComponent({ name: 'ElDatePicker' })
arrival.vm.$emit('update:modelValue', '2026-09-12T16:00:00')
arrival.vm.$emit('change', '2026-09-12T16:00:00')
await flushPromises()
expect(updatePlatformUnifiedOrderPlanning).toHaveBeenNthCalledWith(2,
'2089176869176389634', '2089223263733518337', {
expectedVersion: 8, expectedArrivalAt: '2026-09-12T16:00:00', estimatedVolumeCbm: 0.5,
})
expect(wrapper.getComponent({ name: 'ElDatePicker' }).props('modelValue')).toBe('2026-09-12T16:00:00')
expect(wrapper.text()).toContain('订单版本已变化')
expect(wrapper.text()).toContain('A0409 req-planning trace-planning')
})
})
@@ -98,7 +98,7 @@ describe('UnifiedOrdersView contract', () => {
'actions', 'inboundCode', 'mainStatus', 'customerName', 'receivingWarehouseName',
'destinationWarehouseName', 'channelName', 'totalPackageCount', 'totalPalletCount', 'requestedPickupAt', 'locationCodes', 'measurementReviewStatus', 'actualGrossWeightKg',
'actualVolumeCbm', 'totalNetWeightKg', 'chargeableWeightKg', 'chargeableVolumeCbm', 'expectedShipDate',
'expectedVolumeCbm', 'expectedArrivalAt', 'feeReviewStatus', 'skuNos', 'totalQuantity', 'expressCompany', 'expressTrackingNo',
'estimatedVolumeCbm', 'expectedVolumeCbm', 'expectedArrivalAt', 'feeReviewStatus', 'skuNos', 'totalQuantity', 'expressCompany', 'expressTrackingNo',
'shipmentId', 'referenceId', 'containerNo', 'cabinetNo',
'effectiveSailingAt', 'effectivePortArrivalAt', 'customerRemark', 'latestInternalNote', 'createdAt', 'updatedAt',
]
@@ -118,8 +118,20 @@ describe('UnifiedOrdersView contract', () => {
expect(columnSource).not.toContain("key: 'recommendedPalletType'")
expect(columnSource).toContain("key: 'requestedPickupAt', prop: 'requestedPickupAt', label: '预约提货时间'")
expect(columnSource.match(/defaultVisible: false/g)).toHaveLength(13)
expect(columnSource).toContain("key: 'expectedArrivalAt', prop: 'expectedArrivalAt', label: '预计到货时间', minWidth: 160, slotName: 'field' }")
expect(columnSource).toContain("key: 'expectedVolumeCbm', prop: 'expectedVolumeCbm', label: '预计体积(m³)', minWidth: 130, slotName: 'field' }")
expect(columnSource).toContain("key: 'expectedArrivalAt', prop: 'expectedArrivalAt', label: '预计到货时间', minWidth: 220, slotName: 'expectedArrival' }")
expect(columnSource).toContain("key: 'estimatedVolumeCbm', prop: 'estimatedVolumeCbm', label: '预估体积(m³)', minWidth: 180, slotName: 'estimatedVolume' }")
expect(columnSource).toContain("key: 'expectedVolumeCbm', prop: 'expectedVolumeCbm', label: '下单体积(m³)', minWidth: 130, slotName: 'field' }")
const planningColumnPositions = [
columnSource.indexOf("key: 'expectedArrivalAt'"),
columnSource.indexOf("key: 'estimatedVolumeCbm'"),
columnSource.indexOf("key: 'expectedVolumeCbm'"),
columnSource.indexOf("key: 'actualVolumeCbm'"),
]
expect(planningColumnPositions).toEqual([...planningColumnPositions].sort((left, right) => left - right))
expect(source).toContain("can('platform:order:declaration:edit')")
expect(source).toContain('updatePlatformUnifiedOrderPlanning(String(row.tenantId), String(row.orderId)')
expect(source).toContain('expectedVersion: row.orderVersion!')
expect(source).toContain('<ApiErrorMeta :details="planningDraft(row as UnifiedOrderResource).error!" />')
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(source).toContain('preference-key="unified-orders-table-v2"')
@@ -10,6 +10,7 @@ import {
batchApprovePlatformUnifiedOrders,
addPlatformUnifiedOrderNote,
listPlatformUnifiedOrders,
updatePlatformUnifiedOrderPlanning,
unifiedOrderStatusOptions,
type UnifiedOrderDetailResource,
type UnifiedOrderCoLoadGroupSummary,
@@ -110,6 +111,15 @@ const pageSize = ref(20)
const total = ref(0)
const loading = ref(false)
const loadError = ref<ApiErrorDetails | null>(null)
type PlanningDraft = {
expectedArrivalAt: string
estimatedVolumeCbm: number | null
savedExpectedArrivalAt: string
savedEstimatedVolumeCbm: number | null
saving: boolean
error: ApiErrorDetails | null
}
const planningDrafts = reactive<Record<string, PlanningDraft | undefined>>({})
const customerLoading = ref(false)
const customerLoadError = ref<ApiErrorDetails | null>(null)
const warehouseLoading = ref(false)
@@ -455,18 +465,19 @@ const columns = computed<DataTableColumn[]>(() => [
{ key: 'destinationWarehouseName', prop: 'destinationWarehouseName', label: '目的仓', minWidth: 150, slotName: 'field' },
{ key: 'channelName', prop: 'channelName', label: '渠道', minWidth: 180, slotName: 'channel' },
{ key: 'totalPackageCount', prop: 'totalPackageCount', label: '总箱数', minWidth: 100, slotName: 'field' },
{ key: 'expectedVolumeCbm', prop: 'expectedVolumeCbm', label: '预计体积(m³)', minWidth: 130, slotName: 'field' },
{ key: 'expectedArrivalAt', prop: 'expectedArrivalAt', label: '预计到货时间', minWidth: 220, slotName: 'expectedArrival' },
{ key: 'estimatedVolumeCbm', prop: 'estimatedVolumeCbm', label: '预估体积(m³)', minWidth: 180, slotName: 'estimatedVolume' },
{ key: 'expectedVolumeCbm', prop: 'expectedVolumeCbm', label: '下单体积(m³)', minWidth: 130, slotName: 'field' },
{ key: 'actualVolumeCbm', prop: 'actualVolumeCbm', label: '实测体积(m³)', minWidth: 130, slotName: 'field' },
{ key: 'totalPalletCount', prop: 'totalPalletCount', label: '总托数', minWidth: 100, slotName: 'field' },
{ key: 'requestedPickupAt', prop: 'requestedPickupAt', label: '预约提货时间', minWidth: 160, slotName: 'field' },
{ key: 'locationCodes', prop: 'locationCodes', label: '库位位置', minWidth: 160, slotName: 'field', defaultVisible: false },
{ key: 'measurementReviewStatus', prop: 'measurementReviewStatus', label: '测量数据审核状态', minWidth: 170, slotName: 'measurementReviewStatus' },
{ key: 'actualGrossWeightKg', prop: 'actualGrossWeightKg', label: '实测毛重(kg)', minWidth: 130, slotName: 'field' },
{ key: 'actualVolumeCbm', prop: 'actualVolumeCbm', label: '实测体积(m³)', minWidth: 130, slotName: 'field' },
{ key: 'totalNetWeightKg', prop: 'totalNetWeightKg', label: '货物净重(kg)', minWidth: 130, slotName: 'field' },
{ key: 'chargeableWeightKg', prop: 'chargeableWeightKg', label: '计费毛重', minWidth: 120, slotName: 'field' },
{ key: 'chargeableVolumeCbm', prop: 'chargeableVolumeCbm', label: '计费体积(m³)', minWidth: 130, slotName: 'field' },
{ key: 'expectedShipDate', prop: 'expectedShipDate', label: '预计出运日期', minWidth: 150, slotName: 'field' },
{ key: 'expectedArrivalAt', prop: 'expectedArrivalAt', label: '预计到货时间', minWidth: 160, slotName: 'field' },
{ key: 'feeReviewStatus', prop: 'feeReviewStatus', label: '费用审核状态', minWidth: 140, 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 },
@@ -533,6 +544,60 @@ function can(permission: string) {
return access.can(permission)
}
function planningDraft(row: UnifiedOrderResource) {
const key = String(row.orderId)
let draft = planningDrafts[key]
if (!draft) {
draft = {
expectedArrivalAt: row.expectedArrivalAt || '',
estimatedVolumeCbm: row.estimatedVolumeCbm ?? null,
savedExpectedArrivalAt: row.expectedArrivalAt || '',
savedEstimatedVolumeCbm: row.estimatedVolumeCbm ?? null,
saving: false,
error: null,
}
planningDrafts[key] = draft
}
return draft
}
async function savePlanning(row: UnifiedOrderResource) {
const draft = planningDraft(row)
if (!can('platform:order:declaration:edit') || draft.saving) return
const estimatedVolumeCbm = draft.estimatedVolumeCbm ?? null
draft.estimatedVolumeCbm = estimatedVolumeCbm
if (!Number.isInteger(row.orderVersion)) {
draft.error = { message: '订单版本缺失,请刷新列表后重试' }
return
}
if (estimatedVolumeCbm !== null && (!Number.isFinite(estimatedVolumeCbm) || estimatedVolumeCbm <= 0)) {
draft.error = { message: '预估体积必须大于 0,或清空该值' }
return
}
if (draft.expectedArrivalAt === draft.savedExpectedArrivalAt
&& estimatedVolumeCbm === draft.savedEstimatedVolumeCbm) return
draft.saving = true
draft.error = null
try {
const result = await updatePlatformUnifiedOrderPlanning(String(row.tenantId), String(row.orderId), {
expectedVersion: row.orderVersion!,
expectedArrivalAt: draft.expectedArrivalAt || null,
estimatedVolumeCbm,
})
row.orderVersion = result.version
row.expectedArrivalAt = draft.expectedArrivalAt || null
row.estimatedVolumeCbm = estimatedVolumeCbm
draft.savedExpectedArrivalAt = draft.expectedArrivalAt
draft.savedEstimatedVolumeCbm = estimatedVolumeCbm
ElMessage.success('订单运营预估已更新')
} catch (error) {
draft.error = getApiErrorDetails(error, '订单运营预估保存失败')
} finally {
draft.saving = false
}
}
const canEditDeclaration = computed(() => Boolean(
detail.value
&& detail.value.summary.mainStatus === 'WAIT_MEASURE'
@@ -766,7 +831,7 @@ async function updateMeasurementCalculationMode(row: Record<string, unknown> | n
version: measurement.version,
actualGoodsName: measurement.actualGoodsName,
items: measurement.items.map((item) => ({
inboundDetailId: item.inboundDetailId,
inboundDetailId: item.inboundDetailId || undefined,
boxCode: item.boxCode,
itemNo: item.itemNo,
boxSpecification: item.boxSpecification,
@@ -827,7 +892,7 @@ async function saveWarehouseOuterDimensions(row: Record<string, unknown>) {
version: measurement.version,
actualGoodsName: measurement.actualGoodsName,
items: measurement.items.map((item) => ({
inboundDetailId: item.inboundDetailId,
inboundDetailId: item.inboundDetailId || undefined,
boxCode: item.boxCode,
itemNo: item.itemNo,
boxSpecification: item.boxSpecification,
@@ -1142,6 +1207,7 @@ async function loadOrders() {
loading.value = true
loadError.value = null
coLoadError.value = null
Object.keys(planningDrafts).forEach((key) => delete planningDrafts[key])
try {
const activeFilters = appliedFilters.value
const range = activeFilters.createdRange
@@ -2075,6 +2141,51 @@ onBeforeUnmount(() => {
<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 #channel="{ row }"><div class="channel-cell"><span>{{ (row as UnifiedOrderResource).channelName || '—' }}</span><el-tag v-if="(row as UnifiedOrderResource).channelChanged" size="small" type="warning">曾变更渠道</el-tag></div></template>
<template #estimatedVolume="{ row }">
<div class="planning-cell">
<el-input-number
v-if="can('platform:order:declaration:edit')"
v-model="planningDraft(row as UnifiedOrderResource).estimatedVolumeCbm"
class="planning-volume-input"
:min="0.0001"
:precision="4"
:controls="false"
:disabled="planningDraft(row as UnifiedOrderResource).saving"
placeholder="填写预估体积"
:aria-label="`修改 ${(row as UnifiedOrderResource).inboundCode || '当前订单'} 预估体积`"
@change="savePlanning(row as UnifiedOrderResource)"
/>
<span v-else>{{ formatNumber((row as UnifiedOrderResource).estimatedVolumeCbm, 4) }}</span>
<small v-if="planningDraft(row as UnifiedOrderResource).saving">保存中…</small>
<div v-if="planningDraft(row as UnifiedOrderResource).error" class="planning-error" role="alert">
<span>{{ planningDraft(row as UnifiedOrderResource).error?.message }}</span>
<ApiErrorMeta :details="planningDraft(row as UnifiedOrderResource).error!" />
</div>
</div>
</template>
<template #expectedArrival="{ row }">
<div class="planning-cell">
<el-date-picker
v-if="can('platform:order:declaration:edit')"
v-model="planningDraft(row as UnifiedOrderResource).expectedArrivalAt"
class="planning-arrival-input"
type="datetime"
value-format="YYYY-MM-DDTHH:mm:ss"
format="YYYY-MM-DD HH:mm"
clearable
:disabled="planningDraft(row as UnifiedOrderResource).saving"
placeholder="选择预计到货时间"
:aria-label="`修改 ${(row as UnifiedOrderResource).inboundCode || '当前订单'} 预计到货时间`"
@change="savePlanning(row as UnifiedOrderResource)"
/>
<span v-else>{{ formatDate((row as UnifiedOrderResource).expectedArrivalAt) }}</span>
<small v-if="planningDraft(row as UnifiedOrderResource).saving">保存中…</small>
<div v-if="planningDraft(row as UnifiedOrderResource).error" class="planning-error" role="alert">
<span>{{ planningDraft(row as UnifiedOrderResource).error?.message }}</span>
<ApiErrorMeta :details="planningDraft(row as UnifiedOrderResource).error!" />
</div>
</div>
</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>
<template #mainStatus="{ row }"><StatusBadge :status="statusLabel((row as UnifiedOrderResource).mainStatus)" /></template>
<template #customer="{ row }"><div class="primary-cell"><strong>{{ customerName(row as UnifiedOrderResource) }}</strong></div></template>
@@ -2218,6 +2329,13 @@ onBeforeUnmount(() => {
.table-card :deep(.el-table__body tr:hover > td.el-table__cell) { background: #f3f7fb; }
.row-actions { display: flex; align-items: center; gap: 4px; white-space: nowrap; }
.row-actions :deep(.el-button) { margin-left: 0; }
.planning-cell { display: grid; min-width: 0; gap: 4px; }
.planning-cell > small { color: var(--cc-muted); font-size: 11px; }
.planning-volume-input,.planning-arrival-input { width: 100%; max-width: 100%; }
.planning-cell :deep(.el-input__wrapper) { min-height: 32px; padding-right: 9px; padding-left: 9px; }
.planning-cell :deep(.el-input__inner) { min-width: 0; text-align: left; }
.planning-error { min-width: 0; color: var(--el-color-danger); font-size: 11px; line-height: 1.35; white-space: normal; }
.planning-error :deep(.api-error-meta) { margin-top: 3px; }
.inbound-relation-cell { display: grid; min-width: 0; gap: 5px; }
.inbound-code-value { display: block; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.order-relation-link { width: fit-content; max-width: 100%; height: auto; justify-content: flex-start; padding: 0; font-size: 12px; }