diff --git a/AGENTS.md b/AGENTS.md index 4a7a97b..019eff2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -52,9 +52,9 @@ | 统一订单展示 | `src/modules/order-management/` | `src/modules/first-leg/` 与对应后端订单契约 | | 客户财务账单、入仓码联查、收款、未收余额与客户确认 | `src/modules/finance/`;确认与明细入口 `views/FinanceBillsView.vue`、`api/finance.ts`,入仓码通过 `src/modules/first-leg/public.ts` 复用 `InboundCodeAssociationDialog` | Backend `finance/`;账单行只显示服务端 `inboundCode` 并按登录客户查询业务关系,缺失显示“—”,不得回退内部 ID。收款方式由服务端枚举映射中文。出账或已出账调整后为 `PENDING_CONFIRMATION`,客户确认提交登录成员、`expectedVersion` 和幂等键 | | 客户资金账户、授信与充值申请 | `src/modules/finance/views/FinanceAccountView.vue`、`api/account.ts` | 显示当前企业 CNY 实际余额、冻结金额、授信额度、已用授信、冻结授信、总可用余额和不可变流水;数据加载前显示“—”,不得先伪装为零。充值只展示服务端启用的微信/支付宝静态收款码,客户上传 JPG/PNG 付款截图后进入待审核。客户范围只取登录态,无收款码时禁止提交,不模拟支付成功 | -| 海外采购、美国本土卡与美本土散板 | `src/modules/cross-border/routes.ts`、`components/BusinessOrdersView.vue`、`api/businessOrders.ts` | 三个路由均为 `active`,客户范围只取 Token;Backend `lastleg-service/` 的 `procurement/`、`trucking/`、`looseboard/` 客户接口 | +| 海外采购、美国本土卡与美本土散板 | `src/modules/cross-border/routes.ts`、`components/BusinessOrdersView.vue`、`api/businessOrders.ts` | 三个路由均为 `active`,客户范围只取 Token;采购创建的 `customerName` 只取当前认证会话企业名称,不允许自由输入或冒用其他客户。Backend `lastleg-service/` 的 `procurement/`、`trucking/`、`looseboard/` 客户接口 | | 海外仓入库订单 | `src/modules/order-management/routes.ts`、`views/InboundOrdersView.vue`、`api/inboundPlans.ts` | `orders/inbound` 为只读列表/详情,权限 `merchant:warehouse:inbound:list`;不展示或提交内部 ID | -| 海外仓贴标、仓储转运、货物废弃、退货换标 | `src/modules/overseas-warehouse/routes.ts`、`views/WarehouseServiceView.vue`、`api/warehouseServices.ts` | 四个路由均为 `active`;Backend `lastleg-service/` 的 `relabel/`、`transfer/`、`discard/`,请求统一经 `src/api/overseasHttp.ts` 与 `gateway-overseas/` | +| 海外仓贴标、仓储转运、货物废弃、退货换标 | `src/modules/overseas-warehouse/routes.ts`、`views/WarehouseServiceView.vue`、`api/warehouseServices.ts` | 四个路由均为 `active`;贴标申请先选国外中转仓和有库存历史入仓单,换标申请必须提交正数 CNY `chargeAmount`,换标类型为只换 SKU、只换箱唛、SKU+箱唛。箱唛换标明细一行固定代表一箱,箱数不可编辑,提交也固定为 1。货物明细外层始终提供“SKU 关联”“箱号关联”“SKU 混箱换标”三套模板的下载、上传,不随换标类型切换;上传只进入待导入关系区,客户可勾选部分或全选后导入本次明细,只有进入明细的库存才随申请提交和冻结。明细也支持手动新增编辑,组合换标以旧箱唛连接旧/新 SKU 和旧箱唛/新 FBA 箱唛关系。涉及箱唛换标时必须选择卡车、自提或快递;快递可选择打印面单并上传 PDF/图片。PDA 逐箱换标、上传照片并由仓库复核后,客户详情展示完工照片和“换标完成确认”;客户确认前不得继续出库,确认后卡车进入预排车,自提/快递进入换标出库待创建。历史库存位置不能解释成箱唛与具体托盘快照,客户页面不暴露托盘、库位和内部库存 ID | ## 租户、认证与数据安全 diff --git a/shared/src/index.ts b/shared/src/index.ts index ceb64c2..42c4559 100644 --- a/shared/src/index.ts +++ b/shared/src/index.ts @@ -86,6 +86,7 @@ export const FIRST_LEG_ORDER_STATUSES = [ { code: 'DRAFT', label: '草稿' }, { code: 'PENDING_REVIEW', label: '待审核' }, { code: 'WAIT_MEASURE', label: '待测量数据' }, + { code: 'WAIT_PALLETIZE', label: '待组托' }, { code: 'WAIT_SPLIT', label: '待分柜' }, { code: 'LOADING', label: '装柜中' }, { code: 'LOADED', label: '装柜完成' }, diff --git a/src/components.d.ts b/src/components.d.ts index eb92a23..befd71f 100644 --- a/src/components.d.ts +++ b/src/components.d.ts @@ -23,6 +23,8 @@ declare module 'vue' { ElCheckbox: typeof import('element-plus/es')['ElCheckbox'] ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup'] ElDatePicker: typeof import('element-plus/es')['ElDatePicker'] + ElDescriptions: typeof import('element-plus/es')['ElDescriptions'] + ElDescriptionsItem: typeof import('element-plus/es')['ElDescriptionsItem'] ElDialog: typeof import('element-plus/es')['ElDialog'] ElDrawer: typeof import('element-plus/es')['ElDrawer'] ElDropdown: typeof import('element-plus/es')['ElDropdown'] diff --git a/src/modules/finance/api/account.ts b/src/modules/finance/api/account.ts index 1928d09..703036f 100644 --- a/src/modules/finance/api/account.ts +++ b/src/modules/finance/api/account.ts @@ -8,6 +8,23 @@ export interface PageResult { pages: number } +interface PageResultWire extends Omit, 'total' | 'page' | 'size' | 'pages'> { + total: number | string + page: number | string + size: number | string + pages: number | string +} + +function normalizePageResult(result: PageResultWire): PageResult { + return { + ...result, + total: Number(result.total), + page: Number(result.page), + size: Number(result.size), + pages: Number(result.pages), + } +} + export interface FinanceAccount { currencyCode: 'CNY' actualBalance: string @@ -71,20 +88,22 @@ export function getFinanceAccount() { return apiRequest({ method: 'GET', url: '/client/finance/v1/account' }) } -export function getAccountLedger(page: number, size: number) { - return apiRequest>({ +export async function getAccountLedger(page: number, size: number) { + const result = await apiRequest>({ method: 'GET', url: '/client/finance/v1/account/ledger', params: { page, size }, }) + return normalizePageResult(result) } -export function getRechargeRequests(page: number, size: number) { - return apiRequest>({ +export async function getRechargeRequests(page: number, size: number) { + const result = await apiRequest>({ method: 'GET', url: '/client/finance/v1/recharge-requests', params: { page, size }, }) + return normalizePageResult(result) } export function getRechargeChannels() { diff --git a/src/modules/finance/views/FinanceAccountView.test.mjs b/src/modules/finance/views/FinanceAccountView.test.mjs index 7c1dbbc..db18376 100644 --- a/src/modules/finance/views/FinanceAccountView.test.mjs +++ b/src/modules/finance/views/FinanceAccountView.test.mjs @@ -39,6 +39,11 @@ test('renders server balances, ledger and recharge states without internal ident assert.match(viewSource, /v-loading="pageLoading"/) assert.match(viewSource, /function formatAccountAmount[\s\S]*value == null \? '—' : formatAmount\(value\)/) assert.match(viewSource, /formatAccountAmount\(account\?\.actualBalance\)/) + assert.match(viewSource, /:default-current-page="ledgerPage"/) + assert.match(viewSource, /:default-current-page="rechargePage"/) + assert.doesNotMatch(viewSource, / { void loadAccountData() }) -
共 {{ ledgerTotal }} 条
+
共 {{ ledgerTotal }} 条
diff --git a/src/modules/first-leg/api/firstLeg.ts b/src/modules/first-leg/api/firstLeg.ts index 8cbfa7c..5273066 100644 --- a/src/modules/first-leg/api/firstLeg.ts +++ b/src/modules/first-leg/api/firstLeg.ts @@ -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 diff --git a/src/modules/first-leg/api/inboundBilling.test.mjs b/src/modules/first-leg/api/inboundBilling.test.mjs index d0742c1..c238a74 100644 --- a/src/modules/first-leg/api/inboundBilling.test.mjs +++ b/src/modules/first-leg/api/inboundBilling.test.mjs @@ -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'/) +}) diff --git a/src/modules/first-leg/api/unifiedOrders.test.mjs b/src/modules/first-leg/api/unifiedOrders.test.mjs index ca874ef..9e1ee43 100644 --- a/src/modules/first-leg/api/unifiedOrders.test.mjs +++ b/src/modules/first-leg/api/unifiedOrders.test.mjs @@ -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/) }) diff --git a/src/modules/first-leg/api/unifiedOrders.ts b/src/modules/first-leg/api/unifiedOrders.ts index fad19f2..e32787f 100644 --- a/src/modules/first-leg/api/unifiedOrders.ts +++ b/src/modules/first-leg/api/unifiedOrders.ts @@ -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 diff --git a/src/modules/first-leg/components/OrderProgressPanel.vue b/src/modules/first-leg/components/OrderProgressPanel.vue index 32198cc..d56f20c 100644 --- a/src/modules/first-leg/components/OrderProgressPanel.vue +++ b/src/modules/first-leg/components/OrderProgressPanel.vue @@ -23,7 +23,7 @@ function stageState(index: number) {

订单进度

- 国内装柜进度及后续履约同步 + 国内入仓、组托、装柜及后续履约同步
{{ getFirstLegStatusLabel(order.status) }} diff --git a/src/modules/first-leg/store/firstLegOrders.test.mjs b/src/modules/first-leg/store/firstLegOrders.test.mjs index 095cb15..9cdeba9 100644 --- a/src/modules/first-leg/store/firstLegOrders.test.mjs +++ b/src/modules/first-leg/store/firstLegOrders.test.mjs @@ -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', () => { diff --git a/src/modules/first-leg/store/firstLegOrders.ts b/src/modules/first-leg/store/firstLegOrders.ts index 732728f..5fc9e54 100644 --- a/src/modules/first-leg/store/firstLegOrders.ts +++ b/src/modules/first-leg/store/firstLegOrders.ts @@ -231,6 +231,7 @@ const unifiedActionLabels: Readonly> = { 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) diff --git a/src/modules/first-leg/types/inboundBilling.ts b/src/modules/first-leg/types/inboundBilling.ts index bb34de7..101a2a5 100644 --- a/src/modules/first-leg/types/inboundBilling.ts +++ b/src/modules/first-leg/types/inboundBilling.ts @@ -6,6 +6,7 @@ export type MerchantInboundProcessStatus = | 'WAIT_PRICING' | 'FEE_CONFIRMING' | 'INBOUNDING' + | 'WAIT_PALLETIZE' | 'WAIT_SPLIT' | 'VOIDED' diff --git a/src/modules/first-leg/utils/firstLegOrder.test.mjs b/src/modules/first-leg/utils/firstLegOrder.test.mjs index 6ce01cf..65a4c79 100644 --- a/src/modules/first-leg/utils/firstLegOrder.test.mjs +++ b/src/modules/first-leg/utils/firstLegOrder.test.mjs @@ -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) }) diff --git a/src/modules/first-leg/views/FirstLegOrdersView.test.mjs b/src/modules/first-leg/views/FirstLegOrdersView.test.mjs index 351c9c9..5653e00 100644 --- a/src/modules/first-leg/views/FirstLegOrdersView.test.mjs +++ b/src/modules/first-leg/views/FirstLegOrdersView.test.mjs @@ -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>/) 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\(\[\]\)/) diff --git a/src/modules/first-leg/views/FirstLegOrdersView.vue b/src/modules/first-leg/views/FirstLegOrdersView.vue index 1b66bbf..f9f67a0 100644 --- a/src/modules/first-leg/views/FirstLegOrdersView.vue +++ b/src/modules/first-leg/views/FirstLegOrdersView.vue @@ -1898,7 +1898,7 @@ async function searchOrders(model: QueryModel) {

客户下单

先选择货物如何送到国内收货仓,并填写本次运输安排。

普通头程一件代发测量完成后不办理国内库存入库,将直接推送 PDA 合托。 - 用于绑定领星入库单,提交后可在尾程订单、入库单和出库单中查询。 + 用于绑定领星入库单,提交后可在尾程订单、入库单和出库单中查询。 快递自送我司提货卡车自送