Release/test 合并上生产 #2

Merged
admin merged 736 commits from release/test into main 2026-09-17 11:50:56 +08:00
20 changed files with 103 additions and 81 deletions
Showing only changes of commit f9ff9086d5 - Show all commits
@@ -21,4 +21,11 @@ describe('CustomsCasesView', () => {
expect(source).not.toContain("key: 'orderId', type: 'input'")
expect(source).not.toContain("key: 'inboundId', type: 'input'")
})
it('does not render technical order or inbound ids in the list', () => {
expect(source).toContain("label: '订单关联'")
expect(source).toContain("label: '入库单关联'")
expect(source).toContain("return value ? '已关联' : '未关联'")
expect(source).not.toContain("label: '订单标识'")
expect(source).not.toContain("label: '入库单标识'")
})
})
@@ -18,7 +18,8 @@ import type { CustomsCaseDetail, CustomsCaseResource } from '../types'
const access = useAccessPreviewStore(); const rows = ref<CustomsCaseResource[]>([]); const loading = ref(false); const error = ref<ApiErrorDetails | null>(null); const page = ref(1); const pageSize = ref(20); const total = ref(0)
const filters = ref<QueryModel>({ status: '' }); const fields: QueryField[] = [{ key: 'status', type: 'select', label: '状态', options: customsCaseStatusOptions.map((option) => ({ ...option })) }]
const columns = [{ key: 'actions', label: '操作', fixed: 'left', width: 175, slotName: 'actions', hideable: false, movable: false }, { prop: 'customsNo', label: '报关编号', minWidth: 145 }, { prop: 'orderId', label: '订单标识', minWidth: 155 }, { prop: 'inboundId', label: '入库单标识', minWidth: 155 }, { prop: 'checklistCode', label: '资料清单', minWidth: 125 }, { prop: 'checklistVersionNo', label: '版本', width: 90 }, { prop: 'status', label: '状态', width: 110, slotName: 'status' }, { prop: 'updatedAt', label: '更新时间', minWidth: 160 }]
const columns = [{ key: 'actions', label: '操作', fixed: 'left', width: 175, slotName: 'actions', hideable: false, movable: false }, { prop: 'customsNo', label: '报关编号', minWidth: 145 }, { key: 'orderLink', label: '订单关联', minWidth: 120, formatter: (row: CustomsCaseResource) => associationLabel(row.orderId) }, { key: 'inboundLink', label: '入库单关联', minWidth: 120, formatter: (row: CustomsCaseResource) => associationLabel(row.inboundId) }, { prop: 'checklistCode', label: '资料清单', minWidth: 125 }, { prop: 'checklistVersionNo', label: '版本', width: 90 }, { prop: 'status', label: '状态', width: 110, slotName: 'status' }, { prop: 'updatedAt', label: '更新时间', minWidth: 160 }]
function associationLabel(value: string | null | undefined) { return value ? '已关联' : '未关联' }
async function load() { loading.value = true; error.value = null; try { const result = await listCustomsCases({ page: page.value, size: pageSize.value, status: String(filters.value.status || '') || undefined }); rows.value = result.records; total.value = result.total } catch (cause) { error.value = getApiErrorDetails(cause) } finally { loading.value = false } }
function search() { if (page.value === 1) void load(); else page.value = 1 }
const detailOpen = ref(false); const detail = ref<CustomsCaseDetail | null>(null); const detailLoading = ref(false); const actionError = ref<ApiErrorDetails | null>(null); const submitForm = reactive({ customsAgentName: '', externalReference: '', note: '' }); const canSubmit = computed(() => detail.value?.customsCase.status === 'READY_TO_SUBMIT' && access.can('platform:firstleg:customs:submit'))
@@ -67,7 +67,8 @@ describe('DownloadCenterView', () => {
await flushPromises()
expect(document.body.textContent).toContain('客户列表')
expect(document.querySelector('.failure-cell')?.classList.contains('failure-message')).toBe(false)
expect(document.querySelector('.data-table')?.textContent).not.toContain('失败原因')
expect(document.querySelector('.failure-cell')).toBeNull()
const button = [...document.querySelectorAll<HTMLButtonElement>('button')].find((item) => item.textContent?.trim() === '下载')
button?.click()
await flushPromises()
@@ -91,7 +92,7 @@ describe('DownloadCenterView', () => {
mount(DownloadCenterView, { attachTo: document.body, global: { plugins: [pinia, ElementPlus] } })
await flushPromises()
expect(document.querySelector('.failure-cell')?.classList.contains('failure-message')).toBe(true)
expect(document.querySelector('.failure-cell')).toBeNull()
const button = [...document.querySelectorAll<HTMLButtonElement>('button')].find((item) => item.textContent?.trim() === '重试')
button?.click()
await flushPromises()
@@ -145,7 +146,7 @@ describe('DownloadCenterView', () => {
.some((item) => item.textContent?.trim() === '重试')).toBe(false)
})
it('does not expose customer retry without the customer export permission', async () => {
it('keeps failure details out of the list and does not expose retry without the source export permission', async () => {
vi.mocked(listPlatformExportTasks).mockResolvedValue({
records: [{ ...task, status: 'FAILED', failureMessage: 'IAM timeout' }],
page: 1, size: 20, total: 1, pages: 1,
@@ -155,12 +156,18 @@ describe('DownloadCenterView', () => {
auth.token = 'token'
auth.permissions = ['platform:download:list']
useAccessPreviewStore().initializeAfterLogin()
vi.mocked(getPlatformExportTask).mockResolvedValue({ ...task, status: 'FAILED', failureMessage: 'IAM timeout' })
mount(DownloadCenterView, { attachTo: document.body, global: { plugins: [pinia, ElementPlus] } })
await flushPromises()
expect(document.body.textContent).toContain('IAM timeout')
expect(document.body.textContent).not.toContain('IAM timeout')
expect([...document.querySelectorAll<HTMLButtonElement>('button')]
.some((item) => item.textContent?.trim() === '重试')).toBe(false)
;[...document.querySelectorAll<HTMLButtonElement>('button')]
.find((item) => item.textContent?.trim() === '详情')?.click()
await flushPromises()
expect(document.body.textContent).toContain('IAM timeout')
})
it('announces the initial loading state while the task request is pending', async () => {
@@ -67,7 +67,6 @@ const columns: DataTableColumn[] = [
{ key: 'requestedAt', label: '申请时间', width: 170, slotName: 'requestedAt' },
{ key: 'completedAt', label: '完成时间', width: 170, slotName: 'completedAt' },
{ key: 'expiresAt', label: '过期时间', width: 170, slotName: 'expiresAt' },
{ key: 'failure', label: '失败原因', minWidth: 240, slotName: 'failure' },
]
const hasActiveTasks = computed(() => result.value.records.some((task) => ['PENDING', 'RUNNING'].includes(task.status)))
@@ -267,7 +266,6 @@ onBeforeUnmount(() => {
<template #requestedAt="{ row }">{{ formatDate((row as PlatformExportTask).requestedAt) }}</template>
<template #completedAt="{ row }">{{ formatDate((row as PlatformExportTask).completedAt) }}</template>
<template #expiresAt="{ row }">{{ formatDate((row as PlatformExportTask).expiresAt) }}</template>
<template #failure="{ row }"><span class="failure-cell" :class="{ 'failure-message': Boolean((row as PlatformExportTask).failureMessage) }">{{ (row as PlatformExportTask).failureMessage || '—' }}</span></template>
</DataTable>
</section>
</ListPageLayout>
@@ -14,6 +14,7 @@ describe('CarrierRatesView price-book source contract', () => {
expect(source).toContain('formatEffectivePeriod')
expect(source).toContain('PENDING_CONFIRMATION')
expect(source).toContain('SUPERSEDED')
expect(source).not.toContain('<span>ID {{ (row as PlatformPriceBookVersion).id }}</span>')
})
it('uses only the parsed price-book workflow instead of legacy carrier-rate editing', () => {
@@ -646,7 +646,7 @@ onMounted(() => {
><Download :size="15" />原文件</el-button>
</div>
</template>
<template #version="{ row }"><div class="version-cell"><strong>V{{ (row as PlatformPriceBookVersion).versionNo }}</strong><span>ID {{ (row as PlatformPriceBookVersion).id }}</span></div></template>
<template #version="{ row }"><div class="version-cell"><strong>V{{ (row as PlatformPriceBookVersion).versionNo }}</strong></div></template>
<template #status="{ row }"><el-tag :type="priceBookStatusMeta((row as PlatformPriceBookVersion).status).type" effect="light">{{ priceBookStatusMeta((row as PlatformPriceBookVersion).status).label }}</el-tag></template>
<template #contents="{ row }"><div class="metric-cell"><strong>{{ (row as PlatformPriceBookVersion).channelCount }}</strong><span>渠道</span><strong>{{ (row as PlatformPriceBookVersion).rateCount }}</strong><span>价格行</span></div></template>
<template #issues="{ row }"><div class="issue-counts"><span :class="{ 'is-blocking': (row as PlatformPriceBookVersion).blockingErrorCount }">阻断 {{ (row as PlatformPriceBookVersion).blockingErrorCount }}</span><span>警告 {{ (row as PlatformPriceBookVersion).warningCount }}</span></div></template>
@@ -122,7 +122,7 @@ async function openCreate() {
} else {
const [fees, suppliers] = await Promise.all([
listFinanceFeePool(customerId, 'PAYABLE'),
listPlatformPartners({ page: 1, size: 200, type: 'SUPPLIER', status: 'ACTIVE' }),
listPlatformPartners({ page: 1, size: 200, status: 'ACTIVE' }),
])
feeOptions.value = fees.filter((item) => item.feeSourceId && item.status === 'AVAILABLE')
supplierOptions.value = suppliers.records
@@ -70,7 +70,7 @@ describe('Finance G3/G4 views', () => {
document.body.innerHTML = ''
const customer = { id: '10001', tenantName: '客户A', status: 'ACTIVE' } as PlatformTenantResource
vi.mocked(listPlatformTenants).mockReset().mockResolvedValue({ records: [customer], page: 1, size: 200, total: 1, pages: 1 })
vi.mocked(listPlatformPartners).mockReset().mockResolvedValue({ records: [{ id: '301', tenantId: '0', dataScopeId: '0', partnerCode: 'SUP-301', partnerName: '供应商A', partnerType: 'SUPPLIER', types: ['SUPPLIER'], contactName: '', contactPhone: '', contactEmail: null, countryCode: 'CN', province: null, city: '', district: null, addressLine: '', postalCode: null, status: 'ACTIVE', remark: null } as PlatformPartnerResource], page: 1, size: 200, total: 1, pages: 1 })
vi.mocked(listPlatformPartners).mockReset().mockResolvedValue({ records: [{ id: '301', tenantId: '0', dataScopeId: '0', partnerCode: 'PARTNER-301', partnerName: '往来单位A', partnerType: 'CUSTOMER', types: ['CUSTOMER'], contactName: '', contactPhone: '', contactEmail: null, countryCode: 'CN', province: null, city: '', district: null, addressLine: '', postalCode: null, status: 'ACTIVE', remark: null } as PlatformPartnerResource], page: 1, size: 200, total: 1, pages: 1 })
vi.mocked(listFinanceStatements).mockReset().mockResolvedValue([statement])
vi.mocked(getReconciliationStatement).mockReset().mockResolvedValue(statementDetail)
vi.mocked(actionReconciliationStatement).mockReset().mockResolvedValue(statementDetail)
@@ -106,8 +106,8 @@ describe('Finance G3/G4 views', () => {
const wrapper = await mountWithAccess(FinanceFormalSettlementPage, ['platform:finance:settlement-ap:view', 'platform:finance:settlement-ap:create', 'platform:finance:payment:create'], { kind: 'payable' })
const vm = wrapper.vm as unknown as { load(model: Record<string, string>): Promise<void>; openCreate(): Promise<void>; submitCreate(): Promise<void>; openPayment(row: FinancePayableSettlement): void; submitPayment(): Promise<void>; createForm: { supplierId: string; dueDate: string; feeSourceIds: string[] }; paymentFile: File | null; paymentForm: { amount: number; transactionDate: string; method: 'BANK_TRANSFER'; referenceNo: string; remark: string } }
await vm.load({ customerId: '10001' }); await vm.openCreate()
expect(listPlatformPartners).toHaveBeenCalledWith({ page: 1, size: 200, type: 'SUPPLIER', status: 'ACTIVE' })
expect(document.body.textContent).toContain('供应商A · SUP-301')
expect(listPlatformPartners).toHaveBeenCalledWith({ page: 1, size: 200, status: 'ACTIVE' })
expect(document.body.textContent).toContain('往来单位A · PARTNER-301')
Object.assign(vm.createForm, { supplierId: '301', dueDate: '2026-09-30', feeSourceIds: ['81'] })
await vm.submitCreate()
expect(createPayableSettlement).toHaveBeenCalledWith({ customerId: '10001', supplierId: '301', feeSourceIds: ['81'], dueDate: '2026-09-30', currency: 'CNY' })
@@ -294,19 +294,29 @@ describe('InboundOrdersView', () => {
const columnSource = source.slice(source.indexOf('const columns:'))
const operation = columnSource.indexOf("key: 'actions'")
const inboundCode = columnSource.indexOf("key: 'inboundCode'")
const processStatus = columnSource.indexOf("key: 'processStatus'")
const orderStatus = columnSource.indexOf("key: 'orderStatus'")
const measureReview = columnSource.indexOf("key: 'measureReviewStatus'")
const feeStatus = columnSource.indexOf("key: 'feeStatus'")
const feeReviewStatus = columnSource.indexOf("key: 'feeReviewStatus'")
const customer = columnSource.indexOf("key: 'customerName'")
const warehouse = columnSource.indexOf("key: 'warehouseName'")
expect(operation).toBeGreaterThan(-1)
expect(operation).toBeLessThan(inboundCode)
expect(inboundCode).toBeLessThan(orderStatus)
expect(inboundCode).toBeLessThan(processStatus)
expect(processStatus).toBeLessThan(orderStatus)
expect(orderStatus).toBeLessThan(measureReview)
expect(measureReview).toBeLessThan(feeStatus)
expect(feeStatus).toBeLessThan(customer)
expect(feeStatus).toBeLessThan(feeReviewStatus)
expect(feeReviewStatus).toBeLessThan(customer)
expect(customer).toBeLessThan(warehouse)
expect(columnSource).toContain("fixed: 'left'")
expect(columnSource).toContain("{ key: 'processStatus', label: '入库流程状态'")
expect(columnSource).toContain("{ key: 'orderStatus', label: '订单状态'")
expect(columnSource).toContain("{ key: 'feeStatus', label: '费用状态'")
expect(columnSource).toContain("{ key: 'feeReviewStatus', label: '费用审核状态'")
expect(source).toContain('<template #processStatus="{ row }">')
expect(source).toContain('<template #feeReviewStatus="{ row }">')
expect(toolbarSource).toMatch(/@media \(max-width: 720px\)[\s\S]*width: 100%/)
})
})
@@ -124,10 +124,12 @@ const queryFields = computed(() => showAdvancedFilters.value
const columns: DataTableColumn[] = [
{ key: 'actions', label: '操作', width: 228, fixed: 'left', slotName: 'actions' },
{ key: 'inboundCode', label: '入仓码', minWidth: 166, slotName: 'inbound' },
{ key: 'orderStatus', label: '订单 / 流程状态', minWidth: 156, slotName: 'orderStatus' },
{ key: 'processStatus', label: '入库流程状态', minWidth: 132, slotName: 'processStatus' },
{ key: 'orderStatus', label: '订单状态', minWidth: 120, slotName: 'orderStatus' },
{ key: 'measureReviewStatus', label: '测量数据审核状态', minWidth: 142, slotName: 'measureReviewStatus' },
{ key: 'expectedArrivalAt', label: '预计到达时间', minWidth: 164, slotName: 'expectedArrivalAt' },
{ key: 'feeStatus', label: '费用审核状态', minWidth: 132, slotName: 'feeStatus' },
{ key: 'feeStatus', label: '费用状态', minWidth: 112, slotName: 'feeStatus' },
{ key: 'feeReviewStatus', label: '费用审核状态', minWidth: 132, slotName: 'feeReviewStatus' },
{ key: 'customerName', label: '客户', minWidth: 142, slotName: 'customerName' },
{ key: 'warehouseName', label: '仓库', minWidth: 140, slotName: 'warehouseName' },
{ key: 'deliveryWarehouseName', label: '交货仓', minWidth: 140, slotName: 'deliveryWarehouseName' },
@@ -452,10 +454,12 @@ onBeforeUnmount(() => { disposed = true; loadRequestId += 1; detailRequestId +=
<template #pagination-left>当前显示 {{ currentPageCount }} 条 · 共 {{ result.total }} 条</template>
<template #actions="{ row }"><div class="row-actions"><el-button link type="primary" @click="openWorkspace(row as InboundOrderSummary)"><ClipboardCheck :size="15" />处理</el-button><el-button link :loading="printingInboundId === (row as InboundOrderSummary).id" :disabled="Boolean(printingInboundId)" @click="printCartonLabels(row as InboundOrderSummary)"><Printer :size="15" />打印箱唛</el-button><el-button link @click="openDetail(row as InboundOrderSummary)"><Eye :size="15" />查看</el-button></div></template>
<template #inbound="{ row }"><div class="primary-cell"><strong>{{ text((row as InboundOrderSummary).inboundCode) }}</strong><span>订单 {{ text((row as InboundOrderSummary).orderNo) }}</span></div></template>
<template #orderStatus="{ row }"><div class="status-stack"><el-tag :type="statusType((row as InboundOrderSummary).processStatus)" effect="light">{{ statusLabel((row as InboundOrderSummary).processStatus) }}</el-tag><span>订单:{{ codeLabel((row as InboundOrderSummary).orderStatusSnapshot, inboundOrderStatusLabels) }}</span></div></template>
<template #processStatus="{ row }"><el-tag :type="statusType((row as InboundOrderSummary).processStatus)" effect="light">{{ statusLabel((row as InboundOrderSummary).processStatus) }}</el-tag></template>
<template #orderStatus="{ row }">{{ codeLabel((row as InboundOrderSummary).orderStatusSnapshot, inboundOrderStatusLabels) }}</template>
<template #measureReviewStatus="{ row }">{{ codeLabel((row as InboundOrderSummary).measureReviewStatus, measureReviewLabels) }}</template>
<template #expectedArrivalAt="{ row }">{{ formatDate((row as InboundOrderSummary).expectedArrivalAt) }}</template>
<template #feeStatus="{ row }"><div class="status-stack"><span>{{ codeLabel((row as InboundOrderSummary).feeStatus, feeStatusLabels) }}</span><small v-if="(row as InboundOrderSummary).feeReviewStatus">审核:{{ codeLabel((row as InboundOrderSummary).feeReviewStatus, inboundReviewStatusLabels) }}</small></div></template>
<template #feeStatus="{ row }">{{ codeLabel((row as InboundOrderSummary).feeStatus, feeStatusLabels) }}</template>
<template #feeReviewStatus="{ row }">{{ codeLabel((row as InboundOrderSummary).feeReviewStatus, inboundReviewStatusLabels) }}</template>
<template #customerName="{ row }">{{ text((row as InboundOrderSummary).customerName) }}</template>
<template #warehouseName="{ row }">{{ text((row as InboundOrderSummary).warehouseName) }}</template>
<template #deliveryWarehouseName="{ row }">{{ text((row as InboundOrderSummary).deliveryWarehouseName) }}</template>
@@ -93,7 +93,7 @@ function formatNumber(value?: number | null, digits = 3) {
<template v-if="preloadSummary">
<div><span>船名 / 航次</span><strong>{{ preloadSummary.vesselName || '—' }} / {{ preloadSummary.voyageNo || '—' }}</strong></div>
<div><span>箱型</span><strong>{{ preloadSummary.containerType || '—' }}</strong></div>
<div class="detail-summary__wide"><span>客户</span><strong>{{ preloadSummary.customers.map((item) => item.customerName || item.accountId).join('、') || '—' }}</strong></div>
<div class="detail-summary__wide"><span>客户</span><strong>{{ preloadSummary.customers.map((item) => item.customerName || '客户信息缺失').join('、') || '客户信息缺失' }}</strong></div>
</template>
<div><span>创建时间</span><strong>{{ formatDateTime(summary.createdAt) }}</strong></div>
</section>
@@ -130,7 +130,7 @@ function formatNumber(value?: number | null, digits = 3) {
<div class="history-entry">
<strong>{{ actionLabel(history.actionType) }}</strong>
<span v-if="history.fromStatus || history.toStatus">{{ loadingListStatusLabel(history.fromStatus) }} → {{ loadingListStatusLabel(history.toStatus) }}</span>
<p>{{ history.operatorName || history.operatorId }}<template v-if="history.operationReason"> · {{ history.operationReason }}</template></p>
<p>{{ history.operatorName || '操作人信息缺失' }}<template v-if="history.operationReason"> · {{ history.operationReason }}</template></p>
</div>
</el-timeline-item>
</el-timeline>
@@ -76,4 +76,10 @@ describe('LoadingListsView', () => {
expect(detailSource).toContain("[action] || '状态更新'")
expect(detailSource).not.toContain('[action] || action')
})
it('uses safe labels when customer or operator names are missing', () => {
expect(detailSource).toContain("item.customerName || '客户信息缺失'")
expect(detailSource).toContain("history.operatorName || '操作人信息缺失'")
expect(detailSource).not.toContain('item.customerName || item.accountId')
expect(detailSource).not.toContain('history.operatorName || history.operatorId')
})
})
@@ -89,6 +89,8 @@ describe('ExpressCompaniesView query and table migration', () => {
expect(wrapper.find('.metric-strip').exists()).toBe(false)
expect(wrapper.find('.list-page-layout__state').exists()).toBe(false)
expect(listPlatformExpressCompanies).toHaveBeenCalledWith(1, 20, '', '')
expect(wrapper.find('.data-table').text()).not.toContain('轨迹查询模板')
expect(wrapper.find('.data-table').text()).not.toContain('测试数据')
})
it('renders the error state instead of an empty table panel', async () => {
@@ -122,6 +124,8 @@ describe('ExpressCompaniesView query and table migration', () => {
expect(document.body.textContent).toContain('新增快递公司')
expect(document.body.textContent).not.toContain('请先在筛选条件中选择所属企业')
expect(document.body.textContent).toContain('轨迹查询地址模板')
expect(document.body.textContent).toContain('内部说明')
})
it('keeps the latest express company selected for editing', async () => {
@@ -122,8 +122,6 @@ const expressCompanyColumns: DataTableColumn[] = [
{ key: 'company', label: '快递公司', minWidth: 210, slotName: 'company' },
{ key: 'servicePhone', prop: 'servicePhone', label: '客服电话', minWidth: 130, formatter: (row: PlatformExpressCompanyResource) => row.servicePhone || '—' },
{ key: 'website', label: '官方网站', minWidth: 210, slotName: 'website' },
{ key: 'trackingUrlTemplate', label: '轨迹查询模板', minWidth: 260, slotName: 'trackingUrlTemplate' },
{ key: 'remark', label: '备注', minWidth: 200, slotName: 'remark' },
{ key: 'updatedAt', prop: 'updatedAt', label: '更新时间', minWidth: 150, formatter: (row: PlatformExpressCompanyResource) => formatUpdatedAt(row.updatedAt) },
{ key: 'status', label: '状态', width: 100, slotName: 'status' },
]
@@ -337,8 +335,6 @@ onMounted(() => {
<a v-if="safeHttpUrl((row as PlatformExpressCompanyResource).website)" class="external-link" :href="safeHttpUrl((row as PlatformExpressCompanyResource).website)!" target="_blank" rel="noopener noreferrer">访问官网<ExternalLink :size="13" /></a>
<span v-else class="muted-value">{{ (row as PlatformExpressCompanyResource).website || '—' }}</span>
</template>
<template #trackingUrlTemplate="{ row }"><code class="tracking-template">{{ (row as PlatformExpressCompanyResource).trackingUrlTemplate || '—' }}</code></template>
<template #remark="{ row }"><span class="remark-cell">{{ (row as PlatformExpressCompanyResource).remark || '—' }}</span></template>
<template #status="{ row }"><el-tag :type="(row as PlatformExpressCompanyResource).status === 'ACTIVE' ? 'success' : 'info'" effect="light">{{ (row as PlatformExpressCompanyResource).status === 'ACTIVE' ? '启用' : '停用' }}</el-tag></template>
<template #empty>
<el-empty v-if="!loading" :description="appliedFilters.keyword.trim() || appliedFilters.status ? '暂无符合筛选条件的快递公司' : '暂无快递公司档案'">
@@ -63,4 +63,8 @@ describe('PortSchedulesView', () => {
expect(source).toContain("actionError.value.code === 'A0409'")
expect(source).toContain('ApiErrorMeta')
})
it('shows ETA and backend update time in the list', () => {
expect(source).toContain("prop: 'schedule.etaAt'")
expect(source).toContain("prop: 'schedule.updatedAt'")
})
})
@@ -26,7 +26,7 @@ const columns = [
{ prop: 'schedule.scheduleNo', label: '船期编号', minWidth: 145 }, { prop: 'schedule.sourceName', label: '信息来源', minWidth: 130 },
{ prop: 'schedule.vesselName', label: '船名/航次', minWidth: 155, slotName: 'vessel' }, { prop: 'schedule.departurePortName', label: '起运港', minWidth: 120 },
{ prop: 'schedule.destinationPortName', label: '目的港', minWidth: 120 }, { prop: 'schedule.etdAt', label: '预计开船', minWidth: 160 },
{ prop: 'schedule.observedAt', label: '查询时间', minWidth: 160 }, { prop: 'expired', label: '有效性', width: 95, slotName: 'expired' },
{ prop: 'schedule.etaAt', label: '预计到港', minWidth: 160 }, { prop: 'schedule.observedAt', label: '查询时间', minWidth: 160 }, { prop: 'schedule.updatedAt', label: '更新时间', minWidth: 160 }, { prop: 'expired', label: '有效性', width: 95, slotName: 'expired' },
]
function blank(): PortScheduleData { const now = new Date().toISOString().slice(0, 19); return { customerId: '', oceanBookingId: '', containerBookingId: null, sourceType: 'MANUAL', sourceName: '', sourceRef: '', vesselName: '', voyageNo: '', departurePortCode: '', departurePortName: '', destinationPortCode: '', destinationPortName: '', etdAt: null, etaAt: null, bookingCutoffAt: null, customsCutoffAt: null, portCutoffAt: null, observedAt: now, expiresAt: now, remark: '' } }
type DialogMode = 'create' | 'view' | 'edit'
@@ -31,7 +31,8 @@ describe('TruckAppointmentsView', () => {
await flushPromises()
expect(listTruckAppointments).toHaveBeenCalledWith({ page: 1, size: 20, keyword: '', status: '', containerBookingId: '', containerType: '' })
expect(wrapper.text()).toContain('TA-9001')
expect(wrapper.text()).toContain('77')
expect(wrapper.text()).toContain('CONT-1')
expect(wrapper.text()).not.toContain('77')
expect(wrapper.text()).toContain('宁波运输')
expect(wrapper.find('button[aria-label="查看 TA-9001"]').exists()).toBe(true)
})
@@ -27,7 +27,7 @@ const fields = computed<QueryField[]>(() => [
const columns: DataTableColumn[] = [
{ key: 'actions', label: '操作', width: 150, fixed: 'left', slotName: 'actions' },
{ key: 'appointmentNo', prop: 'appointmentNo', label: '派车单号', minWidth: 170 },
{ key: 'containerBookingId', prop: 'containerBookingId', label: '关联柜体', minWidth: 130, formatter: (row: TruckAppointmentResource) => row.containerBookingId || '—' },
{ key: 'containerNo', prop: 'containerNo', label: '集装箱号', minWidth: 145, formatter: (row: TruckAppointmentResource) => row.containerNo || (row.containerBookingId ? '已关联柜次' : '—') },
{ key: 'company', label: '运输公司', minWidth: 160, slotName: 'company' },
{ key: 'driver', label: '司机', minWidth: 130, slotName: 'driver' },
{ key: 'vehicleNo', prop: 'vehicleNo', label: '车头号', minWidth: 120, formatter: (row: TruckAppointmentResource) => row.vehicleNo || '—' },
@@ -43,9 +43,8 @@ describe('UnifiedOrdersView contract', () => {
expect(source).toContain("{ value: 'UNRECORDED', label: '未录入' }")
expect(source).not.toContain("{ value: 'DRAFT', label: '草稿' }")
expect(source).not.toContain("{ value: 'CANCELLED', label: '已取消' }")
expect(source).not.toContain("key: 'orderNo'")
expect(source.match(/key: 'inboundCode', type: 'input'/g)).toHaveLength(1)
expect(source.match(/key: 'internalOrderStatus'/g)).toHaveLength(1)
expect(source).not.toContain("key: 'orderNo', type:")
expect(source).not.toContain('internalOrderStatus: filters.value.internalOrderStatus')
expect(source).not.toContain("key: 'keyword'")
const prototypeFieldOrder = [
@@ -77,11 +76,26 @@ describe('UnifiedOrdersView contract', () => {
expect(source).toContain(':loading="loading"')
expect(source).toContain('v-model:page="page"')
expect(source).toContain('v-model:page-size="pageSize"')
expect(source).toContain('slotName: \'orderStatus\'')
const columnSource = source.slice(source.indexOf('const columns:'), source.indexOf('function emptyFilters'))
const expectedColumnKeys = [
'actions', 'orderNo', 'inboundCode', 'mainStatus', 'customerName', 'receivingWarehouseName',
'destinationWarehouseName', 'totalPackageCount', 'measurementReviewStatus', 'actualGrossWeightKg',
'actualVolumeCbm', 'chargeableWeightKg', 'expectedShipDate', 'loadingStatus', 'containerNo', 'cabinetNo',
'sailingAt', 'portArrivalAt', 'customerRemark', 'latestInternalNote', 'createdAt', 'updatedAt',
]
for (const key of expectedColumnKeys) expect(columnSource).toContain(`key: '${key}'`)
for (const uncontractedKey of [
'internalOrderStatus', 'expectedArrivalAt', 'feeReviewStatus', 'holdCargo', 'companyName',
'pickupMethod', 'chargeableVolumeCbm', 'recommendedPalletType', 'locationCodes', 'associatedFiles', 'createdBy',
]) expect(columnSource).not.toContain(`key: '${uncontractedKey}'`)
expect(columnSource.indexOf("key: 'actions'")).toBeLessThan(columnSource.indexOf("key: 'orderNo'"))
expect(columnSource.indexOf("key: 'orderNo'")).toBeLessThan(columnSource.indexOf("key: 'inboundCode'"))
expect(columnSource).toContain("{ key: 'mainStatus', prop: 'mainStatus', label: '订单主状态'")
expect(columnSource).toContain("{ key: 'loadingStatus', prop: 'loadingStatus', label: '配载状态', minWidth: 130, slotName: 'field', defaultVisible: false }")
expect(source).toContain('slotName: \'measurementReviewStatus\'')
expect(source).toContain('unifiedOrderReviewStatusLabel(value, \'未录入\')')
expect(source).toContain("{ key: 'customerRemark', prop: 'customerRemark', label: '备注'")
expect(source).toContain("{ key: 'latestInternalNote', prop: 'latestInternalNote', label: '客服内部备注'")
expect(source).toContain("{ key: 'customerRemark', prop: 'customerRemark', label: '客户备注'")
expect(source).toContain("{ key: 'latestInternalNote', prop: 'latestInternalNote', label: '内部备注'")
expect(source.match(/<LineClampText v-if="column\.key === 'remark'"/g)).toHaveLength(3)
})
@@ -200,58 +200,27 @@ const queryFields = computed(() => showAdvancedFilters.value
const columns: DataTableColumn[] = [
{ key: 'actions', label: '操作', width: 300, fixed: 'left', slotName: 'actions' },
{ key: 'orderNo', prop: 'orderNo', label: '订单号', minWidth: 190, slotName: 'order' },
{ key: 'inboundCode', prop: 'inboundCode', label: '入仓码', minWidth: 190, slotName: 'inboundCode' },
{ key: 'internalOrderStatus', prop: 'internalOrderStatus', label: '订单状态', minWidth: 130, slotName: 'orderStatus' },
{ key: 'measurementReviewStatus', prop: 'measurementReviewStatus', label: '测量数据审核状态', minWidth: 170, slotName: 'measurementReviewStatus' },
{ key: 'expectedArrivalAt', prop: 'expectedArrivalAt', label: '预计到达时间', minWidth: 150, slotName: 'field' },
{ key: 'feeReviewStatus', prop: 'feeReviewStatus', label: '费用审核状态', minWidth: 140, slotName: 'field' },
{ 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' },
{ key: 'destinationWarehouseName', prop: 'destinationWarehouseName', label: '交货仓', minWidth: 150, slotName: 'field' },
{ key: 'loadingStatus', prop: 'loadingStatus', label: '配载状态', minWidth: 130, slotName: 'field' },
{ key: 'holdCargo', prop: 'holdCargo', label: '是否扣货', minWidth: 110, slotName: 'field' },
{ key: 'holdReason', prop: 'holdReason', label: '扣货理由', minWidth: 160, slotName: 'field' },
{ key: 'containerNo', prop: 'containerNo', label: '集装箱号', minWidth: 150, slotName: 'field' },
{ key: 'cabinetNo', prop: 'cabinetNo', label: '柜号', minWidth: 130, slotName: 'field' },
{ key: 'companyName', prop: 'companyName', label: '公司', minWidth: 150, slotName: 'field' },
{ key: 'businessType', prop: 'businessType', label: '业务类型', minWidth: 130, slotName: 'field' },
{ key: 'pickupMethod', prop: 'pickupMethod', label: '提货方式', minWidth: 130, slotName: 'field' },
{ key: 'pickupAt', prop: 'pickupAt', label: '提货时间', minWidth: 150, slotName: 'field' },
{ key: 'pickupContactPhone', prop: 'pickupContactPhone', label: '提货联系人电话', minWidth: 150, slotName: 'field' },
{ key: 'pickupAddress', prop: 'pickupAddress', label: '提货地址', minWidth: 200, slotName: 'field' },
{ key: 'pickupPostalCode', prop: 'pickupPostalCode', label: '提货邮编', minWidth: 110, slotName: 'field' },
{ key: 'pickupContactEmail', prop: 'pickupContactEmail', label: '提货联系人邮箱', minWidth: 190, slotName: 'field' },
{ key: 'channelName', prop: 'channelName', label: '渠道', minWidth: 130, slotName: 'field' },
{ key: 'taxMode', prop: 'taxMode', label: '买单/退税', minWidth: 130, slotName: 'field' },
{ key: 'fullContainer', prop: 'fullContainer', label: '是否整柜', minWidth: 110, slotName: 'field' },
{ key: 'customsInspection', prop: 'customsInspection', label: '商检验', minWidth: 110, slotName: 'field' },
{ key: 'sailingAt', prop: 'sailingAt', label: '开船时间', minWidth: 150, slotName: 'field' },
{ key: 'portArrivalAt', prop: 'portArrivalAt', label: '到港时间', minWidth: 150, slotName: 'field' },
{ key: 'receivingWarehouseName', prop: 'receivingWarehouseName', label: '平台收货仓', minWidth: 150, slotName: 'field' },
{ key: 'destinationWarehouseName', prop: 'destinationWarehouseName', label: '目的仓', minWidth: 150, slotName: 'field' },
{ key: 'totalPackageCount', prop: 'totalPackageCount', label: '总箱数', minWidth: 100, slotName: 'field' },
{ key: 'totalGrossWeightKg', prop: 'totalGrossWeightKg', label: '总毛重(T.K.GW)', minWidth: 140, slotName: 'field' },
{ 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: 'chargeableWeightKg', prop: 'chargeableWeightKg', label: '计费毛重', minWidth: 120, slotName: 'field' },
{ key: 'totalVolumeCbm', prop: 'totalVolumeCbm', label: '总体积(m³)', minWidth: 120, slotName: 'field' },
{ key: 'chargeableVolumeCbm', prop: 'chargeableVolumeCbm', label: '计费体积(m³)', minWidth: 130, slotName: 'field' },
{ key: 'recommendedPalletType', prop: 'recommendedPalletType', label: '推荐托盘类型', minWidth: 130, slotName: 'field' },
{ key: 'locationCodes', prop: 'locationCodes', label: '库位位置', minWidth: 130, slotName: 'field' },
{ key: 'volumetricWeightKg', prop: 'volumetricWeightKg', label: '总材积重', minWidth: 120, slotName: 'field' },
{ key: 'totalNetWeightKg', prop: 'totalNetWeightKg', label: '总净重(T.K.NW)', minWidth: 140, slotName: 'field' },
{ key: 'destinationType', prop: 'destinationType', label: '目的仓类型', minWidth: 130, slotName: 'field' },
{ key: 'destinationAddress', prop: 'destinationAddress', label: '地址', minWidth: 200, slotName: 'field' },
{ key: 'destinationPostalCode', prop: 'destinationPostalCode', label: '邮编', minWidth: 100, slotName: 'field' },
{ key: 'destinationContact', prop: 'destinationContact', label: '联系人', minWidth: 120, slotName: 'field' },
{ key: 'destinationContactPhone', prop: 'destinationContactPhone', label: '联系人电话', minWidth: 140, slotName: 'field' },
{ key: 'measurementCompletedAt', prop: 'measurementCompletedAt', label: '测量完成时间', minWidth: 150, slotName: 'field' },
{ key: 'inboundCompletedAt', prop: 'inboundCompletedAt', label: '入库完成时间', minWidth: 150, slotName: 'field' },
{ key: 'associatedFiles', prop: 'associatedFiles', label: '关联文件', minWidth: 120, slotName: 'field' },
{ key: 'markFiles', prop: 'markFiles', label: '唛头文件', minWidth: 120, slotName: 'field' },
{ key: 'pcOrderFiles', prop: 'pcOrderFiles', label: 'PC端下单文件', minWidth: 140, slotName: 'field' },
{ key: 'expressCompany', prop: 'expressCompany', label: '快递公司', minWidth: 130, slotName: 'field' },
{ key: 'expressTrackingNo', prop: 'expressTrackingNo', label: '快递单号', minWidth: 150, slotName: 'field' },
{ key: 'customerRemark', prop: 'customerRemark', label: '备注', minWidth: 180, slotName: 'field' },
{ key: 'latestInternalNote', prop: 'latestInternalNote', label: '客服内部备注', minWidth: 180, slotName: 'field' },
{ key: 'createdBy', prop: 'createdBy', label: '创建人', minWidth: 120, slotName: 'field' },
{ key: 'createdAt', prop: 'createdAt', label: '创建日期', minWidth: 150, slotName: 'field' },
{ key: 'expectedShipDate', prop: 'expectedShipDate', label: '预计出运日期', minWidth: 150, slotName: 'field' },
{ key: 'loadingStatus', prop: 'loadingStatus', label: '配载状态', minWidth: 130, slotName: 'field', defaultVisible: false },
{ key: 'containerNo', prop: 'containerNo', label: '集装箱号', minWidth: 150, slotName: 'field', defaultVisible: false },
{ key: 'cabinetNo', prop: 'cabinetNo', label: '柜号', minWidth: 130, slotName: 'field', defaultVisible: false },
{ key: 'sailingAt', prop: 'sailingAt', label: '开船时间', minWidth: 150, slotName: 'field', defaultVisible: false },
{ key: 'portArrivalAt', prop: 'portArrivalAt', label: '到港时间', minWidth: 150, slotName: 'field', defaultVisible: false },
{ key: 'customerRemark', prop: 'customerRemark', label: '客户备注', minWidth: 180, slotName: 'field', defaultVisible: false },
{ key: 'latestInternalNote', prop: 'latestInternalNote', label: '内部备注', minWidth: 180, slotName: 'field', defaultVisible: false },
{ key: 'createdAt', prop: 'createdAt', label: '创建时间', minWidth: 150, slotName: 'field', defaultVisible: false },
{ key: 'updatedAt', prop: 'updatedAt', label: '更新时间', minWidth: 150, slotName: 'field' },
]
function emptyFilters(): FilterModel {
@@ -357,8 +326,8 @@ function displayField(row: UnifiedOrderResource, property: string | undefined) {
measurementReviewStatus: 'measurement.reviewStatus', expectedArrivalAt: 'expectedArrivalAt',
loadingStatus: 'transport.loadingStatus', containerNo: 'transport.containerNo', cabinetNo: 'transport.cabinetNo',
sailingAt: 'transport.sailingAt', portArrivalAt: 'transport.portArrivalAt',
actualGrossWeightKg: 'measurement.actualGrossWeightKg', actualVolumeCbm: 'measurement.actualVolumeCbm',
chargeableWeightKg: 'measurement.chargeableWeightKg', measurementCompletedAt: 'measurement.completedAt',
totalGrossWeightKg: 'measurement.actualGrossWeightKg', totalVolumeCbm: 'measurement.actualVolumeCbm',
expressTrackingNo: 'delivery.trackingNo',
}
const path = nested[property] || property