优化
This commit is contained in:
@@ -5,6 +5,7 @@ import test from 'node:test'
|
||||
const source = readFileSync(new URL('./FirstLegOrdersView.vue', import.meta.url), 'utf8')
|
||||
const routerSource = readFileSync(new URL('../routes.ts', import.meta.url), 'utf8')
|
||||
const editorShellSource = readFileSync(new URL('../components/FirstLegOrderEditorShell.vue', import.meta.url), 'utf8')
|
||||
const progressSource = readFileSync(new URL('../components/OrderProgressPanel.vue', import.meta.url), 'utf8')
|
||||
|
||||
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'/)
|
||||
@@ -67,14 +68,48 @@ 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, /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.doesNotMatch(source, /order-metrics|订单概览/)
|
||||
})
|
||||
|
||||
test('shows one unified progress line, C02 business summaries, and optional customer-visible nodes', () => {
|
||||
assert.match(source, /import OrderProgressPanel/)
|
||||
assert.match(source, /<OrderProgressPanel :order="detailOrder" \/>/)
|
||||
assert.match(source, /<h3>关键业务摘要<\/h3>/)
|
||||
assert.match(source, /progressSummaryItems/)
|
||||
assert.match(source, /summary\.preloadStatus/)
|
||||
assert.match(source, /summary\.expectedPickCount/)
|
||||
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, /客户确认|差异处理中|派送中/)
|
||||
})
|
||||
|
||||
test('uses only fields present on C02 rows before loading full detail', () => {
|
||||
assert.match(source, /getOrderCargoSummary/)
|
||||
assert.match(source, /目的地 \/ 渠道/)
|
||||
assert.match(source, /运输方式待更新|渠道待更新/)
|
||||
assert.doesNotMatch(source, /key: 'operator'/)
|
||||
assert.doesNotMatch(source, /key: 'transportMode', type: 'select'/)
|
||||
assert.match(source, /store\.draftLoadError/)
|
||||
assert.match(source, /mainStatus: statusValue && statusValue !== 'DRAFT' \? statusValue : undefined/)
|
||||
assert.match(source, /draftOnly: statusValue === 'DRAFT'/)
|
||||
assert.match(source, /createdFrom: range\[0\] \? `\$\{range\[0\]\}T00:00:00`/)
|
||||
})
|
||||
|
||||
test('keeps the seven-stage progress and business summary usable on mobile', () => {
|
||||
assert.match(progressSource, /grid-template-columns: repeat\(7, 82px\)/)
|
||||
assert.match(progressSource, /overflow-x: auto/)
|
||||
assert.match(source, /\.fulfillment-summary \{ grid-template-columns: 1fr; \}/)
|
||||
})
|
||||
|
||||
test('keeps the first viewport compact and moves create into the table toolbar', () => {
|
||||
assert.match(source, /class="order-table-toolbar"/)
|
||||
assert.match(source, /<strong>订单列表<\/strong><span>共 \{\{ filteredOrders\.length \}\} 条<\/span>/)
|
||||
|
||||
@@ -9,6 +9,7 @@ import DataTable, { type DataTableColumn } from '@/components/common/DataTable.v
|
||||
import { ApiError } from '@/api/http'
|
||||
import FirstLegOrderEditorShell from '@/modules/first-leg/components/FirstLegOrderEditorShell.vue'
|
||||
import InboundBillingPanel from '@/modules/first-leg/components/InboundBillingPanel.vue'
|
||||
import OrderProgressPanel from '@/modules/first-leg/components/OrderProgressPanel.vue'
|
||||
import { getMerchantInboundBilling } from '@/modules/first-leg/api/inboundBilling'
|
||||
import { useFirstLegOrdersStore } from '@/modules/first-leg/store/firstLegOrders'
|
||||
import { useTenantAdministrationStore } from '@/modules/tenant-management/public'
|
||||
@@ -40,12 +41,11 @@ 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: '', transportMode: '', createdRange: [] })
|
||||
const filters = ref<QueryModel>({ keyword: '', status: '', createdRange: [] })
|
||||
const appliedFilters = ref<QueryModel>({ ...filters.value })
|
||||
const queryFields: QueryField[] = [
|
||||
{ key: 'keyword', type: 'input', placeholder: '订单号、参考号、路线或品名', props: { 'aria-label': '订单关键词' } },
|
||||
{ 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: 'transportMode', type: 'select', placeholder: '全部运输方式', options: FIRST_LEG_TRANSPORT_MODES.map((item) => ({ label: item.label, value: item.code })), props: { 'aria-label': '运输方式' } },
|
||||
{ key: 'createdRange', type: 'daterange', placeholder: '订单创建日期', props: { 'aria-label': '订单创建日期' } },
|
||||
]
|
||||
const channelOptions = [
|
||||
@@ -57,11 +57,10 @@ const channelOptions = [
|
||||
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: 'route', label: '目的地 / 渠道', minWidth: 190, slotName: 'route' },
|
||||
{ key: 'warehouse', label: '收货仓', prop: 'receivingWarehouseName', minWidth: 120 },
|
||||
{ key: 'cargo', label: '货物', minWidth: 150, slotName: 'cargo' },
|
||||
{ key: 'cargo', label: '货物摘要', minWidth: 150, slotName: 'cargo' },
|
||||
{ key: 'status', label: '状态', width: 120, slotName: 'status' },
|
||||
{ key: 'operator', label: '负责人', width: 110, slotName: 'operator' },
|
||||
{ key: 'updatedAt', label: '更新时间', width: 155, slotName: 'updatedAt' },
|
||||
]
|
||||
const tablePage = ref(1)
|
||||
@@ -226,26 +225,23 @@ const unsavedRouteKey = props.createPage ? 'first-leg-order-create' : 'first-leg
|
||||
useUnsavedWorkRegistration({
|
||||
key: `first-leg-order-editor:${unsavedRouteKey}`,
|
||||
routeKey: unsavedRouteKey,
|
||||
label: props.createPage ? '创建头程订单' : '头程订单编辑',
|
||||
label: props.createPage ? '创建订单' : '订单编辑',
|
||||
dirty: editorDirty,
|
||||
})
|
||||
|
||||
const filteredOrders = computed(() => store.orders.filter((order) => {
|
||||
const keywordValue = appliedFilters.value.keyword
|
||||
const statusValue = appliedFilters.value.status
|
||||
const transportModeValue = appliedFilters.value.transportMode
|
||||
const createdRangeValue = appliedFilters.value.createdRange
|
||||
const keyword = typeof keywordValue === 'string' ? keywordValue.trim().toLowerCase() : ''
|
||||
const status = typeof statusValue === 'string' ? statusValue as FirstLegOrderStatus | '' : ''
|
||||
const transportMode = typeof transportModeValue === 'string' ? transportModeValue : ''
|
||||
const createdRange = Array.isArray(createdRangeValue) ? createdRangeValue.filter((value): value is string => typeof value === 'string') : []
|
||||
const matchesKeyword = !keyword || [order.orderNo, order.referenceNo, order.origin, order.destination, order.items.map((item) => item.cargoNameCn).join(' ')]
|
||||
.filter(Boolean)
|
||||
.some((value) => String(value).toLowerCase().includes(keyword))
|
||||
const matchesStatus = !status || order.status === status
|
||||
const matchesMode = !transportMode || order.transportMode === transportMode
|
||||
const matchesDate = createdRange.length !== 2 || (order.createdAt.slice(0, 10) >= createdRange[0] && order.createdAt.slice(0, 10) <= createdRange[1])
|
||||
return matchesKeyword && matchesStatus && matchesMode && matchesDate
|
||||
return matchesKeyword && matchesStatus && matchesDate
|
||||
}))
|
||||
const pagedOrders = computed(() => {
|
||||
const start = (tablePage.value - 1) * tablePageSize.value
|
||||
@@ -254,6 +250,29 @@ const pagedOrders = computed(() => {
|
||||
|
||||
const detailOrder = computed(() => detailId.value ? store.getById(detailId.value) : undefined)
|
||||
const detailSummary = computed(() => detailOrder.value ? summarizeFirstLegCargo(detailOrder.value.items) : null)
|
||||
const progressSummaryItems = computed(() => {
|
||||
const order = detailOrder.value
|
||||
if (!order) return []
|
||||
const inbound = inboundBilling.value?.inbound.summary
|
||||
const summary = order.businessSummary
|
||||
const formatMetric = (value: number | null | undefined, unit = '') => value == null ? '待更新' : `${value.toLocaleString()}${unit ? ` ${unit}` : ''}`
|
||||
const preloadStatus = ({ PENDING: '待确认', CONFIRMING: '确认中', CONFIRMED: '已确认' } as Record<string, string>)[summary.preloadStatus] ?? summary.preloadStatus
|
||||
const transportReference = [summary.containerNo, summary.cabinetNo, summary.vesselName, summary.voyageNo].filter(Boolean).join(' · ')
|
||||
const pickingProgress = summary.expectedPickCount == null
|
||||
? '待更新'
|
||||
: `${summary.pickedCount ?? 0} / ${summary.expectedPickCount}`
|
||||
return [
|
||||
{ label: '实际箱数', value: formatMetric(summary.totalPackageCount ?? inbound?.totalPackageCount, '箱') },
|
||||
{ label: '实测毛重', value: formatMetric(summary.actualGrossWeightKg ?? inbound?.actualGrossWeightKg, 'kg') },
|
||||
{ label: '实测体积', value: formatMetric(summary.actualVolumeCbm ?? inbound?.actualVolumeCbm, 'm³') },
|
||||
{ label: '预装清单', value: preloadStatus ? `${preloadStatus}${summary.preloadItemCount == null ? '' : ` · ${summary.preloadItemCount} 项`}` : '待更新' },
|
||||
{ label: '拣货进度', value: pickingProgress },
|
||||
{ label: '装柜资料', value: transportReference || summary.sealNo || '待更新' },
|
||||
{ label: '作业证据', value: formatMetric(summary.evidenceCount, '项') },
|
||||
{ label: '计费重量', value: formatMetric(summary.chargeableWeightKg, 'kg') },
|
||||
{ label: '派送信息', value: summary.deliveryTrackingNo || (summary.deliveryCompletedAt ? `完成于 ${formatDateTime(summary.deliveryCompletedAt)}` : '待更新') },
|
||||
]
|
||||
})
|
||||
const selectedWarehouse = computed(() => availableWarehouses.value.find((item) => item.id === form.receivingWarehouseId))
|
||||
const selectedDestinationWarehouse = computed(() => store.warehouses.find((item) => item.id === form.destinationWarehouseId && item.warehouseType === form.destinationType))
|
||||
const hasActiveFilters = computed(() => {
|
||||
@@ -261,11 +280,10 @@ const hasActiveFilters = computed(() => {
|
||||
return Boolean(
|
||||
(typeof values.keyword === 'string' && values.keyword.trim())
|
||||
|| values.status
|
||||
|| values.transportMode
|
||||
|| (Array.isArray(values.createdRange) && values.createdRange.length),
|
||||
)
|
||||
})
|
||||
const emptyDescription = computed(() => hasActiveFilters.value ? '没有符合筛选条件的头程订单' : '还没有头程订单')
|
||||
const emptyDescription = computed(() => hasActiveFilters.value ? '没有符合筛选条件的订单' : '还没有订单')
|
||||
|
||||
async function loadPageData() {
|
||||
supportingDataLoading.value = true
|
||||
@@ -285,6 +303,7 @@ async function loadPageData() {
|
||||
}
|
||||
if (ordersResult.status === 'rejected' && !store.loadError) supportingDataErrors.value.push(`订单加载失败:${errorMessage(ordersResult.reason)}`)
|
||||
if (store.warehouseLoadError) supportingDataErrors.value.push(`公共仓库加载失败:${store.warehouseLoadError}`)
|
||||
if (store.draftLoadError) supportingDataErrors.value.push(`订单草稿加载失败:${store.draftLoadError}`)
|
||||
await autofillWarehouseSelections()
|
||||
supportingDataLoading.value = false
|
||||
}
|
||||
@@ -293,6 +312,12 @@ function errorMessage(error: unknown) {
|
||||
return error instanceof Error ? error.message : '服务暂时不可用'
|
||||
}
|
||||
|
||||
function getOrderCargoSummary(order: FirstLegOrder) {
|
||||
if (order.items.length) return `${order.items[0]?.cargoNameCn || '货物'}${order.items.length > 1 ? `等 ${order.items.length} 项` : ''}`
|
||||
if (order.businessSummary.totalPackageCount != null) return `${order.businessSummary.totalPackageCount} 箱`
|
||||
return '待更新'
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadPrivateAddresses()
|
||||
void loadPageData()
|
||||
@@ -338,7 +363,7 @@ function replaceForm(draft: FirstLegOrderDraft) {
|
||||
function draftFromOrder(order: FirstLegOrder): FirstLegOrderDraft {
|
||||
return {
|
||||
referenceNo: order.referenceNo,
|
||||
transportMode: order.transportMode,
|
||||
transportMode: order.transportMode ?? 'FCL',
|
||||
businessType: order.businessType,
|
||||
origin: order.origin,
|
||||
destination: order.destination,
|
||||
@@ -743,7 +768,17 @@ async function searchOrders(model: QueryModel) {
|
||||
}
|
||||
tablePage.value = 1
|
||||
try {
|
||||
await store.load()
|
||||
const statusValue = typeof model.status === 'string' ? model.status as FirstLegOrderStatus | '' : ''
|
||||
const keyword = typeof model.keyword === 'string' ? model.keyword.trim() : ''
|
||||
const range = Array.isArray(createdRange) ? createdRange.filter((value): value is string => typeof value === 'string') : []
|
||||
await store.load({
|
||||
keyword: keyword || 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,
|
||||
})
|
||||
} catch {
|
||||
// Store keeps the recoverable error state rendered below the query toolbar.
|
||||
}
|
||||
@@ -753,7 +788,7 @@ 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" />
|
||||
|
||||
<div class="client-card order-table-card">
|
||||
<div class="order-table-toolbar">
|
||||
@@ -777,7 +812,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="orderColumns" :loading="store.loading" :total="filteredOrders.length" :page-sizes="[10, 20, 50]" row-key="id" empty-text="暂无订单">
|
||||
<template #actions="{ row }">
|
||||
<div class="order-row-actions">
|
||||
<el-button link type="primary" @click="openDetail(row as FirstLegOrder)"><Eye :size="15" />详情</el-button>
|
||||
@@ -788,10 +823,9 @@ async function searchOrders(model: QueryModel) {
|
||||
<button class="order-number-link" type="button" @click="openDetail(row as FirstLegOrder)">{{ (row as FirstLegOrder).orderNo || '未提交草稿' }}</button>
|
||||
<span class="order-reference">{{ (row as FirstLegOrder).referenceNo || '无客户参考号' }}</span>
|
||||
</template>
|
||||
<template #route="{ row }"><strong>{{ (row as FirstLegOrder).origin }} → {{ (row as FirstLegOrder).destination }}</strong><span class="order-reference">{{ getFirstLegTransportModeLabel((row as FirstLegOrder).transportMode) }}</span></template>
|
||||
<template #cargo="{ row }">{{ (row as FirstLegOrder).items[0]?.cargoNameCn || '-' }}<span v-if="(row as FirstLegOrder).items.length > 1" class="order-reference">等 {{ (row as FirstLegOrder).items.length }} 项</span></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 #status="{ row }"><el-tag :type="getFirstLegStatusTone((row as FirstLegOrder).status)" effect="light">{{ getFirstLegStatusLabel((row as FirstLegOrder).status) }}</el-tag></template>
|
||||
<template #operator="{ row }">{{ (row as FirstLegOrder).operatorName || '-' }}</template>
|
||||
<template #updatedAt="{ row }">{{ formatDateTime((row as FirstLegOrder).updatedAt) }}</template>
|
||||
<template #empty>
|
||||
<el-empty :description="emptyDescription">
|
||||
@@ -805,7 +839,7 @@ async function searchOrders(model: QueryModel) {
|
||||
<article v-for="order in filteredOrders" :key="order.id" class="mobile-order-card">
|
||||
<header><button type="button" @click="openDetail(order)">{{ order.orderNo || '未提交草稿' }}</button><el-tag :type="getFirstLegStatusTone(order.status)" effect="light">{{ getFirstLegStatusLabel(order.status) }}</el-tag></header>
|
||||
<span class="order-reference">{{ order.referenceNo || '无客户参考号' }}</span>
|
||||
<dl><div><dt>运输路线</dt><dd>{{ order.origin }} → {{ order.destination }}</dd></div><div><dt>收货仓</dt><dd>{{ order.receivingWarehouseName }}</dd></div><div><dt>货物</dt><dd>{{ order.items[0]?.cargoNameCn || '-' }}<span v-if="order.items.length > 1">等 {{ order.items.length }} 项</span></dd></div><div><dt>负责人</dt><dd>{{ order.operatorName || '-' }}</dd></div></dl>
|
||||
<dl><div><dt>目的地</dt><dd>{{ order.destination || '待更新' }}</dd></div><div><dt>收货仓</dt><dd>{{ order.receivingWarehouseName || '待更新' }}</dd></div><div><dt>货物摘要</dt><dd>{{ getOrderCargoSummary(order) }}</dd></div><div><dt>运输渠道</dt><dd>{{ order.transportMode ? getFirstLegTransportModeLabel(order.transportMode) : order.channelName || '待更新' }}</dd></div></dl>
|
||||
<footer><span>更新于 {{ formatDateTime(order.updatedAt) }}</span><div><el-button link type="primary" @click="openDetail(order)"><Eye :size="15" />详情</el-button><el-button v-if="canEditOrder && canClientEditFirstLegOrder(order.status)" link type="primary" :loading="openingEditor" @click="openEdit(order)"><Pencil :size="15" />编辑</el-button></div></footer>
|
||||
</article>
|
||||
</div>
|
||||
@@ -824,7 +858,7 @@ async function searchOrders(model: QueryModel) {
|
||||
</div>
|
||||
<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">
|
||||
<FirstLegOrderEditorShell v-model="editorOpen" :page="isCreatePage" :title="editingId ? '编辑订单草稿' : '创建订单'" :before-close="requestCloseEditor" @back="closeEditor">
|
||||
<el-form ref="orderFormRef" v-loading="isCreatePage && (supportingDataLoading || store.loading)" :model="form" :rules="orderFormRules" label-position="top" class="order-form prototype-order-form" scroll-to-error>
|
||||
<section class="order-form-section prototype-section">
|
||||
<div class="order-section-heading"><div><h3>发货信息</h3><p>先选择货物如何送到国内收货仓,并填写本次运输安排。</p></div></div>
|
||||
@@ -918,6 +952,7 @@ async function searchOrders(model: QueryModel) {
|
||||
<div><span>{{ getFirstLegTransportModeLabel(detailOrder.transportMode) }}</span><h2>{{ detailOrder.orderNo || '未提交草稿' }}</h2><p>{{ detailOrder.origin }} → {{ detailOrder.destination }}</p></div>
|
||||
<el-tag :type="getFirstLegStatusTone(detailOrder.status)" effect="light">{{ getFirstLegStatusLabel(detailOrder.status) }}</el-tag>
|
||||
</div>
|
||||
<OrderProgressPanel :order="detailOrder" />
|
||||
<dl class="order-detail-grid">
|
||||
<div><dt>客户参考号</dt><dd>{{ detailOrder.referenceNo || '-' }}</dd></div><div><dt>运输方式</dt><dd>{{ getFirstLegTransportModeLabel(detailOrder.transportMode) }}</dd></div>
|
||||
<div><dt>发货方</dt><dd>{{ detailOrder.shipperPartySnapshot?.name || detailOrder.shipper }}<span v-if="detailOrder.shipperPartySnapshot" class="detail-subline">{{ detailOrder.shipperPartySnapshot.code }} · {{ detailOrder.shipperPartySnapshot.contactName || '未填写联系人' }} · {{ detailOrder.shipperPartySnapshot.phone || '未填写电话' }}<br />{{ detailOrder.shipperPartySnapshot.countryRegion }} {{ detailOrder.shipperPartySnapshot.province }} {{ detailOrder.shipperPartySnapshot.city }} {{ detailOrder.shipperPartySnapshot.address }}</span></dd></div>
|
||||
@@ -925,6 +960,12 @@ async function searchOrders(model: QueryModel) {
|
||||
<div><dt>平台收货仓</dt><dd>{{ detailOrder.receivingWarehouseSnapshot?.name || detailOrder.receivingWarehouseName }}<span v-if="detailOrder.receivingWarehouseSnapshot" class="detail-subline">{{ detailOrder.receivingWarehouseSnapshot.code }} · {{ detailOrder.receivingWarehouseSnapshot.province }}{{ detailOrder.receivingWarehouseSnapshot.city }}{{ detailOrder.receivingWarehouseSnapshot.address }}</span></dd></div><div><dt>负责人</dt><dd>{{ detailOrder.operatorName || '未提供' }}</dd></div>
|
||||
<div><dt>期望出发</dt><dd>{{ detailOrder.expectedDepartureDate || '-' }}</dd></div><div><dt>ETD / ETA</dt><dd>{{ detailOrder.etd || '-' }} / {{ detailOrder.eta || '-' }}</dd></div>
|
||||
</dl>
|
||||
<section v-if="detailOrder.status !== 'DRAFT'" class="detail-section">
|
||||
<h3>关键业务摘要</h3>
|
||||
<dl class="fulfillment-summary">
|
||||
<div v-for="item in progressSummaryItems" :key="item.label"><dt>{{ item.label }}</dt><dd>{{ item.value }}</dd></div>
|
||||
</dl>
|
||||
</section>
|
||||
<section class="detail-section">
|
||||
<h3>履约快递信息</h3>
|
||||
<div v-if="detailOrder.expressCompanyName || detailOrder.trackingNumber" class="express-information"><PackageCheck :size="20" /><div><span>快递公司</span><strong>{{ detailOrder.expressCompanyName || '待平台补充' }}</strong></div><div><span>快递单号</span><strong>{{ detailOrder.trackingNumber || '待平台补充' }}</strong></div></div>
|
||||
@@ -939,7 +980,6 @@ async function searchOrders(model: QueryModel) {
|
||||
:error="inboundBillingError"
|
||||
@retry="retryInboundBilling"
|
||||
/>
|
||||
<section class="detail-section"><h3>处理时间线</h3><el-timeline><el-timeline-item v-for="item in [...detailOrder.timeline].reverse()" :key="item.id" :timestamp="item.occurredAt" placement="top"><strong>{{ item.title }}</strong><p>{{ item.description || '-' }}</p><span>{{ item.operatorName || '未提供' }}</span></el-timeline-item></el-timeline></section>
|
||||
</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>
|
||||
@@ -952,7 +992,7 @@ async function searchOrders(model: QueryModel) {
|
||||
: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(160px, 1.4fr) minmax(100px, .72fr) minmax(100px, .72fr) minmax(190px, 1.15fr); }
|
||||
.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 :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; }
|
||||
.order-eligibility { display: flex; align-items: flex-start; gap: 12px; padding: 14px 16px; border: 1px solid; }
|
||||
@@ -1048,6 +1088,12 @@ async function searchOrders(model: QueryModel) {
|
||||
.detail-subline { display: block; margin-top: 4px; color: var(--muted); font-size: 11px; font-weight: 400; line-height: 1.5; }
|
||||
.detail-section { margin-top: 22px; }
|
||||
.detail-section h3 { margin: 0 0 12px; font-size: 15px; }
|
||||
.fulfillment-summary { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); margin: 0; border: 1px solid var(--border); }
|
||||
.fulfillment-summary > div { min-width: 0; padding: 13px; border-bottom: 1px solid var(--border); }
|
||||
.fulfillment-summary > div:not(:nth-child(3n)) { border-right: 1px solid var(--border); }
|
||||
.fulfillment-summary > div:nth-last-child(-n + 3) { border-bottom: 0; }
|
||||
.fulfillment-summary dt { color: var(--muted); font-size: 10px; }
|
||||
.fulfillment-summary dd { margin: 5px 0 0; overflow-wrap: anywhere; color: var(--navy); font-size: 12px; font-weight: 650; }
|
||||
.express-information { display: grid; grid-template-columns: auto repeat(2, minmax(0, 1fr)); align-items: center; gap: 16px; padding: 15px 16px; background: #f2f7f6; color: var(--primary); }
|
||||
.express-information > div { display: flex; min-width: 0; flex-direction: column; gap: 4px; }
|
||||
.express-information span { color: var(--muted); font-size: 11px; }
|
||||
@@ -1094,7 +1140,9 @@ async function searchOrders(model: QueryModel) {
|
||||
.order-table-toolbar .el-button { width: 100%; min-height: 40px; }
|
||||
.order-eligibility { flex-wrap: wrap; }
|
||||
.order-eligibility .el-button { width: 100%; min-height: 44px; }
|
||||
.order-form-grid, .order-detail-grid, .cargo-primary-grid, .cargo-secondary-grid { grid-template-columns: 1fr; }
|
||||
.order-form-grid, .order-detail-grid, .cargo-primary-grid, .cargo-secondary-grid, .fulfillment-summary { grid-template-columns: 1fr; }
|
||||
.fulfillment-summary > div, .fulfillment-summary > div:not(:nth-child(3n)) { border-right: 0; border-bottom: 1px solid var(--border); }
|
||||
.fulfillment-summary > div:last-child { border-bottom: 0; }
|
||||
.prototype-form-row, .prototype-form-row--wide, .prototype-form-row--compact, .prototype-form-row--short, .prototype-form-row--tabs { display: block; margin-bottom: 18px; }
|
||||
.prototype-form-row :deep(.el-form-item__label) { min-height: auto; justify-content: flex-start; padding: 0 0 8px; line-height: 1.4; }
|
||||
.selected-destination { width: 100%; margin-left: 0; }
|
||||
|
||||
Reference in New Issue
Block a user