优化
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { apiBlobRequest, apiRequest } from '@/api/http'
|
||||
import { ApiError, apiBlobRequest, apiBlobResponseRequest, apiRequest } from '@/api/http'
|
||||
|
||||
export type ApiFirstLegStatus = 'DRAFT' | 'SUBMITTED' | 'APPROVED' | 'ACCEPTED' | 'REJECTED' | 'CANCELLED' | 'PROCESSING' | 'COMPLETED'
|
||||
export type ApiTransportMode = 'SEA' | 'AIR' | 'RAIL' | 'TRUCK'
|
||||
@@ -205,6 +205,18 @@ export interface MerchantFirstLegOrder {
|
||||
referenceId: string | null
|
||||
expressCompany?: string | null
|
||||
expressTrackingNo?: string | null
|
||||
skuNos?: string[] | null
|
||||
totalPackageCount?: number | null
|
||||
totalGrossWeightKg?: number | null
|
||||
totalNetWeightKg?: number | null
|
||||
totalVolumeCbm?: number | null
|
||||
totalQuantity?: number | null
|
||||
chargeableWeightKg?: number | null
|
||||
chargeableVolumeCbm?: number | null
|
||||
inboundCode?: string | null
|
||||
inboundedAt?: string | null
|
||||
businessDate?: string | null
|
||||
internationalOrderNo?: string | null
|
||||
submittedAt?: string | null
|
||||
completedAt?: string | null
|
||||
version: number
|
||||
@@ -212,6 +224,40 @@ export interface MerchantFirstLegOrder {
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
export interface MerchantFirstLegOrderRow {
|
||||
orderId: number | string
|
||||
orderNo: string | null
|
||||
packageCount: number | null
|
||||
weightKg: number | null
|
||||
inboundId: number | string | null
|
||||
inboundCode: string | null
|
||||
channelName: string | null
|
||||
destinationType: ApiDestinationType | null
|
||||
customerName: string | null
|
||||
receivingCompanyName: string | null
|
||||
specificationModel: string | null
|
||||
status: ApiFirstLegStatus
|
||||
totalQuantity: number | null
|
||||
totalGrossWeightKg: number | null
|
||||
totalNetWeightKg: number | null
|
||||
totalVolumeCbm: number | null
|
||||
trackingNo: string | null
|
||||
internationalOrderNo: string | null
|
||||
referenceId: string | null
|
||||
remark: string | null
|
||||
destinationWarehouseName: string | null
|
||||
expressCompany: string | null
|
||||
skuNos: string | string[] | null
|
||||
chargeableWeightKg: number | null
|
||||
chargeableVolumeCbm: number | null
|
||||
billingUnitPrice: number | null
|
||||
inboundedAt: string | null
|
||||
businessDate: string | null
|
||||
shipmentId: string | null
|
||||
createdAt: string
|
||||
cargoes?: Array<{ skuNo?: string | null }>
|
||||
}
|
||||
|
||||
export interface MerchantFirstLegOrderCargo extends FirstLegOrderCargoPayload {
|
||||
id: number | string
|
||||
itemNo: number
|
||||
@@ -251,6 +297,66 @@ export function downloadMerchantProductImage(fileId: number | string) {
|
||||
return apiBlobRequest({ method: 'GET', url: `/merchant/v1/files/product-images/${fileId}` })
|
||||
}
|
||||
|
||||
export async function downloadMerchantInboundCartonLabels(inboundId: number | string) {
|
||||
return downloadMerchantCartonLabelPdf(
|
||||
`/merchant/v1/first-leg/inbounds/${encodeURIComponent(String(inboundId))}/carton-label-pdf`,
|
||||
)
|
||||
}
|
||||
|
||||
export async function downloadMerchantOrderCartonLabels(orderId: number | string) {
|
||||
return downloadMerchantCartonLabelPdf(
|
||||
`/merchant/v1/first-leg/orders/${encodeURIComponent(String(orderId))}/carton-label-pdf`,
|
||||
)
|
||||
}
|
||||
|
||||
export async function downloadMerchantOrderCartonLabelsBatch(orderIds: Array<number | string>) {
|
||||
const response = await apiBlobResponseRequest({
|
||||
method: 'POST',
|
||||
url: '/merchant/v1/first-leg/orders/carton-label-pdf',
|
||||
data: orderIds.map(String),
|
||||
timeout: 120_000,
|
||||
})
|
||||
await requirePdfBlob(response.blob)
|
||||
return {
|
||||
blob: response.blob,
|
||||
failureCount: Number(response.headers['x-carton-label-failure-count'] ?? 0),
|
||||
failures: String(response.headers['x-carton-label-failures'] ?? ''),
|
||||
}
|
||||
}
|
||||
|
||||
async function downloadMerchantCartonLabelPdf(url: string) {
|
||||
const blob = await apiBlobRequest({
|
||||
method: 'POST',
|
||||
url,
|
||||
data: null,
|
||||
timeout: 60_000,
|
||||
})
|
||||
await requirePdfBlob(blob)
|
||||
return blob
|
||||
}
|
||||
|
||||
async function requirePdfBlob(blob: Blob) {
|
||||
const signature = new TextDecoder().decode(await blob.slice(0, 5).arrayBuffer())
|
||||
if (signature === '%PDF-') return
|
||||
|
||||
const text = await blob.text()
|
||||
try {
|
||||
const error = JSON.parse(text) as { code?: unknown; message?: unknown; traceId?: unknown }
|
||||
if (typeof error.message === 'string') {
|
||||
throw new ApiError(
|
||||
error.message || '箱唛生成失败',
|
||||
typeof error.code === 'string' ? error.code : 'INVALID_PDF',
|
||||
undefined,
|
||||
undefined,
|
||||
typeof error.traceId === 'string' ? error.traceId : null,
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof ApiError) throw error
|
||||
}
|
||||
throw new ApiError('箱唛服务未返回有效的 PDF 文件', 'INVALID_PDF')
|
||||
}
|
||||
|
||||
export interface FirstLegOrderQuery {
|
||||
keyword?: string
|
||||
status?: ApiFirstLegStatus
|
||||
@@ -330,7 +436,7 @@ export function estimateMerchantFirstLegPrice(payload: MerchantPriceEstimateRequ
|
||||
}
|
||||
|
||||
export function getMerchantFirstLegOrders(params: FirstLegOrderQuery = {}) {
|
||||
return apiRequest<PageResult<MerchantFirstLegOrder>>({ method: 'GET', url: '/merchant/v1/orders/first-leg', params: { page: 1, size: 200, ...params } })
|
||||
return apiRequest<PageResult<MerchantFirstLegOrderRow>>({ method: 'GET', url: '/merchant/v1/orders/first-leg', params: { page: 1, size: 200, ...params } })
|
||||
}
|
||||
|
||||
export function getMerchantFirstLegOrder(id: number | string) {
|
||||
|
||||
@@ -73,6 +73,15 @@ export interface MerchantUnifiedOrderRow {
|
||||
totalPackageCount: number | null
|
||||
actualGrossWeightKg: number | null
|
||||
actualVolumeCbm: number | null
|
||||
skuNos?: string[] | null
|
||||
expressCompany?: string | null
|
||||
totalNetWeightKg?: number | null
|
||||
totalQuantity?: number | null
|
||||
chargeableVolumeCbm?: number | null
|
||||
shipmentId?: string | null
|
||||
referenceId?: string | null
|
||||
inboundedAt?: string | null
|
||||
businessDate?: string | null
|
||||
measurement: MerchantUnifiedMeasurementSummary | null
|
||||
preload: MerchantUnifiedPreloadSummary | null
|
||||
loading: MerchantUnifiedLoadingSummary | null
|
||||
|
||||
@@ -24,19 +24,21 @@ const emit = defineEmits<{
|
||||
<footer class="editor-page-footer"><slot name="footer" /></footer>
|
||||
</section>
|
||||
|
||||
<el-drawer
|
||||
<el-dialog
|
||||
v-else
|
||||
class="first-leg-editor"
|
||||
class="first-leg-editor-dialog"
|
||||
:model-value="modelValue"
|
||||
:title="title"
|
||||
size="1080px"
|
||||
width="94vw"
|
||||
top="3vh"
|
||||
destroy-on-close
|
||||
append-to-body
|
||||
:before-close="beforeClose"
|
||||
@update:model-value="emit('update:modelValue', $event)"
|
||||
>
|
||||
<slot />
|
||||
<template #footer><slot name="footer" /></template>
|
||||
</el-drawer>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@@ -48,15 +50,24 @@ const emit = defineEmits<{
|
||||
.editor-page-footer :deep(.editor-actions) { width: min(100%, 900px); justify-content: flex-start; }
|
||||
.editor-page-footer :deep(.editor-actions > .el-button) { min-width: 132px; min-height: 42px; }
|
||||
.editor-page-footer :deep(.editor-actions > .el-button:last-child) { min-width: 172px; }
|
||||
:global(.first-leg-editor-dialog.el-dialog) { display: flex; max-width: 1500px; height: 94vh; margin-bottom: 0; flex-direction: column; overflow: hidden; border-radius: 6px; }
|
||||
:global(.first-leg-editor-dialog .el-dialog__header) { flex: 0 0 auto; margin-right: 0; padding: 17px 22px; border-bottom: 1px solid var(--border); }
|
||||
:global(.first-leg-editor-dialog .el-dialog__title) { color: var(--navy); font-size: 16px; font-weight: 700; }
|
||||
:global(.first-leg-editor-dialog .el-dialog__headerbtn) { top: 8px; right: 10px; width: 44px; height: 44px; }
|
||||
:global(.first-leg-editor-dialog .el-dialog__body) { min-height: 0; flex: 1 1 auto; overflow-y: auto; padding: 22px 24px 28px; overscroll-behavior: contain; }
|
||||
:global(.first-leg-editor-dialog .el-dialog__footer) { flex: 0 0 auto; padding: 12px 22px; border-top: 1px solid var(--border); background: #fff; }
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.editor-page-nav { align-items: flex-start; flex-direction: column; gap: 2px; padding: 10px 16px; }
|
||||
.editor-page-nav span { padding-left: 1px; line-height: 1.5; }
|
||||
.editor-page-body { padding: 18px 16px 24px; }
|
||||
.editor-page-footer { padding: 10px 16px; }
|
||||
:global(.first-leg-editor-dialog.el-dialog) { width: calc(100vw - 24px) !important; height: 96vh; margin-top: 2vh; }
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.editor-page-footer { position: static; }
|
||||
:global(.first-leg-editor-dialog.el-dialog) { width: 100vw !important; height: 100vh; margin: 0; border-radius: 0; }
|
||||
:global(.first-leg-editor-dialog .el-dialog__body) { padding: 16px 12px 22px; }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { Check, CircleDot, Clock3, Paperclip } from '@lucide/vue'
|
||||
import { Check, CircleDot } from '@lucide/vue'
|
||||
import type { FirstLegOrder } from '@/modules/first-leg/types/firstLegOrder'
|
||||
import {
|
||||
FIRST_LEG_PROGRESS_STAGES,
|
||||
@@ -11,27 +11,11 @@ import {
|
||||
const props = defineProps<{ order: FirstLegOrder }>()
|
||||
|
||||
const currentIndex = computed(() => getFirstLegProgressIndex(props.order.status))
|
||||
const timeline = computed(() => [...props.order.timeline].reverse())
|
||||
|
||||
function stageState(index: number) {
|
||||
if (index < currentIndex.value) return 'is-complete'
|
||||
if (index === currentIndex.value) return 'is-current'
|
||||
return 'is-pending'
|
||||
}
|
||||
|
||||
function formatDateTime(value: string) {
|
||||
if (!value) return '时间待更新'
|
||||
const date = new Date(value)
|
||||
if (Number.isNaN(date.getTime())) return value
|
||||
return date.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false,
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -54,37 +38,17 @@ function formatDateTime(value: string) {
|
||||
</ol>
|
||||
<p v-if="order.status === 'DRAFT'" class="draft-progress-note">订单提交后开始记录主状态进度。</p>
|
||||
|
||||
<div class="timeline-heading">
|
||||
<h4>进度记录</h4>
|
||||
<span>{{ timeline.length }} 条</span>
|
||||
</div>
|
||||
<el-empty v-if="timeline.length === 0" class="timeline-empty" description="暂无进度记录" :image-size="52" />
|
||||
<el-timeline v-else class="order-timeline">
|
||||
<el-timeline-item v-for="item in timeline" :key="item.id" :timestamp="formatDateTime(item.occurredAt)" placement="top" :type="item.kind === 'MAIN_STATUS' ? 'primary' : 'info'">
|
||||
<div class="timeline-title">
|
||||
<strong>{{ item.title }}</strong>
|
||||
<el-tag v-if="item.kind === 'CUSTOM_NODE'" size="small" effect="plain">业务节点</el-tag>
|
||||
<el-tag v-else-if="item.status" size="small" effect="plain">{{ getFirstLegStatusLabel(item.status) }}</el-tag>
|
||||
</div>
|
||||
<p v-if="item.description">{{ item.description }}</p>
|
||||
<div class="timeline-meta">
|
||||
<span>{{ item.operatorName || '平台' }}</span>
|
||||
<span v-if="item.kind === 'CUSTOM_NODE' && item.plannedAt"><Clock3 :size="13" />计划 {{ formatDateTime(item.plannedAt) }}</span>
|
||||
<span v-if="item.attachmentCount"><Paperclip :size="13" />{{ item.attachmentCount }} 个附件</span>
|
||||
</div>
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.order-progress { margin-top: 22px; }
|
||||
.progress-heading, .timeline-heading, .timeline-title, .timeline-meta { display: flex; align-items: center; }
|
||||
.progress-heading { display: flex; align-items: center; }
|
||||
.progress-heading { justify-content: space-between; gap: 14px; }
|
||||
.progress-heading > div { display: flex; min-width: 0; flex-direction: column; gap: 3px; }
|
||||
.progress-heading h3, .timeline-heading h4 { margin: 0; color: var(--navy); }
|
||||
.progress-heading h3 { margin: 0; color: var(--navy); }
|
||||
.progress-heading h3 { font-size: 15px; }
|
||||
.progress-heading span, .timeline-heading span { color: var(--muted); font-size: 11px; }
|
||||
.progress-heading span { color: var(--muted); font-size: 11px; }
|
||||
.progress-stages { display: grid; grid-template-columns: repeat(7, minmax(76px, 1fr)); margin: 16px 0 0; padding: 0; overflow-x: auto; list-style: none; }
|
||||
.progress-stages li { position: relative; display: flex; min-width: 76px; align-items: center; flex-direction: column; gap: 7px; color: #96a0a6; font-size: 10px; text-align: center; }
|
||||
.progress-stages li::before { position: absolute; top: 10px; right: 50%; left: -50%; height: 2px; background: #e1e6e8; content: ''; }
|
||||
@@ -95,15 +59,6 @@ function formatDateTime(value: string) {
|
||||
.is-complete .stage-marker { background: var(--primary); color: #fff; }
|
||||
.is-current .stage-label { color: var(--navy); font-weight: 700; }
|
||||
.draft-progress-note { margin: 12px 0 0; padding: 10px 12px; background: #f6f8f8; color: var(--muted); font-size: 11px; }
|
||||
.timeline-heading { justify-content: space-between; margin-top: 24px; padding-top: 18px; border-top: 1px solid var(--border); }
|
||||
.timeline-heading h4 { font-size: 13px; }
|
||||
.order-timeline { margin-top: 14px; padding-left: 5px; }
|
||||
.timeline-title { flex-wrap: wrap; gap: 8px; }
|
||||
.timeline-title strong { color: var(--navy); font-size: 12px; }
|
||||
.order-timeline p { margin: 6px 0; overflow-wrap: anywhere; color: var(--muted); font-size: 11px; line-height: 1.55; white-space: pre-wrap; }
|
||||
.timeline-meta { flex-wrap: wrap; gap: 12px; color: var(--muted); font-size: 10px; }
|
||||
.timeline-meta span { display: inline-flex; align-items: center; gap: 4px; }
|
||||
.timeline-empty { padding: 12px 0 0; }
|
||||
@media (max-width: 720px) {
|
||||
.progress-stages { grid-template-columns: repeat(7, 82px); padding-bottom: 8px; }
|
||||
.progress-heading { align-items: flex-start; }
|
||||
|
||||
@@ -6,6 +6,22 @@ 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('maps the first-leg aggregate list fields without loading detail rows', () => {
|
||||
assert.match(apiSource, /interface MerchantFirstLegOrderRow/)
|
||||
assert.match(apiSource, /skuNos: string \| string\[\] \| null/)
|
||||
assert.match(apiSource, /cargoes\?: Array<\{ skuNo\?: string \| null \}>/)
|
||||
assert.match(apiSource, /apiRequest<PageResult<MerchantFirstLegOrderRow>>/)
|
||||
assert.match(storeSource, /function mapFirstLegOrderRow/)
|
||||
for (const field of ['inboundId', 'inboundCode', 'channelName', 'destinationWarehouseName', 'packageCount', 'expressCompany', 'trackingNo', 'totalGrossWeightKg', 'totalNetWeightKg', 'totalVolumeCbm', 'totalQuantity', 'remark', 'shipmentId', 'referenceId', 'createdAt']) {
|
||||
assert.match(storeSource, new RegExp(`row\\.${field}`))
|
||||
}
|
||||
assert.match(storeSource, /row\.skuNos/)
|
||||
assert.match(storeSource, /row\.cargoes/)
|
||||
assert.match(storeSource, /chargeableWeightKg: null/)
|
||||
assert.match(storeSource, /chargeableVolumeCbm: null/)
|
||||
assert.match(storeSource, /orders\.value = orderResult\.value\.records\.map\(mapFirstLegOrderRow\)/)
|
||||
})
|
||||
|
||||
test('declares the documented merchant warehouse and first-leg endpoints', () => {
|
||||
assert.match(apiSource, /url: '\/merchant\/v1\/warehouses'/)
|
||||
assert.match(apiSource, /url: '\/merchant\/v1\/orders\/first-leg'/)
|
||||
@@ -16,6 +32,13 @@ test('declares the documented merchant warehouse and first-leg endpoints', () =>
|
||||
assert.match(apiSource, /url: '\/merchant\/v1\/first-leg\/pricing\/channels'/)
|
||||
assert.match(apiSource, /params: \{ originWarehouseId \}/)
|
||||
assert.match(apiSource, /url: '\/merchant\/v1\/first-leg\/pricing\/estimate'/)
|
||||
assert.match(apiSource, /`\/merchant\/v1\/first-leg\/inbounds\/\$\{encodeURIComponent\(String\(inboundId\)\)\}\/carton-label-pdf`/)
|
||||
assert.match(apiSource, /`\/merchant\/v1\/first-leg\/orders\/\$\{encodeURIComponent\(String\(orderId\)\)\}\/carton-label-pdf`/)
|
||||
assert.match(apiSource, /url: '\/merchant\/v1\/first-leg\/orders\/carton-label-pdf'/)
|
||||
assert.match(apiSource, /data: orderIds\.map\(String\)/)
|
||||
assert.doesNotMatch(apiSource, /`\/merchant\/v1\/inbounds\/\$\{encodeURIComponent\(String\(inboundId\)\)\}\/carton-label-pdf`/)
|
||||
assert.match(apiSource, /signature === '%PDF-'/)
|
||||
assert.match(apiSource, /throw new ApiError\('箱唛服务未返回有效的 PDF 文件', 'INVALID_PDF'\)/)
|
||||
})
|
||||
|
||||
test('maps transport and status fields instead of treating container mode as transport mode', () => {
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
type FirstLegOrderPartyPayload,
|
||||
type FirstLegOrderSaveRequest,
|
||||
type MerchantFirstLegOrder,
|
||||
type MerchantFirstLegOrderRow,
|
||||
type MerchantFirstLegOrderDetail,
|
||||
type MerchantBatchDraftResponse,
|
||||
type MerchantWarehouse,
|
||||
@@ -305,7 +306,10 @@ function mapOrder(order: MerchantFirstLegOrder, warehouses: PlatformReceivingWar
|
||||
receivingWarehouseSnapshot: orderWarehouseSnapshot(order, warehouse), expectedDepartureDate: order.expectedShipDate ?? '', etd: '', eta: '',
|
||||
shipperPartyId: shipper?.id ?? '', shipperPartySnapshot: shipper, shipper: shipper?.name ?? '',
|
||||
consigneePartyId: consignee?.id ?? '', consigneePartySnapshot: consignee, consignee: consignee?.name ?? '',
|
||||
expressCompanyName: order.expressCompany ?? '', trackingNumber: order.expressTrackingNo ?? '', priority: 'NORMAL', customerRemark: order.customerRemark ?? '', operatorName: '',
|
||||
expressCompanyName: order.expressCompany ?? '', trackingNumber: order.expressTrackingNo ?? '', skuNos: order.skuNos ?? [],
|
||||
totalNetWeightKg: order.totalNetWeightKg ?? null, totalQuantity: order.totalQuantity ?? null,
|
||||
chargeableVolumeCbm: null, inboundedAt: order.inboundedAt ?? '', businessDate: '',
|
||||
priority: 'NORMAL', customerRemark: order.customerRemark ?? '', operatorName: '',
|
||||
status: clientStatus(order.status), version: order.version ?? 0,
|
||||
items: (detail?.cargoes ?? []).map((item) => ({
|
||||
id: String(item.id), sku: item.skuNo ?? '', upc: item.upcNo ?? '', cargoNameCn: item.cargoName, cargoNameEn: item.cargoNameEn ?? '',
|
||||
@@ -323,7 +327,14 @@ function mapOrder(order: MerchantFirstLegOrder, warehouses: PlatformReceivingWar
|
||||
productHeightCm: item.productHeightCm ?? 0, shipmentId: item.shipmentId ?? order.shipmentId ?? '',
|
||||
referenceId: item.referenceId ?? order.referenceId ?? '',
|
||||
})),
|
||||
timeline: detail ? timeline(detail) : [], businessSummary: businessSummary(), createdAt: order.createdAt, updatedAt: order.updatedAt,
|
||||
timeline: detail ? timeline(detail) : [], businessSummary: {
|
||||
...businessSummary(),
|
||||
totalPackageCount: order.totalPackageCount ?? null,
|
||||
actualGrossWeightKg: order.totalGrossWeightKg ?? null,
|
||||
actualVolumeCbm: order.totalVolumeCbm ?? null,
|
||||
chargeableWeightKg: order.chargeableWeightKg ?? null,
|
||||
inboundCode: order.inboundCode ?? '',
|
||||
}, createdAt: order.createdAt, updatedAt: order.updatedAt,
|
||||
tradeTerm: order.tradeTerm ?? '', destinationCountryCode: order.destinationCountryCode,
|
||||
destinationWarehouseId: order.destinationWarehouseId ? String(order.destinationWarehouseId) : '',
|
||||
destinationWarehouseName: order.destinationWarehouseName, pickupMethod: order.pickupMethod, channelCode: order.channelCode,
|
||||
@@ -337,23 +348,55 @@ function mapOrder(order: MerchantFirstLegOrder, warehouses: PlatformReceivingWar
|
||||
function mapUnifiedOrderRow(summary: MerchantUnifiedOrderRow, warehouses: PlatformReceivingWarehouse[]): FirstLegOrder {
|
||||
const warehouse = warehouses.find((item) => item.id === String(summary.receivingWarehouseId))
|
||||
return {
|
||||
id: String(summary.orderId), orderNo: summary.orderNo, referenceNo: '', transportMode: null,
|
||||
id: String(summary.orderId), orderNo: summary.orderNo, referenceNo: summary.referenceId ?? '', transportMode: null,
|
||||
businessType: 'EXPORT', 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,
|
||||
shipper: '', consigneePartyId: '', consigneePartySnapshot: null, consignee: '', expressCompanyName: '',
|
||||
trackingNumber: summary.delivery?.trackingNo ?? '', priority: 'NORMAL', customerRemark: '', operatorName: '',
|
||||
shipper: '', consigneePartyId: '', consigneePartySnapshot: null, consignee: '', expressCompanyName: summary.expressCompany ?? '',
|
||||
trackingNumber: summary.delivery?.trackingNo ?? '', skuNos: summary.skuNos ?? [], totalNetWeightKg: summary.totalNetWeightKg ?? null,
|
||||
totalQuantity: summary.totalQuantity ?? null, chargeableVolumeCbm: null,
|
||||
inboundedAt: summary.inboundedAt ?? '', businessDate: '', priority: 'NORMAL', customerRemark: '', operatorName: '',
|
||||
status: summary.mainStatus, version: 0, items: [], timeline: [], businessSummary: businessSummary(summary),
|
||||
createdAt: summary.createdAt, updatedAt: summary.updatedAt, tradeTerm: '', destinationCountryCode: '',
|
||||
destinationWarehouseId: summary.destinationWarehouseId == null ? '' : String(summary.destinationWarehouseId),
|
||||
destinationWarehouseName: summary.destinationWarehouseName ?? '', pickupMethod: 'SELF_EXPRESS', channelCode: '',
|
||||
channelName: summary.channelName ?? '', pricingTaxMode: 'TAX_INCLUDED', carrierId: '', carrierName: '', taxMode: 'BUY_DECLARATION', customsInspection: false,
|
||||
destinationType: 'PRIVATE', amazonWarehouseCode: '', shipmentId: '',
|
||||
destinationType: 'PRIVATE', amazonWarehouseCode: '', shipmentId: summary.shipmentId ?? '',
|
||||
receivingTime: '', submittedAt: '', completedAt: '',
|
||||
}
|
||||
}
|
||||
|
||||
function mapFirstLegOrderRow(row: MerchantFirstLegOrderRow): FirstLegOrder {
|
||||
const skuNos = Array.isArray(row.skuNos)
|
||||
? row.skuNos
|
||||
: (row.skuNos ?? '').split('、').map((value) => value.trim()).filter(Boolean)
|
||||
const resolvedSkuNos = skuNos.length ? skuNos : (row.cargoes ?? [])
|
||||
.map((cargo) => cargo.skuNo?.trim())
|
||||
.filter((value): value is string => Boolean(value))
|
||||
const uniqueSkuNos = [...new Set(resolvedSkuNos)]
|
||||
return {
|
||||
id: String(row.orderId), orderNo: row.orderNo, referenceNo: row.referenceId ?? '', transportMode: null,
|
||||
businessType: 'EXPORT', origin: '', destination: row.destinationWarehouseName ?? '', receivingWarehouseId: '',
|
||||
receivingWarehouseName: '', receivingWarehouseSnapshot: null, expectedDepartureDate: '', etd: '', eta: '',
|
||||
shipperPartyId: '', shipperPartySnapshot: null, shipper: '', consigneePartyId: '', consigneePartySnapshot: null,
|
||||
consignee: row.receivingCompanyName ?? '', expressCompanyName: row.expressCompany ?? '', trackingNumber: row.trackingNo ?? '',
|
||||
skuNos: uniqueSkuNos, totalNetWeightKg: row.totalNetWeightKg, totalQuantity: row.totalQuantity,
|
||||
chargeableVolumeCbm: null, inboundedAt: row.inboundedAt ?? '', businessDate: '', priority: 'NORMAL', customerRemark: row.remark ?? '',
|
||||
operatorName: '', status: clientStatus(row.status), version: 0, items: [], timeline: [],
|
||||
businessSummary: {
|
||||
...businessSummary(), totalPackageCount: row.packageCount, actualGrossWeightKg: row.totalGrossWeightKg ?? row.weightKg,
|
||||
actualVolumeCbm: row.totalVolumeCbm, chargeableWeightKg: null,
|
||||
inboundId: row.inboundId == null ? '' : String(row.inboundId), inboundCode: row.inboundCode ?? '',
|
||||
},
|
||||
createdAt: row.createdAt, updatedAt: row.createdAt, tradeTerm: '', destinationCountryCode: '', destinationWarehouseId: '',
|
||||
destinationWarehouseName: row.destinationWarehouseName ?? '', pickupMethod: 'SELF_EXPRESS', channelCode: '',
|
||||
channelName: row.channelName ?? '', pricingTaxMode: 'TAX_INCLUDED', carrierId: '', carrierName: '', taxMode: 'BUY_DECLARATION',
|
||||
customsInspection: false, destinationType: row.destinationType ?? 'PRIVATE', amazonWarehouseCode: '',
|
||||
shipmentId: row.shipmentId ?? row.internationalOrderNo ?? '', receivingTime: '', submittedAt: '', completedAt: '',
|
||||
}
|
||||
}
|
||||
|
||||
function mapUnifiedOrderDetail(detail: MerchantUnifiedOrderDetail, warehouses: PlatformReceivingWarehouse[]): FirstLegOrder {
|
||||
if (!detail?.summary || !detail.order?.order) throw new Error('订单详情数据不完整,请刷新列表后重试')
|
||||
const mapped = mapOrder(detail.order.order, warehouses, detail.order)
|
||||
@@ -366,8 +409,7 @@ function mapUnifiedOrderDetail(detail: MerchantUnifiedOrderDetail, warehouses: P
|
||||
}
|
||||
|
||||
type OrderListReadResult =
|
||||
| { source: 'unified'; records: MerchantUnifiedOrderRow[]; drafts: MerchantFirstLegOrder[]; draftError: string }
|
||||
| { source: 'legacy'; records: MerchantFirstLegOrder[] }
|
||||
| { source: 'first-leg'; records: MerchantFirstLegOrderRow[] }
|
||||
|
||||
interface ClientOrderReadQuery extends MerchantUnifiedOrderQuery {
|
||||
draftOnly?: boolean
|
||||
@@ -384,41 +426,9 @@ function legacyQuery(query: ClientOrderReadQuery, status?: ApiFirstLegStatus) {
|
||||
}
|
||||
|
||||
async function getOrdersForClient(query: ClientOrderReadQuery = {}): Promise<OrderListReadResult> {
|
||||
if (query.draftOnly) {
|
||||
const drafts = await getMerchantFirstLegOrders(legacyQuery(query, 'DRAFT'))
|
||||
return { source: 'legacy', records: drafts.records }
|
||||
}
|
||||
const includeDrafts = query.includeDrafts ?? true
|
||||
const unifiedQuery: MerchantUnifiedOrderQuery = {
|
||||
keyword: query.keyword,
|
||||
orderNo: query.orderNo,
|
||||
mainStatus: query.mainStatus,
|
||||
receivingWarehouseId: query.receivingWarehouseId,
|
||||
destinationWarehouseId: query.destinationWarehouseId,
|
||||
createdFrom: query.createdFrom,
|
||||
createdTo: query.createdTo,
|
||||
page: query.page,
|
||||
size: query.size,
|
||||
}
|
||||
try {
|
||||
const response = await getMerchantUnifiedOrders(unifiedQuery)
|
||||
if (!includeDrafts) return { source: 'unified', records: response.records, drafts: [], draftError: '' }
|
||||
try {
|
||||
const drafts = await getMerchantFirstLegOrders(legacyQuery(query, 'DRAFT'))
|
||||
return { source: 'unified', records: response.records, drafts: drafts.records, draftError: '' }
|
||||
} catch (error) {
|
||||
return {
|
||||
source: 'unified',
|
||||
records: response.records,
|
||||
drafts: [],
|
||||
draftError: error instanceof Error ? error.message : '订单草稿加载失败',
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (!isUnifiedOrderQueryUnavailable(error)) throw error
|
||||
const response = await getMerchantFirstLegOrders(legacyQuery(query))
|
||||
return { source: 'legacy', records: response.records }
|
||||
}
|
||||
const status = query.draftOnly ? 'DRAFT' : undefined
|
||||
const response = await getMerchantFirstLegOrders(legacyQuery(query, status))
|
||||
return { source: 'first-leg', records: response.records }
|
||||
}
|
||||
|
||||
function mapWarehouse(item: MerchantWarehouse): ClientWarehouse {
|
||||
@@ -483,15 +493,7 @@ export const useFirstLegOrdersStore = defineStore('client-first-leg-orders', ()
|
||||
const [orderResult, warehouseResult] = await Promise.allSettled([getOrdersForClient(query), loadWarehouseType('DOMESTIC', true)])
|
||||
if (warehouseResult.status === 'rejected' && !warehouseLoadError.value) warehouseLoadError.value = warehouseResult.reason instanceof Error ? warehouseResult.reason.message : '公共仓库数据加载失败'
|
||||
if (orderResult.status === 'fulfilled') {
|
||||
if (orderResult.value.source === 'unified') {
|
||||
draftLoadError.value = orderResult.value.draftError
|
||||
orders.value = [
|
||||
...orderResult.value.drafts.map((item) => mapOrder(item, warehouses.value)),
|
||||
...orderResult.value.records.map((item) => mapUnifiedOrderRow(item, warehouses.value)),
|
||||
].sort((left, right) => Date.parse(right.createdAt) - Date.parse(left.createdAt))
|
||||
} else {
|
||||
orders.value = orderResult.value.records.map((item) => mapOrder(item, warehouses.value))
|
||||
}
|
||||
orders.value = orderResult.value.records.map(mapFirstLegOrderRow)
|
||||
} else {
|
||||
loadError.value = orderResult.reason instanceof Error ? orderResult.reason.message : '订单数据加载失败'
|
||||
}
|
||||
|
||||
@@ -114,6 +114,12 @@ export interface FirstLegOrder {
|
||||
consignee: string
|
||||
expressCompanyName: string
|
||||
trackingNumber: string
|
||||
skuNos: string[]
|
||||
totalNetWeightKg: number | null
|
||||
totalQuantity: number | null
|
||||
chargeableVolumeCbm: number | null
|
||||
inboundedAt: string
|
||||
businessDate: string
|
||||
priority: FirstLegPriority
|
||||
customerRemark: string
|
||||
operatorName: string
|
||||
|
||||
@@ -11,7 +11,7 @@ test('routes the confirmed first-leg path to the implemented order view', () =>
|
||||
assert.match(routerSource, /path: 'orders\/first-leg'[\s\S]*?component: \(\) => import\('\.\/views\/FirstLegOrdersView\.vue'\)[\s\S]*?release: 'active'/)
|
||||
})
|
||||
|
||||
test('opens order creation as a dedicated protected page while keeping draft editing in a drawer', () => {
|
||||
test('opens order creation as a dedicated protected page while keeping draft editing in a large dialog', () => {
|
||||
assert.match(routerSource, /path: 'orders\/first-leg\/create'[\s\S]*?name: 'first-leg-order-create'[\s\S]*?release: 'active'[\s\S]*?permission: 'order:first-leg:create'/)
|
||||
assert.match(routerSource, /name: 'first-leg-order-create'[\s\S]*?props: \{ createPage: true \}/)
|
||||
assert.match(source, /router\.push\(\{ name: 'first-leg-order-create' \}\)/)
|
||||
@@ -19,7 +19,10 @@ test('opens order creation as a dedicated protected page while keeping draft edi
|
||||
assert.match(source, /const isCreatePage = computed\(\(\) => props\.createPage\)/)
|
||||
assert.match(source, /<FirstLegOrderEditorShell[^>]*:page="isCreatePage"/)
|
||||
assert.match(editorShellSource, /<section v-if="page" class="first-leg-editor-page">/)
|
||||
assert.match(editorShellSource, /<el-drawer[\s\S]*v-else/)
|
||||
assert.match(editorShellSource, /<el-dialog[\s\S]*v-else/)
|
||||
assert.match(editorShellSource, /class="first-leg-editor-dialog"/)
|
||||
assert.match(editorShellSource, /width="94vw"/)
|
||||
assert.doesNotMatch(editorShellSource, /<el-drawer/)
|
||||
assert.match(editorShellSource, /返回订单列表/)
|
||||
})
|
||||
|
||||
@@ -56,15 +59,24 @@ test('defers approval checks to the server when the member cannot read the compa
|
||||
assert.match(source, /store\.submit\([^\n]*knownCompanyAuditStatus\)/)
|
||||
})
|
||||
|
||||
test('renders dynamic cargo lines and responsive drawers', () => {
|
||||
test('renders dynamic cargo lines and a responsive editor dialog', () => {
|
||||
assert.match(source, /v-for="\(item, index\) in form\.items"/)
|
||||
assert.match(source, /function addCargoItem\(\)/)
|
||||
assert.match(source, /function removeCargoItem\(index: number\)/)
|
||||
assert.match(source, /:global\(\.first-leg-editor\).*width: 100% !important/)
|
||||
assert.match(editorShellSource, /first-leg-editor-dialog\.el-dialog\) \{[^}]*height: 94vh;/)
|
||||
assert.match(editorShellSource, /width: 100vw !important; height: 100vh;/)
|
||||
assert.match(source, /class="mobile-order-list"/)
|
||||
assert.match(source, /\.desktop-order-table \{ display: none; \}/)
|
||||
})
|
||||
|
||||
test('uses the full editor dialog width for cargo details and cost estimates', () => {
|
||||
assert.match(source, /:class="\{ 'is-dialog-order-form': !isCreatePage \}"/)
|
||||
assert.match(source, /\.order-form\.is-dialog-order-form \{ width: 100%; max-width: none; \}/)
|
||||
assert.match(source, /\.is-dialog-order-form > \.cargo-section, \.is-dialog-order-form > \.cost-estimate-section \{ width: 100%; max-width: none; \}/)
|
||||
assert.match(source, /\.cost-estimate-cards \{ display: grid; grid-template-columns: repeat\(3, minmax\(0, 1fr\)\);/)
|
||||
assert.match(source, /\.cargo-table-wrap \{ width: 100%;/)
|
||||
})
|
||||
|
||||
test('keeps primary mobile order actions touch sized', () => {
|
||||
assert.match(source, /\.editor-actions :deep\(\.el-button\) \{ min-height: 44px;/)
|
||||
})
|
||||
@@ -73,15 +85,106 @@ test('uses the shared query toolbar and data table without metric cards', () =>
|
||||
assert.match(source, /import QueryToolbar, \{ type QueryField, type QueryModel \}/)
|
||||
assert.match(source, /import DataTable, \{ type DataTableColumn \}/)
|
||||
assert.match(source, /const filters = ref<QueryModel>/)
|
||||
assert.match(source, /<QueryToolbar v-model="filters" class="first-leg-query-toolbar" :fields="queryFields" ariaLabel="订单查询" @search="searchOrders" \/>/)
|
||||
assert.match(source, /<QueryToolbar v-model="filters" class="first-leg-query-toolbar" :fields="queryFields" ariaLabel="订单查询" @search="searchOrders">/)
|
||||
assert.match(source, /async function searchOrders\(model: QueryModel\)/)
|
||||
assert.match(source, /await store\.load\(\{/)
|
||||
assert.match(source, /<DataTable v-model:page="tablePage" v-model:page-size="tablePageSize" :data="pagedOrders" :columns="orderColumns"/)
|
||||
assert.match(source, /\{ key: 'actions', label: '操作', width: 180, fixed: 'left', slotName: 'actions' \}/)
|
||||
assert.match(source, /<DataTable v-model:page="tablePage" v-model:page-size="tablePageSize" :data="pagedOrders" :columns="visibleOrderColumns"/)
|
||||
assert.match(source, /\{ key: 'actions', label: '操作', width: 128, fixed: 'left', align: 'center', headerAlign: 'center', slotName: 'actions' \}/)
|
||||
assert.doesNotMatch(source, /order-metrics|订单概览/)
|
||||
})
|
||||
|
||||
test('shows one unified progress line, C02 business summaries, and optional customer-visible nodes', () => {
|
||||
test('adds prototype download actions to the order query panel', () => {
|
||||
assert.match(source, /下载唛头/)
|
||||
assert.match(source, /导出订单信息/)
|
||||
assert.match(source, /function downloadShippingMarks\(\)/)
|
||||
assert.match(source, /downloadMerchantOrderCartonLabelsBatch\(orders\.map\(\(order\) => order\.id\)\)/)
|
||||
assert.match(source, /error\.traceId \? `,追踪号 \$\{error\.traceId\}`/)
|
||||
assert.doesNotMatch(source, /order\.businessSummary\.inboundId/)
|
||||
assert.match(source, /批量箱唛-\$\{orders\.length\}单\.pdf/)
|
||||
assert.match(source, /function exportOrderInformation\(\)/)
|
||||
assert.match(source, /selectedOrders\.value\.map/)
|
||||
assert.match(source, /订单信息\.csv/)
|
||||
assert.doesNotMatch(source, /exportOrderInformation[\s\S]{0,1200}loadDetail/)
|
||||
})
|
||||
|
||||
test('selects orders before downloading marks or exporting last-mile data', () => {
|
||||
assert.match(source, /\{ key: 'selection', type: 'selection', width: 48, fixed: 'left', reserveSelection: true \}/)
|
||||
assert.match(source, /const selectedOrders = ref<FirstLegOrder\[\]>\(\[\]\)/)
|
||||
assert.match(source, /const selectedOrderCount = computed\(\(\) => selectedOrders\.value\.length\)/)
|
||||
assert.match(source, /@selection-change="handleOrderSelectionChange"/)
|
||||
assert.match(source, /:disabled="selectedOrderCount === 0" @click="downloadShippingMarks"/)
|
||||
assert.match(source, /:disabled="selectedOrderCount === 0" @click="exportOrderInformation"/)
|
||||
assert.match(source, /const orders = selectedOrders\.value\s*$/m)
|
||||
assert.match(source, /const rows = selectedOrders\.value\.map/)
|
||||
assert.doesNotMatch(source, /const orders = filteredOrders\.value\.filter/)
|
||||
assert.doesNotMatch(source, /const rows = filteredOrders\.value\.map/)
|
||||
})
|
||||
|
||||
test('downloads selected carton labels with one array request', () => {
|
||||
const downloadStart = source.indexOf('async function downloadShippingMarks()')
|
||||
const downloadEnd = source.indexOf('function formatExportDateTime', downloadStart)
|
||||
const downloadSource = source.slice(downloadStart, downloadEnd)
|
||||
|
||||
assert.match(downloadSource, /await downloadMerchantOrderCartonLabelsBatch\(orders\.map\(\(order\) => order\.id\)\)/)
|
||||
assert.match(downloadSource, /saveBlob\(result\.blob/)
|
||||
assert.match(downloadSource, /result\.failureCount/)
|
||||
assert.doesNotMatch(downloadSource, /for \(const order of orders\)/)
|
||||
assert.doesNotMatch(downloadSource, /Promise\.all\(/)
|
||||
assert.doesNotMatch(downloadSource, /businessSummary\.inboundId\)/)
|
||||
assert.doesNotMatch(downloadSource, /没有已生成的入库单|未生成入库单/)
|
||||
})
|
||||
|
||||
test('exports every visible order field with normalized dates', () => {
|
||||
assert.match(source, /function formatExportDateTime\(value: string \| null \| undefined\)/)
|
||||
assert.match(source, /return `\$\{date\.getFullYear\(\)\}-\$\{pad\(date\.getMonth\(\) \+ 1\)\}-\$\{pad\(date\.getDate\(\)\)\} \$\{pad\(date\.getHours\(\)\)\}:\$\{pad\(date\.getMinutes\(\)\)\}:\$\{pad\(date\.getSeconds\(\)\)\}`/)
|
||||
for (const label of ['订单号', '入仓码', '渠道', '目的仓', '当前订单状态', '箱数', '快递公司', '快递单号', 'SKUS', '总毛重', '总净重', '总体积', '总数量', '计费单价', '计费体积', '计费重量', '备注', '货件编号', '跟踪编号', '国内仓入库时间', '业务日期', '创建时间']) {
|
||||
assert.match(source, new RegExp(`'${label}'`))
|
||||
}
|
||||
assert.match(source, /formatExportDateTime\(order\.inboundedAt\)/)
|
||||
assert.match(source, /formatExportDateTime\(order\.businessDate\)/)
|
||||
assert.match(source, /formatExportDateTime\(order\.createdAt\)/)
|
||||
})
|
||||
|
||||
test('matches the prototype with selectable dense order-list columns', () => {
|
||||
assert.match(source, /aria-label="设置订单列表显示列"/)
|
||||
assert.match(source, /<SlidersHorizontal :size="18"/)
|
||||
assert.match(source, /<el-checkbox-group v-model="visibleColumnKeys"/)
|
||||
assert.match(source, /draggable="true"/)
|
||||
assert.match(source, /@drop\.prevent="dropOrderColumn/)
|
||||
assert.match(source, /function moveOrderColumn/)
|
||||
assert.match(source, /function resetOrderColumns/)
|
||||
assert.match(source, />恢复默认<\/el-button>/)
|
||||
assert.match(source, /const visibleOrderColumns = computed/)
|
||||
assert.match(source, /:columns="visibleOrderColumns"/)
|
||||
for (const label of ['入仓码', '渠道', '目的仓', '当前订单状态', '箱数', '快递公司', '快递单号', 'SKUS', '总毛重', '总净重', '总体积', '总数量', '计费单价', '计费体积', '计费重量', '备注', '货件编号', '跟踪编号', '国内仓入库时间', '业务日期']) {
|
||||
assert.match(source, new RegExp(`label: '${label}'`))
|
||||
}
|
||||
assert.match(source, /order\.businessSummary\.totalPackageCount/)
|
||||
assert.match(source, /summarizeFirstLegCargo\(order\.items\)/)
|
||||
assert.match(source, /order\.items\.reduce/)
|
||||
assert.match(source, /<template #chargeableWeight>-<\/template>/)
|
||||
assert.match(source, /<template #billingUnitPrice>-<\/template>/)
|
||||
assert.match(source, /<template #chargeableVolume>-<\/template>/)
|
||||
assert.match(source, /inboundedAt/)
|
||||
assert.match(source, /businessDate/)
|
||||
assert.match(source, /<template #businessDate>-<\/template>/)
|
||||
assert.match(source, /<template #createdAt="\{ row \}">\{\{ formatDateTime\(\(row as FirstLegOrder\)\.createdAt\) \}\}<\/template>/)
|
||||
assert.match(source, /order\.skuNos\.length/)
|
||||
assert.doesNotMatch(source, /pagedOrders[\s\S]{0,100}loadDetail/)
|
||||
})
|
||||
|
||||
test('keeps dense order identifiers on one line and aligns columns by content type', () => {
|
||||
assert.match(source, /key: 'order'[\s\S]{0,160}showOverflowTooltip: true/)
|
||||
assert.match(source, /key: 'status'[\s\S]{0,100}align: 'center'[\s\S]{0,100}headerAlign: 'center'/)
|
||||
assert.match(source, /key: 'grossWeight'[\s\S]{0,100}align: 'right'[\s\S]{0,100}headerAlign: 'right'/)
|
||||
assert.match(source, /class="order-number-link table-code"/)
|
||||
assert.doesNotMatch(source, /<template #order="\{ row \}">[\s\S]{0,400}class="order-reference"/)
|
||||
assert.match(source, /\.table-ellipsis, \.table-multivalue \{[^}]*text-overflow: ellipsis;[^}]*white-space: nowrap;/)
|
||||
assert.match(source, /\.order-number-link \{[^}]*text-overflow: ellipsis;[^}]*white-space: nowrap;/)
|
||||
assert.match(source, /\.prototype-order-table :deep\(\.el-table__body-wrapper td\.el-table__cell\) \{ height: 50px;/)
|
||||
})
|
||||
|
||||
test('shows one unified progress line and C02 business summaries without detail history', () => {
|
||||
assert.match(source, /import OrderProgressPanel/)
|
||||
assert.match(source, /<OrderProgressPanel :order="detailOrder" \/>/)
|
||||
assert.match(source, /<h3>关键业务摘要<\/h3>/)
|
||||
@@ -91,9 +194,7 @@ test('shows one unified progress line, C02 business summaries, and optional cust
|
||||
assert.match(source, /summary\.deliveryTrackingNo/)
|
||||
assert.match(progressSource, /FIRST_LEG_PROGRESS_STAGES/)
|
||||
assert.match(progressSource, /国内装柜进度及后续履约同步/)
|
||||
assert.match(progressSource, /item\.kind === 'CUSTOM_NODE'/)
|
||||
assert.match(progressSource, /业务节点/)
|
||||
assert.match(progressSource, /暂无进度记录/)
|
||||
assert.doesNotMatch(progressSource, /进度记录|el-timeline|timeline/)
|
||||
assert.match(source, /报关回执/)
|
||||
assert.match(source, /船期摘要/)
|
||||
assert.match(source, /柜次进度/)
|
||||
@@ -103,8 +204,9 @@ test('shows one unified progress line, C02 business summaries, and optional cust
|
||||
|
||||
test('uses only fields present on C02 rows before loading full detail', () => {
|
||||
assert.match(source, /getOrderCargoSummary/)
|
||||
assert.match(source, /目的地 \/ 渠道/)
|
||||
assert.match(source, /运输方式待更新|渠道待更新/)
|
||||
assert.match(source, /label: '渠道'/)
|
||||
assert.match(source, /label: '目的仓'/)
|
||||
assert.match(source, /displayListValue/)
|
||||
assert.doesNotMatch(source, /key: 'operator'/)
|
||||
assert.doesNotMatch(source, /key: 'transportMode', type: 'select'/)
|
||||
assert.match(source, /store\.draftLoadError/)
|
||||
@@ -114,9 +216,9 @@ test('uses only fields present on C02 rows before loading full detail', () => {
|
||||
})
|
||||
|
||||
test('shows and searches the inbound code after a draft is submitted', () => {
|
||||
assert.match(source, /placeholder: '订单号、入仓码、渠道或仓库'/)
|
||||
assert.match(source, /label: '入仓码 \/ SKU'/)
|
||||
assert.match(source, /order\.businessSummary\.inboundCode/)
|
||||
assert.match(source, /v-if="\(row as FirstLegOrder\)\.businessSummary\.inboundCode"[^>]*>入仓码/)
|
||||
assert.match(source, /<template #inboundCode="\{ row \}"><span class="table-ellipsis table-code"[^>]*>\{\{ displayListValue\(\(row as FirstLegOrder\)\.businessSummary\.inboundCode\) \}\}<\/span><\/template>/)
|
||||
assert.match(source, /v-if="order\.businessSummary\.inboundCode"[^>]*>入仓码/)
|
||||
assert.match(source, /v-if="detailOrder\.businessSummary\.inboundCode"><dt>入仓码<\/dt>/)
|
||||
})
|
||||
@@ -135,10 +237,10 @@ test('keeps the first viewport compact and moves create into the table toolbar',
|
||||
assert.match(source, /client-main:has\(> \.first-leg-page:not\(\.is-create-route\)\) > \.client-page-heading > div\) \{ position: absolute; width: 1px; height: 1px;/)
|
||||
})
|
||||
|
||||
test('keeps desktop query actions on the first row and only shows the qualification notice before approval', () => {
|
||||
assert.match(source, /\.first-leg-query-toolbar \{ flex-wrap: nowrap; \}/)
|
||||
assert.match(source, /query-toolbar__filters\) \{ display: grid; min-width: 0; flex: 1 1 auto;/)
|
||||
assert.match(source, /query-toolbar__actions\) \{ align-self: flex-start; flex-wrap: nowrap; margin-left: auto;/)
|
||||
test('uses the prototype query grid and only shows the qualification notice before approval', () => {
|
||||
assert.match(source, /\.first-leg-query-toolbar \{ display: block;/)
|
||||
assert.match(source, /query-toolbar__filters\) \{ display: grid; min-width: 0; grid-template-columns: repeat\(4,/)
|
||||
assert.match(source, /query-toolbar__actions\) \{ justify-content: flex-start; margin-top: 14px;/)
|
||||
assert.match(source, /v-if="companySubmissionBlocked && !supportingDataErrors\.some/)
|
||||
assert.doesNotMatch(source, /企业资料已审核,可正常提交订单/)
|
||||
assert.doesNotMatch(source, /order-eligibility\.is-approved/)
|
||||
@@ -161,9 +263,9 @@ test('requires an enabled platform receiving warehouse in the order form', () =>
|
||||
assert.match(source, /v-model="form\.receivingWarehouseId"/)
|
||||
assert.doesNotMatch(source, /selectedWarehouse\.businessHours/)
|
||||
assert.doesNotMatch(source, /class="selected-warehouse" aria-label="所选收货仓详情"/)
|
||||
assert.match(source, /first-leg-editor \.el-drawer__header\) \{[^}]*margin-bottom: 0;[^}]*border-bottom: 1px solid var\(--border\)/)
|
||||
assert.match(source, /first-leg-editor \.el-drawer__body\) \{[^}]*min-height: 0;[^}]*padding: 18px 22px 32px;[^}]*overscroll-behavior: contain/)
|
||||
assert.match(source, /first-leg-editor \.el-drawer__footer\) \{[^}]*border-top: 1px solid var\(--border\);[^}]*background: #fff/)
|
||||
assert.match(editorShellSource, /first-leg-editor-dialog \.el-dialog__header\) \{[^}]*border-bottom: 1px solid var\(--border\)/)
|
||||
assert.match(editorShellSource, /first-leg-editor-dialog \.el-dialog__body\) \{[^}]*min-height: 0;[^}]*overflow-y: auto;[^}]*overscroll-behavior: contain/)
|
||||
assert.match(editorShellSource, /first-leg-editor-dialog \.el-dialog__footer\) \{[^}]*border-top: 1px solid var\(--border\);[^}]*background: #fff/)
|
||||
assert.match(source, /detailOrder\.receivingWarehouseSnapshot\?\.name/)
|
||||
assert.match(source, /detailOrder\.receivingWarehouseSnapshot\.code/)
|
||||
assert.match(source, /loadSelectedWarehouse\(detailedOrder\.receivingWarehouseId\)/)
|
||||
@@ -297,7 +399,7 @@ test('shows fulfillment courier information as read-only client detail', () => {
|
||||
})
|
||||
|
||||
test('shows the full customer-safe order detail without platform-only controls', () => {
|
||||
const detailSource = source.slice(source.indexOf('<el-drawer v-model="detailOpen"'), source.indexOf('</el-drawer>', source.indexOf('<el-drawer v-model="detailOpen"')))
|
||||
const detailSource = source.slice(source.indexOf('<el-dialog v-model="detailOpen"'), source.indexOf('</el-dialog>', source.indexOf('<el-dialog v-model="detailOpen"')))
|
||||
for (const heading of ['订单与运输', '收发货信息', '关键业务摘要', '履约快递信息', '货物明细']) {
|
||||
assert.match(source, new RegExp(`<h3>${heading}<\\/h3>`))
|
||||
}
|
||||
@@ -314,6 +416,21 @@ test('shows the full customer-safe order detail without platform-only controls',
|
||||
assert.doesNotMatch(detailSource, /内部备注|审核通过|添加备注/)
|
||||
})
|
||||
|
||||
test('opens order detail in a large dialog and paginates cargoes as a read-only table', () => {
|
||||
assert.match(source, /<el-dialog v-model="detailOpen" class="first-leg-detail-dialog" title="订单详情" width="94vw" top="3vh"/)
|
||||
assert.doesNotMatch(source, /<el-drawer v-model="detailOpen"/)
|
||||
assert.match(source, /const detailCargoPage = ref\(1\)/)
|
||||
assert.match(source, /const detailCargoPageSize = ref\(5\)/)
|
||||
assert.match(source, /const pagedDetailCargoItems = computed/)
|
||||
assert.match(source, /items\.slice\(start, start \+ detailCargoPageSize\.value\)/)
|
||||
assert.match(source, /<el-table :data="pagedDetailCargoItems"[^>]*class="cargo-detail-table"/)
|
||||
assert.match(source, /v-model:current-page="detailCargoPage"/)
|
||||
assert.match(source, /v-model:page-size="detailCargoPageSize"/)
|
||||
assert.match(source, /:total="detailOrder\.items\.length"/)
|
||||
assert.doesNotMatch(source, /class="cargo-detail-list"/)
|
||||
assert.match(source, /first-leg-detail-dialog\.el-dialog\) \{[^}]*height: 94vh;/)
|
||||
})
|
||||
|
||||
test('loads merchant inbound billing as an isolated read-only detail section', () => {
|
||||
assert.match(source, /import InboundBillingPanel/)
|
||||
assert.match(source, /import \{ getMerchantInboundBilling \}/)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox, type FormInstance, type FormItemRule, type FormRules, type UploadRequestOptions } from 'element-plus'
|
||||
import { AlertTriangle, Box, Download, Eye, FilePlus2, ImagePlus, MapPin, PackageCheck, Pencil, Plus, Send, Trash2, Upload } from '@lucide/vue'
|
||||
import { AlertTriangle, Box, ChevronDown, ChevronUp, Download, Eye, FilePlus2, GripVertical, ImagePlus, MapPin, PackageCheck, Pencil, Plus, RotateCcw, Send, SlidersHorizontal, Trash2, Upload } from '@lucide/vue'
|
||||
import { FIRST_LEG_ORDER_STATUSES, type FirstLegOrderStatus } from '@chongchong/shared'
|
||||
import QueryToolbar, { type QueryField, type QueryModel } from '@/components/common/QueryToolbar.vue'
|
||||
import DataTable, { type DataTableColumn } from '@/components/common/DataTable.vue'
|
||||
@@ -14,6 +14,7 @@ import { getMerchantInboundBilling } from '@/modules/first-leg/api/inboundBillin
|
||||
import { getMerchantCustomsByOrder, isMerchantCustomsUnavailable, type MerchantCustomsCaseDetail } from '@/modules/first-leg/api/customs'
|
||||
import {
|
||||
estimateMerchantFirstLegPrice,
|
||||
downloadMerchantOrderCartonLabelsBatch,
|
||||
downloadMerchantProductImage,
|
||||
getMerchantPricingChannels,
|
||||
uploadMerchantProductImage,
|
||||
@@ -54,13 +55,18 @@ const canCreateOrder = computed(() => auth.hasPermission('order:first-leg:create
|
||||
const canEditOrder = computed(() => auth.hasPermission('order:first-leg:edit'))
|
||||
const availableWarehouses = computed(() => store.warehouses.filter((warehouse) => warehouse.warehouseType === 'DOMESTIC'))
|
||||
const availableDestinationWarehouses = computed(() => store.warehouses.filter((warehouse) => warehouse.warehouseType === form.destinationType))
|
||||
const filters = ref<QueryModel>({ keyword: '', status: '', createdRange: [] })
|
||||
const filters = ref<QueryModel>({ orderNo: '', receivingWarehouseId: '', channelName: '', cargoKeyword: '', createdRange: [], status: '', destinationType: '', trackingNumber: '' })
|
||||
const appliedFilters = ref<QueryModel>({ ...filters.value })
|
||||
const queryFields: QueryField[] = [
|
||||
{ key: 'keyword', type: 'input', placeholder: '订单号、入仓码、渠道或仓库', props: { 'aria-label': '订单关键词' } },
|
||||
{ key: 'status', type: 'select', placeholder: '全部状态', options: FIRST_LEG_ORDER_STATUSES.map((item) => ({ label: item.label, value: item.code })), props: { 'aria-label': '订单状态' } },
|
||||
{ key: 'createdRange', type: 'daterange', placeholder: '订单创建日期', props: { 'aria-label': '订单创建日期' } },
|
||||
]
|
||||
const queryFields = computed<QueryField[]>(() => [
|
||||
{ key: 'orderNo', label: '订单号', type: 'input', placeholder: '请输入订单号', props: { 'aria-label': '订单号' } },
|
||||
{ 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: 'cargoKeyword', label: '入仓码 / SKU', type: 'input', placeholder: '请输入入仓码或 SKU', props: { 'aria-label': '入仓码或 SKU' } },
|
||||
{ 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: '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': '快递单号' } },
|
||||
])
|
||||
const channelOptions = ref<MerchantPricingChannel[]>([])
|
||||
const channelLoading = ref(false)
|
||||
const channelError = ref('')
|
||||
@@ -80,17 +86,76 @@ const cargoTemplateColumns = [
|
||||
] as const
|
||||
|
||||
const cargoNumberKeys = new Set<string>(['lengthCm', 'widthCm', 'heightCm', 'unitsPerPackage', 'packageCount', 'grossWeightKg', 'volumeCbm', 'pieces', 'netWeightKg', 'unitDeclaredValue', 'declaredValue', 'productLengthCm', 'productWidthCm', 'productHeightCm'])
|
||||
const orderColumns: DataTableColumn[] = [
|
||||
{ key: 'actions', label: '操作', width: 180, fixed: 'left', slotName: 'actions' },
|
||||
{ key: 'order', label: '订单', minWidth: 190, slotName: 'order' },
|
||||
{ key: 'route', label: '目的地 / 渠道', minWidth: 190, slotName: 'route' },
|
||||
{ key: 'warehouse', label: '收货仓', prop: 'receivingWarehouseName', minWidth: 120 },
|
||||
{ key: 'cargo', label: '货物摘要', minWidth: 150, slotName: 'cargo' },
|
||||
{ key: 'status', label: '状态', width: 120, slotName: 'status' },
|
||||
{ key: 'updatedAt', label: '更新时间', width: 155, slotName: 'updatedAt' },
|
||||
const fixedOrderColumns: DataTableColumn[] = [
|
||||
{ key: 'selection', type: 'selection', width: 48, fixed: 'left', reserveSelection: true },
|
||||
{ key: 'actions', label: '操作', width: 128, fixed: 'left', align: 'center', headerAlign: 'center', slotName: 'actions' },
|
||||
{ key: 'order', label: '订单号', width: 190, fixed: 'left', align: 'left', headerAlign: 'left', showOverflowTooltip: true, slotName: 'order' },
|
||||
]
|
||||
const selectableOrderColumns: DataTableColumn[] = [
|
||||
{ key: 'inboundCode', label: '入仓码', width: 176, align: 'left', headerAlign: 'left', showOverflowTooltip: true, slotName: 'inboundCode' },
|
||||
{ key: 'channel', label: '渠道', width: 150, align: 'left', headerAlign: 'left', showOverflowTooltip: true, slotName: 'channel' },
|
||||
{ key: 'destinationWarehouse', label: '目的仓', width: 120, align: 'left', headerAlign: 'left', showOverflowTooltip: true, slotName: 'destinationWarehouse' },
|
||||
{ key: 'status', label: '当前订单状态', width: 126, align: 'center', headerAlign: 'center', slotName: 'status' },
|
||||
{ key: 'packageCount', label: '箱数', width: 82, align: 'center', headerAlign: 'center', slotName: 'packageCount' },
|
||||
{ key: 'expressCompany', label: '快递公司', width: 128, align: 'left', headerAlign: 'left', showOverflowTooltip: true, slotName: 'expressCompany' },
|
||||
{ key: 'trackingNumber', label: '快递单号', width: 158, align: 'left', headerAlign: 'left', showOverflowTooltip: true, slotName: 'trackingNumber' },
|
||||
{ key: 'skus', label: 'SKUS', width: 160, align: 'left', headerAlign: 'left', showOverflowTooltip: true, slotName: 'skus' },
|
||||
{ key: 'grossWeight', label: '总毛重', width: 108, align: 'right', headerAlign: 'right', slotName: 'grossWeight' },
|
||||
{ key: 'netWeight', label: '总净重', width: 108, align: 'right', headerAlign: 'right', slotName: 'netWeight' },
|
||||
{ key: 'volume', label: '总体积', width: 106, align: 'right', headerAlign: 'right', slotName: 'volume' },
|
||||
{ key: 'pieces', label: '总数量', width: 92, align: 'right', headerAlign: 'right', slotName: 'pieces' },
|
||||
{ key: 'billingUnitPrice', label: '计费单价', width: 108, align: 'right', headerAlign: 'right', slotName: 'billingUnitPrice' },
|
||||
{ key: 'chargeableVolume', label: '计费体积', width: 108, align: 'right', headerAlign: 'right', slotName: 'chargeableVolume' },
|
||||
{ key: 'chargeableWeight', label: '计费重量', width: 108, align: 'right', headerAlign: 'right', slotName: 'chargeableWeight' },
|
||||
{ key: 'remark', label: '备注', width: 160, align: 'left', headerAlign: 'left', showOverflowTooltip: true, slotName: 'remark' },
|
||||
{ key: 'shipmentId', label: '货件编号', width: 150, align: 'left', headerAlign: 'left', showOverflowTooltip: true, slotName: 'shipmentId' },
|
||||
{ key: 'referenceId', label: '跟踪编号', width: 150, align: 'left', headerAlign: 'left', showOverflowTooltip: true, slotName: 'referenceId' },
|
||||
{ key: 'receivingTime', label: '国内仓入库时间', width: 164, align: 'center', headerAlign: 'center', slotName: 'receivingTime' },
|
||||
{ key: 'businessDate', label: '业务日期', width: 148, align: 'center', headerAlign: 'center', slotName: 'businessDate' },
|
||||
{ key: 'createdAt', label: '创建时间', width: 164, align: 'center', headerAlign: 'center', slotName: 'createdAt' },
|
||||
]
|
||||
const allSelectableColumnKeys = selectableOrderColumns.map((column) => String(column.key))
|
||||
const visibleColumnKeys = ref<string[]>([...allSelectableColumnKeys])
|
||||
const orderedColumnKeys = ref<string[]>([...allSelectableColumnKeys])
|
||||
const draggedColumnKey = ref('')
|
||||
const orderedSelectableColumns = computed(() => orderedColumnKeys.value
|
||||
.map((key) => selectableOrderColumns.find((column) => String(column.key) === key))
|
||||
.filter((column): column is DataTableColumn => Boolean(column)))
|
||||
const visibleOrderColumns = computed(() => [
|
||||
...fixedOrderColumns,
|
||||
...orderedSelectableColumns.value.filter((column) => visibleColumnKeys.value.includes(String(column.key))),
|
||||
])
|
||||
|
||||
function moveOrderColumn(key: string, offset: -1 | 1) {
|
||||
const index = orderedColumnKeys.value.indexOf(key)
|
||||
const targetIndex = index + offset
|
||||
if (index < 0 || targetIndex < 0 || targetIndex >= orderedColumnKeys.value.length) return
|
||||
const nextKeys = [...orderedColumnKeys.value]
|
||||
;[nextKeys[index], nextKeys[targetIndex]] = [nextKeys[targetIndex], nextKeys[index]]
|
||||
orderedColumnKeys.value = nextKeys
|
||||
}
|
||||
|
||||
function dropOrderColumn(targetKey: string) {
|
||||
const sourceKey = draggedColumnKey.value
|
||||
draggedColumnKey.value = ''
|
||||
if (!sourceKey || sourceKey === targetKey) return
|
||||
const nextKeys = orderedColumnKeys.value.filter((key) => key !== sourceKey)
|
||||
const targetIndex = nextKeys.indexOf(targetKey)
|
||||
nextKeys.splice(targetIndex, 0, sourceKey)
|
||||
orderedColumnKeys.value = nextKeys
|
||||
}
|
||||
|
||||
function resetOrderColumns() {
|
||||
orderedColumnKeys.value = [...allSelectableColumnKeys]
|
||||
visibleColumnKeys.value = [...allSelectableColumnKeys]
|
||||
}
|
||||
const tablePage = ref(1)
|
||||
const tablePageSize = ref(10)
|
||||
const downloadingMarks = ref(false)
|
||||
const exportingOrders = ref(false)
|
||||
const selectedOrders = ref<FirstLegOrder[]>([])
|
||||
const selectedOrderCount = computed(() => selectedOrders.value.length)
|
||||
const orderTableVersion = ref(0)
|
||||
const editorOpen = ref(false)
|
||||
const orderFormRef = ref<FormInstance>()
|
||||
const detailOpen = ref(false)
|
||||
@@ -100,6 +165,8 @@ const detailId = ref<string>()
|
||||
const saving = ref(false)
|
||||
const detailLoading = ref(false)
|
||||
const detailError = ref('')
|
||||
const detailCargoPage = ref(1)
|
||||
const detailCargoPageSize = ref(5)
|
||||
const inboundBilling = ref<MerchantInboundBilling | null>(null)
|
||||
const inboundBillingLoading = ref(false)
|
||||
const inboundBillingUnavailable = ref(false)
|
||||
@@ -219,6 +286,14 @@ function formatDateTime(value: string) {
|
||||
})
|
||||
}
|
||||
|
||||
function formatExportDateTime(value: string | null | undefined) {
|
||||
if (!value) return '-'
|
||||
const date = new Date(value)
|
||||
if (Number.isNaN(date.getTime())) return value
|
||||
const pad = (part: number) => String(part).padStart(2, '0')
|
||||
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`
|
||||
}
|
||||
|
||||
const pickupMethodLabels: Record<FirstLegOrder['pickupMethod'], string> = {
|
||||
SELF_EXPRESS: '自行快递到仓',
|
||||
PICKUP: '平台上门提货',
|
||||
@@ -342,18 +417,27 @@ useUnsavedWorkRegistration({
|
||||
})
|
||||
|
||||
const filteredOrders = computed(() => store.orders.filter((order) => {
|
||||
const keywordValue = appliedFilters.value.keyword
|
||||
const statusValue = appliedFilters.value.status
|
||||
const createdRangeValue = appliedFilters.value.createdRange
|
||||
const keyword = typeof keywordValue === 'string' ? keywordValue.trim().toLowerCase() : ''
|
||||
const textValue = (key: string) => typeof appliedFilters.value[key] === 'string' ? String(appliedFilters.value[key]).trim().toLowerCase() : ''
|
||||
const orderNo = textValue('orderNo')
|
||||
const receivingWarehouseId = textValue('receivingWarehouseId')
|
||||
const channelName = textValue('channelName')
|
||||
const cargoKeyword = textValue('cargoKeyword')
|
||||
const destinationType = textValue('destinationType')
|
||||
const trackingNumber = textValue('trackingNumber')
|
||||
const status = typeof statusValue === 'string' ? statusValue as FirstLegOrderStatus | '' : ''
|
||||
const createdRange = Array.isArray(createdRangeValue) ? createdRangeValue.filter((value): value is string => typeof value === 'string') : []
|
||||
const matchesKeyword = !keyword || [order.orderNo, order.businessSummary.inboundCode, order.referenceNo, order.origin, order.destination, order.items.map((item) => item.cargoNameCn).join(' ')]
|
||||
.filter(Boolean)
|
||||
.some((value) => String(value).toLowerCase().includes(keyword))
|
||||
const matchesOrderNo = !orderNo || String(order.orderNo ?? '').toLowerCase().includes(orderNo)
|
||||
const matchesWarehouse = !receivingWarehouseId || order.receivingWarehouseId.toLowerCase() === receivingWarehouseId
|
||||
const matchesChannel = !channelName || order.channelName.toLowerCase() === channelName
|
||||
const matchesCargo = !cargoKeyword || [order.businessSummary.inboundCode, ...order.skuNos, ...order.items.map((item) => item.sku)]
|
||||
.filter(Boolean).some((value) => String(value).toLowerCase().includes(cargoKeyword))
|
||||
const matchesDestinationType = !destinationType || order.destinationType.toLowerCase() === destinationType
|
||||
const matchesTrackingNumber = !trackingNumber || order.trackingNumber.toLowerCase().includes(trackingNumber)
|
||||
const matchesStatus = !status || order.status === status
|
||||
const matchesDate = createdRange.length !== 2 || (order.createdAt.slice(0, 10) >= createdRange[0] && order.createdAt.slice(0, 10) <= createdRange[1])
|
||||
return matchesKeyword && matchesStatus && matchesDate
|
||||
return matchesOrderNo && matchesWarehouse && matchesChannel && matchesCargo && matchesDestinationType && matchesTrackingNumber && matchesStatus && matchesDate
|
||||
}))
|
||||
const pagedOrders = computed(() => {
|
||||
const start = (tablePage.value - 1) * tablePageSize.value
|
||||
@@ -362,6 +446,11 @@ const pagedOrders = computed(() => {
|
||||
|
||||
const detailOrder = computed(() => detailId.value ? store.getById(detailId.value) : undefined)
|
||||
const detailSummary = computed(() => detailOrder.value ? summarizeFirstLegCargo(detailOrder.value.items) : null)
|
||||
const pagedDetailCargoItems = computed(() => {
|
||||
const items = detailOrder.value?.items ?? []
|
||||
const start = (detailCargoPage.value - 1) * detailCargoPageSize.value
|
||||
return items.slice(start, start + detailCargoPageSize.value)
|
||||
})
|
||||
const progressSummaryItems = computed(() => {
|
||||
const order = detailOrder.value
|
||||
if (!order) return []
|
||||
@@ -397,11 +486,7 @@ const selectedPricingChannel = computed(() => channelOptions.value.find((item) =
|
||||
const availablePricingTaxModes = computed(() => selectedPricingChannel.value?.pricingTaxModes ?? [])
|
||||
const hasActiveFilters = computed(() => {
|
||||
const values = appliedFilters.value
|
||||
return Boolean(
|
||||
(typeof values.keyword === 'string' && values.keyword.trim())
|
||||
|| values.status
|
||||
|| (Array.isArray(values.createdRange) && values.createdRange.length),
|
||||
)
|
||||
return Object.values(values).some((value) => Array.isArray(value) ? value.length > 0 : Boolean(typeof value === 'string' ? value.trim() : value))
|
||||
})
|
||||
const emptyDescription = computed(() => hasActiveFilters.value ? '没有符合筛选条件的订单' : '还没有订单')
|
||||
|
||||
@@ -927,6 +1012,7 @@ async function openEdit(order: FirstLegOrder) {
|
||||
|
||||
async function openDetail(order: FirstLegOrder) {
|
||||
detailId.value = order.id
|
||||
detailCargoPage.value = 1
|
||||
detailOpen.value = true
|
||||
detailLoading.value = true
|
||||
detailError.value = ''
|
||||
@@ -1059,6 +1145,128 @@ function removeCargoItem(index: number) {
|
||||
resetEditorValidation()
|
||||
}
|
||||
|
||||
function displayListValue(value: string | number | null | undefined) {
|
||||
return value === '' || value == null ? '-' : String(value)
|
||||
}
|
||||
|
||||
function uniqueItemValues(order: FirstLegOrder, key: 'sku' | 'shipmentId' | 'referenceId') {
|
||||
if (key === 'sku' && order.skuNos.length) return [...new Set(order.skuNos)].join('、')
|
||||
const values = order.items.map((item) => item[key]?.trim()).filter((value): value is string => Boolean(value))
|
||||
return [...new Set(values)].join('、') || '-'
|
||||
}
|
||||
|
||||
function orderPackageCount(order: FirstLegOrder) {
|
||||
return order.businessSummary.totalPackageCount ?? (order.items.length ? summarizeFirstLegCargo(order.items).packageCount : null)
|
||||
}
|
||||
|
||||
function orderGrossWeight(order: FirstLegOrder) {
|
||||
return order.businessSummary.actualGrossWeightKg ?? (order.items.length ? summarizeFirstLegCargo(order.items).grossWeightKg : null)
|
||||
}
|
||||
|
||||
function orderVolume(order: FirstLegOrder) {
|
||||
return order.businessSummary.actualVolumeCbm ?? (order.items.length ? summarizeFirstLegCargo(order.items).volumeCbm : null)
|
||||
}
|
||||
|
||||
function orderItemTotal(order: FirstLegOrder, key: 'netWeightKg' | 'pieces') {
|
||||
if (key === 'netWeightKg' && order.totalNetWeightKg != null) return order.totalNetWeightKg
|
||||
if (key === 'pieces' && order.totalQuantity != null) return order.totalQuantity
|
||||
if (!order.items.length) return null
|
||||
return order.items.reduce((total, item) => total + (Number(item[key]) || 0), 0)
|
||||
}
|
||||
|
||||
function formatListMetric(value: number | null | undefined, unit = '') {
|
||||
if (value == null) return '-'
|
||||
return `${value.toLocaleString('zh-CN', { maximumFractionDigits: 3 })}${unit ? ` ${unit}` : ''}`
|
||||
}
|
||||
|
||||
function csvCell(value: unknown) {
|
||||
return `"${String(value ?? '').replaceAll('"', '""')}"`
|
||||
}
|
||||
|
||||
function downloadCsv(fileName: string, rows: unknown[][]) {
|
||||
const content = `\uFEFF${rows.map((row) => row.map(csvCell).join(',')).join('\r\n')}`
|
||||
const url = URL.createObjectURL(new Blob([content], { type: 'text/csv;charset=utf-8' }))
|
||||
const anchor = document.createElement('a')
|
||||
anchor.href = url
|
||||
anchor.download = fileName
|
||||
anchor.click()
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
|
||||
function saveBlob(blob: Blob, fileName: string) {
|
||||
const url = URL.createObjectURL(blob)
|
||||
const anchor = document.createElement('a')
|
||||
anchor.href = url
|
||||
anchor.download = fileName
|
||||
anchor.click()
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
|
||||
function handleOrderSelectionChange(rows: FirstLegOrder[]) {
|
||||
selectedOrders.value = rows
|
||||
}
|
||||
|
||||
async function downloadShippingMarks() {
|
||||
if (downloadingMarks.value) return
|
||||
const orders = selectedOrders.value
|
||||
if (!orders.length) return
|
||||
downloadingMarks.value = true
|
||||
try {
|
||||
const result = await downloadMerchantOrderCartonLabelsBatch(orders.map((order) => order.id))
|
||||
saveBlob(result.blob, `批量箱唛-${orders.length}单.pdf`)
|
||||
if (result.failureCount > 0) {
|
||||
ElMessage.warning(`箱唛已下载,${result.failureCount} 份订单生成失败:${result.failures}`)
|
||||
} else {
|
||||
ElMessage.success(`已下载 ${orders.length} 份订单箱唛`)
|
||||
}
|
||||
} catch (error) {
|
||||
const reason = error instanceof ApiError
|
||||
? `${error.message}${error.code ? ` [${error.code}]` : ''}${error.traceId ? `,追踪号 ${error.traceId}` : ''}`
|
||||
: (error instanceof Error ? error.message : '箱唛生成失败')
|
||||
ElMessage.error(reason)
|
||||
} finally {
|
||||
downloadingMarks.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function exportOrderInformation() {
|
||||
exportingOrders.value = true
|
||||
try {
|
||||
const rows = selectedOrders.value.map((order) => [
|
||||
displayListValue(order.orderNo),
|
||||
displayListValue(order.businessSummary.inboundCode),
|
||||
displayListValue(order.channelName),
|
||||
displayListValue(order.destinationWarehouseName),
|
||||
getFirstLegStatusLabel(order.status),
|
||||
formatListMetric(orderPackageCount(order)),
|
||||
displayListValue(order.expressCompanyName),
|
||||
displayListValue(order.trackingNumber),
|
||||
uniqueItemValues(order, 'sku'),
|
||||
formatListMetric(orderGrossWeight(order), 'kg'),
|
||||
formatListMetric(orderItemTotal(order, 'netWeightKg'), 'kg'),
|
||||
formatListMetric(orderVolume(order), 'm³'),
|
||||
formatListMetric(orderItemTotal(order, 'pieces')),
|
||||
'-',
|
||||
'-',
|
||||
'-',
|
||||
displayListValue(order.customerRemark),
|
||||
displayListValue(order.shipmentId || uniqueItemValues(order, 'shipmentId')),
|
||||
displayListValue(order.referenceNo || uniqueItemValues(order, 'referenceId')),
|
||||
formatExportDateTime(order.inboundedAt),
|
||||
formatExportDateTime(order.businessDate),
|
||||
formatExportDateTime(order.createdAt),
|
||||
])
|
||||
downloadCsv('订单信息.csv', [[
|
||||
'订单号', '入仓码', '渠道', '目的仓', '当前订单状态', '箱数', '快递公司', '快递单号', 'SKUS',
|
||||
'总毛重', '总净重', '总体积', '总数量', '计费单价', '计费体积', '计费重量', '备注',
|
||||
'货件编号', '跟踪编号', '国内仓入库时间', '业务日期', '创建时间',
|
||||
], ...rows])
|
||||
ElMessage.success(`已导出 ${rows.length} 条订单信息`)
|
||||
} finally {
|
||||
exportingOrders.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const allCargoSelected = computed(() => form.items.length > 0 && selectedCargoIds.value.length === form.items.length)
|
||||
const someCargoSelected = computed(() => selectedCargoIds.value.length > 0 && !allCargoSelected.value)
|
||||
|
||||
@@ -1240,16 +1448,21 @@ async function searchOrders(model: QueryModel) {
|
||||
tablePage.value = 1
|
||||
try {
|
||||
const statusValue = typeof model.status === 'string' ? model.status as FirstLegOrderStatus | '' : ''
|
||||
const keyword = typeof model.keyword === 'string' ? model.keyword.trim() : ''
|
||||
const orderNo = typeof model.orderNo === 'string' ? model.orderNo.trim() : ''
|
||||
const cargoKeyword = typeof model.cargoKeyword === 'string' ? model.cargoKeyword.trim() : ''
|
||||
const range = Array.isArray(createdRange) ? createdRange.filter((value): value is string => typeof value === 'string') : []
|
||||
await store.load({
|
||||
keyword: keyword || undefined,
|
||||
keyword: cargoKeyword || undefined,
|
||||
orderNo: orderNo || undefined,
|
||||
receivingWarehouseId: typeof model.receivingWarehouseId === 'string' && model.receivingWarehouseId ? model.receivingWarehouseId : undefined,
|
||||
mainStatus: statusValue && statusValue !== 'DRAFT' ? statusValue : undefined,
|
||||
draftOnly: statusValue === 'DRAFT',
|
||||
includeDrafts: !statusValue,
|
||||
createdFrom: range[0] ? `${range[0]}T00:00:00` : undefined,
|
||||
createdTo: range[1] ? `${range[1]}T23:59:59` : undefined,
|
||||
})
|
||||
selectedOrders.value = []
|
||||
orderTableVersion.value += 1
|
||||
} catch {
|
||||
// Store keeps the recoverable error state rendered below the query toolbar.
|
||||
}
|
||||
@@ -1259,12 +1472,45 @@ async function searchOrders(model: QueryModel) {
|
||||
<template>
|
||||
<section class="first-leg-page" :class="{ 'is-create-route': isCreatePage }">
|
||||
<template v-if="!isCreatePage">
|
||||
<QueryToolbar v-model="filters" class="first-leg-query-toolbar" :fields="queryFields" ariaLabel="订单查询" @search="searchOrders" />
|
||||
<QueryToolbar v-model="filters" class="first-leg-query-toolbar" :fields="queryFields" ariaLabel="订单查询" @search="searchOrders">
|
||||
<template #actions>
|
||||
<el-button class="query-export-button" type="primary" :loading="downloadingMarks" :disabled="selectedOrderCount === 0" @click="downloadShippingMarks"><Download :size="16" />下载唛头</el-button>
|
||||
<el-button class="query-export-button" type="primary" :loading="exportingOrders" :disabled="selectedOrderCount === 0" @click="exportOrderInformation"><Download :size="16" />导出订单信息</el-button>
|
||||
</template>
|
||||
</QueryToolbar>
|
||||
|
||||
<div class="client-card order-table-card">
|
||||
<div class="order-table-toolbar">
|
||||
<div><strong>订单列表</strong><span>共 {{ filteredOrders.length }} 条</span></div>
|
||||
<el-button v-if="auth.hasPermission('order:first-leg:create')" type="primary" :loading="pageDataLoading" :disabled="pageDataLoading || availableWarehouses.length === 0" @click="openCreate"><FilePlus2 :size="17" />创建订单</el-button>
|
||||
<div class="order-table-toolbar-actions">
|
||||
<el-popover placement="bottom-end" :width="320" trigger="click" popper-class="order-column-popper">
|
||||
<template #reference>
|
||||
<el-button class="column-settings-trigger" aria-label="设置订单列表显示列" title="设置显示列"><SlidersHorizontal :size="18" /></el-button>
|
||||
</template>
|
||||
<div class="column-settings-panel">
|
||||
<p>拖动调整顺序,也可使用上下移动按钮。</p>
|
||||
<el-checkbox-group v-model="visibleColumnKeys" aria-label="订单列表显示列">
|
||||
<div
|
||||
v-for="(column, index) in orderedSelectableColumns"
|
||||
:key="String(column.key)"
|
||||
class="column-settings-item"
|
||||
draggable="true"
|
||||
@dragstart="draggedColumnKey = String(column.key)"
|
||||
@dragend="draggedColumnKey = ''"
|
||||
@dragover.prevent
|
||||
@drop.prevent="dropOrderColumn(String(column.key))"
|
||||
>
|
||||
<GripVertical :size="17" aria-hidden="true" />
|
||||
<el-checkbox :value="String(column.key)">{{ column.label }}</el-checkbox>
|
||||
<el-button link :disabled="index === 0" :aria-label="`上移${column.label}`" :title="`上移${column.label}`" @click="moveOrderColumn(String(column.key), -1)"><ChevronUp :size="17" /></el-button>
|
||||
<el-button link :disabled="index === orderedSelectableColumns.length - 1" :aria-label="`下移${column.label}`" :title="`下移${column.label}`" @click="moveOrderColumn(String(column.key), 1)"><ChevronDown :size="17" /></el-button>
|
||||
</div>
|
||||
</el-checkbox-group>
|
||||
<el-button link class="column-settings-reset" @click="resetOrderColumns"><RotateCcw :size="17" />恢复默认</el-button>
|
||||
</div>
|
||||
</el-popover>
|
||||
<el-button v-if="auth.hasPermission('order:first-leg:create')" type="primary" :loading="pageDataLoading" :disabled="pageDataLoading || availableWarehouses.length === 0" @click="openCreate"><FilePlus2 :size="17" />创建订单</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="companySubmissionBlocked && !supportingDataErrors.some((message) => message.startsWith('企业资料'))" class="order-eligibility is-pending" role="status">
|
||||
<AlertTriangle :size="20" />
|
||||
@@ -1283,7 +1529,7 @@ async function searchOrders(model: QueryModel) {
|
||||
</div>
|
||||
<template v-else>
|
||||
<div class="desktop-order-table">
|
||||
<DataTable v-model:page="tablePage" v-model:page-size="tablePageSize" :data="pagedOrders" :columns="orderColumns" :loading="store.loading" :total="filteredOrders.length" :page-sizes="[10, 20, 50]" row-key="id" empty-text="暂无订单">
|
||||
<DataTable v-model:page="tablePage" v-model:page-size="tablePageSize" :data="pagedOrders" :columns="visibleOrderColumns" :key="orderTableVersion" :loading="store.loading" :total="filteredOrders.length" :page-sizes="[10, 20, 50]" row-key="id" empty-text="暂无订单" class="prototype-order-table" @selection-change="handleOrderSelectionChange">
|
||||
<template #actions="{ row }">
|
||||
<div class="order-row-actions">
|
||||
<el-button link type="primary" @click="openDetail(row as FirstLegOrder)"><Eye :size="15" />详情</el-button>
|
||||
@@ -1291,14 +1537,29 @@ async function searchOrders(model: QueryModel) {
|
||||
</div>
|
||||
</template>
|
||||
<template #order="{ row }">
|
||||
<button class="order-number-link" type="button" @click="openDetail(row as FirstLegOrder)">{{ (row as FirstLegOrder).orderNo || '未提交草稿' }}</button>
|
||||
<span v-if="(row as FirstLegOrder).businessSummary.inboundCode" class="order-reference">入仓码 {{ (row as FirstLegOrder).businessSummary.inboundCode }}</span>
|
||||
<span class="order-reference">{{ (row as FirstLegOrder).referenceNo || '无 Reference ID' }}</span>
|
||||
<button class="order-number-link table-code" type="button" :title="(row as FirstLegOrder).orderNo || '未提交草稿'" @click="openDetail(row as FirstLegOrder)">{{ (row as FirstLegOrder).orderNo || '未提交草稿' }}</button>
|
||||
</template>
|
||||
<template #route="{ row }"><strong>{{ (row as FirstLegOrder).destination || '目的地待更新' }}</strong><span class="order-reference">{{ (row as FirstLegOrder).transportMode ? getFirstLegTransportModeLabel((row as FirstLegOrder).transportMode) : (row as FirstLegOrder).channelName || '渠道待更新' }}</span></template>
|
||||
<template #cargo="{ row }">{{ getOrderCargoSummary(row as FirstLegOrder) }}</template>
|
||||
<template #inboundCode="{ row }"><span class="table-ellipsis table-code" :title="displayListValue((row as FirstLegOrder).businessSummary.inboundCode)">{{ displayListValue((row as FirstLegOrder).businessSummary.inboundCode) }}</span></template>
|
||||
<template #channel="{ row }"><span class="table-ellipsis" :title="displayListValue((row as FirstLegOrder).channelName)">{{ displayListValue((row as FirstLegOrder).channelName) }}</span></template>
|
||||
<template #destinationWarehouse="{ row }"><span class="table-ellipsis" :title="displayListValue((row as FirstLegOrder).destinationWarehouseName)">{{ displayListValue((row as FirstLegOrder).destinationWarehouseName) }}</span></template>
|
||||
<template #status="{ row }"><el-tag :type="getFirstLegStatusTone((row as FirstLegOrder).status)" effect="light">{{ getFirstLegStatusLabel((row as FirstLegOrder).status) }}</el-tag></template>
|
||||
<template #updatedAt="{ row }">{{ formatDateTime((row as FirstLegOrder).updatedAt) }}</template>
|
||||
<template #packageCount="{ row }">{{ formatListMetric(orderPackageCount(row as FirstLegOrder)) }}</template>
|
||||
<template #expressCompany="{ row }"><span class="table-ellipsis" :title="displayListValue((row as FirstLegOrder).expressCompanyName)">{{ displayListValue((row as FirstLegOrder).expressCompanyName) }}</span></template>
|
||||
<template #trackingNumber="{ row }"><span class="table-ellipsis table-code" :title="displayListValue((row as FirstLegOrder).trackingNumber)">{{ displayListValue((row as FirstLegOrder).trackingNumber) }}</span></template>
|
||||
<template #skus="{ row }"><span class="table-ellipsis table-code" :title="uniqueItemValues(row as FirstLegOrder, 'sku')">{{ uniqueItemValues(row as FirstLegOrder, 'sku') }}</span></template>
|
||||
<template #grossWeight="{ row }">{{ formatListMetric(orderGrossWeight(row as FirstLegOrder), 'kg') }}</template>
|
||||
<template #netWeight="{ row }">{{ formatListMetric(orderItemTotal(row as FirstLegOrder, 'netWeightKg'), 'kg') }}</template>
|
||||
<template #volume="{ row }">{{ formatListMetric(orderVolume(row as FirstLegOrder), 'm³') }}</template>
|
||||
<template #pieces="{ row }">{{ formatListMetric(orderItemTotal(row as FirstLegOrder, 'pieces')) }}</template>
|
||||
<template #billingUnitPrice>-</template>
|
||||
<template #chargeableVolume>-</template>
|
||||
<template #chargeableWeight>-</template>
|
||||
<template #remark="{ row }"><span class="table-ellipsis" :title="(row as FirstLegOrder).customerRemark">{{ displayListValue((row as FirstLegOrder).customerRemark) }}</span></template>
|
||||
<template #shipmentId="{ row }"><span class="table-ellipsis table-code" :title="displayListValue((row as FirstLegOrder).shipmentId || uniqueItemValues(row as FirstLegOrder, 'shipmentId'))">{{ displayListValue((row as FirstLegOrder).shipmentId || uniqueItemValues(row as FirstLegOrder, 'shipmentId')) }}</span></template>
|
||||
<template #referenceId="{ row }"><span class="table-ellipsis table-code" :title="displayListValue((row as FirstLegOrder).referenceNo || uniqueItemValues(row as FirstLegOrder, 'referenceId'))">{{ displayListValue((row as FirstLegOrder).referenceNo || uniqueItemValues(row as FirstLegOrder, 'referenceId')) }}</span></template>
|
||||
<template #receivingTime="{ row }">{{ (row as FirstLegOrder).inboundedAt ? formatDateTime((row as FirstLegOrder).inboundedAt) : '-' }}</template>
|
||||
<template #businessDate>-</template>
|
||||
<template #createdAt="{ row }">{{ formatDateTime((row as FirstLegOrder).createdAt) }}</template>
|
||||
<template #empty>
|
||||
<el-empty :description="emptyDescription">
|
||||
<el-button v-if="auth.hasPermission('order:first-leg:create')" type="primary" :loading="pageDataLoading" :disabled="pageDataLoading || availableWarehouses.length === 0" @click="openCreate"><Plus :size="16" />{{ pageDataLoading ? '加载收货仓中' : (availableWarehouses.length ? '创建第一笔订单' : '暂无可用收货仓') }}</el-button>
|
||||
@@ -1332,7 +1593,7 @@ async function searchOrders(model: QueryModel) {
|
||||
<el-alert v-if="isCreatePage && !supportingDataLoading && availableWarehouses.length === 0" title="当前没有可用的平台收货仓" description="请联系平台维护并启用收货仓后再创建订单。" type="warning" :closable="false" show-icon />
|
||||
|
||||
<FirstLegOrderEditorShell v-model="editorOpen" :page="isCreatePage" :title="editingId ? '编辑订单草稿' : '创建订单'" :before-close="requestCloseEditor" @back="closeEditor">
|
||||
<el-form ref="orderFormRef" v-loading="isCreatePage && supportingDataLoading" :model="form" :rules="orderFormRules" label-position="top" class="order-form prototype-order-form" scroll-to-error>
|
||||
<el-form ref="orderFormRef" v-loading="isCreatePage && supportingDataLoading" :model="form" :rules="orderFormRules" label-position="top" class="order-form prototype-order-form" :class="{ 'is-dialog-order-form': !isCreatePage }" scroll-to-error>
|
||||
<section class="order-form-section prototype-section">
|
||||
<div class="order-section-heading"><div><h3>客户下单</h3><p>先选择货物如何送到国内收货仓,并填写本次运输安排。</p></div></div>
|
||||
<div class="prototype-form-flow shipping-form-flow">
|
||||
@@ -1448,7 +1709,7 @@ async function searchOrders(model: QueryModel) {
|
||||
<template #footer><div class="editor-actions"><el-button :disabled="saving" @click="closeEditor">返回</el-button><el-button v-if="editingId ? canEditOrder : canCreateOrder" :loading="saving" @click="saveDraft">保存草稿</el-button><el-button v-if="auth.hasPermission('order:first-leg:submit')" type="primary" :loading="saving" @click="submitOrder"><Send :size="16" />{{ companySubmissionBlocked ? '提交前需完成资料审核' : '提交订单' }}</el-button></div></template>
|
||||
</FirstLegOrderEditorShell>
|
||||
|
||||
<el-drawer v-model="detailOpen" class="first-leg-detail" title="订单详情" size="820px">
|
||||
<el-dialog v-model="detailOpen" class="first-leg-detail-dialog" title="订单详情" width="94vw" top="3vh" destroy-on-close append-to-body>
|
||||
<div v-loading="detailLoading" class="order-detail-content">
|
||||
<div v-if="detailError" class="order-load-state is-compact" role="alert">
|
||||
<AlertTriangle :size="24" />
|
||||
@@ -1527,24 +1788,44 @@ async function searchOrders(model: QueryModel) {
|
||||
<section class="detail-section">
|
||||
<h3>货物明细</h3>
|
||||
<div v-if="detailSummary" class="cargo-summary"><div><Box :size="18" /><span>明细</span><strong>{{ detailOrder.items.length }} 项</strong></div><div><span>件数</span><strong>{{ detailSummary.pieces }}</strong></div><div><span>毛重</span><strong>{{ detailSummary.grossWeightKg.toLocaleString() }} kg</strong></div><div><span>体积</span><strong>{{ detailSummary.volumeCbm }} m³</strong></div></div>
|
||||
<div v-if="detailOrder.items.length" class="cargo-detail-list">
|
||||
<article v-for="(item, index) in detailOrder.items" :key="item.id">
|
||||
<header><span>货物 {{ index + 1 }}</span><strong>{{ item.cargoNameCn || '未命名货物' }}</strong><small>{{ item.cargoNameEn || '' }}</small></header>
|
||||
<div v-if="item.productImageFileId" class="cargo-detail-image"><img v-if="item.productImagePreviewUrl" :src="item.productImagePreviewUrl" :alt="`${item.cargoNameCn || `货物 ${index + 1}`}产品图片`" /><span v-else-if="item.productImageUploading">图片加载中...</span><span v-else>{{ item.productImageError || '产品图片暂不可用' }}</span></div>
|
||||
<dl>
|
||||
<div><dt>SKU NO.</dt><dd>{{ item.sku || '-' }}</dd></div><div><dt>UPC NO.</dt><dd>{{ item.upc || '-' }}</dd></div>
|
||||
<div><dt>报关 HS CODE</dt><dd>{{ item.hsCode || '-' }}</dd></div><div><dt>清关 HS CODE</dt><dd>{{ item.customsHsCode || '-' }}</dd></div>
|
||||
<div><dt>箱规(长×宽×高)</dt><dd>{{ item.lengthCm || '-' }} × {{ item.widthCm || '-' }} × {{ item.heightCm || '-' }} cm</dd></div>
|
||||
<div><dt>每箱数量</dt><dd>{{ item.unitsPerPackage }} 件</dd></div><div><dt>箱数</dt><dd>{{ item.packageCount }} 箱</dd></div><div><dt>总数量</dt><dd>{{ item.pieces }} 件</dd></div>
|
||||
<div><dt>总毛重</dt><dd>{{ item.grossWeightKg }} kg</dd></div><div><dt>总净重</dt><dd>{{ item.netWeightKg ? `${item.netWeightKg} kg` : '-' }}</dd></div><div><dt>总体积</dt><dd>{{ item.volumeCbm }} m³</dd></div>
|
||||
<div><dt>清关申报单价</dt><dd>{{ item.unitDeclaredValue ? `USD ${item.unitDeclaredValue.toLocaleString()}` : '-' }}</dd></div><div><dt>清关申报金额</dt><dd>{{ item.declaredValue ? `USD ${item.declaredValue.toLocaleString()}` : '-' }}</dd></div>
|
||||
<div><dt>中英文材质</dt><dd>{{ item.material || '-' }}</dd></div><div><dt>中英文用途</dt><dd>{{ item.usage || '-' }}</dd></div><div><dt>型号</dt><dd>{{ item.model || '-' }}</dd></div><div><dt>品牌</dt><dd>{{ item.brand || '-' }}</dd></div>
|
||||
<div><dt>产品规格(长×宽×高)</dt><dd>{{ item.productLengthCm || '-' }} × {{ item.productWidthCm || '-' }} × {{ item.productHeightCm || '-' }} cm</dd></div>
|
||||
<div><dt>Shipment ID</dt><dd>{{ item.shipmentId || '-' }}</dd></div><div><dt>Reference ID</dt><dd>{{ item.referenceId || '-' }}</dd></div>
|
||||
<div><dt>包装类型</dt><dd>{{ packageTypeLabels[item.packageType] || item.packageType || '-' }}</dd></div><div><dt>危险品</dt><dd>{{ item.dangerousGoods ? '是' : '否' }}</dd></div>
|
||||
</dl>
|
||||
<p v-if="item.remark">{{ item.remark }}</p>
|
||||
</article>
|
||||
<div v-if="detailOrder.items.length" class="cargo-detail-table-wrap">
|
||||
<el-table :data="pagedDetailCargoItems" border stripe class="cargo-detail-table" row-key="id">
|
||||
<el-table-column type="index" label="#" width="54" align="center" :index="(index: number) => (detailCargoPage - 1) * detailCargoPageSize + index + 1" />
|
||||
<el-table-column label="SKU NO." width="150" show-overflow-tooltip><template #default="{ row }"><span class="table-code table-ellipsis">{{ row.sku || '-' }}</span></template></el-table-column>
|
||||
<el-table-column label="中文品名" min-width="150" show-overflow-tooltip><template #default="{ row }">{{ row.cargoNameCn || '-' }}</template></el-table-column>
|
||||
<el-table-column label="图片" width="66" align="center"><template #default="{ row }"><div class="cargo-detail-thumbnail"><img v-if="row.productImagePreviewUrl" :src="row.productImagePreviewUrl" :alt="`${row.cargoNameCn || '货物'}产品图片`" /><span v-else>-</span></div></template></el-table-column>
|
||||
<el-table-column label="英文品名" min-width="150" show-overflow-tooltip><template #default="{ row }">{{ row.cargoNameEn || '-' }}</template></el-table-column>
|
||||
<el-table-column label="包装类型" width="100" align="center"><template #default="{ row }">{{ packageTypeLabels[row.packageType] || row.packageType || '-' }}</template></el-table-column>
|
||||
<el-table-column label="箱规 长×宽×高(cm)" width="170" align="center"><template #default="{ row }">{{ row.lengthCm || '-' }} × {{ row.widthCm || '-' }} × {{ row.heightCm || '-' }}</template></el-table-column>
|
||||
<el-table-column prop="unitsPerPackage" label="每箱数量" width="88" align="right" />
|
||||
<el-table-column prop="packageCount" label="箱数" width="72" align="right" />
|
||||
<el-table-column prop="pieces" label="总数量" width="82" align="right" />
|
||||
<el-table-column label="总毛重" width="94" align="right"><template #default="{ row }">{{ row.grossWeightKg }} kg</template></el-table-column>
|
||||
<el-table-column label="总净重" width="94" align="right"><template #default="{ row }">{{ row.netWeightKg ? `${row.netWeightKg} kg` : '-' }}</template></el-table-column>
|
||||
<el-table-column label="总体积" width="98" align="right"><template #default="{ row }">{{ row.volumeCbm }} m³</template></el-table-column>
|
||||
<el-table-column label="报关 HS CODE" width="128" show-overflow-tooltip><template #default="{ row }"><span class="table-code">{{ row.hsCode || '-' }}</span></template></el-table-column>
|
||||
<el-table-column label="清关 HS CODE" width="128" show-overflow-tooltip><template #default="{ row }"><span class="table-code">{{ row.customsHsCode || '-' }}</span></template></el-table-column>
|
||||
<el-table-column label="申报单价" width="104" align="right"><template #default="{ row }">{{ row.unitDeclaredValue ? `USD ${row.unitDeclaredValue.toLocaleString()}` : '-' }}</template></el-table-column>
|
||||
<el-table-column label="申报金额" width="104" align="right"><template #default="{ row }">{{ row.declaredValue ? `USD ${row.declaredValue.toLocaleString()}` : '-' }}</template></el-table-column>
|
||||
<el-table-column label="Shipment ID" width="150" show-overflow-tooltip><template #default="{ row }"><span class="table-code table-ellipsis">{{ row.shipmentId || '-' }}</span></template></el-table-column>
|
||||
<el-table-column label="Reference ID" width="150" show-overflow-tooltip><template #default="{ row }"><span class="table-code table-ellipsis">{{ row.referenceId || '-' }}</span></template></el-table-column>
|
||||
<el-table-column label="UPC NO." width="130" show-overflow-tooltip><template #default="{ row }"><span class="table-code table-ellipsis">{{ row.upc || '-' }}</span></template></el-table-column>
|
||||
<el-table-column label="中英文材质" width="120" show-overflow-tooltip><template #default="{ row }">{{ row.material || '-' }}</template></el-table-column>
|
||||
<el-table-column label="中英文用途" width="120" show-overflow-tooltip><template #default="{ row }">{{ row.usage || '-' }}</template></el-table-column>
|
||||
<el-table-column label="型号" width="110" show-overflow-tooltip><template #default="{ row }">{{ row.model || '-' }}</template></el-table-column>
|
||||
<el-table-column label="品牌" width="110" show-overflow-tooltip><template #default="{ row }">{{ row.brand || '-' }}</template></el-table-column>
|
||||
<el-table-column label="产品规格 长×宽×高(cm)" width="180" align="center"><template #default="{ row }">{{ row.productLengthCm || '-' }} × {{ row.productWidthCm || '-' }} × {{ row.productHeightCm || '-' }}</template></el-table-column>
|
||||
<el-table-column label="危险品" width="82" align="center"><template #default="{ row }">{{ row.dangerousGoods ? '是' : '否' }}</template></el-table-column>
|
||||
<el-table-column label="备注" min-width="160" show-overflow-tooltip><template #default="{ row }">{{ row.remark || '-' }}</template></el-table-column>
|
||||
</el-table>
|
||||
<el-pagination
|
||||
v-model:current-page="detailCargoPage"
|
||||
v-model:page-size="detailCargoPageSize"
|
||||
class="cargo-detail-pagination"
|
||||
:page-sizes="[5, 10, 20]"
|
||||
:total="detailOrder.items.length"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
/>
|
||||
</div>
|
||||
<el-empty v-else description="暂无货物明细" :image-size="56" />
|
||||
</section>
|
||||
@@ -1560,7 +1841,7 @@ async function searchOrders(model: QueryModel) {
|
||||
</template>
|
||||
</div>
|
||||
<template #footer><div v-if="detailOrder" class="editor-actions"><el-button @click="detailOpen = false">关闭</el-button><el-button v-if="canEditOrder && canClientEditFirstLegOrder(detailOrder.status)" :loading="openingEditor" @click="openEdit(detailOrder)"><Pencil :size="16" />编辑草稿</el-button></div></template>
|
||||
</el-drawer>
|
||||
</el-dialog>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
@@ -1568,10 +1849,14 @@ async function searchOrders(model: QueryModel) {
|
||||
.first-leg-page { display: flex; flex-direction: column; gap: 14px; }
|
||||
:global(.client-main:has(> .first-leg-page:not(.is-create-route)) > .client-page-heading) { margin-bottom: 0; }
|
||||
:global(.client-main:has(> .first-leg-page:not(.is-create-route)) > .client-page-heading > div) { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; border: 0; clip: rect(0 0 0 0); white-space: nowrap; }
|
||||
.first-leg-query-toolbar { flex-wrap: nowrap; }
|
||||
.first-leg-query-toolbar :deep(.query-toolbar__filters) { display: grid; min-width: 0; flex: 1 1 auto; grid-template-columns: minmax(180px, 1.4fr) minmax(120px, .72fr) minmax(210px, 1.15fr); }
|
||||
.first-leg-query-toolbar { display: block; padding: 16px 18px 14px; border: 0; border-radius: 10px; background: #fff; box-shadow: 0 3px 14px rgba(31, 48, 78, .05); }
|
||||
.first-leg-query-toolbar :deep(.query-toolbar__filters) { display: grid; min-width: 0; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px 18px; }
|
||||
.first-leg-query-toolbar :deep(.query-toolbar__field) { min-width: 0; }
|
||||
.first-leg-query-toolbar :deep(.query-toolbar__field .el-input), .first-leg-query-toolbar :deep(.query-toolbar__field .el-select), .first-leg-query-toolbar :deep(.query-toolbar__field .el-date-editor) { flex: 1 1 auto; width: 100%; }
|
||||
.first-leg-query-toolbar :deep(.query-toolbar__filters .el-input), .first-leg-query-toolbar :deep(.query-toolbar__filters .el-select), .first-leg-query-toolbar :deep(.query-toolbar__filters .el-date-editor) { width: 100%; }
|
||||
.first-leg-query-toolbar :deep(.query-toolbar__actions) { align-self: flex-start; flex-wrap: nowrap; margin-left: auto; }
|
||||
.first-leg-query-toolbar :deep(.query-toolbar__actions) { justify-content: flex-start; margin-top: 14px; padding-top: 12px; border-top: 1px solid #edf0f5; }
|
||||
.first-leg-query-toolbar :deep(.query-toolbar__actions .el-button) { min-height: 36px; margin-left: 0; }
|
||||
.first-leg-query-toolbar :deep(.query-toolbar__actions .query-export-button) { order: -1; }
|
||||
.order-eligibility { display: flex; align-items: flex-start; gap: 12px; padding: 14px 16px; border: 1px solid; }
|
||||
.order-eligibility svg { flex: 0 0 20px; margin-top: 1px; }
|
||||
.order-eligibility div { display: flex; min-width: 0; flex: 1; flex-direction: column; gap: 3px; }
|
||||
@@ -1588,6 +1873,35 @@ async function searchOrders(model: QueryModel) {
|
||||
.order-table-toolbar > div { display: flex; min-width: 0; align-items: baseline; gap: 8px; }
|
||||
.order-table-toolbar strong { color: var(--navy); font-size: 14px; }
|
||||
.order-table-toolbar span { color: var(--muted); font-size: 11px; }
|
||||
.order-table-toolbar > .order-table-toolbar-actions { align-items: center; gap: 8px; }
|
||||
.order-table-toolbar-actions :deep(.el-button + .el-button) { margin-left: 0; }
|
||||
.column-settings-trigger { width: 40px; min-width: 40px; height: 36px; padding: 0; color: #53627b; }
|
||||
:global(.order-column-popper.el-popover) { padding: 0; border-color: #e3e8f0; border-radius: 4px; box-shadow: 0 8px 24px rgba(29, 45, 74, .12); }
|
||||
:global(.order-column-popper .column-settings-panel) { padding: 14px 10px 10px; }
|
||||
:global(.order-column-popper .column-settings-panel > p) { margin: 0 8px 10px; color: #778398; font-size: 12px; line-height: 1.5; }
|
||||
:global(.order-column-popper .el-checkbox-group) { max-height: 430px; overflow-y: auto; padding: 0 2px; scrollbar-width: thin; }
|
||||
:global(.order-column-popper .column-settings-item) { display: grid; min-height: 42px; grid-template-columns: 24px minmax(0, 1fr) 28px 28px; align-items: center; padding: 0 4px; color: #596982; }
|
||||
:global(.order-column-popper .column-settings-item:hover) { background: #f7f8fb; }
|
||||
:global(.order-column-popper .column-settings-item > svg) { cursor: grab; color: #71809a; }
|
||||
:global(.order-column-popper .column-settings-item > svg:active) { cursor: grabbing; }
|
||||
:global(.order-column-popper .column-settings-item .el-checkbox) { height: 42px; margin-right: 0; overflow: hidden; }
|
||||
:global(.order-column-popper .column-settings-item .el-checkbox__label) { overflow: hidden; color: #4f5f78; font-size: 13px; text-overflow: ellipsis; white-space: nowrap; }
|
||||
:global(.order-column-popper .column-settings-item .el-checkbox.is-checked .el-checkbox__label) { color: var(--primary); }
|
||||
:global(.order-column-popper .column-settings-item .el-button) { width: 28px; height: 28px; padding: 0; margin: 0; color: #45556e; }
|
||||
:global(.order-column-popper .column-settings-reset) { min-height: 36px; margin: 8px 6px 0; color: #40516b; }
|
||||
.prototype-order-table { font-size: 11px; }
|
||||
.prototype-order-table :deep(.el-table) { color: #626b78; font-size: 11px; }
|
||||
.prototype-order-table :deep(.el-table__header-wrapper th.el-table__cell) { height: 44px; padding: 0; background: #fafbfc; color: #687386; font-size: 11px; font-weight: 650; vertical-align: middle; }
|
||||
.prototype-order-table :deep(.el-table__body-wrapper td.el-table__cell) { height: 50px; padding: 0; vertical-align: middle; }
|
||||
.prototype-order-table :deep(.el-table .cell) { min-width: 0; padding: 0 12px; line-height: 18px; white-space: nowrap; }
|
||||
.prototype-order-table :deep(.el-table__header-wrapper .cell) { overflow: visible; line-height: 16px; text-overflow: clip; }
|
||||
.prototype-order-table :deep(.el-table__body-wrapper .cell) { overflow: hidden; }
|
||||
.prototype-order-table :deep(.el-tag) { height: 24px; padding: 0 9px; line-height: 22px; white-space: nowrap; }
|
||||
.prototype-order-table :deep(.el-table__row:nth-child(even) td.el-table__cell) { background: #fcfcfd; }
|
||||
.prototype-order-table :deep(.el-table__row:hover > td.el-table__cell) { background: #f6f8fc; }
|
||||
.prototype-order-table :deep(.data-table__pagination) { min-height: 54px; padding: 10px 16px; }
|
||||
.table-ellipsis, .table-multivalue { display: block; width: 100%; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.table-code { font-variant-numeric: tabular-nums; letter-spacing: 0; }
|
||||
.order-table-card > .order-eligibility, .order-table-card > .supporting-data-error { border-width: 0 0 1px; }
|
||||
.order-load-state { display: grid; min-height: 220px; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; gap: 14px; padding: 28px; color: #9a5a09; }
|
||||
.order-load-state > div { display: flex; min-width: 0; flex-direction: column; gap: 5px; }
|
||||
@@ -1595,20 +1909,27 @@ async function searchOrders(model: QueryModel) {
|
||||
.order-load-state span { overflow-wrap: anywhere; color: var(--muted); font-size: 12px; line-height: 1.55; }
|
||||
.order-load-state.is-compact { min-height: 180px; }
|
||||
.order-detail-content { min-height: 180px; }
|
||||
:global(.first-leg-detail-dialog.el-dialog) { display: flex; max-width: 1500px; height: 94vh; margin-bottom: 0; flex-direction: column; overflow: hidden; border-radius: 6px; }
|
||||
:global(.first-leg-detail-dialog .el-dialog__header) { flex: 0 0 auto; margin-right: 0; padding: 17px 22px; border-bottom: 1px solid var(--border); }
|
||||
:global(.first-leg-detail-dialog .el-dialog__title) { color: var(--navy); font-size: 16px; font-weight: 700; }
|
||||
:global(.first-leg-detail-dialog .el-dialog__headerbtn) { top: 8px; right: 10px; width: 44px; height: 44px; }
|
||||
:global(.first-leg-detail-dialog .el-dialog__body) { min-height: 0; flex: 1 1 auto; overflow-y: auto; padding: 22px 24px 28px; overscroll-behavior: contain; }
|
||||
:global(.first-leg-detail-dialog .el-dialog__footer) { flex: 0 0 auto; padding: 12px 22px; border-top: 1px solid var(--border); background: #fff; }
|
||||
.mobile-order-surface { display: none; min-height: 220px; }
|
||||
.mobile-order-list { display: none; }
|
||||
.order-number-link { display: block; padding: 0; border: 0; background: none; color: var(--primary); cursor: pointer; font-weight: 700; text-align: left; }
|
||||
.order-number-link { display: block; width: 100%; max-width: 100%; overflow: hidden; padding: 0; border: 0; background: none; color: var(--primary); cursor: pointer; font-size: 11px; font-weight: 700; line-height: 18px; text-align: left; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.order-number-link:focus-visible { outline: 2px solid var(--primary); outline-offset: 3px; }
|
||||
.order-reference { display: block; margin-top: 4px; color: var(--muted); font-size: 11px; font-weight: 400; }
|
||||
:global(.first-leg-editor .el-drawer__header) { flex: 0 0 auto; min-height: 58px; margin-bottom: 0; padding: 17px 22px; border-bottom: 1px solid var(--border); }
|
||||
:global(.first-leg-editor .el-drawer__body) { min-height: 0; padding: 18px 22px 32px; overscroll-behavior: contain; }
|
||||
:global(.first-leg-editor .el-drawer__footer) { flex: 0 0 auto; padding: 12px 22px; border-top: 1px solid var(--border); background: #fff; }
|
||||
.order-row-actions { align-items: center; justify-content: center; gap: 6px; white-space: nowrap; }
|
||||
.order-row-actions :deep(.el-button) { margin-left: 0; padding: 3px 1px; font-size: 11px; }
|
||||
.is-create-route { gap: 0; }
|
||||
.is-create-route .first-leg-editor-page { width: 100%; }
|
||||
.is-create-route .supporting-data-error, .is-create-route > .el-alert { margin-bottom: 14px; }
|
||||
.is-create-route .prototype-order-form { width: 100%; margin: 0; padding: 0 0 16px; }
|
||||
.is-create-route .prototype-section:not(.cargo-section):not(.cost-estimate-section):not(.destination-section) { width: min(100%, 900px); }
|
||||
.order-form { width: min(100%, 1000px); margin: 0; padding: 0 0 12px; }
|
||||
.order-form.is-dialog-order-form { width: 100%; max-width: none; }
|
||||
.is-dialog-order-form > .cargo-section, .is-dialog-order-form > .cost-estimate-section { width: 100%; max-width: none; }
|
||||
.order-form :deep(.el-form-item__error) { position: static; padding-top: 6px; overflow-wrap: anywhere; line-height: 1.45; }
|
||||
.order-form-section + .order-form-section { margin-top: 30px; padding-top: 28px; border-top: 1px solid #edf0f5; }
|
||||
.order-form-section h3 { margin: 0 0 16px; color: var(--navy); font-size: 16px; }
|
||||
@@ -1794,20 +2115,19 @@ async function searchOrders(model: QueryModel) {
|
||||
.cargo-summary div { display: flex; min-height: 74px; align-items: center; justify-content: center; flex-direction: column; gap: 4px; border-right: 1px solid var(--border); }
|
||||
.cargo-summary div:last-child { border-right: 0; }
|
||||
.cargo-summary span { color: var(--muted); font-size: 11px; }
|
||||
.cargo-detail-list { display: grid; gap: 10px; margin-top: 10px; }
|
||||
.cargo-detail-list article { padding: 14px 15px; border: 1px solid var(--border); }
|
||||
.cargo-detail-list header { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: baseline; gap: 10px; }
|
||||
.cargo-detail-list header span, .cargo-detail-list header small { color: var(--muted); font-size: 10px; }
|
||||
.cargo-detail-list header strong { overflow-wrap: anywhere; color: var(--navy); font-size: 13px; }
|
||||
.cargo-detail-list header small { overflow-wrap: anywhere; text-align: right; }
|
||||
.cargo-detail-list dl { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 11px 16px; margin: 13px 0 0; }
|
||||
.cargo-detail-list dt { color: var(--muted); font-size: 10px; }
|
||||
.cargo-detail-list dd { margin: 4px 0 0; overflow-wrap: anywhere; color: var(--navy); font-size: 11px; font-weight: 600; }
|
||||
.cargo-detail-list article > p { margin: 11px 0 0; padding-top: 10px; border-top: 1px solid var(--border); color: var(--muted); font-size: 11px; }
|
||||
.cargo-detail-table-wrap { overflow: hidden; margin-top: 10px; border: 1px solid var(--border); border-radius: 5px; background: #fff; }
|
||||
.cargo-detail-table { width: 100%; font-size: 11px; }
|
||||
.cargo-detail-table :deep(.el-table__header th.el-table__cell) { height: 42px; padding: 0; background: #f7f8fb; color: #657187; font-size: 11px; font-weight: 650; white-space: nowrap; }
|
||||
.cargo-detail-table :deep(.el-table__body td.el-table__cell) { height: 48px; padding: 0; color: #4f5b6d; vertical-align: middle; }
|
||||
.cargo-detail-table :deep(.cell) { min-width: 0; padding: 0 10px; line-height: 18px; white-space: nowrap; }
|
||||
.cargo-detail-thumbnail { display: flex; width: 34px; height: 34px; align-items: center; justify-content: center; margin: 0 auto; overflow: hidden; border: 1px solid #e1e6ee; border-radius: 3px; color: var(--muted); }
|
||||
.cargo-detail-thumbnail img { width: 100%; height: 100%; object-fit: contain; }
|
||||
.cargo-detail-pagination { min-height: 52px; justify-content: flex-end; box-sizing: border-box; padding: 9px 14px; border-top: 1px solid var(--border); }
|
||||
:deep(.el-timeline-item__content p), :deep(.el-timeline-item__content > span) { color: var(--muted); }
|
||||
:deep(.el-timeline-item__content p) { margin: 5px 0; font-size: 12px; }
|
||||
:deep(.el-timeline-item__content > span) { font-size: 11px; }
|
||||
@media (max-width: 1280px) {
|
||||
.first-leg-query-toolbar :deep(.query-toolbar__filters) { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
.desktop-order-table { display: none; }
|
||||
.mobile-order-surface { display: block; }
|
||||
.mobile-order-list { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; padding: 10px; }
|
||||
@@ -1824,12 +2144,11 @@ async function searchOrders(model: QueryModel) {
|
||||
.mobile-order-card footer > div { display: flex; }
|
||||
}
|
||||
@media (min-width: 721px) and (max-width: 1050px) {
|
||||
.first-leg-query-toolbar :deep(.query-toolbar__filters) { grid-template-columns: repeat(2, minmax(140px, 1fr)); }
|
||||
.cargo-form-grid, .cargo-form-grid--basic { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
.product-image-field { grid-row: auto; }
|
||||
}
|
||||
@media (max-width: 720px) {
|
||||
.first-leg-query-toolbar { flex-wrap: wrap; }
|
||||
.first-leg-query-toolbar { padding: 12px; }
|
||||
.first-leg-query-toolbar :deep(.query-toolbar__filters) { display: flex; }
|
||||
:global(.client-main:has(> .first-leg-page:not(.is-create-route)) > .client-page-heading) { margin-bottom: 10px; }
|
||||
.order-table-toolbar { align-items: stretch; flex-direction: column; gap: 10px; }
|
||||
@@ -1858,9 +2177,6 @@ async function searchOrders(model: QueryModel) {
|
||||
.cargo-import-actions, .cargo-table-actions { flex-wrap: wrap; }
|
||||
.cost-estimate-cards { grid-template-columns: 1fr; }
|
||||
.cost-estimate-card { min-height: 140px; }
|
||||
:global(.first-leg-editor .el-drawer__header) { min-height: 56px; padding: 15px 20px; }
|
||||
:global(.first-leg-editor .el-drawer__body) { padding: 16px 20px 28px; }
|
||||
:global(.first-leg-editor .el-drawer__footer) { padding: 10px 20px; }
|
||||
.form-span-full { grid-column: auto; }
|
||||
.editor-actions :deep(.el-button) { min-height: 44px; }
|
||||
.desktop-order-table { display: none; }
|
||||
@@ -1886,16 +2202,15 @@ async function searchOrders(model: QueryModel) {
|
||||
.cargo-summary { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
.cargo-summary div:nth-child(2) { border-right: 0; }
|
||||
.cargo-summary div:nth-child(-n+2) { border-bottom: 1px solid var(--border); }
|
||||
.cargo-detail-list header { grid-template-columns: 1fr; gap: 4px; }
|
||||
.cargo-detail-list header small { text-align: left; }
|
||||
.cargo-detail-list dl { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
.cabinet-progress-card dl { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
:global(.first-leg-editor), :global(.first-leg-detail) { width: 100% !important; }
|
||||
:global(.first-leg-detail-dialog.el-dialog) { width: calc(100vw - 24px) !important; height: 96vh; margin-top: 2vh; }
|
||||
}
|
||||
@media (max-width: 360px) {
|
||||
.cargo-detail-list dl { grid-template-columns: 1fr; }
|
||||
.cargo-detail-pagination { justify-content: flex-start; overflow-x: auto; }
|
||||
}
|
||||
@media (max-width: 480px) {
|
||||
:global(.first-leg-detail-dialog.el-dialog) { width: 100vw !important; height: 100vh; margin: 0; border-radius: 0; }
|
||||
:global(.first-leg-detail-dialog .el-dialog__body) { padding: 16px 12px 22px; }
|
||||
.editor-actions { display: grid; width: 100%; grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
.editor-actions :deep(.el-button) { width: 100%; min-width: 0; margin-left: 0; }
|
||||
.editor-actions :deep(.el-button:nth-child(3)) { grid-column: 1 / -1; }
|
||||
|
||||
Reference in New Issue
Block a user