优化
This commit is contained in:
@@ -6,6 +6,8 @@ const source = readFileSync(new URL('./FirstLegOrdersView.vue', import.meta.url)
|
||||
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')
|
||||
const customsDialogSource = readFileSync(new URL('../components/CustomsDocumentsDialog.vue', import.meta.url), 'utf8')
|
||||
const customsApiSource = readFileSync(new URL('../api/customs.ts', import.meta.url), 'utf8')
|
||||
|
||||
test('routes the confirmed first-leg path to the implemented order view', () => {
|
||||
assert.match(routerSource, /path: 'orders\/first-leg'[\s\S]*?component: \(\) => import\('\.\/views\/FirstLegOrdersView\.vue'\)[\s\S]*?release: 'active'/)
|
||||
@@ -198,7 +200,7 @@ test('shows one unified progress line and C02 business summaries without detail
|
||||
assert.match(progressSource, /FIRST_LEG_PROGRESS_STAGES/)
|
||||
assert.match(progressSource, /国内装柜进度及后续履约同步/)
|
||||
assert.doesNotMatch(progressSource, /进度记录|el-timeline|timeline/)
|
||||
assert.match(source, /报关回执/)
|
||||
assert.doesNotMatch(source, /<h3>报关回执<\/h3>/)
|
||||
assert.match(source, /船期摘要/)
|
||||
assert.match(source, /柜次进度/)
|
||||
assert.doesNotMatch(source, /summary\.sealNo \|\|/)
|
||||
@@ -492,6 +494,34 @@ test('copies reactive order drafts into plain request data', () => {
|
||||
assert.doesNotMatch(source, /structuredClone\((?:form|order\.)/)
|
||||
})
|
||||
|
||||
test('opens customer customs documents only for server-confirmed loaded tax-refund orders', () => {
|
||||
assert.match(source, /order\.status !== 'LOADED'/)
|
||||
assert.match(source, /item\.orderId === order\.id && item\.taxMode === 'TAX_REFUND'/)
|
||||
assert.match(source, /getMerchantCustomsForOrder\(order\.id\)/)
|
||||
assert.match(source, /v-if="eligibleCustomsDetail\(row as FirstLegOrder\)"[\s\S]*?报关资料/)
|
||||
assert.match(source, /<CustomsDocumentsDialog[\s\S]*?:can-edit="auth\.hasPermission\('order:first-leg:edit'\)"/)
|
||||
assert.doesNotMatch(customsApiSource, /customs\/cases\//)
|
||||
})
|
||||
|
||||
test('supports the frozen customs document workflow and customer-safe downloads', () => {
|
||||
for (const path of [
|
||||
'/merchant/v1/customs/orders/',
|
||||
'/merchant/v1/customs/loading-plans/',
|
||||
'/merchant/v1/customs/documents/',
|
||||
'/inbounds',
|
||||
'/completion/download',
|
||||
]) assert.match(customsApiSource, new RegExp(path.replaceAll('/', '\\/')))
|
||||
assert.match(customsApiSource, /MAX_CUSTOMS_FILE_SIZE = 50 \* 1024 \* 1024/)
|
||||
assert.match(customsApiSource, /inboundIds: inboundIds\.map\(String\)/)
|
||||
assert.match(customsApiSource, /requestId: error\.requestId/)
|
||||
assert.match(customsDialogSource, /报关开始后仅可查看和下载,不提供退回或解锁/)
|
||||
assert.match(customsDialogSource, /detail\.status === 'COMPLETED' && item\.completion/)
|
||||
assert.match(customsDialogSource, /资料版本历史/)
|
||||
assert.match(customsDialogSource, /尚未上传退税报关资料/)
|
||||
assert.match(customsDialogSource, /退税报关资料加载失败/)
|
||||
assert.doesNotMatch(customsDialogSource, /accept=/)
|
||||
})
|
||||
|
||||
test('uses the global inbound-code policy to require old-system codes during parallel running', () => {
|
||||
assert.match(source, /getMerchantInboundCodePolicy\(\)/)
|
||||
assert.match(source, /async function validateEditorForm\(\)[\s\S]*?const policy = await getMerchantInboundCodePolicy\(\)/)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox, type FormInstance, type FormItemRule, type FormRules, type UploadRequestOptions } from 'element-plus'
|
||||
import { AlertTriangle, Box, ChevronDown, ChevronUp, Download, Eye, FilePlus2, GripVertical, ImagePlus, MapPin, PackageCheck, Pencil, Plus, RotateCcw, Send, SlidersHorizontal, Trash2, Upload } from '@lucide/vue'
|
||||
import { AlertTriangle, Box, ChevronDown, ChevronUp, Download, Eye, FilePlus2, FileText, GripVertical, ImagePlus, MapPin, PackageCheck, Pencil, Plus, RotateCcw, Send, SlidersHorizontal, Trash2, Upload } from '@lucide/vue'
|
||||
import { FIRST_LEG_ORDER_STATUSES, type FirstLegOrderStatus } from '@chongchong/shared'
|
||||
import QueryToolbar, { type QueryField, type QueryModel } from '@/components/common/QueryToolbar.vue'
|
||||
import DataTable, { type DataTableColumn } from '@/components/common/DataTable.vue'
|
||||
@@ -10,8 +10,9 @@ import { ApiError } from '@/api/http'
|
||||
import FirstLegOrderEditorShell from '@/modules/first-leg/components/FirstLegOrderEditorShell.vue'
|
||||
import InboundBillingPanel from '@/modules/first-leg/components/InboundBillingPanel.vue'
|
||||
import OrderProgressPanel from '@/modules/first-leg/components/OrderProgressPanel.vue'
|
||||
import CustomsDocumentsDialog from '@/modules/first-leg/components/CustomsDocumentsDialog.vue'
|
||||
import { getMerchantInboundBilling } from '@/modules/first-leg/api/inboundBilling'
|
||||
import { getMerchantCustomsByOrder, isMerchantCustomsUnavailable, type MerchantCustomsCaseDetail } from '@/modules/first-leg/api/customs'
|
||||
import { getMerchantCustomsForOrder, isMerchantCustomsUnavailable, type MerchantCustomsContainer } from '@/modules/first-leg/api/customs'
|
||||
import {
|
||||
estimateMerchantFirstLegPrice,
|
||||
downloadMerchantOrderCartonLabelsBatch,
|
||||
@@ -95,7 +96,7 @@ const cargoTemplateColumns = [
|
||||
const cargoNumberKeys = new Set<string>(['lengthCm', 'widthCm', 'heightCm', 'unitsPerPackage', 'packageCount', 'grossWeightKg', 'volumeCbm', 'pieces', 'netWeightKg', 'unitDeclaredValue', 'declaredValue', 'productLengthCm', 'productWidthCm', 'productHeightCm'])
|
||||
const fixedOrderColumns: DataTableColumn[] = [
|
||||
{ key: 'selection', type: 'selection', width: 48, fixed: 'left', reserveSelection: true },
|
||||
{ key: 'actions', label: '操作', width: 128, fixed: 'left', align: 'center', headerAlign: 'center', slotName: 'actions' },
|
||||
{ key: 'actions', label: '操作', width: 216, fixed: 'left', align: 'center', headerAlign: 'center', slotName: 'actions' },
|
||||
{ key: 'order', label: '订单号', width: 190, fixed: 'left', align: 'left', headerAlign: 'left', showOverflowTooltip: true, slotName: 'order' },
|
||||
]
|
||||
const selectableOrderColumns: DataTableColumn[] = [
|
||||
@@ -188,9 +189,10 @@ const inboundBillingLoading = ref(false)
|
||||
const inboundBillingUnavailable = ref(false)
|
||||
const inboundBillingUnavailableMessage = ref('入库及费用结果尚未发布')
|
||||
const inboundBillingError = ref('')
|
||||
const customsDetail = ref<MerchantCustomsCaseDetail | null>(null)
|
||||
const customsLoading = ref(false)
|
||||
const customsUnavailable = ref(false)
|
||||
const customsByOrder = reactive<Record<string, MerchantCustomsContainer | null | undefined>>({})
|
||||
const customsEligibilityPending = new Set<string>()
|
||||
const customsDialogOpen = ref(false)
|
||||
const customsDialogOrderId = ref('')
|
||||
const supportingDataLoading = ref(false)
|
||||
const supportingDataErrors = ref<string[]>([])
|
||||
const destinationWarehousesLoading = ref(false)
|
||||
@@ -461,6 +463,41 @@ const pagedOrders = computed(() => {
|
||||
return filteredOrders.value.slice(start, start + tablePageSize.value)
|
||||
})
|
||||
|
||||
function eligibleCustomsDetail(order: FirstLegOrder) {
|
||||
const value = customsByOrder[order.id]
|
||||
if (order.status !== 'LOADED' || !value?.merchantStatus) return null
|
||||
return value.inbounds.some((item) => item.orderId === order.id && item.taxMode === 'TAX_REFUND') ? value : null
|
||||
}
|
||||
|
||||
async function loadCustomsEligibility(order: FirstLegOrder) {
|
||||
if (order.status !== 'LOADED' || customsByOrder[order.id] !== undefined || customsEligibilityPending.has(order.id)) return
|
||||
customsEligibilityPending.add(order.id)
|
||||
try {
|
||||
customsByOrder[order.id] = await getMerchantCustomsForOrder(order.id)
|
||||
} catch (error) {
|
||||
customsByOrder[order.id] = null
|
||||
if (!isMerchantCustomsUnavailable(error)) {
|
||||
// Keep the operation hidden when eligibility cannot be proven by the customer-scoped server response.
|
||||
}
|
||||
} finally {
|
||||
customsEligibilityPending.delete(order.id)
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => pagedOrders.value.map((order) => `${order.id}:${order.status}`).join('|'), () => {
|
||||
pagedOrders.value.forEach((order) => void loadCustomsEligibility(order))
|
||||
}, { immediate: true })
|
||||
|
||||
function openCustomsDocuments(order: FirstLegOrder) {
|
||||
if (!eligibleCustomsDetail(order)) return
|
||||
customsDialogOrderId.value = order.id
|
||||
customsDialogOpen.value = true
|
||||
}
|
||||
|
||||
function updateCustomsDetail(value: MerchantCustomsContainer) {
|
||||
if (customsDialogOrderId.value) customsByOrder[customsDialogOrderId.value] = value
|
||||
}
|
||||
|
||||
const detailOrder = computed(() => detailId.value ? store.getById(detailId.value) : undefined)
|
||||
const detailSummary = computed(() => detailOrder.value ? summarizeFirstLegCargo(detailOrder.value.items) : null)
|
||||
const pagedDetailCargoItems = computed(() => {
|
||||
@@ -486,7 +523,6 @@ const progressSummaryItems = computed(() => {
|
||||
{ label: '预装清单', value: preloadStatus ? `${preloadStatus}${summary.preloadItemCount == null ? '' : ` · ${summary.preloadItemCount} 项`}` : '待更新' },
|
||||
{ label: '拣货进度', value: pickingProgress },
|
||||
{ label: '柜次进度', value: summary.cabinetNo || summary.containerNo || '待更新' },
|
||||
{ label: '报关回执', value: customsDetail.value?.receipts.length ? `${customsDetail.value.receipts.length} 份回执` : customsUnavailable.value ? '暂无回执' : '查询中' },
|
||||
{ label: '船期摘要', value: transportReference || '待更新' },
|
||||
{ label: '计费重量', value: formatMetric(summary.chargeableWeightKg, 'kg') },
|
||||
{ label: '后续履约', value: summary.deliveryTrackingNo || (summary.deliveryCompletedAt ? `海外回传于 ${formatDateTime(summary.deliveryCompletedAt)}` : '待海外同步') },
|
||||
@@ -598,9 +634,6 @@ watch(isCreatePage, (isCreate, wasCreate) => {
|
||||
watch(detailOpen, (open) => {
|
||||
if (!open) {
|
||||
resetInboundBillingState()
|
||||
customsDetail.value = null
|
||||
customsLoading.value = false
|
||||
customsUnavailable.value = false
|
||||
if (detailOrder.value) releaseProductImagePreviews(detailOrder.value.items)
|
||||
}
|
||||
})
|
||||
@@ -1073,7 +1106,6 @@ async function openDetail(order: FirstLegOrder) {
|
||||
detailLoading.value = true
|
||||
detailError.value = ''
|
||||
void loadInboundBilling(order)
|
||||
void loadCustoms(order)
|
||||
try {
|
||||
const detailedOrder = await store.loadDetail(order.id)
|
||||
// Load previews from the persisted file IDs after the store has replaced the detail row.
|
||||
@@ -1138,21 +1170,6 @@ async function loadInboundBilling(order: FirstLegOrder) {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadCustoms(order: FirstLegOrder) {
|
||||
customsDetail.value = null
|
||||
customsUnavailable.value = false
|
||||
if (order.status === 'DRAFT') return
|
||||
customsLoading.value = true
|
||||
try {
|
||||
customsDetail.value = await getMerchantCustomsByOrder(order.id)
|
||||
} catch (error) {
|
||||
if (isMerchantCustomsUnavailable(error)) customsUnavailable.value = true
|
||||
else ElMessage.warning(error instanceof Error ? error.message : '报关回执加载失败')
|
||||
} finally {
|
||||
customsLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function retryInboundBilling() {
|
||||
const order = detailId.value ? store.getById(detailId.value) : undefined
|
||||
if (order) await loadInboundBilling(order)
|
||||
@@ -1632,6 +1649,7 @@ async function searchOrders(model: QueryModel) {
|
||||
<div class="order-row-actions">
|
||||
<el-button link type="primary" @click="openDetail(row as FirstLegOrder)"><Eye :size="15" />详情</el-button>
|
||||
<el-button v-if="canEditOrder && canClientEditFirstLegOrder((row as FirstLegOrder).status)" link type="primary" :loading="openingEditor" @click="openEdit(row as FirstLegOrder)"><Pencil :size="15" />编辑</el-button>
|
||||
<el-button v-if="eligibleCustomsDetail(row as FirstLegOrder)" link type="primary" @click="openCustomsDocuments(row as FirstLegOrder)"><FileText :size="15" />报关资料</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<template #order="{ row }">
|
||||
@@ -1892,16 +1910,6 @@ async function searchOrders(model: QueryModel) {
|
||||
</div>
|
||||
<el-empty v-else description="暂无柜次分配信息" :image-size="50" />
|
||||
</section>
|
||||
<section v-if="detailOrder.status !== 'DRAFT'" class="detail-section">
|
||||
<h3>报关回执</h3>
|
||||
<el-skeleton v-if="customsLoading" :rows="2" animated />
|
||||
<el-empty v-else-if="customsUnavailable || !customsDetail?.receipts.length" description="暂无客户可见报关回执" :image-size="50" />
|
||||
<div v-else class="customs-receipt-list">
|
||||
<article v-for="receipt in customsDetail.receipts" :key="receipt.id">
|
||||
<strong>{{ receipt.receiptType }}</strong><span>版本 {{ receipt.receiptVersion ?? '-' }}</span><span>{{ receipt.uploadedAt ? formatDateTime(receipt.uploadedAt) : '时间待更新' }}</span>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
<section class="detail-section">
|
||||
<h3>履约快递信息</h3>
|
||||
<div v-if="detailOrder.expressCompanyName || detailOrder.trackingNumber" class="express-information"><PackageCheck :size="20" /><div><span>快递公司</span><strong>{{ detailOrder.expressCompanyName || '待平台补充' }}</strong></div><div><span>快递单号</span><strong>{{ detailOrder.trackingNumber || '待平台补充' }}</strong></div></div>
|
||||
@@ -1964,6 +1972,13 @@ async function searchOrders(model: QueryModel) {
|
||||
</div>
|
||||
<template #footer><div v-if="detailOrder" class="editor-actions"><el-button @click="detailOpen = false">关闭</el-button><el-button v-if="canEditOrder && canClientEditFirstLegOrder(detailOrder.status)" :loading="openingEditor" @click="openEdit(detailOrder)"><Pencil :size="16" />编辑草稿</el-button></div></template>
|
||||
</el-dialog>
|
||||
<CustomsDocumentsDialog
|
||||
v-model="customsDialogOpen"
|
||||
:order-id="customsDialogOrderId"
|
||||
:can-edit="auth.hasPermission('order:first-leg:edit')"
|
||||
:initial-detail="customsByOrder[customsDialogOrderId]"
|
||||
@updated="updateCustomsDetail"
|
||||
/>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
@@ -2226,9 +2241,6 @@ async function searchOrders(model: QueryModel) {
|
||||
.cabinet-progress-card dl { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px; margin: 13px 0 0; }
|
||||
.cabinet-progress-card dt { color: var(--muted); font-size: 10px; }
|
||||
.cabinet-progress-card dd { margin: 4px 0 0; overflow-wrap: anywhere; color: var(--navy); font-size: 11px; font-weight: 600; }
|
||||
.customs-receipt-list { display: grid; gap: 8px; }
|
||||
.customs-receipt-list article { display: flex; flex-wrap: wrap; gap: 10px; padding: 11px 13px; border: 1px solid var(--border); color: var(--muted); font-size: 11px; }
|
||||
.customs-receipt-list strong { color: var(--navy); }
|
||||
.express-information { display: grid; grid-template-columns: auto repeat(2, minmax(0, 1fr)); align-items: center; gap: 16px; padding: 15px 16px; background: #f2f7f6; color: var(--primary); }
|
||||
.express-information > div { display: flex; min-width: 0; flex-direction: column; gap: 4px; }
|
||||
.express-information span { color: var(--muted); font-size: 11px; }
|
||||
|
||||
Reference in New Issue
Block a user