merge origin/dev and resolve frontend conflicts
This commit is contained in:
+11
-2
@@ -39,14 +39,23 @@ export async function overseasApiRequest<T>(config: AxiosRequestConfig) {
|
||||
try {
|
||||
const response = await transport.request<ApiEnvelope<T>>(config)
|
||||
if (!isApiEnvelope(response.data) || response.data.code !== '200') {
|
||||
throw new ApiError(isApiEnvelope(response.data) ? response.data.message : '海外接口响应格式错误', isApiEnvelope(response.data) ? response.data.code : 'INVALID_RESPONSE')
|
||||
throw new ApiError(
|
||||
isApiEnvelope(response.data) ? response.data.message : '海外接口响应格式错误',
|
||||
isApiEnvelope(response.data) ? response.data.code : 'INVALID_RESPONSE',
|
||||
undefined,
|
||||
undefined,
|
||||
isApiEnvelope(response.data) ? response.data.traceId : null,
|
||||
isApiEnvelope(response.data) ? response.data.requestId : null,
|
||||
)
|
||||
}
|
||||
return response.data.data
|
||||
} catch (error) {
|
||||
if (error instanceof ApiError) throw error
|
||||
if (axios.isAxiosError(error)) {
|
||||
const body = error.response?.data
|
||||
if (isApiEnvelope(body)) throw new ApiError(body.message, body.code, error.response?.status)
|
||||
if (isApiEnvelope(body)) {
|
||||
throw new ApiError(body.message, body.code, error.response?.status, undefined, body.traceId, body.requestId)
|
||||
}
|
||||
throw new ApiError(error.code === 'ERR_NETWORK' ? '无法连接海外服务' : error.message, error.code || 'OVERSEAS_HTTP_ERROR', error.response?.status)
|
||||
}
|
||||
throw new ApiError(error instanceof Error ? error.message : '海外接口请求失败')
|
||||
|
||||
Vendored
+1
@@ -57,6 +57,7 @@ declare module 'vue' {
|
||||
QueryToolbar: typeof import('./components/common/QueryToolbar.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
SingleLineText: typeof import('./components/common/SingleLineText.vue')['default']
|
||||
WorkspaceTabs: typeof import('./components/WorkspaceTabs.vue')['default']
|
||||
}
|
||||
export interface GlobalDirectives {
|
||||
|
||||
@@ -7,6 +7,9 @@ export interface DataTableColumn {
|
||||
columnKey?: string
|
||||
slotName?: string
|
||||
headerSlotName?: string
|
||||
singleLine?: boolean
|
||||
showOverflowTooltip?: boolean
|
||||
className?: string
|
||||
[attribute: string]: unknown
|
||||
}
|
||||
|
||||
@@ -88,10 +91,34 @@ watch([page, pageCount], ([value, count]) => {
|
||||
if (value !== nextValue) page.value = nextValue
|
||||
}, { immediate: true })
|
||||
|
||||
function columnAttrs({ key, slotName, headerSlotName, ...attrs }: DataTableColumn) {
|
||||
function columnAttrs({ key, slotName, headerSlotName, singleLine, ...attrs }: DataTableColumn) {
|
||||
return attrs
|
||||
}
|
||||
|
||||
function systemColumn(column: DataTableColumn) {
|
||||
return column.type === 'selection'
|
||||
|| column.type === 'index'
|
||||
|| column.type === 'expand'
|
||||
|| column.slotName === 'actions'
|
||||
|| column.label === '操作'
|
||||
}
|
||||
|
||||
function singleLineColumn(column: DataTableColumn) {
|
||||
return column.singleLine !== false && !systemColumn(column)
|
||||
}
|
||||
|
||||
function columnOverflowTooltip(column: DataTableColumn) {
|
||||
return singleLineColumn(column) ? column.showOverflowTooltip ?? true : column.showOverflowTooltip
|
||||
}
|
||||
|
||||
function columnClassName(column: DataTableColumn) {
|
||||
return [
|
||||
column.className,
|
||||
systemColumn(column) ? 'data-table__system-cell' : '',
|
||||
singleLineColumn(column) ? 'data-table__single-line-cell' : '',
|
||||
].filter(Boolean).join(' ')
|
||||
}
|
||||
|
||||
function columnRenderKey(column: DataTableColumn, index: number) {
|
||||
return String(column.key ?? column.columnKey ?? column.prop ?? column.property ?? index)
|
||||
}
|
||||
@@ -119,6 +146,8 @@ function setPage(value: number) {
|
||||
v-for="(column, index) in columns"
|
||||
:key="columnRenderKey(column, index)"
|
||||
v-bind="columnAttrs(column)"
|
||||
:class-name="columnClassName(column)"
|
||||
:show-overflow-tooltip="columnOverflowTooltip(column)"
|
||||
>
|
||||
<template v-if="hasSlot(column.headerSlotName)" #header="scope">
|
||||
<slot :name="column.headerSlotName" v-bind="scope" />
|
||||
@@ -168,6 +197,9 @@ function setPage(value: number) {
|
||||
|
||||
<style scoped>
|
||||
.data-table { min-width: 0; }
|
||||
.data-table :deep(.data-table__single-line-cell .cell) { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.data-table :deep(.data-table__system-cell .cell) { white-space: nowrap; }
|
||||
.data-table :deep(.data-table__system-cell .el-button + .el-button), .data-table :deep(.data-table__system-cell .el-button + .el-dropdown) { margin-left: 8px; }
|
||||
.data-table__pagination { display: flex; min-height: 58px; align-items: center; justify-content: space-between; gap: 16px; padding: 12px 18px; border-top: 1px solid var(--border); color: var(--muted); font-size: 12px; }
|
||||
.data-table__pagination-left { display: flex; min-width: 0; flex: 1; align-items: center; gap: 8px; }
|
||||
.data-table__pagination-controls, .data-table__page-size, .data-table__page-buttons { display: flex; align-items: center; gap: 8px; }
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
<script setup lang="ts">
|
||||
withDefaults(defineProps<{
|
||||
value?: string | number | null
|
||||
emptyText?: string
|
||||
}>(), {
|
||||
value: null,
|
||||
emptyText: '—',
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span class="single-line-text" :title="value == null || value === '' ? undefined : String(value)">
|
||||
{{ value == null || value === '' ? emptyText : value }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.single-line-text {
|
||||
display: block;
|
||||
max-width: 220px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
@@ -217,6 +217,7 @@ export interface MerchantFirstLegOrder {
|
||||
customerId: number | string | null
|
||||
customerName: string
|
||||
fulfillmentType?: 'STANDARD' | 'ONE_CLICK_DROPSHIP' | null
|
||||
lxCode?: string | null
|
||||
status: ApiFirstLegStatus
|
||||
transportMode?: ApiTransportMode
|
||||
tradeTerm: string | null
|
||||
|
||||
@@ -35,3 +35,7 @@ test('keeps all backend Long identifiers as strings', () => {
|
||||
assert.doesNotMatch(typesSource, /(?:id|Id): number/)
|
||||
assert.doesNotMatch(typesSource, /internalNotes|statusHistory|files/)
|
||||
})
|
||||
|
||||
test('accepts the one-click dropship palletization process status', () => {
|
||||
assert.match(typesSource, /MerchantInboundProcessStatus[\s\S]*?'WAIT_PALLETIZE'[\s\S]*?'WAIT_SPLIT'/)
|
||||
})
|
||||
|
||||
@@ -4,8 +4,11 @@ import test from 'node:test'
|
||||
|
||||
const source = readFileSync(new URL('./unifiedOrders.ts', import.meta.url), 'utf8')
|
||||
|
||||
test('keeps the merchant unified-order boundary and phase-two WAIT_SPLIT status', () => {
|
||||
test('keeps the merchant unified-order boundary and one-click palletization status', () => {
|
||||
assert.match(source, /const unifiedOrderBaseUrl = '\/merchant\/v1\/orders'/)
|
||||
assert.match(source, /ApiUnifiedOrderMainStatus[\s\S]*?'WAIT_PALLETIZE'/)
|
||||
assert.match(source, /ApiUnifiedOrderMainStatus[\s\S]*?'WAIT_SPLIT'/)
|
||||
assert.match(source, /fulfillmentType\?: 'STANDARD' \| 'ONE_CLICK_DROPSHIP' \| null/)
|
||||
assert.match(source, /lxCode\?: string \| null/)
|
||||
assert.match(source, /mainStatus\?: ApiUnifiedOrderMainStatus/)
|
||||
})
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { ApiContainerMode, ApiDestinationType, ApiFirstLegStatus, ApiPickup
|
||||
export type ApiUnifiedOrderMainStatus =
|
||||
| 'PENDING_REVIEW'
|
||||
| 'WAIT_MEASURE'
|
||||
| 'WAIT_PALLETIZE'
|
||||
| 'WAIT_SPLIT'
|
||||
| 'LOADING'
|
||||
| 'LOADED'
|
||||
@@ -54,6 +55,8 @@ export interface MerchantUnifiedOrderRow {
|
||||
orderId: number | string
|
||||
tenantId: number | string
|
||||
orderNo: string | null
|
||||
fulfillmentType?: 'STANDARD' | 'ONE_CLICK_DROPSHIP' | null
|
||||
lxCode?: string | null
|
||||
mainStatus: ApiUnifiedOrderMainStatus
|
||||
nextAction: string | null
|
||||
internalOrderStatus: ApiFirstLegStatus | null
|
||||
|
||||
@@ -23,7 +23,7 @@ function stageState(index: number) {
|
||||
<div class="progress-heading">
|
||||
<div>
|
||||
<h3 id="order-progress-title">订单进度</h3>
|
||||
<span>国内装柜进度及后续履约同步</span>
|
||||
<span>国内入仓、组托、装柜及后续履约同步</span>
|
||||
</div>
|
||||
<el-tag :type="order.status === 'DELIVERED' ? 'success' : order.status === 'DRAFT' || order.status === 'PENDING_REVIEW' ? 'warning' : 'info'" effect="light">
|
||||
{{ getFirstLegStatusLabel(order.status) }}
|
||||
|
||||
@@ -80,6 +80,8 @@ test('declares the final C02 row and detail contract in the API adapter', () =>
|
||||
assert.match(unifiedApiSource, /orderId: number \| string/)
|
||||
assert.match(unifiedApiSource, /mainStatus: ApiUnifiedOrderMainStatus/)
|
||||
assert.match(unifiedApiSource, /nextAction: string \| null/)
|
||||
assert.match(unifiedApiSource, /fulfillmentType\?: 'STANDARD' \| 'ONE_CLICK_DROPSHIP' \| null/)
|
||||
assert.match(unifiedApiSource, /lxCode\?: string \| null/)
|
||||
assert.match(unifiedApiSource, /customerRemark: string \| null/)
|
||||
for (const field of ['pickupMethod', 'requestedPickupAt', 'containerMode', 'destinationType', 'destinationCode', 'skuNos', 'totalQuantity', 'totalNetWeightKg', 'chargeableVolumeCbm', 'inboundCompletedAt', 'shipmentId', 'referenceId']) {
|
||||
assert.match(unifiedApiSource, new RegExp(`${field}:`))
|
||||
@@ -117,6 +119,10 @@ test('maps C02 authoritative status, transport, cargo and pickup snapshots', ()
|
||||
assert.match(storeSource, /id: String\(summary\.orderId\)/)
|
||||
assert.match(storeSource, /transportMode: unifiedTransport\(summary\.containerMode\)/)
|
||||
assert.match(storeSource, /status: summary\.mainStatus/)
|
||||
assert.match(storeSource, /fulfillmentType: summary\.fulfillmentType \?\? 'STANDARD'/)
|
||||
assert.match(storeSource, /lxCode: summary\.lxCode \?\? ''/)
|
||||
assert.match(storeSource, /mapped\.fulfillmentType = detail\.summary\.fulfillmentType \?\? mapped\.fulfillmentType/)
|
||||
assert.match(storeSource, /mapped\.lxCode = detail\.summary\.lxCode \?\? mapped\.lxCode/)
|
||||
assert.match(storeSource, /requestedPickupAt: summary\.requestedPickupAt \?\? ''/)
|
||||
assert.match(storeSource, /chargeableVolumeCbm: summary\.chargeableVolumeCbm \?\? null/)
|
||||
assert.match(storeSource, /inboundedAt: summary\.inboundCompletedAt \?\? ''/)
|
||||
@@ -127,6 +133,7 @@ test('maps C02 authoritative status, transport, cargo and pickup snapshots', ()
|
||||
assert.match(storeSource, /summary\?\.loading\?\.pickingVersion/)
|
||||
assert.match(storeSource, /summary\?\.inboundId == null \? '' : String\(summary\.inboundId\)/)
|
||||
assert.match(storeSource, /customerRemark: summary\.customerRemark \?\? ''/)
|
||||
assert.match(storeSource, /PALLETIZE_DROPSHIP: '一件代发组托'/)
|
||||
})
|
||||
|
||||
test('sends a manually entered old-system inbound code with the draft payload', () => {
|
||||
|
||||
@@ -231,6 +231,7 @@ const unifiedActionLabels: Readonly<Record<string, string>> = {
|
||||
SUBMIT_ORDER: '订单已提交',
|
||||
APPROVE_ORDER: '订单审核通过',
|
||||
CONFIRM_MEASUREMENT: '测量数据已确认',
|
||||
PALLETIZE_DROPSHIP: '一件代发组托',
|
||||
SMART_SPLIT: '分柜方案已更新',
|
||||
CONFIRM_PRELOAD: '预装清单已确认',
|
||||
COMPLETE_LOADING: '装柜已完成',
|
||||
@@ -279,7 +280,7 @@ function mapOrder(order: MerchantFirstLegOrder, warehouses: PlatformReceivingWar
|
||||
return {
|
||||
id: String(order.id ?? order.orderId ?? ''), orderNo: order.orderNo, referenceNo: order.referenceId ?? '',
|
||||
transportMode: order.transportMode ? clientTransport(order) : null,
|
||||
businessType: 'EXPORT', fulfillmentType: order.fulfillmentType ?? 'STANDARD', origin: shipper?.city ?? '', destination: order.destinationWarehouseName || order.destinationCountryCode || '',
|
||||
businessType: 'EXPORT', fulfillmentType: order.fulfillmentType ?? 'STANDARD', lxCode: order.lxCode ?? '', origin: shipper?.city ?? '', destination: order.destinationWarehouseName || order.destinationCountryCode || '',
|
||||
receivingWarehouseId: String(order.receivingWarehouseId), receivingWarehouseName: order.receivingWarehouseName,
|
||||
receivingWarehouseSnapshot: orderWarehouseSnapshot(order, warehouse), expectedDepartureDate: order.expectedShipDate ?? '', etd: '', eta: '',
|
||||
shipperPartyId: shipper?.id ?? '', shipperPartySnapshot: shipper, shipper: shipper?.name ?? '',
|
||||
@@ -328,7 +329,7 @@ function mapUnifiedOrderRow(summary: MerchantUnifiedOrderRow, warehouses: Platfo
|
||||
const warehouse = warehouses.find((item) => item.id === String(summary.receivingWarehouseId))
|
||||
return {
|
||||
id: String(summary.orderId), orderNo: summary.orderNo, referenceNo: summary.referenceId ?? '', transportMode: unifiedTransport(summary.containerMode),
|
||||
businessType: 'EXPORT', origin: '', destination: summary.destinationWarehouseName ?? '',
|
||||
businessType: 'EXPORT', fulfillmentType: summary.fulfillmentType ?? 'STANDARD', lxCode: summary.lxCode ?? '', origin: '', destination: summary.destinationWarehouseName ?? '',
|
||||
receivingWarehouseId: summary.receivingWarehouseId == null ? '' : String(summary.receivingWarehouseId),
|
||||
receivingWarehouseName: summary.receivingWarehouseName ?? '', receivingWarehouseSnapshot: warehouseSnapshot(warehouse),
|
||||
expectedDepartureDate: summary.expectedShipDate ?? '', etd: '', eta: '', shipperPartyId: '', shipperPartySnapshot: null,
|
||||
@@ -381,6 +382,8 @@ function mapUnifiedOrderDetail(detail: MerchantUnifiedOrderDetail, warehouses: P
|
||||
const mapped = mapOrder(detail.order.order, warehouses, detail.order)
|
||||
mapped.id = String(detail.summary.orderId)
|
||||
mapped.status = detail.summary.mainStatus
|
||||
mapped.fulfillmentType = detail.summary.fulfillmentType ?? mapped.fulfillmentType
|
||||
mapped.lxCode = detail.summary.lxCode ?? mapped.lxCode
|
||||
mapped.businessSummary = businessSummary(detail.summary)
|
||||
mapped.timeline = unifiedTimeline(detail)
|
||||
const summary = mapUnifiedOrderRow(detail.summary, warehouses)
|
||||
|
||||
@@ -6,6 +6,7 @@ export type MerchantInboundProcessStatus =
|
||||
| 'WAIT_PRICING'
|
||||
| 'FEE_CONFIRMING'
|
||||
| 'INBOUNDING'
|
||||
| 'WAIT_PALLETIZE'
|
||||
| 'WAIT_SPLIT'
|
||||
| 'VOIDED'
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@ import {
|
||||
canClientCancelFirstLegOrder,
|
||||
canClientEditFirstLegOrder,
|
||||
createEmptyCargoItem,
|
||||
FIRST_LEG_PROGRESS_STAGES,
|
||||
getFirstLegProgressIndex,
|
||||
getFirstLegStatusLabel,
|
||||
getFirstLegTransportModeLabel,
|
||||
summarizeFirstLegCargo,
|
||||
@@ -58,12 +60,16 @@ function validDraft() {
|
||||
|
||||
test('uses the confirmed status and transport labels', () => {
|
||||
assert.equal(getFirstLegStatusLabel('WAIT_MEASURE'), '待测量数据')
|
||||
assert.equal(getFirstLegStatusLabel('WAIT_SPLIT'), '待分柜')
|
||||
assert.equal(getFirstLegStatusLabel('WAIT_PALLETIZE'), '待组托')
|
||||
assert.equal(getFirstLegStatusLabel('WAIT_SPLIT'), '待分柜')
|
||||
assert.equal(getFirstLegStatusLabel('LOADING'), '装柜中')
|
||||
assert.equal(getFirstLegStatusLabel('LOADED'), '装柜完成')
|
||||
assert.equal(getFirstLegStatusLabel('DELIVERED'), '派送完成')
|
||||
assert.equal(getFirstLegTransportModeLabel('FCL'), '海运整箱 FCL')
|
||||
assert.deepEqual(FIRST_LEG_PROGRESS_STAGES.map((item) => item.code), [
|
||||
'PENDING_REVIEW', 'WAIT_MEASURE', 'WAIT_PALLETIZE', 'WAIT_SPLIT', 'LOADING', 'LOADED', 'DELIVERED',
|
||||
])
|
||||
assert.equal(getFirstLegProgressIndex('WAIT_PALLETIZE'), 2)
|
||||
})
|
||||
|
||||
test('only allows draft orders to be edited in the confirmed phase-one flow', () => {
|
||||
@@ -72,6 +78,7 @@ test('only allows draft orders to be edited in the confirmed phase-one flow', ()
|
||||
assert.equal(canClientCancelFirstLegOrder('DRAFT'), true)
|
||||
assert.equal(canClientCancelFirstLegOrder('PENDING_REVIEW'), true)
|
||||
assert.equal(canClientCancelFirstLegOrder('WAIT_MEASURE'), false)
|
||||
assert.equal(canClientCancelFirstLegOrder('WAIT_PALLETIZE'), false)
|
||||
assert.equal(canClientCancelFirstLegOrder('WAIT_SPLIT'), false)
|
||||
})
|
||||
|
||||
|
||||
@@ -199,7 +199,7 @@ test('shows one unified progress line and C02 business summaries without detail
|
||||
assert.match(source, /summary\.expectedPickCount/)
|
||||
assert.match(source, /summary\.deliveryTrackingNo/)
|
||||
assert.match(progressSource, /FIRST_LEG_PROGRESS_STAGES/)
|
||||
assert.match(progressSource, /国内装柜进度及后续履约同步/)
|
||||
assert.match(progressSource, /国内入仓、组托、装柜及后续履约同步/)
|
||||
assert.doesNotMatch(progressSource, /进度记录|el-timeline|timeline/)
|
||||
assert.doesNotMatch(source, /<h3>报关回执<\/h3>/)
|
||||
assert.match(source, /船期摘要/)
|
||||
@@ -369,6 +369,12 @@ test('distills order creation into the confirmed prototype sections with server
|
||||
assert.doesNotMatch(source, /v-model="form\.(?:businessType|origin|destination|priority|tradeTerm)"/)
|
||||
})
|
||||
|
||||
test('shows an optional LX inbound order number for one-click dropship orders', () => {
|
||||
assert.match(source, /v-if="form\.fulfillmentType === 'ONE_CLICK_DROPSHIP'" label="LX入库单号" prop="lxCode"/)
|
||||
assert.match(source, /v-model="form\.lxCode"[^>]*placeholder="请输入LX入库单号"/)
|
||||
assert.doesNotMatch(source, /label="LX入库单号"[^>]*required/)
|
||||
})
|
||||
|
||||
test('loads only active server channels for the selected domestic warehouse', () => {
|
||||
assert.match(source, /getMerchantPricingChannels\(warehouseId\)/)
|
||||
assert.match(source, /const channelOptions = ref<MerchantPricingChannel\[\]>\(\[\]\)/)
|
||||
|
||||
@@ -1898,7 +1898,7 @@ async function searchOrders(model: QueryModel) {
|
||||
<div class="order-section-heading"><div><h3>客户下单</h3><p>先选择货物如何送到国内收货仓,并填写本次运输安排。</p></div></div>
|
||||
<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"><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 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>
|
||||
<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">
|
||||
|
||||
@@ -33,5 +33,7 @@ test('keeps overseas warehouse operations first and renders readable list values
|
||||
assert.match(overseas, /function statusLabel\(value: string\)/)
|
||||
assert.match(overseas, /function deliveryMethodLabel\(value: string\)/)
|
||||
assert.match(overseas, /function formatDateTime\(value: string\)/)
|
||||
assert.match(overseas, /import SingleLineText from '@\/components\/common\/SingleLineText\.vue'/)
|
||||
assert.match(overseas, /<SingleLineText :value="row\[String\(index\)\]"/)
|
||||
assert.match(overseas, /<td><button class="warehouse-link"[^>]*>.*?查看<\/button><\/td><td v-for="\(_, index\) in config\.columns\.slice\(1\)"/s)
|
||||
})
|
||||
|
||||
@@ -17,8 +17,8 @@ export interface InboundPlanSummary {
|
||||
planNo: string
|
||||
inboundCode: string
|
||||
orderType: string | null
|
||||
warehouseCode: string | null
|
||||
warehouseName: string | null
|
||||
overseasWarehouseCode?: string | null
|
||||
overseasWarehouseName?: string | null
|
||||
fbaWarehouseCode: string | null
|
||||
packageCount: number
|
||||
pickedPackageCount: number
|
||||
@@ -52,6 +52,8 @@ export interface InboundPlanPallet {
|
||||
export interface InboundPlanDetail {
|
||||
planNo: string
|
||||
containerNo: string | null
|
||||
overseasWarehouseCode?: string | null
|
||||
overseasWarehouseName?: string | null
|
||||
status: string
|
||||
lineCount: number
|
||||
palletCount: number
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import test from 'node:test'
|
||||
import { overseasWarehouseLabel } from './inboundPlanDisplay.ts'
|
||||
|
||||
const api = readFileSync(new URL('./api/inboundPlans.ts', import.meta.url), 'utf8')
|
||||
const routes = readFileSync(new URL('./routes.ts', import.meta.url), 'utf8')
|
||||
const view = readFileSync(new URL('./views/InboundOrdersView.vue', import.meta.url), 'utf8')
|
||||
const overseasHttp = readFileSync(new URL('../../api/overseasHttp.ts', import.meta.url), 'utf8')
|
||||
|
||||
test('activates the customer-scoped overseas inbound list and detail', () => {
|
||||
assert.match(routes, /path: 'orders\/inbound'[\s\S]*?release: 'active'[\s\S]*?permission: 'merchant:warehouse:inbound:list'/)
|
||||
assert.match(api, /\/merchant\/v1\/last-leg\/inbound-plans/)
|
||||
assert.match(api, /encodeURIComponent\(planNo\)/)
|
||||
assert.doesNotMatch(api, /\/platform\/|\/internal\/|customerId|tenantId/)
|
||||
assert.doesNotMatch(view, /customerId|tenantId/)
|
||||
})
|
||||
|
||||
test('keeps actions first and exposes filters loading errors empty state pagination and business identifiers', () => {
|
||||
@@ -23,3 +26,49 @@ test('keeps actions first and exposes filters loading errors empty state paginat
|
||||
assert.match(view, /detail\.planNo/)
|
||||
assert.doesNotMatch(view, /planId|orderId|warehouseId|customerId/)
|
||||
})
|
||||
|
||||
test('shows only the server-provided overseas receiving warehouse code and name', () => {
|
||||
assert.equal((api.match(/overseasWarehouseCode\?: string \| null/g) || []).length, 2)
|
||||
assert.equal((api.match(/overseasWarehouseName\?: string \| null/g) || []).length, 2)
|
||||
assert.doesNotMatch(api, /overseasWarehouseId/)
|
||||
assert.match(view, /label="海外接柜仓"/)
|
||||
assert.match(view, /overseasWarehouseLabel\(scope\.row\.overseasWarehouseCode, scope\.row\.overseasWarehouseName\)/)
|
||||
assert.match(view, /overseasWarehouseLabel\(detail\.overseasWarehouseCode, detail\.overseasWarehouseName\)/)
|
||||
assert.doesNotMatch(view, /warehouseName \|\| warehouseCode|destinationWarehouse(?:Id|Code|Name).*overseasWarehouse/i)
|
||||
})
|
||||
|
||||
test('renders a dash when either formal receiving warehouse field is absent', () => {
|
||||
assert.equal(overseasWarehouseLabel('US-LAX-01', '洛杉矶仓'), 'US-LAX-01 · 洛杉矶仓')
|
||||
assert.equal(overseasWarehouseLabel(' US-LAX-01 ', ' 洛杉矶仓 '), 'US-LAX-01 · 洛杉矶仓')
|
||||
for (const pair of [
|
||||
[undefined, undefined], [null, null], ['', '洛杉矶仓'], ['US-LAX-01', ''],
|
||||
[undefined, '洛杉矶仓'], ['US-LAX-01', undefined], [' ', '洛杉矶仓'], ['US-LAX-01', ' '],
|
||||
]) assert.equal(overseasWarehouseLabel(pair[0], pair[1]), '—')
|
||||
})
|
||||
|
||||
test('preserves overseas API diagnostic metadata without rendering it as a business identifier', () => {
|
||||
assert.match(overseasHttp, /response\.data\.traceId/)
|
||||
assert.match(overseasHttp, /response\.data\.requestId/)
|
||||
assert.match(overseasHttp, /body\.traceId, body\.requestId/)
|
||||
assert.doesNotMatch(view, /requestId|traceId/)
|
||||
})
|
||||
|
||||
test('renders inbound workflow statuses in Chinese and keeps desktop values on one line', () => {
|
||||
for (const [status, label] of [
|
||||
['PENDING_ASSIGNMENT', '待分配'], ['PDA_PENDING', '待收货'], ['ASSIGNED', '已分配'],
|
||||
['SIGNED', '已签收'], ['PICKING', '拣货中'], ['PARTIAL_INBOUND', '部分入库'],
|
||||
]) assert.match(view, new RegExp(`${status}: '${label}'`))
|
||||
assert.match(view, /label="状态" width="120" show-overflow-tooltip/)
|
||||
assert.match(view, /\.detail-grid div \{ min-width: 0;/)
|
||||
assert.match(view, /\.detail-grid dd \{[^}]*overflow: hidden;[^}]*text-overflow: ellipsis;[^}]*white-space: nowrap;/)
|
||||
})
|
||||
|
||||
test('uses the formal inbound status selector and presents destination fields without mixing receiving warehouses', () => {
|
||||
assert.match(view, /const inboundStatusOptions = \[/)
|
||||
assert.match(view, /label="作业状态"><el-select v-model="query\.status"/)
|
||||
assert.doesNotMatch(view, /label="作业状态"><el-input/)
|
||||
assert.match(view, /destinationWarehouseLabel\(scope\.row\.fbaWarehouseCode, scope\.row\.destinationWarehouseCode, scope\.row\.destinationWarehouseName\)/)
|
||||
assert.match(view, /destinationWarehouseLabel\(scope\.row\.destinationWarehouseCode, scope\.row\.destinationWarehouseName\)/)
|
||||
assert.doesNotMatch(view, /label="目的仓代码"|label="目的仓名称"/)
|
||||
assert.doesNotMatch(view, /destinationWarehouseLabel\([^)]*overseasWarehouse/)
|
||||
})
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
export function overseasWarehouseLabel(code: unknown, name: unknown) {
|
||||
if (typeof code !== 'string' || !code.trim() || typeof name !== 'string' || !name.trim()) return '—'
|
||||
return `${code.trim()} · ${name.trim()}`
|
||||
}
|
||||
@@ -2,21 +2,42 @@
|
||||
import { onMounted, reactive, ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { getInboundPlan, listInboundPlans, type InboundPlanDetail, type InboundPlanSummary } from '../api/inboundPlans'
|
||||
import { overseasWarehouseLabel } from '../inboundPlanDisplay'
|
||||
|
||||
const loading = ref(false)
|
||||
const error = ref('')
|
||||
const rows = ref<InboundPlanSummary[]>([])
|
||||
const detail = ref<InboundPlanDetail | null>(null)
|
||||
const total = ref(0)
|
||||
const inboundStatusOptions = [
|
||||
{ label: '待分配', value: 'PENDING_ASSIGNMENT' },
|
||||
{ label: '已分配', value: 'ASSIGNED' },
|
||||
{ label: '待收货', value: 'PDA_PENDING' },
|
||||
{ label: '已签收', value: 'SIGNED' },
|
||||
{ label: '拣货中', value: 'PICKING' },
|
||||
{ label: '部分入库', value: 'PARTIAL_INBOUND' },
|
||||
{ label: '已完成', value: 'COMPLETED' },
|
||||
]
|
||||
const query = reactive({
|
||||
page: 1, size: 20, planNo: '', inboundCode: '', destinationWarehouseCode: '', status: '',
|
||||
appointmentStatus: '', dispatchStatus: '', createdDates: [] as string[],
|
||||
})
|
||||
|
||||
function text(value: unknown) { return value === null || value === undefined || value === '' ? '—' : String(value) }
|
||||
function destinationWarehouseLabel(...values: unknown[]) {
|
||||
const parts = values
|
||||
.filter((value): value is string => typeof value === 'string' && Boolean(value.trim()))
|
||||
.map((value) => value.trim())
|
||||
return [...new Set(parts)].join(' · ') || '—'
|
||||
}
|
||||
function dateTime(value: unknown) { return value ? new Date(String(value)).toLocaleString('zh-CN', { hour12: false }) : '—' }
|
||||
function statusLabel(value: unknown) {
|
||||
return ({ PENDING: '待处理', PROCESSING: '处理中', COMPLETED: '已完成', CANCELLED: '已取消', UNAPPOINTED: '未预约', SUCCESS: '预约成功', FAILED: '预约失败', SCHEDULED: '已排车', UNSCHEDULED: '未排车' } as Record<string, string>)[String(value)] || text(value)
|
||||
return ({
|
||||
PENDING: '待处理', PROCESSING: '处理中', PENDING_ASSIGNMENT: '待分配', PDA_PENDING: '待收货',
|
||||
ASSIGNED: '已分配', SIGNED: '已签收', PICKING: '拣货中', PARTIAL_INBOUND: '部分入库',
|
||||
COMPLETED: '已完成', CANCELLED: '已取消', UNAPPOINTED: '未预约', SUCCESS: '预约成功',
|
||||
FAILED: '预约失败', SCHEDULED: '已排车', UNSCHEDULED: '未排车',
|
||||
} as Record<string, string>)[String(value)] || text(value)
|
||||
}
|
||||
function errorMessage(cause: unknown) { return cause instanceof Error ? cause.message : '请求失败,请稍后重试' }
|
||||
|
||||
@@ -70,7 +91,7 @@ onMounted(() => void load())
|
||||
<el-form-item label="计划号"><el-input v-model.trim="query.planNo" clearable placeholder="请输入计划号" /></el-form-item>
|
||||
<el-form-item label="入仓码"><el-input v-model.trim="query.inboundCode" clearable placeholder="请输入入仓码" /></el-form-item>
|
||||
<el-form-item label="目的仓"><el-input v-model.trim="query.destinationWarehouseCode" clearable placeholder="请输入仓库代码" /></el-form-item>
|
||||
<el-form-item label="作业状态"><el-input v-model.trim="query.status" clearable placeholder="请输入状态" /></el-form-item>
|
||||
<el-form-item label="作业状态"><el-select v-model="query.status" clearable placeholder="全部"><el-option v-for="option in inboundStatusOptions" :key="option.value" :label="option.label" :value="option.value" /></el-select></el-form-item>
|
||||
<el-form-item label="预约状态"><el-select v-model="query.appointmentStatus" clearable placeholder="全部"><el-option label="未预约" value="UNAPPOINTED" /><el-option label="待处理" value="PENDING" /><el-option label="成功" value="SUCCESS" /><el-option label="失败" value="FAILED" /><el-option label="已取消" value="CANCELLED" /></el-select></el-form-item>
|
||||
<el-form-item label="排车状态"><el-select v-model="query.dispatchStatus" clearable placeholder="全部"><el-option label="未排车" value="UNSCHEDULED" /><el-option label="已排车" value="SCHEDULED" /></el-select></el-form-item>
|
||||
<el-form-item label="创建时间"><el-date-picker v-model="query.createdDates" type="daterange" value-format="YYYY-MM-DD" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期" /></el-form-item>
|
||||
@@ -82,15 +103,15 @@ onMounted(() => void load())
|
||||
<el-table-column label="操作" width="90" fixed="left"><template #default="scope"><el-button link type="primary" @click="openDetail(scope.row)">查看</el-button></template></el-table-column>
|
||||
<el-table-column prop="planNo" label="计划号" min-width="180" show-overflow-tooltip />
|
||||
<el-table-column prop="inboundCode" label="入仓码" min-width="170" show-overflow-tooltip />
|
||||
<el-table-column label="海外仓" min-width="160" show-overflow-tooltip><template #default="scope">{{ text(scope.row.warehouseName || scope.row.warehouseCode) }}</template></el-table-column>
|
||||
<el-table-column label="海外接柜仓" min-width="220" show-overflow-tooltip><template #default="scope">{{ overseasWarehouseLabel(scope.row.overseasWarehouseCode, scope.row.overseasWarehouseName) }}</template></el-table-column>
|
||||
<el-table-column label="目的仓" min-width="150" show-overflow-tooltip><template #default="scope">{{ text(scope.row.fbaWarehouseCode) }}</template></el-table-column>
|
||||
<el-table-column prop="packageCount" label="计划箱数" width="100" />
|
||||
<el-table-column prop="pickedPackageCount" label="已拣箱数" width="100" />
|
||||
<el-table-column prop="palletCount" label="托盘数" width="90" />
|
||||
<el-table-column label="状态" width="120"><template #default="scope">{{ statusLabel(scope.row.status) }}</template></el-table-column>
|
||||
<el-table-column label="预约状态" width="120"><template #default="scope">{{ statusLabel(scope.row.appointmentStatus) }}</template></el-table-column>
|
||||
<el-table-column label="排车状态" width="110"><template #default="scope">{{ statusLabel(scope.row.dispatchStatus) }}</template></el-table-column>
|
||||
<el-table-column label="创建时间" width="180"><template #default="scope">{{ dateTime(scope.row.createdAt) }}</template></el-table-column>
|
||||
<el-table-column label="状态" width="120" show-overflow-tooltip><template #default="scope">{{ statusLabel(scope.row.status) }}</template></el-table-column>
|
||||
<el-table-column label="预约状态" width="120" show-overflow-tooltip><template #default="scope">{{ statusLabel(scope.row.appointmentStatus) }}</template></el-table-column>
|
||||
<el-table-column label="排车状态" width="110" show-overflow-tooltip><template #default="scope">{{ statusLabel(scope.row.dispatchStatus) }}</template></el-table-column>
|
||||
<el-table-column label="创建时间" width="180" show-overflow-tooltip><template #default="scope">{{ dateTime(scope.row.createdAt) }}</template></el-table-column>
|
||||
</el-table>
|
||||
<el-pagination v-model:current-page="query.page" v-model:page-size="query.size" class="pagination" :total="total" :page-sizes="[10, 20, 50, 100]" layout="total, sizes, prev, pager, next" @change="load" />
|
||||
</el-card>
|
||||
@@ -100,6 +121,7 @@ onMounted(() => void load())
|
||||
<el-card shadow="never">
|
||||
<dl class="detail-grid">
|
||||
<div><dt>计划号</dt><dd>{{ detail.planNo }}</dd></div><div><dt>集装箱号</dt><dd>{{ text(detail.containerNo) }}</dd></div>
|
||||
<div><dt>海外接柜仓</dt><dd>{{ overseasWarehouseLabel(detail.overseasWarehouseCode, detail.overseasWarehouseName) }}</dd></div>
|
||||
<div><dt>状态</dt><dd>{{ statusLabel(detail.status) }}</dd></div><div><dt>入仓码数</dt><dd>{{ detail.lineCount }}</dd></div>
|
||||
<div><dt>托盘数</dt><dd>{{ detail.palletCount }}</dd></div><div><dt>总体积(m³)</dt><dd>{{ detail.totalVolumeCbm }}</dd></div>
|
||||
<div><dt>计划箱数</dt><dd>{{ detail.expectedPackageCount }}</dd></div><div><dt>已拣箱数</dt><dd>{{ detail.pickedPackageCount }}</dd></div>
|
||||
@@ -107,14 +129,14 @@ onMounted(() => void load())
|
||||
</el-card>
|
||||
<el-card shadow="never" header="入仓明细">
|
||||
<el-table :data="detail.lines" border empty-text="暂无入仓明细">
|
||||
<el-table-column prop="inboundCode" label="入仓码" min-width="180" /><el-table-column prop="destinationWarehouseCode" label="目的仓代码" min-width="140" />
|
||||
<el-table-column prop="destinationWarehouseName" label="目的仓名称" min-width="160" /><el-table-column prop="requiredPackageCount" label="计划箱数" width="100" />
|
||||
<el-table-column prop="inboundCode" label="入仓码" min-width="180" /><el-table-column label="目的仓" min-width="220" show-overflow-tooltip><template #default="scope">{{ destinationWarehouseLabel(scope.row.fbaWarehouseCode, scope.row.destinationWarehouseCode, scope.row.destinationWarehouseName) }}</template></el-table-column>
|
||||
<el-table-column prop="requiredPackageCount" label="计划箱数" width="100" />
|
||||
<el-table-column prop="plannedPalletCount" label="计划托数" width="100" /><el-table-column prop="volumeCbm" label="体积(m³)" width="110" />
|
||||
</el-table>
|
||||
</el-card>
|
||||
<el-card shadow="never" header="托盘明细">
|
||||
<el-table :data="detail.pallets" border empty-text="暂无托盘">
|
||||
<el-table-column prop="palletNo" label="托盘码" min-width="180" /><el-table-column prop="destinationWarehouseCode" label="目的仓" min-width="150" />
|
||||
<el-table-column prop="palletNo" label="托盘码" min-width="180" /><el-table-column label="目的仓" min-width="220" show-overflow-tooltip><template #default="scope">{{ destinationWarehouseLabel(scope.row.destinationWarehouseCode, scope.row.destinationWarehouseName) }}</template></el-table-column>
|
||||
<el-table-column prop="capacityCbm" label="容量(m³)" width="120" /><el-table-column prop="allocatedVolumeCbm" label="已分配体积(m³)" width="140" />
|
||||
</el-table>
|
||||
</el-card>
|
||||
@@ -131,7 +153,7 @@ onMounted(() => void load())
|
||||
.filter-card :deep(.el-input), .filter-card :deep(.el-select) { width: 190px; }
|
||||
.pagination { justify-content: flex-end; margin-top: 18px; }
|
||||
.detail-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); margin: 0; }
|
||||
.detail-grid div { padding: 18px 20px; border-right: 1px solid #ebeef5; border-bottom: 1px solid #ebeef5; }
|
||||
.detail-grid div { min-width: 0; padding: 18px 20px; border-right: 1px solid #ebeef5; border-bottom: 1px solid #ebeef5; }
|
||||
.detail-grid dt { margin-bottom: 7px; color: #8492a6; font-size: 13px; }
|
||||
.detail-grid dd { margin: 0; overflow-wrap: anywhere; color: #1f2937; }
|
||||
.detail-grid dd { margin: 0; overflow: hidden; color: #1f2937; text-overflow: ellipsis; white-space: nowrap; }
|
||||
</style>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { ArrowLeft, Download, Eye, Plus, Search, Trash2, Upload } from '@lucide/vue'
|
||||
import SingleLineText from '@/components/common/SingleLineText.vue'
|
||||
import {
|
||||
createDiscard, createRelabel, createTransfer, getDiscard, getRelabel, getTransfer,
|
||||
listDiscardCandidates, listDiscards, listLastlegOrders, listRelabelInventoryCandidates, listRelabels, listTransfers,
|
||||
@@ -236,15 +237,6 @@ function selectDiscardInbound(orderId: string) {
|
||||
}
|
||||
function dateTime(value: string, end = false) { return value ? `${value}T${end ? '23:59:59' : '00:00:00'}` : undefined }
|
||||
function row(values: Array<string | number | undefined | null>) { return Object.fromEntries(values.map((value, index) => [String(index), value == null || value === '' ? '—' : String(value)])) }
|
||||
function statusLabel(value: string) {
|
||||
return statusOptions.value.find((option) => option.value === value)?.label || value
|
||||
}
|
||||
function deliveryMethodLabel(value: string) {
|
||||
return ({ TRUCK: '卡车派送', EXPRESS: '快递派送', '卡车派送': '卡车派送', '快递派送': '快递派送' } as Record<string, string>)[value] || value
|
||||
}
|
||||
function formatDateTime(value: string) {
|
||||
return value ? value.replace('T', ' ').slice(0, 19) : '—'
|
||||
}
|
||||
function warehouseDisplay(value?: string) {
|
||||
if (!value) return '—'
|
||||
return value.split(',').map((code) => {
|
||||
@@ -252,6 +244,23 @@ function warehouseDisplay(value?: string) {
|
||||
return warehouse ? warehouseLabel(warehouse) : code
|
||||
}).join('、')
|
||||
}
|
||||
function statusLabel(value: string) {
|
||||
const labels: Record<string, string> = {
|
||||
|
||||
PENDING_PROCESSING: '待处理', PROCESSING: '处理中', COMPLETED: '已完成', CANCELLED: '已取消',
|
||||
PENDING_REVIEW: '待审核', APPROVED: '已通过', REJECTED: '已拒绝', PENDING_ASSIGNMENT: '待分配',
|
||||
ALLOCATED: '已分配', OUTBOUNDED: '已出库', PARTIALLY_RECEIVED: '部分收货', RECEIVED: '已收货',
|
||||
|
||||
}
|
||||
return labels[value] || value || '—'
|
||||
}
|
||||
function deliveryMethodLabel(value: string) {
|
||||
const labels: Record<string, string> = { TRUCK: '卡车派送', EXPRESS: '快递派送' }
|
||||
return labels[value] || value || '—'
|
||||
}
|
||||
function formatDateTime(value: string) {
|
||||
return value ? value.replace('T', ' ').slice(0, 19) : '—'
|
||||
}
|
||||
async function load() {
|
||||
loading.value = true
|
||||
try {
|
||||
@@ -409,7 +418,7 @@ function selectRelabelInventory(inboundOrderId: string) {
|
||||
<div class="warehouse-filter-actions"><el-button type="primary" @click="query"><Search :size="16" />查询</el-button><el-button @click="resetFilters(); query()">重置</el-button></div>
|
||||
</div>
|
||||
<div v-loading="loading" class="warehouse-card warehouse-table-card">
|
||||
<div class="warehouse-table-scroll"><table><thead><tr><th v-for="column in config.columns" :key="column">{{ column }}</th></tr></thead><tbody><tr v-for="(row, rowIndex) in submittedRows" :key="rowIndex"><td><button class="warehouse-link" type="button" @click="openDetail(row)"><Eye :size="15" />查看</button></td><td v-for="(_, index) in config.columns.slice(1)" :key="index">{{ row[String(index)] }}</td></tr><tr v-if="submittedRows.length === 0"><td :colspan="config.columns.length"><div class="warehouse-empty">{{ config.emptyText }}</div></td></tr></tbody></table></div>
|
||||
<div class="warehouse-table-scroll"><table><thead><tr><th v-for="column in config.columns" :key="column">{{ column }}</th></tr></thead><tbody><tr v-for="(row, rowIndex) in submittedRows" :key="rowIndex"><td><button class="warehouse-link" type="button" @click="openDetail(row)"><Eye :size="15" />查看</button></td><td v-for="(_, index) in config.columns.slice(1)" :key="index"><SingleLineText :value="row[String(index)]" /></td></tr><tr v-if="submittedRows.length === 0"><td :colspan="config.columns.length"><div class="warehouse-empty">{{ config.emptyText }}</div></td></tr></tbody></table></div>
|
||||
<div class="warehouse-pagination"><span>共 <strong>{{ submittedRows.length }}</strong> 条</span><el-button size="small" disabled>上一页</el-button><button type="button" class="warehouse-page-number">1</button><el-button size="small" disabled>下一页</el-button></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user