This commit is contained in:
2026-08-04 21:01:34 +08:00
parent 161bf580c7
commit 2313fd238c
12 changed files with 716 additions and 24 deletions
@@ -181,6 +181,30 @@ test('shows fulfillment courier information as read-only client detail', () => {
assert.doesNotMatch(source, /v-model="form\.trackingNumber"/)
})
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, /<InboundBillingPanel/)
assert.match(source, /@retry="retryInboundBilling"/)
assert.match(source, /error instanceof ApiError && error\.code === 'A0404'/)
assert.match(source, /order\.status === 'DRAFT'/)
assert.match(source, /订单提交后可查看入库结果/)
assert.doesNotMatch(source, /confirmMerchantInbound|adjustMerchantInbound|sendMerchantInbound/)
})
test('keeps platform enterprise scope and tenant terminology out of the customer order UI', () => {
const templateSource = source.slice(source.indexOf('<template>'), source.lastIndexOf('</template>'))
assert.doesNotMatch(templateSource, /租户|tenantId|所属企业|企业筛选/)
})
test('discards stale inbound billing responses when the drawer closes or the user changes orders', () => {
assert.match(source, /let inboundBillingRequestSequence = 0/)
assert.match(source, /const sequence = \+\+inboundBillingRequestSequence/)
assert.match(source, /sequence === inboundBillingRequestSequence[\s\S]*detailOpen\.value[\s\S]*detailId\.value === orderId/)
assert.match(source, /watch\(detailOpen,[\s\S]*resetInboundBillingState\(\)/)
})
test('protects unsaved drafts and exposes recoverable load failures', () => {
assert.match(source, /onBeforeRouteLeave/)
assert.match(source, /beforeunload/)
@@ -6,12 +6,16 @@ import { AlertTriangle, Box, Eye, FilePlus2, MapPin, PackageCheck, Pencil, Plus,
import { FIRST_LEG_ORDER_STATUSES, FIRST_LEG_TRANSPORT_MODES, 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'
import { ApiError } from '@/api/http'
import FirstLegOrderEditorShell from '@/modules/first-leg/components/FirstLegOrderEditorShell.vue'
import InboundBillingPanel from '@/modules/first-leg/components/InboundBillingPanel.vue'
import { getMerchantInboundBilling } from '@/modules/first-leg/api/inboundBilling'
import { useFirstLegOrdersStore } from '@/modules/first-leg/store/firstLegOrders'
import { useTenantAdministrationStore } from '@/modules/tenant-management/public'
import { useAuthStore } from '@/modules/auth/public'
import type { FirstLegPartyRole, FirstLegPartySnapshot } from '@/modules/first-leg/data/firstLegParties'
import type { FirstLegOrder, FirstLegOrderDraft } from '@/modules/first-leg/types/firstLegOrder'
import type { MerchantInboundBilling } from '@/modules/first-leg/types/inboundBilling'
import {
canClientEditFirstLegOrder,
createEmptyCargoItem,
@@ -70,10 +74,16 @@ const detailId = ref<string>()
const saving = ref(false)
const detailLoading = ref(false)
const detailError = ref('')
const inboundBilling = ref<MerchantInboundBilling | null>(null)
const inboundBillingLoading = ref(false)
const inboundBillingUnavailable = ref(false)
const inboundBillingUnavailableMessage = ref('入库及费用结果尚未发布')
const inboundBillingError = ref('')
const supportingDataLoading = ref(false)
const supportingDataErrors = ref<string[]>([])
const editorBaseline = ref('')
let cargoSequence = 0
let inboundBillingRequestSequence = 0
interface CachedPrivateAddress {
id: string
@@ -293,7 +303,14 @@ watch(isCreatePage, (isCreate, wasCreate) => {
resetEditorValidation()
}, { immediate: true })
onBeforeUnmount(() => window.removeEventListener('beforeunload', handleBeforeUnload))
watch(detailOpen, (open) => {
if (!open) resetInboundBillingState()
})
onBeforeUnmount(() => {
window.removeEventListener('beforeunload', handleBeforeUnload)
resetInboundBillingState()
})
onBeforeRouteLeave(async () => {
if (!editorDirty.value) return true
@@ -536,6 +553,7 @@ async function openDetail(order: FirstLegOrder) {
detailOpen.value = true
detailLoading.value = true
detailError.value = ''
void loadInboundBilling(order)
try {
await store.loadDetail(order.id)
} catch (error) {
@@ -551,6 +569,58 @@ async function retryDetail() {
if (order) await openDetail(order)
}
function resetInboundBillingState() {
inboundBillingRequestSequence += 1
inboundBilling.value = null
inboundBillingLoading.value = false
inboundBillingUnavailable.value = false
inboundBillingUnavailableMessage.value = '入库及费用结果尚未发布'
inboundBillingError.value = ''
}
function isCurrentInboundBillingRequest(sequence: number, orderId: string) {
return sequence === inboundBillingRequestSequence
&& detailOpen.value
&& detailId.value === orderId
}
async function loadInboundBilling(order: FirstLegOrder) {
const sequence = ++inboundBillingRequestSequence
inboundBilling.value = null
inboundBillingLoading.value = false
inboundBillingUnavailable.value = false
inboundBillingUnavailableMessage.value = '入库及费用结果尚未发布'
inboundBillingError.value = ''
if (order.status === 'DRAFT') {
inboundBillingUnavailable.value = true
inboundBillingUnavailableMessage.value = '订单提交后可查看入库结果'
return
}
inboundBillingLoading.value = true
try {
const result = await getMerchantInboundBilling(order.id)
if (!isCurrentInboundBillingRequest(sequence, order.id)) return
inboundBilling.value = result
} catch (error) {
if (!isCurrentInboundBillingRequest(sequence, order.id)) return
if (error instanceof ApiError && error.code === 'A0404') {
inboundBillingUnavailable.value = true
inboundBillingUnavailableMessage.value = '入库及费用结果尚未发布'
} else {
inboundBillingError.value = error instanceof Error ? error.message : '入库与费用加载失败'
}
} finally {
if (isCurrentInboundBillingRequest(sequence, order.id)) inboundBillingLoading.value = false
}
}
async function retryInboundBilling() {
const order = detailId.value ? store.getById(detailId.value) : undefined
if (order) await loadInboundBilling(order)
}
async function confirmDiscardChanges() {
try {
await ElMessageBox.confirm('当前订单还有未保存的修改。离开后这些内容将丢失。', '放弃未保存的修改?', {
@@ -865,6 +935,14 @@ async function searchOrders(model: QueryModel) {
<div v-else class="express-empty"><PackageCheck :size="20" /><div><strong>平台尚未维护快递信息</strong><span>快递公司和快递单号由平台后台维护,客户端仅只读展示。</span></div></div>
</section>
<section class="detail-section"><h3>货物汇总</h3><div v-if="detailSummary" class="cargo-summary"><div><Box :size="18" /><span>明细</span><strong>{{ detailOrder.items.length }} 项</strong></div><div><span>件数</span><strong>{{ detailSummary.pieces }}</strong></div><div><span>毛重</span><strong>{{ detailSummary.grossWeightKg.toLocaleString() }} kg</strong></div><div><span>体积</span><strong>{{ detailSummary.volumeCbm }} m³</strong></div></div><ul class="cargo-detail-list"><li v-for="item in detailOrder.items" :key="item.id"><strong>{{ item.cargoNameCn }}</strong><span>{{ item.sku || '无 SKU' }} · {{ item.pieces }} 件 · {{ item.grossWeightKg }} kg</span></li></ul></section>
<InboundBillingPanel
:data="inboundBilling"
:loading="inboundBillingLoading"
:unavailable="inboundBillingUnavailable"
:unavailable-message="inboundBillingUnavailableMessage"
:error="inboundBillingError"
@retry="retryInboundBilling"
/>
<section class="detail-section"><h3>处理时间线</h3><el-timeline><el-timeline-item v-for="item in [...detailOrder.timeline].reverse()" :key="item.id" :timestamp="item.occurredAt" placement="top"><strong>{{ item.title }}</strong><p>{{ item.description || '-' }}</p><span>{{ item.operatorName || '未提供' }}</span></el-timeline-item></el-timeline></section>
</template>
</div>