feat(first-leg): reorder pickup choices and collect optional courier tracking

This commit is contained in:
2026-10-19 17:40:25 +08:00
parent 17580fb0a2
commit d910b24755
8 changed files with 52 additions and 7 deletions
+11
View File
@@ -0,0 +1,11 @@
# 头程下单交货方式调整
依据:当前对话 2026-10-19 用户确认“卡车自送排第一,我司提货排第二,快递自提排第三;快递自提增加非必填快递单号”。该确认优先于历史原型对应字段。
| 页面 | 操作 | 承载方式 | 布局 | 当前实现 | 结论 |
|---|---|---|---|---|---|
| 头程下单 | 新增 | 现有独立页面 | 保持现有国内交货区布局 | 快递自送、我司提货、卡车自送;无快递单号输入 | 按确认顺序排列;第三项改为快递自提,下方条件显示选填快递单号 |
| 头程订单 | 编辑草稿 | 现有居中弹框 | 复用新增表单 | 无快递单号输入 | 同步新增规则,回显已保存单号 |
| 头程订单 | 详情 | 现有独立页面 | 保持现有信息区 | 已有快递单号只读展示 | 复用当前展示;说明客户可在快递自提草稿填写单号 |
自动检查与人工视觉验收分别记录;1920×1080、100% 缩放的页面效果待人工截图确认。
+1
View File
@@ -139,6 +139,7 @@ export interface FirstLegOrderSaveRequest {
privateDestinationAddressId?: number | string
cargoes: FirstLegOrderCargoPayload[]
pickupMethod: ApiPickupMethod
expressTrackingNo?: string
receivingWarehouseId: number | string
receivingWarehouseName: string
receivingWarehouseAddress?: string | null
@@ -8,6 +8,21 @@ const storeSource = readFileSync(new URL('./firstLegOrders.ts', import.meta.url)
const apiSource = readFileSync(new URL('../api/firstLeg.ts', import.meta.url), 'utf8')
const unifiedApiSource = readFileSync(new URL('../api/unifiedOrders.ts', import.meta.url), 'utf8')
test('saves optional express tracking, trims it, and omits it for other pickup methods', () => {
const functions = storeSource.slice(storeSource.indexOf('function apiTransport('), storeSource.indexOf('function prepareDraftForSave('))
const javascript = ts.transpileModule(functions, { compilerOptions: { target: ts.ScriptTarget.ES2022 } }).outputText
const save = runInNewContext(`${javascript}; savePayload`)
const draft = { pickupMethod: 'SELF_EXPRESS', trackingNumber: ' SF123456789 ', destinationCountryCode: 'US', destinationType: 'OVERSEAS', items: [] }
const warehouse = { id: '1', name: '收货仓' }
assert.equal(save(draft, warehouse, '客户').expressTrackingNo, 'SF123456789')
for (const trackingNumber of ['', ' ']) {
assert.equal(save({ ...draft, trackingNumber }, warehouse, '客户').expressTrackingNo, undefined)
}
for (const pickupMethod of ['SELF_TRUCK', 'PICKUP']) {
assert.equal(save({ ...draft, pickupMethod }, warehouse, '客户').expressTrackingNo, undefined)
}
})
test('detail fetches authoritative progress on direct entry and refresh, and clears stale progress on failure', async () => {
const functionSource = storeSource.slice(storeSource.indexOf(' async function loadDetail('), storeSource.indexOf(' async function loadWarehouseDetail('))
const progressSource = readFileSync(new URL('../utils/orderProgress.ts', import.meta.url), 'utf8')
@@ -120,6 +120,7 @@ function savePayload(draft: FirstLegOrderDraft, warehouse: ClientWarehouse, cust
productImageFileId: item.productImageFileId || undefined,
})),
pickupMethod: draft.pickupMethod,
expressTrackingNo: draft.pickupMethod === 'SELF_EXPRESS' ? draft.trackingNumber.trim() || undefined : undefined,
receivingWarehouseId: warehouse.id,
receivingWarehouseName: warehouse.name,
channelCode: draft.channelCode,
@@ -183,6 +183,7 @@ export type FirstLegOrderDraft = Omit<Pick<
| 'destinationWarehouseId'
| 'destinationWarehouseName'
| 'pickupMethod'
| 'trackingNumber'
| 'channelCode'
| 'channelName'
| 'pricingTaxMode'
@@ -549,9 +549,19 @@ 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.match(source, /快递公司由平台维护/)
assert.doesNotMatch(source, /v-model="form\.expressCompanyName"/)
assert.doesNotMatch(source, /v-model="form\.trackingNumber"/)
})
test('orders pickup choices and offers an optional tracking number only for express pickup', () => {
const group = source.match(/<el-radio-group v-model="form\.pickupMethod"[\s\S]*?<\/el-radio-group>/)[0]
assert.deepEqual([...group.matchAll(/value="([A-Z_]+)"/g)].map(match => match[1]), ['SELF_TRUCK', 'PICKUP', 'SELF_EXPRESS'])
assert.match(group, /value="SELF_EXPRESS">快递自提/)
const field = source.match(/<el-form-item v-if="form\.pickupMethod === 'SELF_EXPRESS'"[^>]*label="快递单号"[\s\S]*?<\/el-form-item>/)[0]
assert.match(field, /v-model="form\.trackingNumber" maxlength="128"/)
assert.doesNotMatch(field, /\brequired\b|:rules=/)
assert.match(source, /trackingNumber: order\.pickupMethod === 'SELF_EXPRESS' \? order\.trackingNumber : ''/)
assert.match(source, /if \(form\.pickupMethod !== 'SELF_EXPRESS'\) form\.trackingNumber = ''/)
})
test('shows the full customer-safe order detail without platform-only controls', () => {
@@ -372,9 +372,9 @@ function formatExportDateTime(value: string | null | undefined) {
}
const pickupMethodLabels: Record<FirstLegOrder['pickupMethod'], string> = {
SELF_EXPRESS: '自行快递到仓',
PICKUP: '平台上门提货',
SELF_TRUCK: '自行送货到仓',
SELF_TRUCK: '卡车自送',
PICKUP: '我司提货',
SELF_EXPRESS: '快递自提',
}
const destinationTypeLabels: Record<FirstLegOrder['destinationType'], string> = {
FBA: 'FBA 仓',
@@ -424,6 +424,7 @@ function emptyDraft(): FirstLegOrderDraft {
destinationWarehouseId: '',
destinationWarehouseName: '',
pickupMethod: 'SELF_EXPRESS',
trackingNumber: '',
channelCode: '',
channelName: '',
pricingTaxMode: 'TAX_INCLUDED',
@@ -920,6 +921,7 @@ function draftFromOrder(order: FirstLegOrder): FirstLegOrderDraft {
destinationWarehouseId: order.destinationWarehouseId,
destinationWarehouseName: order.destinationWarehouseName,
pickupMethod: order.pickupMethod,
trackingNumber: order.pickupMethod === 'SELF_EXPRESS' ? order.trackingNumber : '',
channelCode: order.channelCode,
channelName: order.channelName,
pricingTaxMode: order.pricingTaxMode,
@@ -942,6 +944,7 @@ function copyDraftFromOrder(order: FirstLegOrder): FirstLegOrderDraft {
return {
...draft,
inboundCode: '',
trackingNumber: '',
items: draft.items.map((item) => ({
...item,
id: `copied-cargo-${++cargoSequence}`,
@@ -1134,6 +1137,7 @@ async function changeDestinationType() {
}
function changePickupMethod() {
if (form.pickupMethod !== 'SELF_EXPRESS') form.trackingNumber = ''
if (form.pickupMethod === 'PICKUP') {
form.shipperPartySnapshot ??= createPickupPartySnapshot()
} else {
@@ -2241,7 +2245,8 @@ async function searchOrders(model: QueryModel) {
<div class="prototype-form-flow shipping-form-flow">
<el-form-item label="履约类型" prop="fulfillmentType" required class="prototype-form-row prototype-form-row--tabs"><el-radio-group v-model="form.fulfillmentType" class="segmented-options" @change="changeFulfillmentType"><el-radio-button value="STANDARD">普通头程</el-radio-button><el-radio-button value="ONE_CLICK_DROPSHIP">一件代发</el-radio-button></el-radio-group><small v-if="form.fulfillmentType === 'ONE_CLICK_DROPSHIP'" class="field-guidance">测量完成后不办理国内库存入库,将直接推送 PDA 合托。</small></el-form-item>
<el-form-item v-if="form.fulfillmentType === 'ONE_CLICK_DROPSHIP'" label="LX入库单号" prop="lxCode" class="prototype-form-row prototype-form-row--wide"><el-input v-model="form.lxCode" maxlength="128" placeholder="请输入LX入库单号" /><small class="field-guidance">用于绑定领星入库单,提交后可在尾程订单、入库单和出库单中查询。</small></el-form-item>
<el-form-item label="取货方式" prop="pickupMethod" required class="prototype-form-row prototype-form-row--tabs"><el-radio-group v-model="form.pickupMethod" class="segmented-options" @change="changePickupMethod"><el-radio-button value="SELF_EXPRESS">快递自送</el-radio-button><el-radio-button value="PICKUP">我司提货</el-radio-button><el-radio-button value="SELF_TRUCK">卡车自送</el-radio-button></el-radio-group></el-form-item>
<el-form-item label="取货方式" prop="pickupMethod" required class="prototype-form-row prototype-form-row--tabs"><el-radio-group v-model="form.pickupMethod" class="segmented-options" @change="changePickupMethod"><el-radio-button value="SELF_TRUCK">卡车自送</el-radio-button><el-radio-button value="PICKUP">我司提货</el-radio-button><el-radio-button value="SELF_EXPRESS">快递自提</el-radio-button></el-radio-group></el-form-item>
<el-form-item v-if="form.pickupMethod === 'SELF_EXPRESS'" label="快递单号" prop="trackingNumber" class="prototype-form-row prototype-form-row--wide"><el-input v-model="form.trackingNumber" maxlength="128" placeholder="请输入快递单号(选填)" /></el-form-item>
<template v-if="form.pickupMethod === 'PICKUP' && form.shipperPartySnapshot">
<el-form-item label="企业提货地址簿" prop="senderAddressId" required :rules="requiredSelectRules('请选择企业提货地址')" class="prototype-form-row prototype-form-row--wide address-book-row">
<div class="address-book-control"><el-select v-model="form.senderAddressId" clearable filterable :loading="orderAddressesLoading" placeholder="选择企业保存的提货地址" @change="selectPickupAddress"><el-option v-for="address in pickupAddresses" :key="String(address.id)" :label="addressLabel(address)" :value="String(address.id)" /></el-select><el-button @click="savePickupAddress">保存当前地址</el-button><el-button v-if="form.senderAddressId" link type="danger" @click="removeOrderAddress('PICKUP')">停用</el-button></div>
@@ -2474,7 +2479,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><span>快递自提可在下单或编辑草稿时选填快递单号,快递公司由平台维护。</span></div></div>
</section>
<section class="detail-section">
<h3>货物明细</h3>
@@ -202,6 +202,7 @@ function buildOrderDraft(row: QuickOrderRow, channel: QuickChannelOption): First
destinationWarehouseId: row.destinationWarehouseId,
destinationWarehouseName: row.destinationWarehouseName,
pickupMethod: 'SELF_EXPRESS',
trackingNumber: '',
channelCode: channel.channelCode,
channelName: channel.channelName,
pricingTaxMode,