fix(first-leg): show cancelled orders as terminal details
This commit is contained in:
@@ -26,14 +26,15 @@ function stageState(index: number) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ol class="progress-stages" aria-label="订单主状态阶段">
|
||||
<p v-if="order.status === 'CANCELLED'" class="draft-progress-note">订单已取消,不再继续履约。</p>
|
||||
<ol v-else class="progress-stages" aria-label="订单主状态阶段">
|
||||
<li v-for="(stage, index) in FIRST_LEG_PROGRESS_STAGES" :key="stage.code" :class="stageState(index)" :aria-current="index === currentIndex ? 'step' : undefined">
|
||||
<span class="stage-marker" aria-hidden="true"><Check v-if="index < currentIndex" :size="13" /><CircleDot v-else :size="13" /></span>
|
||||
<span class="stage-label">{{ stage.label }}</span>
|
||||
</li>
|
||||
</ol>
|
||||
<p v-if="order.status === 'DRAFT'" class="draft-progress-note">订单提交后开始记录主状态进度。</p>
|
||||
<div class="progress-cabinet-detail"><slot name="cabinet" /></div>
|
||||
<div v-if="order.status !== 'CANCELLED'" class="progress-cabinet-detail"><slot name="cabinet" /></div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -28,7 +28,10 @@ function harness(t) {
|
||||
const module = compile('./firstLegOrders.ts', {
|
||||
vue, pinia,
|
||||
'@/modules/first-leg/api/firstLeg': api,
|
||||
'@/modules/first-leg/api/unifiedOrders': {},
|
||||
'@/modules/first-leg/api/unifiedOrders': {
|
||||
getMerchantUnifiedOrder: async () => ({ summary: { orderId: 'one', mainStatus: null }, order: { order: { id: 'one', status: behavior.status, version: behavior.version, receivingWarehouseId: 'warehouse' } }, timeline: [] }),
|
||||
isUnifiedOrderQueryUnavailable: () => false,
|
||||
},
|
||||
'@/modules/first-leg/utils/firstLegOrder': { canClientEditFirstLegOrder: status => status === 'DRAFT', getFirstLegStatusLabel: value => value },
|
||||
'@/modules/first-leg/utils/warehousePagination': {},
|
||||
})
|
||||
@@ -38,6 +41,28 @@ function harness(t) {
|
||||
return { store, api, calls, behavior, draft }
|
||||
}
|
||||
|
||||
test('raw and unified cancelled detail remain CANCELLED even when unified mainStatus is null', async t => {
|
||||
const h = harness(t)
|
||||
h.behavior.status = 'CANCELLED'
|
||||
const raw = await h.store.loadDraftDetail('one')
|
||||
assert.equal(raw.status, 'CANCELLED')
|
||||
const unified = await h.store.loadDetail('one')
|
||||
assert.equal(unified.status, 'CANCELLED')
|
||||
h.behavior.status = 'APPROVED'
|
||||
const unknown = await h.store.loadDetail('one')
|
||||
assert.notEqual(unknown.status, 'CANCELLED', 'unknown summary must not be reclassified as cancellation')
|
||||
})
|
||||
|
||||
test('cancelled detail summary has no pending overseas synchronization entries', () => {
|
||||
const source = readFileSync(new URL('../views/FirstLegOrdersView.vue', import.meta.url), 'utf8')
|
||||
const script = source.match(/<script setup lang="ts">([\s\S]*?)<\/script>/)[1]
|
||||
const ast = ts.createSourceFile('view.ts', script, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS)
|
||||
const declaration = ast.statements.flatMap(node => ts.isVariableStatement(node) ? [...node.declarationList.declarations] : []).find(node => node.name.getText(ast) === 'progressSummaryItems')
|
||||
const code = ts.transpileModule('const progressSummaryItems = ' + declaration.initializer.getText(ast), { compilerOptions: { target: ts.ScriptTarget.ES2022 } }).outputText
|
||||
const summary = new Function('computed', 'detailOrder', code + '; return progressSummaryItems.value')(vue.computed, { value: { status: 'CANCELLED' } })
|
||||
assert.deepEqual(summary, [])
|
||||
})
|
||||
|
||||
test('edit freezes detail version rather than cached list version and replays same key/body after uncertain result', async t => {
|
||||
const h = harness(t)
|
||||
const original = await h.store.loadDraftDetail('one')
|
||||
|
||||
@@ -47,7 +47,7 @@ function clientStatus(status: ApiFirstLegStatus): FirstLegOrder['status'] {
|
||||
if (status === 'REJECTED') return 'PENDING_REVIEW'
|
||||
if (status === 'APPROVED' || status === 'ACCEPTED' || status === 'PROCESSING') return 'WAIT_MEASURE'
|
||||
if (status === 'COMPLETED') return 'COMPLETED'
|
||||
if (status === 'CANCELLED') return 'PENDING_REVIEW'
|
||||
if (status === 'CANCELLED') return 'CANCELLED'
|
||||
return 'PENDING_REVIEW'
|
||||
}
|
||||
|
||||
@@ -384,7 +384,7 @@ function mapUnifiedOrderDetail(detail: MerchantUnifiedOrderDetail, warehouses: P
|
||||
if (!detail?.summary || !detail.order?.order) throw new Error('订单详情数据不完整,请刷新列表后重试')
|
||||
const mapped = mapOrder(detail.order.order, warehouses, detail.order)
|
||||
mapped.id = String(detail.summary.orderId)
|
||||
mapped.status = detail.summary.mainStatus
|
||||
mapped.status = detail.order.order.status === 'CANCELLED' ? 'CANCELLED' : detail.summary.mainStatus
|
||||
mapped.fulfillmentType = detail.summary.fulfillmentType ?? mapped.fulfillmentType
|
||||
mapped.lxCode = detail.summary.lxCode ?? mapped.lxCode
|
||||
mapped.businessSummary = businessSummary(detail.summary)
|
||||
|
||||
@@ -60,6 +60,12 @@ function validDraft() {
|
||||
}
|
||||
|
||||
test('uses the confirmed status and transport labels', () => {
|
||||
assert.equal(getFirstLegStatusLabel('CANCELLED'), '已取消')
|
||||
assert.equal(getFirstLegStatusLabel('NOT_A_STATUS'), '未知状态')
|
||||
assert.equal(getFirstLegStatusTone('CANCELLED'), 'info')
|
||||
assert.equal(getFirstLegProgressIndex('CANCELLED'), -1)
|
||||
assert.equal(canClientEditFirstLegOrder('CANCELLED'), false)
|
||||
assert.equal(canClientCancelFirstLegOrder('CANCELLED'), false)
|
||||
assert.equal(getFirstLegStatusLabel('WAIT_MEASURE'), '待测量数据')
|
||||
assert.equal(getFirstLegStatusLabel('WAIT_PALLETIZE'), '待组托')
|
||||
assert.equal(getFirstLegStatusLabel('WAIT_SPLIT'), '待分柜')
|
||||
|
||||
@@ -41,11 +41,11 @@ export function canClientCancelFirstLegOrder(status: FirstLegOrderStatus) {
|
||||
}
|
||||
|
||||
export const FIRST_LEG_PROGRESS_STAGES = FIRST_LEG_ORDER_STATUSES.filter(
|
||||
(item): item is (typeof FIRST_LEG_ORDER_STATUSES)[number] & { code: Exclude<FirstLegOrderStatus, 'DRAFT'> } => item.code !== 'DRAFT',
|
||||
(item): item is (typeof FIRST_LEG_ORDER_STATUSES)[number] & { code: Exclude<FirstLegOrderStatus, 'DRAFT' | 'CANCELLED'> } => item.code !== 'DRAFT' && item.code !== 'CANCELLED',
|
||||
)
|
||||
|
||||
export function getFirstLegProgressIndex(status: FirstLegOrderStatus) {
|
||||
if (status === 'DRAFT') return -1
|
||||
if (status === 'DRAFT' || status === 'CANCELLED') return -1
|
||||
return FIRST_LEG_PROGRESS_STAGES.findIndex((item) => item.code === status)
|
||||
}
|
||||
|
||||
|
||||
@@ -245,7 +245,7 @@ test('uses only fields present on C02 rows before loading full detail', () => {
|
||||
assert.doesNotMatch(source, /key: 'operator'/)
|
||||
assert.doesNotMatch(source, /key: 'transportMode', type: 'select'/)
|
||||
assert.match(source, /store\.draftLoadError/)
|
||||
assert.match(source, /mainStatus: statusValue && statusValue !== 'DRAFT' \? statusValue : undefined/)
|
||||
assert.match(source, /mainStatus: statusValue && statusValue !== 'DRAFT' && statusValue !== 'CANCELLED' \? statusValue : undefined/)
|
||||
assert.match(source, /draftOnly: statusValue === 'DRAFT'/)
|
||||
assert.match(source, /createdFrom: range\[0\] \? `\$\{range\[0\]\}T00:00:00`/)
|
||||
})
|
||||
|
||||
@@ -137,7 +137,7 @@ const queryFields = computed<QueryField[]>(() => [
|
||||
{ key: 'receivingWarehouseId', label: '收货仓', type: 'select', placeholder: '全部', options: availableWarehouses.value.map((item) => ({ label: item.name, value: item.id })), props: { 'aria-label': '收货仓' } },
|
||||
{ key: 'channelName', label: '渠道名称', type: 'select', placeholder: '全部', options: [...new Set(store.orders.map((item) => item.channelName).filter(Boolean))].map((name) => ({ label: name, value: name })), props: { 'aria-label': '渠道名称' } },
|
||||
{ key: 'createdRange', label: '创建时间', type: 'daterange', placeholder: '请选择创建时间', props: { 'aria-label': '创建时间' } },
|
||||
{ key: 'status', label: '当前状态', type: 'select', placeholder: '全部', options: FIRST_LEG_ORDER_STATUSES.map((item) => ({ label: item.label, value: item.code })), props: { 'aria-label': '当前状态' } },
|
||||
{ key: 'status', label: '当前状态', type: 'select', placeholder: '全部', options: FIRST_LEG_ORDER_STATUSES.filter(item => item.code !== 'CANCELLED').map((item) => ({ label: item.label, value: item.code })), props: { 'aria-label': '当前状态' } },
|
||||
{ key: 'destinationType', label: '目的仓类型', type: 'select', placeholder: '全部', options: [{ label: 'FBA 仓', value: 'FBA' }, { label: '海外仓', value: 'OVERSEAS' }, { label: '私人地址', value: 'PRIVATE' }], props: { 'aria-label': '目的仓类型' } },
|
||||
{ key: 'trackingNumber', label: '快递单号', type: 'input', placeholder: '请输入快递单号', props: { 'aria-label': '快递单号' } },
|
||||
])
|
||||
@@ -597,7 +597,7 @@ const pagedDetailCargoItems = computed(() => {
|
||||
})
|
||||
const progressSummaryItems = computed(() => {
|
||||
const order = detailOrder.value
|
||||
if (!order) return []
|
||||
if (!order || order.status === 'CANCELLED') return []
|
||||
const inbound = inboundBilling.value?.inbound.summary
|
||||
const summary = order.businessSummary
|
||||
const formatMetric = (value: number | null | undefined, unit = '') => value == null ? '待更新' : `${value.toLocaleString()}${unit ? ` ${unit}` : ''}`
|
||||
@@ -1445,7 +1445,8 @@ async function loadDetailPage(orderId: string) {
|
||||
detailError.value = ''
|
||||
try {
|
||||
const detailedOrder = await store.loadDetail(orderId)
|
||||
void loadInboundBilling(detailedOrder)
|
||||
if (detailedOrder.status === 'CANCELLED') resetInboundBillingState()
|
||||
else void loadInboundBilling(detailedOrder)
|
||||
// Load previews from the persisted file IDs after the store has replaced the detail row.
|
||||
await Promise.all((detailOrder.value?.items ?? detailedOrder.items).map(ensureProductImagePreview))
|
||||
} catch (error) {
|
||||
@@ -2084,7 +2085,7 @@ async function searchOrders(model: QueryModel) {
|
||||
keyword: undefined,
|
||||
inboundCode: cargoKeyword || undefined,
|
||||
receivingWarehouseId: typeof model.receivingWarehouseId === 'string' && model.receivingWarehouseId ? model.receivingWarehouseId : undefined,
|
||||
mainStatus: statusValue && statusValue !== 'DRAFT' ? statusValue : undefined,
|
||||
mainStatus: statusValue && statusValue !== 'DRAFT' && statusValue !== 'CANCELLED' ? statusValue : undefined,
|
||||
draftOnly: statusValue === 'DRAFT',
|
||||
includeDrafts: !statusValue,
|
||||
createdFrom: range[0] ? `${range[0]}T00:00:00` : undefined,
|
||||
@@ -2411,16 +2412,16 @@ async function searchOrders(model: QueryModel) {
|
||||
<div class="order-detail-heading">
|
||||
<div class="detail-heading-main">
|
||||
<el-button link type="primary" class="detail-back-action" @click="router.push({ name: 'first-leg-orders' })">← 返回列表</el-button>
|
||||
<div><h2>{{ detailOrder.businessSummary.inboundCode || (detailOrder.status === 'DRAFT' ? '入仓码提交后生成' : '入仓码待生成') }}</h2><p>{{ getFirstLegTransportModeLabel(detailOrder.transportMode) }} · {{ detailOrder.origin || '起运地待更新' }} → {{ detailOrder.destination || '目的地待更新' }}</p></div>
|
||||
<div><h2>{{ detailOrder.businessSummary.inboundCode || (detailOrder.status === 'CANCELLED' ? (detailOrder.orderNo || '—') : detailOrder.status === 'DRAFT' ? '入仓码提交后生成' : '入仓码待生成') }}</h2><p v-if="detailOrder.status === 'CANCELLED'">订单已取消。</p><p v-else>{{ getFirstLegTransportModeLabel(detailOrder.transportMode) }} · {{ detailOrder.origin || '起运地待更新' }} → {{ detailOrder.destination || '目的地待更新' }}</p></div>
|
||||
</div>
|
||||
<div class="detail-heading-actions"><el-tag :type="getFirstLegStatusTone(detailOrder.status)" effect="light">{{ getFirstLegStatusLabel(detailOrder.status) }}</el-tag><el-button v-if="canEditOrder && canClientEditFirstLegOrder(detailOrder.status)" type="primary" plain :loading="openingEditor" @click="openEdit(detailOrder)"><Pencil :size="16" />编辑草稿</el-button><el-button v-if="canOfferCancellation(detailOrder)" type="danger" plain :loading="cancellingOrderId === detailOrder.id" :disabled="!!cancellingOrderId" @click="cancelOrder(detailOrder)">{{ pendingCancellation?.id === detailOrder.id ? '重试取消' : '取消订单' }}</el-button></div>
|
||||
</div>
|
||||
<section class="detail-section">
|
||||
<h3>订单与运输</h3>
|
||||
<dl class="order-detail-grid">
|
||||
<div><dt>入仓码</dt><dd><button v-if="detailOrder.businessSummary.inboundCode" class="inbound-code-link table-code" type="button" @click="openInboundCodeAssociation(detailOrder.businessSummary.inboundCode)">{{ detailOrder.businessSummary.inboundCode }}</button><span v-else>{{ detailOrder.status === 'DRAFT' ? '提交后生成' : '待生成' }}</span></dd></div>
|
||||
<div><dt>入仓码</dt><dd><button v-if="detailOrder.businessSummary.inboundCode" class="inbound-code-link table-code" type="button" @click="openInboundCodeAssociation(detailOrder.businessSummary.inboundCode)">{{ detailOrder.businessSummary.inboundCode }}</button><span v-else>{{ detailOrder.status === 'CANCELLED' ? '—' : detailOrder.status === 'DRAFT' ? '提交后生成' : '待生成' }}</span></dd></div>
|
||||
<div><dt>创建时间</dt><dd>{{ formatDateTime(detailOrder.createdAt) }}</dd></div>
|
||||
<div><dt>运输方式</dt><dd>{{ getFirstLegTransportModeLabel(detailOrder.transportMode) }}</dd></div>
|
||||
<div><dt>运输方式</dt><dd>{{ detailOrder.status === 'CANCELLED' && !detailOrder.transportMode ? '—' : getFirstLegTransportModeLabel(detailOrder.transportMode) }}</dd></div>
|
||||
<div><dt>贸易条款</dt><dd>{{ displayValue(detailOrder.tradeTerm) }}</dd></div>
|
||||
<div><dt>发货方式</dt><dd>{{ pickupMethodLabels[detailOrder.pickupMethod] }}</dd></div>
|
||||
<div><dt>预约提货时间</dt><dd>{{ detailOrder.pickupMethod === 'PICKUP' && detailOrder.requestedPickupAt ? formatDateTime(detailOrder.requestedPickupAt) : '-' }}</dd></div>
|
||||
@@ -2445,7 +2446,7 @@ async function searchOrders(model: QueryModel) {
|
||||
</dl>
|
||||
<div v-if="detailOrder.customerRemark" class="customer-remark"><strong>客户备注</strong><p>{{ detailOrder.customerRemark }}</p></div>
|
||||
</section>
|
||||
<section v-if="detailOrder.status !== 'DRAFT'" class="detail-section">
|
||||
<section v-if="detailOrder.status !== 'DRAFT' && detailOrder.status !== 'CANCELLED'" class="detail-section">
|
||||
<h3>关键业务摘要</h3>
|
||||
<dl class="fulfillment-summary">
|
||||
<div v-for="item in progressSummaryItems" :key="item.label"><dt>{{ item.label }}</dt><dd>{{ item.value }}</dd></div>
|
||||
@@ -2465,9 +2466,9 @@ async function searchOrders(model: QueryModel) {
|
||||
</div>
|
||||
</template>
|
||||
</OrderProgressPanel>
|
||||
<section class="detail-section">
|
||||
<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 || '待平台补充' }}</dd></div><div><dt>快递单号</dt><dd class="table-code">{{ detailOrder.trackingNumber || '待平台补充' }}</dd></div></dl>
|
||||
<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>
|
||||
</section>
|
||||
<section class="detail-section">
|
||||
@@ -2517,6 +2518,7 @@ async function searchOrders(model: QueryModel) {
|
||||
<el-empty v-else description="暂无货物明细" :image-size="56" />
|
||||
</section>
|
||||
<InboundBillingPanel
|
||||
v-if="detailOrder.status !== 'CANCELLED'"
|
||||
:data="inboundBilling"
|
||||
:loading="inboundBillingLoading"
|
||||
:unavailable="inboundBillingUnavailable"
|
||||
|
||||
Reference in New Issue
Block a user