feat(admin): edit domestic order declarations
This commit is contained in:
@@ -8,6 +8,7 @@ import {
|
|||||||
downloadPlatformProductImage,
|
downloadPlatformProductImage,
|
||||||
listPlatformUnifiedOrders,
|
listPlatformUnifiedOrders,
|
||||||
listPlatformUnifiedOrderFeeDetails,
|
listPlatformUnifiedOrderFeeDetails,
|
||||||
|
updatePlatformUnifiedOrderDeclaration,
|
||||||
} from './unifiedOrders'
|
} from './unifiedOrders'
|
||||||
|
|
||||||
vi.mock('@/shared/api/http', () => ({
|
vi.mock('@/shared/api/http', () => ({
|
||||||
@@ -147,6 +148,18 @@ describe('unified orders API contract', () => {
|
|||||||
expect(http.post).toHaveBeenCalledWith('/platform/v1/orders/90001/internal-notes', { content: '已确认船期' }, { params: { tenantId: '208' } })
|
expect(http.post).toHaveBeenCalledWith('/platform/v1/orders/90001/internal-notes', { content: '已确认船期' }, { params: { tenantId: '208' } })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('updates customer declaration fields through the versioned tenant-scoped contract', async () => {
|
||||||
|
const request = {
|
||||||
|
expectedVersion: 7, customerRemark: '易碎', expectedArrivalAt: '2026-09-06T10:00:00',
|
||||||
|
channelCode: 'US-SEA', channelName: '美西海运',
|
||||||
|
cargoes: [{ itemNo: 1, cargoName: '台灯', packageCount: 2, totalQuantity: 20, lengthCm: 40, widthCm: 30, heightCm: 20, grossWeightKg: 18, volumeCbm: 0.048 }],
|
||||||
|
}
|
||||||
|
vi.mocked(http.put).mockResolvedValue({ data: { orderNo: 'FL-1', inboundCode: 'RK-1', previousVersion: 7, version: 8, channelChangedBefore: false, channelChanged: true, changes: [] } })
|
||||||
|
|
||||||
|
await expect(updatePlatformUnifiedOrderDeclaration('208', '90001', request)).resolves.toMatchObject({ version: 8, channelChanged: true })
|
||||||
|
expect(http.put).toHaveBeenCalledWith('/platform/v1/orders/90001/declaration', request, { params: { tenantId: '208' } })
|
||||||
|
})
|
||||||
|
|
||||||
it('loads fee detail rows from the inbound fee-detail contract', async () => {
|
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 }] })
|
vi.mocked(http.get).mockResolvedValue({ data: [{ id: '1', inboundId: '88', reviewStatus: 'APPROVED', amount: 100 }] })
|
||||||
await expect(listPlatformUnifiedOrderFeeDetails('208', '88')).resolves.toHaveLength(1)
|
await expect(listPlatformUnifiedOrderFeeDetails('208', '88')).resolves.toHaveLength(1)
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ export interface UnifiedOrderResource {
|
|||||||
customerName: string | null
|
customerName: string | null
|
||||||
channelCode: string | null
|
channelCode: string | null
|
||||||
channelName: string | null
|
channelName: string | null
|
||||||
|
channelChanged?: boolean
|
||||||
receivingWarehouseId: number | string | null
|
receivingWarehouseId: number | string | null
|
||||||
receivingWarehouseName: string | null
|
receivingWarehouseName: string | null
|
||||||
destinationWarehouseId: number | string | null
|
destinationWarehouseId: number | string | null
|
||||||
@@ -102,6 +103,7 @@ export interface UnifiedOrderResource {
|
|||||||
inboundCode: string | null
|
inboundCode: string | null
|
||||||
expectedShipDate: string | null
|
expectedShipDate: string | null
|
||||||
totalPackageCount: number | null
|
totalPackageCount: number | null
|
||||||
|
expectedVolumeCbm?: number | null
|
||||||
locationCodes: string | null
|
locationCodes: string | null
|
||||||
actualGrossWeightKg: number | null
|
actualGrossWeightKg: number | null
|
||||||
actualVolumeCbm: number | null
|
actualVolumeCbm: number | null
|
||||||
@@ -170,6 +172,7 @@ export interface UnifiedOrderTimelineEvent {
|
|||||||
|
|
||||||
export interface UnifiedOrderDetailOrderSnapshot {
|
export interface UnifiedOrderDetailOrderSnapshot {
|
||||||
id: number | string | null
|
id: number | string | null
|
||||||
|
version: number | null
|
||||||
tenantId: number | string | null
|
tenantId: number | string | null
|
||||||
orderNo: string | null
|
orderNo: string | null
|
||||||
customerId: number | string | null
|
customerId: number | string | null
|
||||||
@@ -329,6 +332,58 @@ function id(value: number | string | null | undefined) {
|
|||||||
return value === null || value === undefined || value === '' ? null : String(value)
|
return value === null || value === undefined || value === '' ? null : String(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface UnifiedOrderDeclarationCargo {
|
||||||
|
itemNo: number
|
||||||
|
cargoName: string
|
||||||
|
cargoNameEn?: string | null
|
||||||
|
skuNo?: string | null
|
||||||
|
model?: string | null
|
||||||
|
packageCount: number
|
||||||
|
totalQuantity: number
|
||||||
|
lengthCm: number
|
||||||
|
widthCm: number
|
||||||
|
heightCm: number
|
||||||
|
grossWeightKg: number
|
||||||
|
volumeCbm: number
|
||||||
|
netWeightKg?: number | null
|
||||||
|
hsCode?: string | null
|
||||||
|
customsHsCode?: string | null
|
||||||
|
unitsPerPackage?: number | null
|
||||||
|
unitDeclaredValue?: number | null
|
||||||
|
declaredValue?: number | null
|
||||||
|
currency?: string | null
|
||||||
|
material?: string | null
|
||||||
|
usage?: string | null
|
||||||
|
brand?: string | null
|
||||||
|
cargoRemark?: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateUnifiedOrderDeclarationRequest {
|
||||||
|
expectedVersion: number
|
||||||
|
customerRemark?: string | null
|
||||||
|
expectedArrivalAt?: string | null
|
||||||
|
channelCode: string
|
||||||
|
channelName: string
|
||||||
|
cargoes: UnifiedOrderDeclarationCargo[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UnifiedOrderDeclarationChange {
|
||||||
|
itemNo: number | null
|
||||||
|
field: string
|
||||||
|
beforeValue: string | null
|
||||||
|
afterValue: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateUnifiedOrderDeclarationResult {
|
||||||
|
orderNo: string | null
|
||||||
|
inboundCode: string | null
|
||||||
|
previousVersion: number
|
||||||
|
version: number
|
||||||
|
channelChangedBefore: boolean
|
||||||
|
channelChanged: boolean
|
||||||
|
changes: UnifiedOrderDeclarationChange[]
|
||||||
|
}
|
||||||
|
|
||||||
function optionalText(value: string | null | undefined) {
|
function optionalText(value: string | null | undefined) {
|
||||||
const normalized = value?.trim()
|
const normalized = value?.trim()
|
||||||
return normalized || null
|
return normalized || null
|
||||||
@@ -357,6 +412,8 @@ function normalizeRow(raw: UnifiedOrderResource): UnifiedOrderResource {
|
|||||||
childCount: integer(raw.relation.childCount, 0),
|
childCount: integer(raw.relation.childCount, 0),
|
||||||
} : null,
|
} : null,
|
||||||
totalPackageCount: nullableInteger(raw.totalPackageCount),
|
totalPackageCount: nullableInteger(raw.totalPackageCount),
|
||||||
|
expectedVolumeCbm: numberValue(raw.expectedVolumeCbm),
|
||||||
|
channelChanged: Boolean(raw.channelChanged),
|
||||||
actualGrossWeightKg: numberValue(raw.actualGrossWeightKg),
|
actualGrossWeightKg: numberValue(raw.actualGrossWeightKg),
|
||||||
actualVolumeCbm: numberValue(raw.actualVolumeCbm),
|
actualVolumeCbm: numberValue(raw.actualVolumeCbm),
|
||||||
totalPalletCount: nullableInteger(raw.totalPalletCount),
|
totalPalletCount: nullableInteger(raw.totalPalletCount),
|
||||||
@@ -413,7 +470,26 @@ export async function getPlatformUnifiedOrder(tenantId: string, orderId: string)
|
|||||||
{ params: { tenantId } },
|
{ params: { tenantId } },
|
||||||
)
|
)
|
||||||
const result = unwrapApiResponse(response)
|
const result = unwrapApiResponse(response)
|
||||||
return { ...result, summary: normalizeRow(result.summary) }
|
return {
|
||||||
|
...result,
|
||||||
|
summary: normalizeRow(result.summary),
|
||||||
|
order: {
|
||||||
|
...result.order,
|
||||||
|
order: result.order.order ? { ...result.order.order, version: nullableInteger(result.order.order.version) } : null,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updatePlatformUnifiedOrderDeclaration(
|
||||||
|
tenantId: string,
|
||||||
|
orderId: string,
|
||||||
|
request: UpdateUnifiedOrderDeclarationRequest,
|
||||||
|
) {
|
||||||
|
return http.put<ApiEnvelope<UpdateUnifiedOrderDeclarationResult>, ApiEnvelope<UpdateUnifiedOrderDeclarationResult>>(
|
||||||
|
`${unifiedOrdersApiPath}/${orderId}/declaration`,
|
||||||
|
request,
|
||||||
|
{ params: { tenantId } },
|
||||||
|
).then(unwrapApiResponse)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function downloadPlatformProductImage(tenantId: string, fileId: string) {
|
export function downloadPlatformProductImage(tenantId: string, fileId: string) {
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import ElementPlus, { ElMessageBox } from 'element-plus'
|
||||||
|
import { flushPromises, mount } from '@vue/test-utils'
|
||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
import { updatePlatformUnifiedOrderDeclaration } from '@/modules/unified-orders/api/unifiedOrders'
|
||||||
|
import OrderDeclarationEditorDialog from './OrderDeclarationEditorDialog.vue'
|
||||||
|
|
||||||
|
vi.mock('@/modules/unified-orders/api/unifiedOrders', () => ({ updatePlatformUnifiedOrderDeclaration: vi.fn() }))
|
||||||
|
|
||||||
|
const detail = {
|
||||||
|
summary: {
|
||||||
|
orderId: '90001', tenantId: '208', inboundCode: 'RK-001', mainStatus: 'WAIT_MEASURE',
|
||||||
|
receivingWarehouseName: '深圳仓', destinationWarehouseName: '洛杉矶中转仓',
|
||||||
|
channelCode: 'OLD', channelName: '原渠道', channelChanged: false,
|
||||||
|
expectedArrivalAt: '2026-09-06T10:00:00', customerRemark: '原备注',
|
||||||
|
},
|
||||||
|
order: {
|
||||||
|
order: { version: 7, channelCode: 'OLD', channelName: '原渠道', customerRemark: '原备注' },
|
||||||
|
cargoes: [{ itemNo: 1, cargoName: '台灯', packageCount: 2, totalQuantity: 20, lengthCm: 40, widthCm: 30, heightCm: 20, grossWeightKg: 18, volumeCbm: 0.048 }],
|
||||||
|
parties: [], internalNotes: [], statusHistory: [],
|
||||||
|
},
|
||||||
|
timeline: [],
|
||||||
|
} as never
|
||||||
|
|
||||||
|
function createWrapper() {
|
||||||
|
return mount(OrderDeclarationEditorDialog, {
|
||||||
|
props: { modelValue: true, detail },
|
||||||
|
global: {
|
||||||
|
plugins: [ElementPlus],
|
||||||
|
stubs: {
|
||||||
|
AppDialog: {
|
||||||
|
props: ['modelValue', 'confirmLoading'], emits: ['confirm', 'update:modelValue'],
|
||||||
|
template: '<section v-if="modelValue"><slot /><button data-testid="save" @click="$emit(\'confirm\')">保存</button></section>',
|
||||||
|
},
|
||||||
|
LoadErrorState: { props: ['title', 'message'], template: '<div class="load-error">{{ title }} {{ message }}<slot name="details" /></div>' },
|
||||||
|
ApiErrorMeta: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('OrderDeclarationEditorDialog', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.mocked(updatePlatformUnifiedOrderDeclaration).mockReset()
|
||||||
|
vi.spyOn(ElMessageBox, 'confirm').mockResolvedValue('confirm' as never)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('requires a second confirmation when the channel changes and keeps inbound/warehouse facts read-only', async () => {
|
||||||
|
vi.mocked(updatePlatformUnifiedOrderDeclaration).mockResolvedValue({ version: 8 } as never)
|
||||||
|
const wrapper = createWrapper()
|
||||||
|
expect(wrapper.text()).toContain('国内收货仓和中转仓不可修改,如需变更请重新下单')
|
||||||
|
expect(wrapper.text()).toContain('RK-001')
|
||||||
|
const inputs = wrapper.findAll('input')
|
||||||
|
await inputs[0]!.setValue('NEW')
|
||||||
|
await wrapper.get('[data-testid="save"]').trigger('click')
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
expect(ElMessageBox.confirm).toHaveBeenCalledWith(expect.stringContaining('现有入仓码和箱唛不会改变'), '确认变更渠道', expect.any(Object))
|
||||||
|
expect(updatePlatformUnifiedOrderDeclaration).toHaveBeenCalledWith('208', '90001', expect.objectContaining({ expectedVersion: 7, channelCode: 'NEW' }))
|
||||||
|
expect(wrapper.emitted('updated')).toHaveLength(1)
|
||||||
|
expect(wrapper.emitted('update:modelValue')).toContainEqual([false])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('preserves the editor and entered values when the versioned update fails', async () => {
|
||||||
|
vi.mocked(updatePlatformUnifiedOrderDeclaration).mockRejectedValue(new Error('订单版本已变化,请刷新详情后重试'))
|
||||||
|
const wrapper = createWrapper()
|
||||||
|
const inputs = wrapper.findAll('input')
|
||||||
|
await inputs[1]!.setValue('保留填写的渠道名')
|
||||||
|
await wrapper.get('[data-testid="save"]').trigger('click')
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
expect(wrapper.find('.load-error').exists()).toBe(true)
|
||||||
|
expect((inputs[1]!.element as HTMLInputElement).value).toBe('保留填写的渠道名')
|
||||||
|
expect(wrapper.emitted('update:modelValue')).toBeUndefined()
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,227 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { reactive, ref, watch } from 'vue'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import AppDialog from '@/shared/components/AppDialog.vue'
|
||||||
|
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
||||||
|
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
|
||||||
|
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
|
||||||
|
import {
|
||||||
|
updatePlatformUnifiedOrderDeclaration,
|
||||||
|
type UnifiedOrderDeclarationCargo,
|
||||||
|
type UnifiedOrderDetailResource,
|
||||||
|
} from '@/modules/unified-orders/api/unifiedOrders'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
modelValue: boolean
|
||||||
|
detail: UnifiedOrderDetailResource
|
||||||
|
}>()
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:modelValue': [value: boolean]
|
||||||
|
updated: []
|
||||||
|
}>()
|
||||||
|
|
||||||
|
type EditableCargo = UnifiedOrderDeclarationCargo & { key: string }
|
||||||
|
const saving = ref(false)
|
||||||
|
const submitError = ref<ApiErrorDetails | null>(null)
|
||||||
|
const originalChannel = reactive({ code: '', name: '' })
|
||||||
|
const form = reactive({
|
||||||
|
expectedVersion: 0,
|
||||||
|
customerRemark: '',
|
||||||
|
expectedArrivalAt: '',
|
||||||
|
channelCode: '',
|
||||||
|
channelName: '',
|
||||||
|
cargoes: [] as EditableCargo[],
|
||||||
|
})
|
||||||
|
|
||||||
|
function text(row: Record<string, unknown>, key: string) {
|
||||||
|
const value = row[key]
|
||||||
|
return value === null || value === undefined ? '' : String(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
function number(row: Record<string, unknown>, key: string, fallback = 0) {
|
||||||
|
const parsed = Number(row[key])
|
||||||
|
return Number.isFinite(parsed) ? parsed : fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
function optionalNumber(row: Record<string, unknown>, key: string) {
|
||||||
|
const value = row[key]
|
||||||
|
if (value === null || value === undefined || value === '') return null
|
||||||
|
const parsed = Number(value)
|
||||||
|
return Number.isFinite(parsed) ? parsed : null
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetForm() {
|
||||||
|
const snapshot = props.detail.order.order
|
||||||
|
form.expectedVersion = Number(snapshot?.version ?? 0)
|
||||||
|
form.customerRemark = snapshot?.customerRemark || props.detail.summary.customerRemark || ''
|
||||||
|
form.expectedArrivalAt = props.detail.summary.expectedArrivalAt || ''
|
||||||
|
form.channelCode = snapshot?.channelCode || props.detail.summary.channelCode || ''
|
||||||
|
form.channelName = snapshot?.channelName || props.detail.summary.channelName || ''
|
||||||
|
originalChannel.code = form.channelCode.trim()
|
||||||
|
originalChannel.name = form.channelName.trim()
|
||||||
|
form.cargoes = props.detail.order.cargoes.map((source, index) => {
|
||||||
|
const row = source as Record<string, unknown>
|
||||||
|
const itemNo = number(row, 'itemNo', index + 1)
|
||||||
|
return {
|
||||||
|
key: `${itemNo}-${index}`,
|
||||||
|
itemNo,
|
||||||
|
cargoName: text(row, 'cargoName'), cargoNameEn: text(row, 'cargoNameEn'),
|
||||||
|
skuNo: text(row, 'skuNo'), model: text(row, 'model'),
|
||||||
|
packageCount: number(row, 'packageCount'), totalQuantity: number(row, 'totalQuantity'),
|
||||||
|
lengthCm: number(row, 'lengthCm'), widthCm: number(row, 'widthCm'), heightCm: number(row, 'heightCm'),
|
||||||
|
grossWeightKg: number(row, 'grossWeightKg'), volumeCbm: number(row, 'volumeCbm'),
|
||||||
|
netWeightKg: optionalNumber(row, 'netWeightKg'),
|
||||||
|
hsCode: text(row, 'hsCode'), customsHsCode: text(row, 'customsHsCode'),
|
||||||
|
unitsPerPackage: optionalNumber(row, 'unitsPerPackage'),
|
||||||
|
unitDeclaredValue: optionalNumber(row, 'unitDeclaredValue'), declaredValue: optionalNumber(row, 'declaredValue'),
|
||||||
|
currency: text(row, 'currency'), material: text(row, 'material'), usage: text(row, 'usage'),
|
||||||
|
brand: text(row, 'brand'), cargoRemark: text(row, 'cargoRemark') || text(row, 'remark'),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
submitError.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(() => props.modelValue, (open) => { if (open) resetForm() }, { immediate: true })
|
||||||
|
|
||||||
|
function nullable(value: string | null | undefined) {
|
||||||
|
return value?.trim() || null
|
||||||
|
}
|
||||||
|
|
||||||
|
function validate() {
|
||||||
|
if (!form.channelCode.trim() || !form.channelName.trim()) return '渠道编码和渠道名称均不能为空'
|
||||||
|
if (!form.cargoes.length) return '订单缺少货品明细,不能保存'
|
||||||
|
for (const cargo of form.cargoes) {
|
||||||
|
if (!cargo.cargoName.trim()) return `第 ${cargo.itemNo} 项中文品名不能为空`
|
||||||
|
if (cargo.packageCount < 1 || cargo.totalQuantity < 1) return `第 ${cargo.itemNo} 项箱数和总数量必须大于 0`
|
||||||
|
if ([cargo.lengthCm, cargo.widthCm, cargo.heightCm, cargo.grossWeightKg, cargo.volumeCbm].some((value) => !(value > 0))) {
|
||||||
|
return `第 ${cargo.itemNo} 项尺寸、毛重和体积必须大于 0`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
async function save() {
|
||||||
|
const validationMessage = validate()
|
||||||
|
if (validationMessage) { ElMessage.warning(validationMessage); return }
|
||||||
|
const channelChanged = form.channelCode.trim() !== originalChannel.code || form.channelName.trim() !== originalChannel.name
|
||||||
|
if (channelChanged) {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(
|
||||||
|
'渠道变更后,系统会重新校验分柜、同柜及计费约束;现有入仓码和箱唛不会改变。确认继续吗?',
|
||||||
|
'确认变更渠道',
|
||||||
|
{ type: 'warning', confirmButtonText: '确认变更', cancelButtonText: '返回检查' },
|
||||||
|
)
|
||||||
|
} catch { return }
|
||||||
|
}
|
||||||
|
const snapshot = props.detail.order.order
|
||||||
|
if (!snapshot?.version && snapshot?.version !== 0) { ElMessage.error('订单版本缺失,请刷新详情后重试'); return }
|
||||||
|
saving.value = true
|
||||||
|
submitError.value = null
|
||||||
|
try {
|
||||||
|
await updatePlatformUnifiedOrderDeclaration(String(props.detail.summary.tenantId), String(props.detail.summary.orderId), {
|
||||||
|
expectedVersion: form.expectedVersion,
|
||||||
|
customerRemark: nullable(form.customerRemark),
|
||||||
|
expectedArrivalAt: nullable(form.expectedArrivalAt),
|
||||||
|
channelCode: form.channelCode.trim(),
|
||||||
|
channelName: form.channelName.trim(),
|
||||||
|
cargoes: form.cargoes.map(({ key: _key, ...cargo }) => ({
|
||||||
|
...cargo,
|
||||||
|
cargoName: cargo.cargoName.trim(), cargoNameEn: nullable(cargo.cargoNameEn),
|
||||||
|
skuNo: nullable(cargo.skuNo), model: nullable(cargo.model), hsCode: nullable(cargo.hsCode),
|
||||||
|
customsHsCode: nullable(cargo.customsHsCode), currency: nullable(cargo.currency)?.toUpperCase() || null,
|
||||||
|
material: nullable(cargo.material), usage: nullable(cargo.usage), brand: nullable(cargo.brand),
|
||||||
|
cargoRemark: nullable(cargo.cargoRemark),
|
||||||
|
})),
|
||||||
|
})
|
||||||
|
ElMessage.success('客户申报资料已更新')
|
||||||
|
emit('update:modelValue', false)
|
||||||
|
emit('updated')
|
||||||
|
} catch (error) {
|
||||||
|
submitError.value = getApiErrorDetails(error, '客户申报资料保存失败,请核对权限、订单状态或刷新版本后重试')
|
||||||
|
} finally {
|
||||||
|
saving.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<AppDialog
|
||||||
|
:model-value="modelValue"
|
||||||
|
title="编辑客户申报资料"
|
||||||
|
width="min(1500px, calc(100vw - 36px))"
|
||||||
|
confirm-text="保存申报资料"
|
||||||
|
:confirm-loading="saving"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
@update:model-value="emit('update:modelValue', $event)"
|
||||||
|
@confirm="save"
|
||||||
|
>
|
||||||
|
<div class="declaration-editor">
|
||||||
|
<el-alert type="warning" :closable="false" title="仅修改客户原始申报资料;仓库/PDA 实测数据不会被覆盖。国内收货仓和中转仓不可修改,如需变更请重新下单。" />
|
||||||
|
<LoadErrorState v-if="submitError" compact title="保存失败,当前填写内容已保留" :message="submitError.message" :show-retry="false">
|
||||||
|
<template #details><ApiErrorMeta :details="submitError" /></template>
|
||||||
|
</LoadErrorState>
|
||||||
|
|
||||||
|
<el-descriptions :column="4" border class="readonly-facts">
|
||||||
|
<el-descriptions-item label="入仓码">{{ detail.summary.inboundCode || '—' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="国内收货仓">{{ detail.summary.receivingWarehouseName || '—' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="目的仓/中转仓">{{ detail.summary.destinationWarehouseName || '—' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="版本">{{ form.expectedVersion }}</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
|
||||||
|
<el-form label-position="top" class="order-fields">
|
||||||
|
<el-row :gutter="14">
|
||||||
|
<el-col :span="6"><el-form-item label="渠道编码" required><el-input v-model="form.channelCode" maxlength="64" /></el-form-item></el-col>
|
||||||
|
<el-col :span="6"><el-form-item label="渠道名称" required><el-input v-model="form.channelName" maxlength="128" /></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-row>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<h3>货品申报明细 <small>不允许新增、删除或调整明细序号</small></h3>
|
||||||
|
<div class="cargo-list">
|
||||||
|
<section v-for="cargo in form.cargoes" :key="cargo.key" class="cargo-card">
|
||||||
|
<header>第 {{ cargo.itemNo }} 项</header>
|
||||||
|
<div class="cargo-grid">
|
||||||
|
<label><span>中文品名 *</span><el-input v-model="cargo.cargoName" maxlength="128" /></label>
|
||||||
|
<label><span>英文品名</span><el-input v-model="cargo.cargoNameEn" maxlength="128" /></label>
|
||||||
|
<label><span>SKU NO.</span><el-input v-model="cargo.skuNo" maxlength="64" /></label>
|
||||||
|
<label><span>型号</span><el-input v-model="cargo.model" maxlength="64" /></label>
|
||||||
|
<label><span>箱数 *</span><el-input-number v-model="cargo.packageCount" :min="1" :precision="0" controls-position="right" /></label>
|
||||||
|
<label><span>总数量 *</span><el-input-number v-model="cargo.totalQuantity" :min="1" :precision="0" controls-position="right" /></label>
|
||||||
|
<label><span>长(cm) *</span><el-input-number v-model="cargo.lengthCm" :min="0.01" :precision="2" controls-position="right" /></label>
|
||||||
|
<label><span>宽(cm) *</span><el-input-number v-model="cargo.widthCm" :min="0.01" :precision="2" controls-position="right" /></label>
|
||||||
|
<label><span>高(cm) *</span><el-input-number v-model="cargo.heightCm" :min="0.01" :precision="2" controls-position="right" /></label>
|
||||||
|
<label><span>毛重(kg) *</span><el-input-number v-model="cargo.grossWeightKg" :min="0.001" :precision="3" controls-position="right" /></label>
|
||||||
|
<label><span>净重(kg)</span><el-input-number v-model="cargo.netWeightKg" :min="0.001" :precision="3" controls-position="right" /></label>
|
||||||
|
<label><span>体积(m³) *</span><el-input-number v-model="cargo.volumeCbm" :min="0.0001" :precision="4" controls-position="right" /></label>
|
||||||
|
<label><span>报关 HS CODE</span><el-input v-model="cargo.hsCode" maxlength="20" /></label>
|
||||||
|
<label><span>清关 HS CODE</span><el-input v-model="cargo.customsHsCode" maxlength="20" /></label>
|
||||||
|
<label><span>每箱数量</span><el-input-number v-model="cargo.unitsPerPackage" :min="1" :precision="0" controls-position="right" /></label>
|
||||||
|
<label><span>单价</span><el-input-number v-model="cargo.unitDeclaredValue" :min="0" :precision="2" controls-position="right" /></label>
|
||||||
|
<label><span>申报总价</span><el-input-number v-model="cargo.declaredValue" :min="0" :precision="2" controls-position="right" /></label>
|
||||||
|
<label><span>币种</span><el-input v-model="cargo.currency" maxlength="3" placeholder="USD" /></label>
|
||||||
|
<label><span>材质</span><el-input v-model="cargo.material" maxlength="128" /></label>
|
||||||
|
<label><span>用途</span><el-input v-model="cargo.usage" maxlength="128" /></label>
|
||||||
|
<label><span>品牌</span><el-input v-model="cargo.brand" maxlength="64" /></label>
|
||||||
|
<label class="wide"><span>货品备注</span><el-input v-model="cargo.cargoRemark" maxlength="500" /></label>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AppDialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.declaration-editor { display: grid; gap: 14px; max-height: calc(100vh - 190px); overflow: auto; padding-right: 4px; }
|
||||||
|
.readonly-facts { margin-top: 2px; }
|
||||||
|
.order-fields :deep(.el-date-editor), .order-fields :deep(.el-input) { width: 100%; }
|
||||||
|
h3 { margin: 0; color: #344054; font-size: 15px; } h3 small { margin-left: 8px; color: #98a2b3; font-weight: 400; }
|
||||||
|
.cargo-list { display: grid; gap: 12px; }
|
||||||
|
.cargo-card { overflow: hidden; border: 1px solid var(--cc-border); border-radius: 8px; background: #fff; }
|
||||||
|
.cargo-card > header { padding: 9px 12px; border-bottom: 1px solid var(--cc-border); background: #f8fafc; color: #475467; font-weight: 600; }
|
||||||
|
.cargo-grid { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); gap: 10px 12px; padding: 12px; }
|
||||||
|
.cargo-grid label { display: grid; min-width: 0; gap: 5px; color: #667085; font-size: 12px; }
|
||||||
|
.cargo-grid label.wide { grid-column: span 2; }
|
||||||
|
.cargo-grid :deep(.el-input-number), .cargo-grid :deep(.el-input) { width: 100%; }
|
||||||
|
@media (max-width: 1200px) { .cargo-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
|
||||||
|
</style>
|
||||||
@@ -98,7 +98,7 @@ describe('UnifiedOrdersView contract', () => {
|
|||||||
'actions', 'inboundCode', 'mainStatus', 'customerName', 'receivingWarehouseName',
|
'actions', 'inboundCode', 'mainStatus', 'customerName', 'receivingWarehouseName',
|
||||||
'destinationWarehouseName', 'channelName', 'totalPackageCount', 'totalPalletCount', 'requestedPickupAt', 'locationCodes', 'measurementReviewStatus', 'actualGrossWeightKg',
|
'destinationWarehouseName', 'channelName', 'totalPackageCount', 'totalPalletCount', 'requestedPickupAt', 'locationCodes', 'measurementReviewStatus', 'actualGrossWeightKg',
|
||||||
'actualVolumeCbm', 'totalNetWeightKg', 'chargeableWeightKg', 'chargeableVolumeCbm', 'expectedShipDate',
|
'actualVolumeCbm', 'totalNetWeightKg', 'chargeableWeightKg', 'chargeableVolumeCbm', 'expectedShipDate',
|
||||||
'expectedArrivalAt', 'feeReviewStatus', 'skuNos', 'totalQuantity', 'expressCompany', 'expressTrackingNo',
|
'expectedVolumeCbm', 'expectedArrivalAt', 'feeReviewStatus', 'skuNos', 'totalQuantity', 'expressCompany', 'expressTrackingNo',
|
||||||
'shipmentId', 'referenceId', 'containerNo', 'cabinetNo',
|
'shipmentId', 'referenceId', 'containerNo', 'cabinetNo',
|
||||||
'effectiveSailingAt', 'effectivePortArrivalAt', 'customerRemark', 'latestInternalNote', 'createdAt', 'updatedAt',
|
'effectiveSailingAt', 'effectivePortArrivalAt', 'customerRemark', 'latestInternalNote', 'createdAt', 'updatedAt',
|
||||||
]
|
]
|
||||||
@@ -119,6 +119,7 @@ describe('UnifiedOrdersView contract', () => {
|
|||||||
expect(columnSource).toContain("key: 'requestedPickupAt', prop: 'requestedPickupAt', label: '预约提货时间'")
|
expect(columnSource).toContain("key: 'requestedPickupAt', prop: 'requestedPickupAt', label: '预约提货时间'")
|
||||||
expect(columnSource.match(/defaultVisible: false/g)).toHaveLength(13)
|
expect(columnSource.match(/defaultVisible: false/g)).toHaveLength(13)
|
||||||
expect(columnSource).toContain("key: 'expectedArrivalAt', prop: 'expectedArrivalAt', label: '预计到货时间', minWidth: 160, slotName: 'field' }")
|
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: '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"')
|
||||||
@@ -411,6 +412,15 @@ describe('UnifiedOrdersView contract', () => {
|
|||||||
expect(routeSource).not.toContain('unified-order-workflow')
|
expect(routeSource).not.toContain('unified-order-workflow')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('only exposes declaration editing on approved editable details and marks channel changes', () => {
|
||||||
|
expect(source).toContain("detail.value.summary.mainStatus === 'WAIT_MEASURE'")
|
||||||
|
expect(source).toContain("can('platform:order:declaration:edit')")
|
||||||
|
expect(source).toContain('编辑客户申报资料')
|
||||||
|
expect(source).toContain('<OrderDeclarationEditorDialog')
|
||||||
|
expect(source).toContain('曾变更渠道')
|
||||||
|
expect(source).toContain('@updated="handleDeclarationUpdated"')
|
||||||
|
})
|
||||||
|
|
||||||
it('supports the measurement-review deep link with an inbound-code lookup and recoverable return path', () => {
|
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).toContain("name: 'unified-order-detail-by-inbound-code'")
|
||||||
expect(routeSource.indexOf("path: 'orders/inbound/:inboundCode'")).toBeLessThan(routeSource.indexOf("path: 'orders/:orderId'"))
|
expect(routeSource.indexOf("path: 'orders/inbound/:inboundCode'")).toBeLessThan(routeSource.indexOf("path: 'orders/:orderId'"))
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
import { cancelPlatformCoLoadGroup, createPlatformCoLoadGroup, getPlatformCoLoadGroupByInboundCode, type CoLoadBusinessSummary, type CoLoadGroup } from '@/modules/unified-orders/api/coLoadGroups'
|
import { cancelPlatformCoLoadGroup, createPlatformCoLoadGroup, getPlatformCoLoadGroupByInboundCode, type CoLoadBusinessSummary, type CoLoadGroup } from '@/modules/unified-orders/api/coLoadGroups'
|
||||||
import OrderFulfillmentTreePanel from '@/modules/unified-orders/components/OrderFulfillmentTreePanel.vue'
|
import OrderFulfillmentTreePanel from '@/modules/unified-orders/components/OrderFulfillmentTreePanel.vue'
|
||||||
import InboundFeeDetailManager from '@/modules/unified-orders/components/InboundFeeDetailManager.vue'
|
import InboundFeeDetailManager from '@/modules/unified-orders/components/InboundFeeDetailManager.vue'
|
||||||
|
import OrderDeclarationEditorDialog from '@/modules/unified-orders/components/OrderDeclarationEditorDialog.vue'
|
||||||
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'
|
||||||
@@ -108,6 +109,7 @@ const warehouseLoadError = ref<ApiErrorDetails | null>(null)
|
|||||||
const detail = ref<UnifiedOrderDetailResource | null>(null)
|
const detail = ref<UnifiedOrderDetailResource | null>(null)
|
||||||
const detailLoading = ref(false)
|
const detailLoading = ref(false)
|
||||||
const detailError = ref<ApiErrorDetails | null>(null)
|
const detailError = ref<ApiErrorDetails | null>(null)
|
||||||
|
const declarationEditorOpen = ref(false)
|
||||||
const activeDetailTab = ref('basic')
|
const activeDetailTab = ref('basic')
|
||||||
const automaticFee = ref<AutomaticFeeSummary | null>(null)
|
const automaticFee = ref<AutomaticFeeSummary | null>(null)
|
||||||
const otherFeeSummary = ref<OtherFeeSummary>({ currency: 'CNY', approvedReceivable: 0, approvedPayable: 0 })
|
const otherFeeSummary = ref<OtherFeeSummary>({ currency: 'CNY', approvedReceivable: 0, approvedPayable: 0 })
|
||||||
@@ -281,8 +283,9 @@ const columns = computed<DataTableColumn[]>(() => [
|
|||||||
{ key: 'customerName', prop: 'customerName', label: '客户', minWidth: 150, slotName: 'field' },
|
{ key: 'customerName', prop: 'customerName', label: '客户', minWidth: 150, slotName: 'field' },
|
||||||
{ key: 'receivingWarehouseName', prop: 'receivingWarehouseName', label: '平台收货仓', minWidth: 150, slotName: 'field' },
|
{ key: 'receivingWarehouseName', prop: 'receivingWarehouseName', label: '平台收货仓', minWidth: 150, slotName: 'field' },
|
||||||
{ key: 'destinationWarehouseName', prop: 'destinationWarehouseName', label: '目的仓', minWidth: 150, slotName: 'field' },
|
{ key: 'destinationWarehouseName', prop: 'destinationWarehouseName', label: '目的仓', minWidth: 150, slotName: 'field' },
|
||||||
{ key: 'channelName', prop: 'channelName', label: '渠道', minWidth: 140, slotName: 'field' },
|
{ key: 'channelName', prop: 'channelName', label: '渠道', minWidth: 180, slotName: 'channel' },
|
||||||
{ key: 'totalPackageCount', prop: 'totalPackageCount', label: '总箱数', minWidth: 100, slotName: 'field' },
|
{ key: 'totalPackageCount', prop: 'totalPackageCount', label: '总箱数', minWidth: 100, slotName: 'field' },
|
||||||
|
{ key: 'expectedVolumeCbm', prop: 'expectedVolumeCbm', label: '预计体积(m³)', minWidth: 130, slotName: 'field' },
|
||||||
{ key: 'totalPalletCount', prop: 'totalPalletCount', label: '总托数', minWidth: 100, slotName: 'field' },
|
{ key: 'totalPalletCount', prop: 'totalPalletCount', label: '总托数', minWidth: 100, slotName: 'field' },
|
||||||
{ key: 'requestedPickupAt', prop: 'requestedPickupAt', label: '预约提货时间', minWidth: 160, slotName: 'field' },
|
{ key: 'requestedPickupAt', prop: 'requestedPickupAt', label: '预约提货时间', minWidth: 160, slotName: 'field' },
|
||||||
{ key: 'locationCodes', prop: 'locationCodes', label: '库位位置', minWidth: 160, slotName: 'field', defaultVisible: false },
|
{ key: 'locationCodes', prop: 'locationCodes', label: '库位位置', minWidth: 160, slotName: 'field', defaultVisible: false },
|
||||||
@@ -360,6 +363,19 @@ function can(permission: string) {
|
|||||||
return access.can(permission)
|
return access.can(permission)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const canEditDeclaration = computed(() => Boolean(
|
||||||
|
detail.value
|
||||||
|
&& detail.value.summary.mainStatus === 'WAIT_MEASURE'
|
||||||
|
&& detail.value.order.order?.version !== null
|
||||||
|
&& detail.value.order.order?.version !== undefined
|
||||||
|
&& can('platform:order:declaration:edit'),
|
||||||
|
))
|
||||||
|
|
||||||
|
async function handleDeclarationUpdated() {
|
||||||
|
activeDetailTab.value = 'basic'
|
||||||
|
await loadDetail(true)
|
||||||
|
}
|
||||||
|
|
||||||
function customerName(row: UnifiedOrderResource) {
|
function customerName(row: UnifiedOrderResource) {
|
||||||
return row.customerName?.trim() || customers.value.find((item) => String(item.id) === String(row.tenantId))?.tenantName || '客户名称待补充'
|
return row.customerName?.trim() || customers.value.find((item) => String(item.id) === String(row.tenantId))?.tenantName || '客户名称待补充'
|
||||||
}
|
}
|
||||||
@@ -1260,7 +1276,7 @@ onBeforeUnmount(() => {
|
|||||||
<div><dt>目的仓邮编</dt><dd>{{ detail.summary.destinationPostalCode || '—' }}</dd></div>
|
<div><dt>目的仓邮编</dt><dd>{{ detail.summary.destinationPostalCode || '—' }}</dd></div>
|
||||||
<div><dt>目的仓联系人</dt><dd>{{ detail.summary.destinationContact || '—' }}</dd></div>
|
<div><dt>目的仓联系人</dt><dd>{{ detail.summary.destinationContact || '—' }}</dd></div>
|
||||||
<div><dt>目的仓联系人电话</dt><dd>{{ detail.summary.destinationContactPhone || '—' }}</dd></div>
|
<div><dt>目的仓联系人电话</dt><dd>{{ detail.summary.destinationContactPhone || '—' }}</dd></div>
|
||||||
<div><dt>渠道</dt><dd>{{ detail.summary.channelName || '—' }}</dd></div>
|
<div><dt>渠道</dt><dd>{{ detail.summary.channelName || '—' }} <el-tag v-if="detail.summary.channelChanged" size="small" type="warning">曾变更渠道</el-tag></dd></div>
|
||||||
<div><dt>业务类型</dt><dd>{{ firstLegBusinessTypeLabel(detail.summary.businessType) }}</dd></div>
|
<div><dt>业务类型</dt><dd>{{ firstLegBusinessTypeLabel(detail.summary.businessType) }}</dd></div>
|
||||||
<div><dt>贸易条款</dt><dd>{{ firstLegTradeTermLabel(detail.order.order?.tradeTerm) }}</dd></div>
|
<div><dt>贸易条款</dt><dd>{{ firstLegTradeTermLabel(detail.order.order?.tradeTerm) }}</dd></div>
|
||||||
<div><dt>买单 / 退税</dt><dd>{{ firstLegTaxModeLabel(detail.order.order?.taxMode) }}</dd></div>
|
<div><dt>买单 / 退税</dt><dd>{{ firstLegTaxModeLabel(detail.order.order?.taxMode) }}</dd></div>
|
||||||
@@ -1312,7 +1328,7 @@ onBeforeUnmount(() => {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-else-if="activeDetailTab === 'cargo'">
|
<template v-else-if="activeDetailTab === 'cargo'">
|
||||||
<div class="panel-title"><div><h2>货品信息</h2><p>共 {{ detail.order.cargoes.length }} 条</p></div><PackageOpen :size="19" /></div>
|
<div class="panel-title"><div><h2>货品信息</h2><p>共 {{ detail.order.cargoes.length }} 条</p></div><el-button v-if="canEditDeclaration" type="primary" size="small" @click="declarationEditorOpen = true">编辑客户申报资料</el-button></div>
|
||||||
<div class="detail-table-wrap">
|
<div class="detail-table-wrap">
|
||||||
<el-table :data="detail.order.cargoes" border stripe empty-text="暂无货品信息">
|
<el-table :data="detail.order.cargoes" border stripe empty-text="暂无货品信息">
|
||||||
<el-table-column prop="itemNo" label="#" width="64" />
|
<el-table-column prop="itemNo" label="#" width="64" />
|
||||||
@@ -1570,6 +1586,7 @@ onBeforeUnmount(() => {
|
|||||||
</template>
|
</template>
|
||||||
<template #orderStatus="{ row }"><StatusBadge :status="orderStatusLabel((row as UnifiedOrderResource).internalOrderStatus || (row as UnifiedOrderResource).mainStatus)" /></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 #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 #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 #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 #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>
|
<template #customer="{ row }"><div class="primary-cell"><strong>{{ customerName(row as UnifiedOrderResource) }}</strong></div></template>
|
||||||
@@ -1625,6 +1642,13 @@ onBeforeUnmount(() => {
|
|||||||
<el-alert title="确认订单资料无误后审核通过,订单将进入待测量阶段。" type="info" :closable="false" show-icon />
|
<el-alert title="确认订单资料无误后审核通过,订单将进入待测量阶段。" type="info" :closable="false" show-icon />
|
||||||
</AppDialog>
|
</AppDialog>
|
||||||
|
|
||||||
|
<OrderDeclarationEditorDialog
|
||||||
|
v-if="detail"
|
||||||
|
v-model="declarationEditorOpen"
|
||||||
|
:detail="detail"
|
||||||
|
@updated="handleDeclarationUpdated"
|
||||||
|
/>
|
||||||
|
|
||||||
<AppDialog
|
<AppDialog
|
||||||
v-model="measurementPreviewOpen"
|
v-model="measurementPreviewOpen"
|
||||||
:title="measurementPreviewTitle"
|
:title="measurementPreviewTitle"
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ describe('unified order status labels', () => {
|
|||||||
expect(unifiedOrderStatusLabel('WAIT_SPLIT')).toBe('待分柜')
|
expect(unifiedOrderStatusLabel('WAIT_SPLIT')).toBe('待分柜')
|
||||||
expect(unifiedOrderReviewStatusLabel('APPROVED')).toBe('审核通过')
|
expect(unifiedOrderReviewStatusLabel('APPROVED')).toBe('审核通过')
|
||||||
expect(unifiedOrderLoadingStatusLabel('LOADED')).toBe('已配载')
|
expect(unifiedOrderLoadingStatusLabel('LOADED')).toBe('已配载')
|
||||||
|
expect(unifiedOrderHistoryActionLabel('DECLARATION_UPDATE')).toBe('客户申报资料变更')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('never exposes unknown backend enums', () => {
|
it('never exposes unknown backend enums', () => {
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ const historyActionLabels: Record<string, string> = {
|
|||||||
UI_FIXTURE_MEASURE_APPROVE: '测量数据审核通过',
|
UI_FIXTURE_MEASURE_APPROVE: '测量数据审核通过',
|
||||||
UI_FIXTURE_INBOUND_BEGIN: '开始入库',
|
UI_FIXTURE_INBOUND_BEGIN: '开始入库',
|
||||||
STATUS_UPDATE: '状态更新',
|
STATUS_UPDATE: '状态更新',
|
||||||
|
DECLARATION_UPDATE: '客户申报资料变更',
|
||||||
}
|
}
|
||||||
|
|
||||||
const fulfillmentRoleLabels: Record<string, string> = {
|
const fulfillmentRoleLabels: Record<string, string> = {
|
||||||
|
|||||||
Reference in New Issue
Block a user