This commit is contained in:
2026-09-20 14:22:12 +08:00
parent 1305753b71
commit 60a41af3ac
10 changed files with 414 additions and 3 deletions
@@ -11,6 +11,7 @@ import {
listPlatformUnifiedOrderFeeDetails,
updatePlatformUnifiedOrderDeclaration,
updatePlatformUnifiedOrderPlanning,
importPlatformLegacyOrders,
} from './unifiedOrders'
vi.mock('@/shared/api/http', () => ({
@@ -25,6 +26,19 @@ describe('unified orders API contract', () => {
vi.mocked(http.put).mockReset()
})
it('uploads the old-system workbook with a retry-safe key', async () => {
vi.mocked(http.post).mockResolvedValue({ data: { total: 1, succeeded: 1, failed: 0, items: [] } })
const file = new File(['xlsx'], 'legacy.xlsx', { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' })
await expect(importPlatformLegacyOrders(file, 'legacy-key')).resolves.toMatchObject({ succeeded: 1 })
const [path, body, config] = vi.mocked(http.post).mock.calls[0]
expect(path).toBe('/platform/v1/orders/first-leg/legacy-import')
expect(body).toBeInstanceOf(FormData)
expect((body as FormData).get('file')).toBe(file)
expect(config).toEqual({ headers: { 'Content-Type': 'multipart/form-data', 'Idempotency-Key': 'legacy-key' } })
})
it('uses the C02 platform orders endpoint and contract filters', async () => {
vi.mocked(http.get).mockResolvedValue({ data: { records: [], page: '2', size: '20', total: '0', pages: '0' } })
await expect(listPlatformUnifiedOrders({
@@ -611,6 +611,37 @@ export interface UnifiedOrderRemarksUpdateRequest {
customerServiceRemark: string | null
}
export interface LegacyOrderImportItem {
excelRow: number
success: boolean
legacyInboundCode: string | null
orderId: number | string | null
orderNo: string | null
errorMessage: string | null
}
export interface LegacyOrderImportResult {
total: number
succeeded: number
failed: number
items: LegacyOrderImportItem[]
}
export function importPlatformLegacyOrders(file: File, idempotencyKey: string) {
const form = new FormData()
form.append('file', file)
return http.post<ApiEnvelope<LegacyOrderImportResult>, ApiEnvelope<LegacyOrderImportResult>>(
'/platform/v1/orders/first-leg/legacy-import',
form,
{ headers: { 'Content-Type': 'multipart/form-data', 'Idempotency-Key': idempotencyKey } },
).then(unwrapApiResponse)
}
export function createLegacyOrderImportIdempotencyKey() {
const uuid = globalThis.crypto?.randomUUID?.()
return uuid ? `legacy-order-import-${uuid}` : `legacy-order-import-${Date.now()}-${Math.random().toString(36).slice(2)}`
}
export interface UnifiedOrderRemarksUpdateResponse {
version: number
customerRemark: string | null
@@ -9,6 +9,15 @@ const routeSource = readFileSync(resolve(process.cwd(), 'src/modules/unified-ord
const densitySource = readFileSync(resolve(process.cwd(), 'src/styles/adminDensity.css'), 'utf8')
describe('UnifiedOrdersView contract', () => {
it('imports old-system workbooks only for the fixed test customer', () => {
expect(source).toContain("can('platform:order:import')")
expect(source).toContain('导入旧系统订单')
expect(source).toContain('固定归属客户')
expect(source).toContain('IT测试公司')
expect(source).toContain('accept=".xlsx"')
expect(source).toContain('legacyImportResult.items.filter((item) => !item.success)')
})
it('exposes desktop pallet printing only for one-click dropship orders', () => {
expect(source).toContain("detail.value?.summary.fulfillmentType === 'ONE_CLICK_DROPSHIP'")
expect(source).toContain("{ key: 'dropship-print', label: '托盘打印', icon: '🖨️' }")
@@ -48,7 +57,7 @@ describe('UnifiedOrdersView contract', () => {
it('exposes the product main statuses including dropship palletizing', () => {
expect(unifiedOrderStatusOptions.map((item) => item.label)).toEqual([
'待审核', '待测量数据', '待一件代发组托', '待分柜', '装柜中', '装柜完成', '派送完成',
'待审核', '待测量数据', '待一件代发组托', '待分柜', '装柜中', '装柜完成', '派送完成', '已完成',
])
for (const legacyLabel of ['待客户确认', '差异处理中', '派送中', '进入分柜']) {
expect(source).not.toContain(legacyLabel)
@@ -151,7 +160,7 @@ describe('UnifiedOrdersView contract', () => {
expect(source).toContain('v-model:page-size="pageSize"')
const columnSource = source.slice(source.indexOf('const columns ='), source.indexOf('function emptyFilters'))
const expectedColumnKeys = [
'actions', 'inboundCode', 'mainStatus', 'customerName', 'receivingWarehouseName',
'actions', 'inboundCode', 'orderType', 'mainStatus', 'customerName', 'receivingWarehouseName',
'destinationWarehouseName', 'channelName', 'totalPackageCount', 'totalPalletCount', 'requestedPickupAt', 'locationCodes', 'measurementReviewStatus', 'actualGrossWeightKg',
'actualVolumeCbm', 'totalNetWeightKg', 'chargeableWeightKg', 'chargeableVolumeCbm', 'expectedShipDate',
'estimatedVolumeCbm', 'expectedVolumeCbm', 'expectedArrivalAt', 'feeReviewStatus', 'skuNos', 'totalQuantity', 'expressCompany', 'expressTrackingNo',
@@ -171,6 +180,10 @@ describe('UnifiedOrdersView contract', () => {
expect(columnSource).not.toContain("key: 'coLoadGroup'")
expect(columnSource).not.toContain("key: 'orderNo'")
expect(columnSource).toContain("key: 'inboundCode', prop: 'inboundCode', label: '入仓码', minWidth: 160, slotName: 'inboundCode'")
expect(columnSource).toContain("key: 'orderType', label: '订单类型', minWidth: 120, slotName: 'orderType'")
expect(source).toContain("if (row.fulfillmentType === 'ONE_CLICK_DROPSHIP') return '一件代发'")
expect(source).toContain("if (row.businessType === 'FBA' || row.businessType === 'FBA_SEA') return 'FBA 头程'")
expect(source).toContain("if (row.businessType === 'OVERSEAS_SEA') return '海外中转'")
expect(columnSource).toContain("{ key: 'mainStatus', prop: 'mainStatus', label: '订单主状态'")
expect(columnSource).not.toContain("key: 'loadingStatus'")
expect(columnSource).not.toContain("key: 'businessDate'")
@@ -19,6 +19,9 @@ import {
type UnifiedOrderResource,
type UnifiedOrderBatchApprovalPreviewItem,
type UnifiedOrderBatchReviewItem,
importPlatformLegacyOrders,
createLegacyOrderImportIdempotencyKey,
type LegacyOrderImportResult,
} from '@/modules/unified-orders/api/unifiedOrders'
import { cancelPlatformCoLoadGroup, createPlatformCoLoadGroup, getPlatformCoLoadGroupByInboundCode, type CoLoadBusinessSummary, type CoLoadGroup } from '@/modules/unified-orders/api/coLoadGroups'
import OrderFulfillmentTreePanel from '@/modules/unified-orders/components/OrderFulfillmentTreePanel.vue'
@@ -117,6 +120,12 @@ const pageSize = ref(20)
const total = ref(0)
const loading = ref(false)
const loadError = ref<ApiErrorDetails | null>(null)
const legacyImportOpen = ref(false)
const legacyImportFile = ref<File | null>(null)
const legacyImportIdempotencyKey = ref('')
const legacyImportSaving = ref(false)
const legacyImportResult = ref<LegacyOrderImportResult | null>(null)
const legacyImportError = ref<ApiErrorDetails | null>(null)
type PlanningDraft = {
expectedArrivalAt: string
estimatedVolumeCbm: number | null
@@ -633,6 +642,7 @@ const columns = computed<DataTableColumn[]>(() => [
selectable: () => !batchApprovalSaving.value && !coLoadSaving.value && !exportSubmitting.value,
} satisfies DataTableColumn] : []),
{ key: 'inboundCode', prop: 'inboundCode', label: '入仓码', minWidth: 160, slotName: 'inboundCode' },
{ key: 'orderType', label: '订单类型', minWidth: 120, slotName: 'orderType' },
{ key: 'mainStatus', prop: 'mainStatus', label: '订单主状态', minWidth: 120, slotName: 'mainStatus' },
{ key: 'customerName', prop: 'customerName', label: '客户', minWidth: 120, slotName: 'field' },
{ key: 'receivingWarehouseName', prop: 'receivingWarehouseName', label: '平台收货仓', minWidth: 100, slotName: 'field' },
@@ -1811,6 +1821,48 @@ function coLoadGroup(row: UnifiedOrderResource) {
return row.coLoadGroup || null
}
function openLegacyImport() {
legacyImportFile.value = null
legacyImportResult.value = null
legacyImportError.value = null
legacyImportIdempotencyKey.value = ''
legacyImportOpen.value = true
}
function selectLegacyImportFile(uploadFile: UploadFile) {
legacyImportFile.value = uploadFile.raw || null
legacyImportResult.value = null
legacyImportError.value = null
legacyImportIdempotencyKey.value = createLegacyOrderImportIdempotencyKey()
}
async function submitLegacyImport() {
if (!legacyImportFile.value || legacyImportSaving.value) return
legacyImportSaving.value = true
legacyImportError.value = null
try {
legacyImportResult.value = await importPlatformLegacyOrders(
legacyImportFile.value,
legacyImportIdempotencyKey.value,
)
const result = legacyImportResult.value
if (result.failed) ElMessage.warning(`导入完成:成功 ${result.succeeded} 条,失败 ${result.failed} 条`)
else ElMessage.success(`已导入 ${result.succeeded} 条旧系统订单`)
await loadOrders()
} catch (error) {
legacyImportError.value = getApiErrorDetails(error, '旧系统订单导入失败,请核对文件后重试')
} finally {
legacyImportSaving.value = false
}
}
function orderTypeLabel(row: UnifiedOrderResource) {
if (row.fulfillmentType === 'ONE_CLICK_DROPSHIP') return '一件代发'
if (row.businessType === 'FBA' || row.businessType === 'FBA_SEA') return 'FBA 头程'
if (row.businessType === 'OVERSEAS_SEA') return '海外中转'
return firstLegBusinessTypeLabel(row.businessType)
}
function unifiedOrderRowClassName({ row }: { row: UnifiedOrderResource }) {
return row.coLoadGroup && row.coLoadGroup.status !== 'CANCELLED' ? 'is-coload-group-row' : ''
}
@@ -2654,6 +2706,7 @@ onBeforeUnmount(() => {
<div v-if="coLoadError" class="coload-error" role="alert"><span>{{ coLoadError.message }}</span><ApiErrorMeta :details="coLoadError" /></div>
<DataTable v-model:page="page" v-model:page-size="pageSize" :data="records" :columns="columns" preference-key="unified-orders-table-v2" :loading="loading" :total="total" :page-sizes="[20, 50, 100]" :row-class-name="unifiedOrderRowClassName" row-key="orderId" border stripe empty-text="暂无符合条件的订单" aria-label="统一订单列表" @selection-change="handleOrderSelectionChange">
<template #toolbar-actions>
<el-button v-if="can('platform:order:import')" :disabled="loading" @click="openLegacyImport"><FileUp :size="16" />导入旧系统订单</el-button>
<el-button v-if="can('platform:order:export') && can('platform:download:list')" :loading="exportSubmitting" :disabled="loading || exportSubmitting" @click="exportOrders"><Download :size="16" />导出<span v-if="selectedOrders.length">(已选 {{ selectedOrders.length }})</span></el-button>
<el-button v-if="can('platform:order:approve')" type="success" :loading="batchApprovalLoading || batchApprovalSaving" :disabled="loading || !selectedApprovalOrders.length" @click="openBatchApproval"><CircleCheck :size="16" />批量审核通过<span v-if="selectedApprovalOrders.length">({{ selectedApprovalOrders.length }})</span></el-button>
<div v-if="can('platform:firstleg:smart-loading:assign')" class="coload-toolbar-action">
@@ -2677,6 +2730,7 @@ onBeforeUnmount(() => {
><GitBranch :size="14" />{{ orderRelationLabel(row as UnifiedOrderResource) }}</el-button>
</div>
</template>
<template #orderType="{ row }"><el-tag size="small" effect="light" type="info">{{ orderTypeLabel(row as UnifiedOrderResource) }}</el-tag></template>
<template #orderStatus="{ row }"><StatusBadge :status="orderStatusLabel((row as UnifiedOrderResource).internalOrderStatus || (row as UnifiedOrderResource).mainStatus)" /></template>
<template #measurementReviewStatus="{ row }"><StatusBadge :status="measurementStatusLabel((row as UnifiedOrderResource).measurement?.reviewStatus)" /></template>
<template #channel="{ row }"><div class="channel-cell"><LineClampText :content="(row as UnifiedOrderResource).channelName" /><el-tag v-if="(row as UnifiedOrderResource).channelChanged" size="small" type="warning">曾变更渠道</el-tag></div></template>
@@ -2731,7 +2785,29 @@ onBeforeUnmount(() => {
</DataTable>
</div>
</template>
</ListPageLayout>
<AppDialog class="admin-density-dialog" v-model="legacyImportOpen" title="导入旧系统订单" width="min(760px, calc(100vw - 28px))" confirm-text="开始导入" :confirm-loading="legacyImportSaving" :confirm-disabled="!legacyImportFile || legacyImportSaving || !!legacyImportResult" destroy-on-close @confirm="submitLegacyImport">
<div class="legacy-import-content">
<dl class="legacy-import-facts">
<div><dt>固定归属客户</dt><dd>IT测试公司</dd></div>
<div><dt>导入结果状态</dt><dd>待审核</dd></div>
</dl>
<el-upload :auto-upload="false" :limit="1" accept=".xlsx" :on-change="selectLegacyImportFile">
<el-button><FileUp :size="16" />选择 Excel 文件</el-button>
<template #tip><div class="el-upload__tip">仅支持旧系统“订单统计导出” .xlsx 文件,最多 200 条、5MB;入仓码保持不变。</div></template>
</el-upload>
<LoadErrorState v-if="legacyImportError" compact title="旧系统订单导入失败" :message="legacyImportError.message" :show-retry="false"><template #details><ApiErrorMeta :details="legacyImportError" /></template></LoadErrorState>
<div v-if="legacyImportResult" class="legacy-import-result">
<p>共 {{ legacyImportResult.total }} 条,成功 {{ legacyImportResult.succeeded }} 条,失败 {{ legacyImportResult.failed }} 条。</p>
<el-table v-if="legacyImportResult.failed" :data="legacyImportResult.items.filter((item) => !item.success)" border stripe max-height="280">
<el-table-column prop="excelRow" label="Excel 行" width="90" />
<el-table-column prop="legacyInboundCode" label="入仓码" min-width="160" />
<el-table-column prop="errorMessage" label="失败原因" min-width="300" />
</el-table>
</div>
</div>
</AppDialog>
<SmartLoadingDialog v-model="smartLoadingOpen" />
<AppDialog class="admin-density-dialog" v-model="batchApprovalOpen" title="批量审核订单" confirm-text="确认批量审核" confirm-type="success" width="min(1100px, calc(100vw - 28px))" :loading="batchApprovalLoading" :confirm-loading="batchApprovalSaving" :confirm-disabled="batchApprovalLoading || !batchApprovalSelectedRows.length" destroy-on-close @confirm="batchApproveOrders">
<el-alert title="请逐票核对入仓码、客户、中文品名、渠道和目的仓;审核通过后订单进入待测量。" type="info" :closable="false" show-icon />
@@ -3016,6 +3092,12 @@ onBeforeUnmount(() => {
.hold-summary dt { flex: 0 0 auto; margin: 0; color: var(--cc-muted); font-size: var(--admin-font-caption, 12px); }
.hold-summary dd { margin: 0; color: var(--cc-text); font-size: var(--admin-font-body, 13px); overflow-wrap: anywhere; }
.hold-actions { display: flex; flex: 0 0 auto; gap: var(--admin-space-3, 8px); }
.legacy-import-content { display: grid; gap: var(--admin-space-5, 12px); }
.legacy-import-facts { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); margin: 0; border: 1px solid var(--cc-border); }
.legacy-import-facts > div { display: grid; grid-template-columns: 120px minmax(0, 1fr); align-items: center; padding-block: var(--admin-space-2, 6px); }
.legacy-import-facts dt { padding: 8px 10px; color: var(--cc-text-muted); }
.legacy-import-facts dd { margin: 0; padding: 8px 10px; font-weight: 600; }
.legacy-import-result > p { margin: 0 0 var(--admin-space-3, 8px); }
.hold-actions :deep(.el-button) { margin-left: 0; }
.hold-block-reason { margin: var(--admin-space-5, 12px) 0 0; padding: 9px var(--admin-space-5, 12px); border: 1px solid var(--el-color-warning-light-5); border-radius: var(--admin-radius, 4px); background: var(--el-color-warning-light-9); color: var(--el-color-warning-dark-2); font-size: var(--admin-font-caption, 12px); }
.hold-history-title { margin: var(--admin-space-6, 16px) 0 var(--admin-space-3, 8px); color: #344054; font-size: var(--admin-font-section, 14px); }