9 changed files with 24 additions and 39 deletions
+2
View File
@@ -14,6 +14,8 @@
| 本土卡车列表、建单与详情 | `views/DomesticTruckingView.vue`、`components/TransportDemandForm.vue`;`cross-border/domestic-trucking`、`.../create`、`.../:orderNo/detail` | `api/businessOrders.ts`、`transportDemandForm.ts`;`lastleg-service/trucking/` |
| 美本土散板列表、建单与详情 | `views/PalletShippingView.vue`;`cross-border/pallet-shipping`、`.../create`、`.../:orderNo/detail` | `api/businessOrders.ts`;`lastleg-service/looseboard/` |
| 通用业务单展示与车次安排 | `components/BusinessOrdersView.vue`、`fleetArrangement.ts` | `api/businessOrders.ts`;`lastleg-service/fleet/trip/` |
| 采购结果确认/驳回与实收照片 | `components/ProcurementDecisionDialog.vue` | `api/procurement.ts` 的 `confirmProcurement/rejectProcurement/getProcurementWorkflow`;仅客户确认节点,不是平台审核 |
| 采购/卡车受理中、冻结失败与重试 | `components/OrderFundingStatus.vue` | `api/orderBudgets.ts` 的 `readOrderFunding/retryOrderFunding`;服务端归 `lastleg-service/procurement/funding/` |
| 柜型与规格换算 | `containerSpecifications.ts` | 本地换算,服务端复核 |
## 关键边界
@@ -279,7 +279,7 @@ async function confirmReceipt() {
if (!documents.length) return
let confirmedCount = 0
try {
await ElMessageBox.confirm(`确认已核对 ${documents.length} 份报关行回执吗?确认后平台会把确认结果发送给报关行执行。`, '确认报关行回执', { type: 'warning' })
await ElMessageBox.confirm('确认报关行回执吗?确认后平台会把确认结果发送给报关行执行。', '确认报关行回执', { type: 'warning' })
mutating.value = true
for (const document of documents) {
await confirmMerchantCustomsReceipt(document)
@@ -320,19 +320,6 @@ function formatDateTime(value: string | null | undefined) {
return Number.isNaN(date.getTime()) ? value : date.toLocaleString('zh-CN', { hour12: false })
}
function receiptHistoryLabel(document: MerchantCustomsDocument) {
return (document.receiptHistory || []).map((item) => `V${item.version}`).join('、') || '—'
}
function confirmationActionLabel(action: MerchantCustomsDocument['confirmationHistory'][number]['action']) {
return action === 'CONFIRM' ? '确认' : '打回'
}
function formatSize(bytes: number) {
if (bytes < 1024) return `${bytes} B`
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
return `${(bytes / 1024 / 1024).toFixed(1)} MB`
}
function clearanceResultLabel(value: MerchantClearanceResult) {
return ({ INSPECTION: '查验', SUPPLEMENT_REQUIRED: '补充资料', RELEASED: '放行', DETAINED: '扣留', OTHER: '其他' } as Record<string, string>)[value.resultType] || value.resultType
}
@@ -418,7 +405,7 @@ function clearanceResultStatusLabel(value: MerchantClearanceResult) {
<section class="customs-section">
<div class="customs-section-title">
<h3>资料版本历史</h3>
<h3>资料文件</h3>
<div v-if="pendingReceipts.length && detail.status === 'DECLARING'" class="customs-receipt-actions">
<el-button type="success" :disabled="mutating" @click="confirmReceipt">确认全部回执</el-button>
<el-button type="danger" plain :disabled="mutating" @click="rejectReceipt">打回全部回执</el-button>
@@ -431,10 +418,6 @@ function clearanceResultStatusLabel(value: MerchantClearanceResult) {
<FileText :size="20" />
<div>
<strong>{{ item.source.fileName }}</strong>
<span>版本 V{{ item.source.version }} · {{ formatSize(item.source.sizeBytes) }} · {{ formatDateTime(item.source.uploadedAt) }}</span>
<small v-if="item.receiptHistory?.length">报关行回执历史:{{ receiptHistoryLabel(item) }}</small>
<small v-for="history in item.confirmationHistory" :key="history.id">{{ confirmationActionLabel(history.action) }} · {{ formatDateTime(history.createdAt) }} · {{ history.operatorName || '—' }}<span v-if="history.reason"> · {{ history.reason }}</span></small>
<small>关联入仓码:{{ item.inboundCodes.join('、') || '—' }}</small>
</div>
</div>
<div class="customs-document-actions">
@@ -18,7 +18,6 @@ const emit = defineEmits<{
<section v-if="page" class="first-leg-editor-page">
<div class="editor-page-nav">
<el-button link type="primary" @click="emit('back')"><ArrowLeft :size="17" />返回订单列表</el-button>
<span>可先保存草稿,确认无误后再提交平台审核</span>
</div>
<div class="editor-page-body"><slot /></div>
<footer class="editor-page-footer"><slot name="footer" /></footer>
@@ -44,7 +43,6 @@ const emit = defineEmits<{
<style scoped>
.first-leg-editor-page { min-width: 0; padding-bottom: 72px; border: 1px solid #edf0f5; background: #fff; box-shadow: 0 5px 18px rgba(15, 31, 58, .04); }
.editor-page-nav { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 5px 16px; border-bottom: 1px solid #edf0f5; }
.editor-page-nav span { color: var(--muted); font-size: 12px; }
.editor-page-body { padding: 16px 20px 24px; }
.editor-page-footer { position: fixed; z-index: 18; right: 0; bottom: 0; left: 224px; display: flex; min-height: 68px; align-items: center; justify-content: center; padding: 10px 24px; border-top: 1px solid #dfe6ee; background: rgba(255, 255, 255, 0.97); box-shadow: 0 -6px 18px rgba(15, 31, 58, .08); backdrop-filter: blur(10px); }
.editor-page-footer :deep(.editor-actions) { width: min(100%, 900px); justify-content: center; }
@@ -70,7 +68,6 @@ const emit = defineEmits<{
@media (max-width: 720px) {
.first-leg-editor-page { padding-bottom: 0; }
.editor-page-nav { align-items: flex-start; flex-direction: column; gap: 2px; padding: 10px 16px; }
.editor-page-nav span { padding-left: 1px; line-height: 1.5; }
.editor-page-body { padding: 18px 16px 24px; }
.editor-page-footer { position: sticky; bottom: 0; left: auto; padding: 10px 16px; }
:global(.first-leg-editor-dialog.el-dialog) { width: calc(100vw - 24px) !important; height: 96vh; margin-top: 2vh; }
@@ -56,7 +56,8 @@ test('supports a standalone dialog and keeps the unpushed billing-weight state v
})
test('shows the published merchant inbound, measurement details and customer confirmation entry', () => {
for (const label of ['入仓码', '平台收货仓', '实际毛重', '实测体积', '换算与精度', '最终计费量', '单价', '基础费用', '附加费用', '费用合计', '平台备注']) {
assert.doesNotMatch(source, /平台备注|inbound\.publicRemark/)
for (const label of ['入仓码', '平台收货仓', '实际毛重', '实测体积', '换算与精度', '最终计费量', '单价', '基础费用', '附加费用', '费用合计']) {
assert.ok(source.includes(label), `missing label: ${label}`)
}
assert.match(source, /formatInboundMoney\(props\.data\.latestFee\.totalAmount/)
@@ -113,7 +113,7 @@ async function confirmFee() {
</dl>
<div class="inbound-result-section">
<div class="inbound-section-title"><h4>测量数据</h4><el-tag size="small" type="warning" effect="light">后台推送</el-tag></div>
<div class="inbound-section-title"><h4>测量数据</h4></div>
<dl class="inbound-measurements">
<div><dt>实际箱数</dt><dd>{{ formatInboundNumber(props.data.inbound.summary.totalPackageCount, 0) }}</dd></div>
<div><dt>实际毛重</dt><dd>{{ formatInboundMeasure(props.data.inbound.summary.actualGrossWeightKg, 'kg') }}</dd></div>
@@ -139,7 +139,7 @@ async function confirmFee() {
<div class="inbound-result-section fee-result">
<div class="inbound-section-title">
<h4>费用结果</h4>
<span>{{ props.data.latestFee.feeNo }}</span>
</div>
<dl class="fee-calculation-grid">
<div><dt>计费方式</dt><dd>{{ getChargeModeLabel(props.data.latestFee.chargeMode, props.data.latestFee.actualWeightKg, props.data.latestFee.actualVolumeCbm, props.data.latestFee.convertedQuantity) }} · {{ getTaxModeLabel(props.data.latestFee.taxMode) }}</dd></div>
@@ -167,10 +167,6 @@ async function confirmFee() {
</div>
</div>
<div v-if="props.data.inbound.publicRemark" class="inbound-public-remark">
<strong>平台备注</strong>
<p>{{ props.data.inbound.publicRemark }}</p>
</div>
</template>
</section>
@@ -22,7 +22,6 @@ function stageState(index: number) {
<div class="progress-heading">
<div>
<h3 id="order-progress-title">履约进度</h3>
<span>订单主节点与关联柜次统一展示</span>
</div>
</div>
@@ -63,6 +63,10 @@ function rowActions(row: object): TableAction[] {
return [{ key: 'documents', label: '查看资料', disabled: loading.value, run: () => openDetail(row) }]
}
function rowClassName({ row }: { row: CustomsDocumentRow }) {
return row.detail.merchantStatus === 'DECLARING' ? 'customs-row-declaring' : ''
}
function statusLabel(detail: MerchantCustomsContainer) {
return detail.merchantStatus ? MERCHANT_CUSTOMS_STATUS_LABELS[detail.merchantStatus] ?? '—' : '—'
}
@@ -98,7 +102,7 @@ onMounted(() => { void load() })
<div class="customs-page">
<QueryToolbar v-model="queryModel" layout="grid" ariaLabel="报关资料查询条件" :fields="queryFields" @search="search" />
<section class="customs-card">
<DataTable v-model:page="page" v-model:page-size="pageSize" :data="pagedRecords" :columns="columns" :loading="loading" :total="filteredRecords.length" :page-sizes="[10, 20, 50, 100]" :row-actions="rowActions" row-key="order.id" :empty-text="emptyText">
<DataTable v-model:page="page" v-model:page-size="pageSize" :data="pagedRecords" :columns="columns" :loading="loading" :total="filteredRecords.length" :page-sizes="[10, 20, 50, 100]" :row-actions="rowActions" :row-class-name="rowClassName" row-key="order.id" :empty-text="emptyText">
<template #toolbar><strong class="customs-list-heading">资料列表<CustomsHelp title="资料统计口径"><p>统计为本客户同柜退税货物汇总,资料覆盖按入仓码计。</p></CustomsHelp></strong></template>
<template #toolbar-actions><el-button :loading="loading" @click="load"><RefreshCcw :size="16" />刷新</el-button></template>
<template v-if="errorMessage" #error><div class="state-error" role="alert"><span>{{ errorMessage }}</span><el-button :loading="loading" @click="load">重新加载</el-button></div></template>
@@ -117,5 +121,7 @@ onMounted(() => { void load() })
.customs-page { display: grid; gap: var(--client-section-gap); }
.customs-list-heading { display: flex; align-items: center; gap: 4px; }
.customs-card { overflow: hidden; border: 1px solid var(--client-border); border-radius: var(--client-radius); background: #fff; }
.customs-card :deep(.el-table__row.customs-row-declaring > .el-table__cell) { background-color: #eff8ff; }
.customs-card :deep(.el-table__row.customs-row-declaring:hover > .el-table__cell) { background-color: #dbeafe; }
.state-error { display: flex; min-height: 84px; align-items: center; justify-content: center; gap: 12px; padding: 14px; color: #b42318; }
</style>
@@ -226,7 +226,7 @@ test('shows one unified progress line and C02 business summaries without detail
assert.match(source, /label: '后续履约', value: orderProgressLabel\(store\.orderProgress\[order\.id\]\)/)
assert.doesNotMatch(source, /待海外同步/)
assert.match(progressSource, /FIRST_LEG_PROGRESS_STAGES/)
assert.match(progressSource, /订单主节点与关联柜次统一展示/)
assert.doesNotMatch(progressSource, /订单主节点与关联柜次统一展示/)
assert.match(progressSource, /履约进度/)
assert.doesNotMatch(progressSource, /进度记录|el-timeline|timeline/)
assert.doesNotMatch(source, /<h3>报关回执<\/h3>/)
@@ -312,7 +312,7 @@ test('requires an enabled platform receiving warehouse in the order form', () =>
assert.match(editorShellSource, /first-leg-editor-dialog \.el-dialog__body\) \{[^}]*min-height: 0;[^}]*overflow-y: auto;[^}]*overscroll-behavior: contain/)
assert.match(editorShellSource, /first-leg-editor-dialog \.el-dialog__footer\) \{[^}]*border-top: 1px solid var\(--border\);[^}]*background: #fff/)
assert.match(source, /detailOrder\.receivingWarehouseSnapshot\?\.name/)
assert.match(source, /detailOrder\.receivingWarehouseSnapshot\.code/)
assert.doesNotMatch(source, /detailOrder\.receivingWarehouseSnapshot\.code/)
assert.match(source, /loadSelectedWarehouse\(detailedOrder\.receivingWarehouseId\)/)
})
@@ -549,7 +549,7 @@ test('shows fulfillment courier information as read-only client detail', () => {
assert.match(source, /履约快递信息/)
assert.match(source, /detailOrder\.expressCompanyName/)
assert.match(source, /detailOrder\.trackingNumber/)
assert.match(source, /快递公司由平台维护/)
assert.doesNotMatch(source, /快递公司由平台维护/)
assert.doesNotMatch(source, /v-model="form\.expressCompanyName"/)
})
@@ -751,7 +751,8 @@ test('supports the frozen customs document workflow and customer-safe downloads'
assert.match(customsDialogSource, /v-if="detail.status === 'COMPLETED'"[^>]*><strong>报关已完成/)
assert.match(customsDialogSource, /真实报关完税文件已齐全/)
assert.match(customsDialogSource, /item\.finalDocument/)
assert.match(customsDialogSource, /资料版本历史/)
assert.match(customsDialogSource, /资料文件/)
assert.doesNotMatch(customsDialogSource, /资料版本历史|报关行回执历史|item\.confirmationHistory|版本 V/)
assert.match(customsDialogSource, /尚未上传退税报关资料/)
assert.match(customsDialogSource, /退税报关资料加载失败/)
assert.doesNotMatch(customsDialogSource, /accept=/)
@@ -2401,8 +2401,8 @@ async function searchOrders(model: QueryModel) {
<div><dt>发货方式</dt><dd>{{ pickupMethodLabels[detailOrder.pickupMethod] }}</dd></div>
<div><dt>预约提货时间</dt><dd>{{ detailOrder.pickupMethod === 'PICKUP' && detailOrder.requestedPickupAt ? formatDateTime(detailOrder.requestedPickupAt) : '-' }}</dd></div>
<div><dt>报关方式</dt><dd>{{ taxModeLabels[detailOrder.taxMode] }}</dd></div>
<div><dt>运输渠道</dt><dd>{{ detailOrder.channelName || detailOrder.channelCode || '-' }}<span v-if="detailOrder.channelName && detailOrder.channelCode" class="detail-subline">{{ detailOrder.channelCode }}</span></dd></div>
<div><dt>国内收货仓</dt><dd>{{ detailOrder.receivingWarehouseSnapshot?.name || detailOrder.receivingWarehouseName || '—' }}<span v-if="detailOrder.receivingWarehouseSnapshot" class="detail-subline">{{ detailOrder.receivingWarehouseSnapshot.code || '—' }} · {{ detailOrder.receivingWarehouseSnapshot.province }}{{ detailOrder.receivingWarehouseSnapshot.city }}{{ detailOrder.receivingWarehouseSnapshot.address }}</span></dd></div>
<div><dt>运输渠道</dt><dd>{{ detailOrder.channelName || detailOrder.channelCode || '-' }}</dd></div>
<div><dt>国内收货仓</dt><dd>{{ detailOrder.receivingWarehouseSnapshot?.name || detailOrder.receivingWarehouseName || '—' }}</dd></div>
<div><dt>收货要求</dt><dd>{{ displayValue(detailOrder.receivingTime) }}</dd></div>
<div><dt>目的仓类型</dt><dd>{{ destinationTypeLabels[detailOrder.destinationType] }}</dd></div>
<div><dt>目的国家 / 仓库</dt><dd>{{ detailOrder.destinationCountryCode || '-' }} · {{ detailOrder.destinationWarehouseName || '-' }}</dd></div>
@@ -2417,7 +2417,7 @@ async function searchOrders(model: QueryModel) {
<h3>收发货信息</h3>
<dl class="party-detail-grid">
<div><dt>发货方</dt><dd><strong>{{ detailOrder.shipperPartySnapshot?.name || detailOrder.shipper || '未提供' }}</strong><span v-if="detailOrder.shipperPartySnapshot">{{ detailOrder.shipperPartySnapshot.contactName || '未填写联系人' }} · {{ detailOrder.shipperPartySnapshot.phone || '未填写电话' }} · {{ detailOrder.shipperPartySnapshot.email || '未填写邮箱' }}<br />{{ detailOrder.shipperPartySnapshot.countryRegion }} {{ detailOrder.shipperPartySnapshot.province }} {{ detailOrder.shipperPartySnapshot.city }} {{ detailOrder.shipperPartySnapshot.address }} {{ detailOrder.shipperPartySnapshot.postalCode }}</span></dd></div>
<div><dt>收货方</dt><dd><strong>{{ detailOrder.consigneePartySnapshot?.name || detailOrder.consignee || detailOrder.destinationWarehouseName || '未提供' }}</strong><span v-if="detailOrder.consigneePartySnapshot">{{ detailOrder.consigneePartySnapshot.contactName || '未填写联系人' }} · {{ detailOrder.consigneePartySnapshot.phone || '未填写电话' }} · {{ detailOrder.consigneePartySnapshot.email || '未填写邮箱' }}<br />{{ detailOrder.consigneePartySnapshot.countryRegion }} {{ detailOrder.consigneePartySnapshot.province }} {{ detailOrder.consigneePartySnapshot.city }} {{ detailOrder.consigneePartySnapshot.address }} {{ detailOrder.consigneePartySnapshot.postalCode }}</span><span v-else>平台目的仓地址以最终履约安排为准</span></dd></div>
<div><dt>收货方</dt><dd><strong>{{ detailOrder.consigneePartySnapshot?.name || detailOrder.consignee || detailOrder.destinationWarehouseName || '未提供' }}</strong><span v-if="detailOrder.consigneePartySnapshot">{{ detailOrder.consigneePartySnapshot.contactName || '未填写联系人' }} · {{ detailOrder.consigneePartySnapshot.phone || '未填写电话' }} · {{ detailOrder.consigneePartySnapshot.email || '未填写邮箱' }}<br />{{ detailOrder.consigneePartySnapshot.countryRegion }} {{ detailOrder.consigneePartySnapshot.province }} {{ detailOrder.consigneePartySnapshot.city }} {{ detailOrder.consigneePartySnapshot.address }} {{ detailOrder.consigneePartySnapshot.postalCode }}</span></dd></div>
</dl>
<div v-if="detailOrder.customerRemark" class="customer-remark"><strong>客户备注</strong><p>{{ detailOrder.customerRemark }}</p></div>
</section>
@@ -2445,7 +2445,7 @@ async function searchOrders(model: QueryModel) {
<section v-if="detailOrder.status !== 'CANCELLED' || detailOrder.expressCompanyName || detailOrder.trackingNumber" class="detail-section">
<h3>履约快递信息</h3>
<dl v-if="detailOrder.expressCompanyName || detailOrder.trackingNumber" class="express-information"><div><dt>快递公司</dt><dd>{{ detailOrder.expressCompanyName || (detailOrder.status === 'CANCELLED' ? '—' : '待平台补充') }}</dd></div><div><dt>快递单号</dt><dd class="table-code">{{ detailOrder.trackingNumber || (detailOrder.status === 'CANCELLED' ? '—' : '待平台补充') }}</dd></div></dl>
<div v-else class="express-empty"><PackageCheck :size="20" /><div><strong>暂无快递信息</strong><span>快递自提可在下单或编辑草稿时选填快递单号,快递公司由平台维护。</span></div></div>
<div v-else class="express-empty"><PackageCheck :size="20" /><div><strong>暂无快递信息</strong></div></div>
</section>
<section class="detail-section">
<h3>货物明细</h3>