This commit is contained in:
2026-09-18 16:48:35 +08:00
parent e3cca80ff2
commit 1d407620d2
59 changed files with 1879 additions and 1545 deletions
@@ -5,6 +5,7 @@ import test from 'node:test'
const source = readFileSync(new URL('./FirstLegOrdersView.vue', import.meta.url), 'utf8')
const tableSource = readFileSync(new URL('../../../components/common/DataTable.vue', import.meta.url), 'utf8')
const querySource = readFileSync(new URL('../../../components/common/QueryToolbar.vue', import.meta.url), 'utf8')
const clientUiSource = readFileSync(new URL('../../../styles/client-ui.css', import.meta.url), 'utf8')
const routerSource = readFileSync(new URL('../routes.ts', import.meta.url), 'utf8')
const editorShellSource = readFileSync(new URL('../components/FirstLegOrderEditorShell.vue', import.meta.url), 'utf8')
const progressSource = readFileSync(new URL('../components/OrderProgressPanel.vue', import.meta.url), 'utf8')
@@ -20,7 +21,7 @@ test('opens order creation as a dedicated protected page while keeping draft edi
assert.match(routerSource, /path: 'orders\/first-leg\/create'[\s\S]*?name: 'first-leg-order-create'[\s\S]*?release: 'active'[\s\S]*?permission: 'order:first-leg:create'/)
assert.match(routerSource, /name: 'first-leg-order-create'[\s\S]*?props: \{ createPage: true \}/)
assert.match(source, /router\.push\(\{ name: 'first-leg-order-create' \}\)/)
assert.match(source, /defineProps<\{ createPage\?: boolean \}>/)
assert.match(source, /defineProps<\{ createPage\?: boolean; detailPage\?: boolean; showCreateAction\?: boolean \}>/)
assert.match(source, /const isCreatePage = computed\(\(\) => props\.createPage\)/)
assert.match(source, /<FirstLegOrderEditorShell[^>]*:page="isCreatePage"/)
assert.match(editorShellSource, /<section v-if="page" class="first-leg-editor-page">/)
@@ -38,10 +39,10 @@ test('exposes real list, draft, submit, and detail workflow entries without canc
assert.match(source, /function submitOrder\(\)/)
assert.match(source, /async function openDetail\(order: FirstLegOrder\)/)
assert.match(source, /Promise\.allSettled\(\[/)
assert.match(source, /const orderPageRequest = isCreatePage\.value\s*\? store\.loadWarehouseType\('DOMESTIC', true\)\s*:\s*store\.load\(\{ includeDrafts: true \}\)/)
assert.match(source, /const orderPageRequest = isCreatePage\.value \|\| isDetailPage\.value\s*\? store\.loadWarehouseType\('DOMESTIC', true\)\s*:\s*store\.load\(\{ includeDrafts: true \}\)/)
assert.match(source, /Promise\.allSettled\(\[\s*orderPageRequest,\s*companyRequest/)
assert.match(source, /tenantStore\.loadCompanyContext\(\)/)
assert.match(source, /await store\.loadDetail\(order\.id\)/)
assert.match(source, /await store\.loadDetail\(orderId\)/)
assert.doesNotMatch(source, /cancelOrder|取消订单/)
})
@@ -71,8 +72,8 @@ test('renders dynamic cargo lines and a responsive editor dialog', () => {
assert.match(source, /function removeCargoItem\(index: number\)/)
assert.match(editorShellSource, /first-leg-editor-dialog\.el-dialog\) \{[^}]*height: 94vh;/)
assert.match(editorShellSource, /width: 100vw !important; height: 100vh;/)
assert.match(source, /class="mobile-order-list"/)
assert.match(source, /\.prototype-order-table :deep\(> \.el-table\), \.prototype-order-table :deep\(> \.data-table__pagination\) \{ display: none; \}/)
assert.doesNotMatch(source, /class="mobile-order-(?:list|card|surface)"/)
assert.doesNotMatch(source, /\.prototype-order-table :deep\(> \.el-table\).*display: none/)
})
test('uses the full editor dialog width for cargo details and cost estimates', () => {
@@ -143,7 +144,7 @@ test('downloads selected carton labels with one array request', () => {
test('exports every visible order field with normalized dates', () => {
assert.match(source, /function formatExportDateTime\(value: string \| null \| undefined\)/)
assert.match(source, /return `\$\{date\.getFullYear\(\)\}-\$\{pad\(date\.getMonth\(\) \+ 1\)\}-\$\{pad\(date\.getDate\(\)\)\} \$\{pad\(date\.getHours\(\)\)\}:\$\{pad\(date\.getMinutes\(\)\)\}:\$\{pad\(date\.getSeconds\(\)\)\}`/)
for (const label of ['订单号', '入仓码', '渠道', '运输方式', '交货方式', '预约提货时间', '目的仓', '目的仓类型', '当前订单状态', '箱数', '快递公司', '快递单号', 'SKUS', '总毛重', '总净重', '总体积', '总数量', '计费单价', '计费体积', '计费重量', '备注', '货件编号', '跟踪编号', '国内仓入库时间', '创建时间']) {
for (const label of ['入仓码', '渠道', '运输方式', '交货方式', '预约提货时间', '目的仓', '目的仓类型', '当前订单状态', '箱数', '快递公司', '快递单号', 'SKUS', '总毛重', '总净重', '总体积', '总数量', '计费单价', '计费体积', '计费重量', '备注', '货件编号', '跟踪编号', '国内仓入库时间', '创建时间']) {
assert.match(source, new RegExp(`'${label}'`))
}
assert.match(source, /formatExportDateTime\(order\.inboundedAt\)/)
@@ -155,6 +156,9 @@ test('exports every visible order field with normalized dates', () => {
test('matches the prototype with selectable dense order-list columns', () => {
assert.match(source, /aria-label="设置订单列表显示列"/)
assert.match(source, /显示字段筛选/)
assert.match(source, /const defaultVisibleColumnKeys = \['status', 'channel', 'transportMode', 'pickupMethod', 'receivingWarehouse', 'createdAt'\]/)
assert.match(source, /key: 'inboundCode', label: '入仓码', width: 230, fixed: 'left'/)
assert.match(source, /<SlidersHorizontal :size="18"/)
assert.match(source, /<el-checkbox-group v-model="visibleColumnKeys"/)
assert.match(source, /draggable="true"/)
@@ -183,36 +187,39 @@ test('matches the prototype with selectable dense order-list columns', () => {
})
test('keeps dense order identifiers on one line and aligns columns by content type', () => {
assert.match(source, /key: 'order'[\s\S]{0,160}showOverflowTooltip: true/)
assert.match(source, /key: 'inboundCode'[\s\S]{0,180}showOverflowTooltip: true/)
assert.match(source, /key: 'status'[\s\S]{0,100}align: 'center'[\s\S]{0,100}headerAlign: 'center'/)
assert.match(source, /key: 'grossWeight'[\s\S]{0,100}align: 'right'[\s\S]{0,100}headerAlign: 'right'/)
assert.match(source, /class="order-number-link table-code"/)
assert.doesNotMatch(source, /<template #order="\{ row \}">[\s\S]{0,400}class="order-reference"/)
assert.match(source, /class="inbound-code-primary table-ellipsis table-code"/)
assert.doesNotMatch(source, /<template #order="\{ row \}">/)
assert.match(source, /\.table-ellipsis, \.table-multivalue \{[^}]*text-overflow: ellipsis;[^}]*white-space: nowrap;/)
assert.match(source, /\.order-number-link \{[^}]*text-overflow: ellipsis;[^}]*white-space: nowrap;/)
assert.match(source, /\.inbound-code-primary \{[^}]*text-overflow: ellipsis;[^}]*white-space: nowrap;/)
assert.match(tableSource, /height: var\(--client-table-row-height\)/)
})
test('shows one unified progress line and C02 business summaries without detail history', () => {
assert.match(source, /import OrderProgressPanel/)
assert.match(source, /<OrderProgressPanel :order="detailOrder" \/>/)
assert.match(source, /<OrderProgressPanel :order="detailOrder">[\s\S]*?<template #cabinet>/)
assert.match(source, /<h3>关键业务摘要<\/h3>/)
assert.match(source, /progressSummaryItems/)
assert.match(source, /summary\.preloadStatus/)
assert.match(source, /summary\.expectedPickCount/)
assert.match(source, /summary\.deliveryTrackingNo/)
assert.match(progressSource, /FIRST_LEG_PROGRESS_STAGES/)
assert.match(progressSource, /国内入仓、组托、装柜及后续履约同步/)
assert.match(progressSource, /订单主节点与关联柜次统一展示/)
assert.match(progressSource, /履约进度/)
assert.doesNotMatch(progressSource, /进度记录|el-timeline|timeline/)
assert.doesNotMatch(source, /<h3>报关回执<\/h3>/)
assert.match(source, /船期摘要/)
assert.match(source, /柜次进度/)
assert.match(source, /柜次明细/)
assert.match(source, /尚未分配柜次,后续柜次节点将在这里同步。/)
assert.doesNotMatch(source, /暂无柜次分配信息/)
assert.doesNotMatch(source, /summary\.sealNo \|\|/)
assert.doesNotMatch(progressSource, /客户确认|差异处理中|派送中/)
})
test('uses only fields present on C02 rows before loading full detail', () => {
assert.match(source, /getOrderCargoSummary/)
assert.match(source, /orderPackageCount/)
assert.match(source, /label: '渠道'/)
assert.match(source, /label: '目的仓'/)
assert.match(source, /displayListValue/)
@@ -227,10 +234,11 @@ test('uses only fields present on C02 rows before loading full detail', () => {
test('shows and searches the inbound code after a draft is submitted', () => {
assert.match(source, /label: '入仓码 \/ SKU'/)
assert.match(source, /order\.businessSummary\.inboundCode/)
assert.match(source, /<template #inboundCode="\{ row \}">[\s\S]*?class="inbound-code-link table-ellipsis table-code"/)
assert.match(source, /v-if="order\.businessSummary\.inboundCode" class="order-reference inbound-code-link"/)
assert.match(source, /<template #inboundCode="\{ row \}">[\s\S]*?class="inbound-code-primary table-ellipsis table-code"/)
assert.match(source, /@click="openDetail\(row as FirstLegOrder\)"/)
assert.match(source, /<dt>入仓码<\/dt><dd><button v-if="detailOrder\.businessSummary\.inboundCode"/)
assert.match(source, /<span v-else>—<\/span>/)
assert.match(source, /提交后生成/)
assert.doesNotMatch(source, /label: '订单号'/)
})
test('keeps all progress stages and business summary usable on mobile', () => {
@@ -242,7 +250,7 @@ test('keeps all progress stages and business summary usable on mobile', () => {
test('keeps the first viewport compact and moves create into the table toolbar', () => {
const tableToolbarSource = source.slice(source.indexOf('#toolbar-actions'), source.indexOf('class="order-eligibility is-pending"'))
assert.match(source, /#toolbar-actions/)
assert.match(source, /<strong>订单列表<\/strong><span>共 \{\{ filteredOrders\.length \}\} 条<\/span>/)
assert.match(source, /class="order-queue-tabs"[\s\S]*确认计费重[\s\S]*上传资料[\s\S]*共 \{\{ filteredOrders\.length \}\} 条/)
assert.match(source, /#toolbar-actions[\s\S]*order:first-leg:create[\s\S]*创建订单[\s\S]*class="order-eligibility is-pending"/)
assert.match(tableToolbarSource, /下载唛头[\s\S]*导出订单信息[\s\S]*批量提交草稿[\s\S]*创建订单/)
assert.doesNotMatch(source.slice(source.indexOf('<QueryToolbar'), source.indexOf('<div class="client-card order-table-card">')), /下载唛头|导出订单信息|创建订单/)
@@ -321,7 +329,7 @@ test('lets the create-page instance own destination loading and exposes a recove
})
test('shows a loading state before the list can create an order', () => {
assert.match(source, /<el-button v-if="auth\.hasPermission\('order:first-leg:create'\)" type="primary" :loading="pageDataLoading" :disabled="pageDataLoading \|\| availableWarehouses\.length === 0" @click="openCreate"><FilePlus2/)
assert.match(source, /<el-button v-if="props\.showCreateAction && auth\.hasPermission\('order:first-leg:create'\)" type="primary" :loading="pageDataLoading" :disabled="pageDataLoading \|\| availableWarehouses\.length === 0" @click="openCreate"><FilePlus2/)
assert.match(source, /pageDataLoading \? '加载收货仓中' : \(availableWarehouses\.length \? '创建第一笔订单'/)
})
@@ -486,7 +494,7 @@ test('shows fulfillment courier information as read-only client detail', () => {
})
test('shows the full customer-safe order detail without platform-only controls', () => {
const detailSource = source.slice(source.indexOf('<el-dialog v-model="detailOpen"'), source.indexOf('</el-dialog>', source.indexOf('<el-dialog v-model="detailOpen"')))
const detailSource = source.slice(source.indexOf('<section v-if="isDetailPage"'), source.indexOf('</section>', source.indexOf('<section v-if="isDetailPage"')))
for (const heading of ['订单与运输', '收发货信息', '关键业务摘要', '履约快递信息', '货物明细']) {
assert.match(source, new RegExp(`<h3>${heading}<\\/h3>`))
}
@@ -503,9 +511,15 @@ test('shows the full customer-safe order detail without platform-only controls',
assert.doesNotMatch(detailSource, /内部备注|审核通过|添加备注/)
})
test('opens order detail in a large dialog and paginates cargoes as a read-only table', () => {
assert.match(source, /<el-dialog v-model="detailOpen" class="first-leg-detail-dialog" title="订单详情" width="94vw" top="3vh"/)
assert.doesNotMatch(source, /<el-drawer v-model="detailOpen"/)
test('opens order detail as a protected workspace page and paginates cargoes as a read-only table', () => {
assert.match(routerSource, /path: 'orders\/first-leg\/:orderId\/detail'[\s\S]*?name: 'first-leg-order-detail'[\s\S]*?props: \{ detailPage: true \}[\s\S]*?permission: 'order:first-leg:list'/)
assert.match(source, /router\.push\(\{ name: 'first-leg-order-detail', params: \{ orderId: order\.id \} \}\)/)
assert.match(source, /<section v-if="isDetailPage" class="first-leg-detail-page" aria-label="订单详情">/)
assert.doesNotMatch(source, /<el-dialog v-model="detailOpen"/)
assert.match(source, /first-leg-page\.is-detail-route\)[^\n]*client-page-heading\) \{ display: none; \}/)
assert.doesNotMatch(source, /class="detail-page-actions"/)
assert.match(source, /\.order-detail-grid \{[^}]*grid-template-columns: repeat\(4, minmax\(0, 1fr\)\)/)
assert.match(source, /\.order-detail-grid div \{[^}]*grid-template-columns: 78px minmax\(0, 1fr\)/)
assert.match(source, /const detailCargoPage = ref\(1\)/)
assert.match(source, /const detailCargoPageSize = ref\(5\)/)
assert.match(source, /const pagedDetailCargoItems = computed/)
@@ -515,13 +529,24 @@ test('opens order detail in a large dialog and paginates cargoes as a read-only
assert.match(source, /v-model:page-size="detailCargoPageSize"/)
assert.match(source, /:total="detailOrder\.items\.length"/)
assert.doesNotMatch(source, /class="cargo-detail-list"/)
assert.match(source, /first-leg-detail-dialog\.el-dialog\) \{[^}]*height: 94vh;/)
assert.match(source, /\.first-leg-detail-page \{[^}]*padding: 10px 14px 20px;[^}]*border: 1px solid var\(--client-border\);[^}]*border-radius: var\(--client-radius\);[^}]*background: #fff;/)
assert.match(source, /\.order-detail-grid div \{[^}]*min-height: 34px;[^}]*padding: 3px 8px;/)
})
test('keeps shared button hierarchy compact and consistent', () => {
assert.match(querySource, /class="query-toolbar__search" type="primary"/)
assert.match(querySource, /class="query-toolbar__reset"/)
assert.match(tableSource, /v-for="action in splitTableActions\(rowActions\(row\)\)\.visible"[\s\S]*?link type="primary"/)
assert.match(clientUiSource, /--client-control-height: 32px/)
assert.match(clientUiSource, /\.client-ui \.el-button \{[\s\S]*?min-width: 68px;[\s\S]*?font-weight: 600;/)
assert.match(clientUiSource, /\.client-ui \.el-button\.is-link, \.client-ui \.el-button\.is-text \{[\s\S]*?min-width: 0;[\s\S]*?background: transparent;/)
assert.match(clientUiSource, /\.client-ui \.el-button svg \{[\s\S]*?width: 16px; height: 16px;/)
})
test('loads merchant inbound billing as an isolated read-only detail section', () => {
assert.match(source, /import InboundBillingPanel/)
assert.match(source, /import \{ getMerchantInboundBilling \}/)
assert.match(source, /void loadInboundBilling\(order\)/)
assert.match(source, /void loadInboundBilling\(detailedOrder\)/)
assert.match(source, /<InboundBillingPanel/)
assert.match(source, /@retry="retryInboundBilling"/)
assert.match(source, /error instanceof ApiError && error\.code === 'A0404'/)
@@ -547,9 +572,6 @@ test('opens billing-weight confirmation independently from order details', () =>
})
test('shows billing-weight confirmation only when the backend marks it pending', () => {
const templateSource = source.slice(source.indexOf('<template>'), source.lastIndexOf('</template>'))
const entries = templateSource.match(/<el-button v-if="canConfirmInboundFee && [^"]*customerFeeConfirmationRequired" link type="primary" @click="openFeeConfirmation\([^)]*\)">确认计费重<\/el-button>/g) ?? []
assert.equal(entries.length, 1)
assert.match(source, /if \(canConfirmInboundFee\.value && order\.businessSummary\.customerFeeConfirmationRequired\) \{\s*actions\.push\(\{ key: 'fee'/)
assert.match(source, /:row-actions="getOrderRowActions"/)
assert.match(orderStoreSource, /customerFeeConfirmationRequired: summary\?\.customerFeeConfirmationRequired \?\? false/)
@@ -565,8 +587,8 @@ test('submits only selected draft orders in one batch action', () => {
test('discards stale inbound billing responses when billing views close or the user changes orders', () => {
assert.match(source, /let inboundBillingRequestSequence = 0/)
assert.match(source, /const sequence = \+\+inboundBillingRequestSequence/)
assert.match(source, /sequence === inboundBillingRequestSequence[\s\S]*inboundBillingOrderId\.value === orderId[\s\S]*detailOpen\.value \|\| feeConfirmationOpen\.value/)
assert.match(source, /watch\(detailOpen,[\s\S]*resetInboundBillingState\(\)/)
assert.match(source, /sequence === inboundBillingRequestSequence[\s\S]*inboundBillingOrderId\.value === orderId[\s\S]*isDetailPage\.value \|\| feeConfirmationOpen\.value/)
assert.doesNotMatch(source, /watch\(detailOpen/)
assert.match(source, /watch\(feeConfirmationOpen,[\s\S]*resetInboundBillingState\(\)/)
})
@@ -595,11 +617,10 @@ test('copies reactive order drafts into plain request data', () => {
assert.doesNotMatch(source, /structuredClone\((?:form|order\.)/)
})
test('opens the server-owned inbound-code association from desktop, mobile, and order detail', () => {
test('opens the server-owned inbound-code association from the order detail', () => {
assert.match(source, /import InboundCodeAssociationDialog/)
assert.match(source, /function openInboundCodeAssociation\(inboundCode: string\)/)
assert.match(source, /#inboundCode[\s\S]*?openInboundCodeAssociation\(\(row as FirstLegOrder\)\.businessSummary\.inboundCode\)/)
assert.match(source, /class="order-reference inbound-code-link"[\s\S]*?openInboundCodeAssociation\(order\.businessSummary\.inboundCode\)/)
assert.doesNotMatch(source, /#inboundCode[\s\S]*?openInboundCodeAssociation\(\(row as FirstLegOrder\)\.businessSummary\.inboundCode\)/)
assert.match(source, /<dt>入仓码<\/dt><dd><button[\s\S]*?openInboundCodeAssociation\(detailOrder\.businessSummary\.inboundCode\)/)
assert.match(source, /<InboundCodeAssociationDialog[\s\S]*?:inbound-code="inboundAssociationCode"/)
assert.doesNotMatch(source, /openInboundCodeAssociation\([^)]*\.id\)/)
@@ -610,7 +631,7 @@ test('opens customer customs documents only for server-confirmed loaded tax-refu
assert.match(source, /item\.orderId === order\.id && item\.taxMode === 'TAX_REFUND'/)
assert.match(source, /getMerchantCustomsForOrder\(order\.id\)/)
assert.match(source, /if \(eligibleCustomsDetail\(order\)\) \{\s*actions\.push\(\{ key: 'customs', label: '报关资料'/)
assert.match(source, /class="mobile-order-card"[\s\S]*?v-if="eligibleCustomsDetail\(order\)"[\s\S]*?openCustomsDocuments\(order\)[\s\S]*?报关资料/)
assert.match(source, /if \(eligibleCustomsDetail\(order\)\) \{\s*actions\.push\(\{ key: 'customs', label: '报关资料'/)
assert.match(source, /<CustomsDocumentsDialog[\s\S]*?:can-edit="auth\.hasPermission\('order:first-leg:edit'\)"/)
assert.doesNotMatch(customsApiSource, /customs\/cases\//)
})
@@ -655,7 +676,7 @@ test('shows authoritative warehouse, loading and sailing fields on desktop and n
test('loads complete order detail before editing and rejects an unavailable historical warehouse', () => {
assert.match(source, /async function openEdit\(order: FirstLegOrder\)/)
assert.match(source, /const detailedOrder = await store\.loadDetail\(order\.id\)/)
assert.match(source, /const \[detailedOrder\] = await Promise\.all\(\[store\.loadDetail\(order\.id\)/)
assert.match(source, /draftFromOrder\(detailedOrder\)/)
assert.match(source, /draft\.receivingWarehouseId = ''/)
assert.match(source, /draft\.channelCode = ''/)