This commit is contained in:
2026-08-07 10:45:19 +08:00
parent 2449c07e5e
commit e7cc0cf18b
3 changed files with 137 additions and 85 deletions
@@ -146,15 +146,32 @@ export interface UnifiedOrderDetailOrderSnapshot {
customerId: number | string | null customerId: number | string | null
customerName: string | null customerName: string | null
transportMode: string | null transportMode: string | null
tradeTerm?: string | null
destinationCountryCode: string | null destinationCountryCode: string | null
destinationWarehouseId: number | string | null destinationWarehouseId: number | string | null
destinationWarehouseName: string | null destinationWarehouseName: string | null
expectedShipDate: string | null expectedShipDate: string | null
customerRemark?: string | null
pickupMethod?: string | null
receivingWarehouseId: number | string | null receivingWarehouseId: number | string | null
receivingWarehouseName: string | null receivingWarehouseName: string | null
receivingWarehouseAddress?: string | null
receivingTime?: string | null
channelCode?: string | null
channelName: string | null channelName: string | null
carrierId: number | string | null carrierId: number | string | null
carrierName: string | null carrierName: string | null
taxMode?: string | null
containerMode?: string | null
customsInspection?: boolean | null
destinationType?: string | null
amazonWarehouseCode?: string | null
shipmentId?: string | null
referenceId?: string | null
expressCompany?: string | null
expressTrackingNo?: string | null
submittedAt?: string | null
completedAt?: string | null
createdAt: string | null createdAt: string | null
updatedAt: string | null updatedAt: string | null
} }
@@ -56,6 +56,9 @@ describe('UnifiedOrdersView contract', () => {
expect(source).toContain(':loading="loading"') expect(source).toContain(':loading="loading"')
expect(source).toContain('v-model:page="page"') expect(source).toContain('v-model:page="page"')
expect(source).toContain('v-model:page-size="pageSize"') expect(source).toContain('v-model:page-size="pageSize"')
expect(source).toContain('slotName: \'orderStatus\'')
expect(source).toContain('slotName: \'measurementReviewStatus\'')
expect(source).toContain("NOT_SUBMITTED: '未提交'")
}) })
it('keeps only confirmed business actions as explicit boundaries', () => { it('keeps only confirmed business actions as explicit boundaries', () => {
@@ -69,14 +72,15 @@ describe('UnifiedOrdersView contract', () => {
expect(source).not.toContain('/actions/') expect(source).not.toContain('/actions/')
}) })
it('implements pending review with an active public carrier assignment', () => { it('approves pending review orders without requiring a carrier assignment', () => {
expect(source).toContain("row.nextAction === 'APPROVE_ORDER'") expect(source).toContain("row.nextAction === 'APPROVE_ORDER'")
expect(source).toContain("can('platform:order:approve')") expect(source).toContain("can('platform:order:approve')")
expect(source).toContain("listPlatformPartners({ page: currentPage, size: 200, type: 'CARRIER', status: 'ACTIVE' })")
expect(source).toContain('await assignPlatformUnifiedOrderCarrier(tenantId, orderId, String(carrier.id))')
expect(source).toContain('await approvePlatformUnifiedOrder(tenantId, orderId)') expect(source).toContain('await approvePlatformUnifiedOrder(tenantId, orderId)')
expect(source.indexOf('await assignPlatformUnifiedOrderCarrier')).toBeLessThan(source.indexOf('await approvePlatformUnifiedOrder')) expect(source).toContain('title="审核订单"')
expect(source).toContain('暂无启用的承运商,请先到往来单位维护承运商主数据。') expect(source).toContain('confirm-text="审核通过"')
expect(source).not.toContain('assignPlatformUnifiedOrderCarrier')
expect(source).not.toContain('listPlatformPartners')
expect(source).not.toContain('请选择启用的承运商')
expect(source).toContain("detail.order.order?.carrierName || '待平台指派'") expect(source).toContain("detail.order.order?.carrierName || '待平台指派'")
}) })
@@ -100,6 +104,16 @@ describe('UnifiedOrdersView contract', () => {
expect(source).not.toContain('detailOpen') expect(source).not.toContain('detailOpen')
}) })
it('keeps the basic information compact and maps available order fields', () => {
expect(source).toContain('grid-template-columns: repeat(4, minmax(0, 1fr))')
for (const label of ['业务类型', '买单 / 退税', '提货方式', '配载状态', '目的仓类型', '总净重', '商检验', '提货联系人', '目的地地址']) {
expect(source).toContain(`<dt>${label}</dt>`)
}
expect(source).toContain("partyValue(detail.order.parties, 'SENDER', 'phone')")
expect(source).toContain("cargoTotal(detail.order.cargoes, 'netWeightKg')")
expect(source).toContain('暂无费用数据')
})
it('does not expose the unconfirmed downstream workflow', () => { it('does not expose the unconfirmed downstream workflow', () => {
expect(source).not.toContain('后半程作业') expect(source).not.toContain('后半程作业')
expect(source).not.toContain('openWorkflow') expect(source).not.toContain('openWorkflow')
@@ -1,12 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, nextTick, onMounted, reactive, ref, watch } from 'vue' import { computed, nextTick, onMounted, ref, watch } from 'vue'
import { ElMessage, type FormInstance, type FormRules } from 'element-plus' import { ElMessage } from 'element-plus'
import { ArrowLeft, Box, ChevronDown, ChevronUp, CircleCheck, Eye, History, ListChecks, MoreHorizontal, PackageOpen, RefreshCw, Ruler, SlidersHorizontal, StickyNote, WalletCards } from '@lucide/vue' import { ArrowLeft, Box, ChevronDown, ChevronUp, CircleCheck, Eye, History, ListChecks, MoreHorizontal, PackageOpen, RefreshCw, Ruler, SlidersHorizontal, StickyNote, WalletCards } from '@lucide/vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import { import {
getPlatformUnifiedOrder, getPlatformUnifiedOrder,
approvePlatformUnifiedOrder, approvePlatformUnifiedOrder,
assignPlatformUnifiedOrderCarrier,
addPlatformUnifiedOrderNote, addPlatformUnifiedOrderNote,
listPlatformUnifiedOrders, listPlatformUnifiedOrders,
unifiedOrderStatusOptions, unifiedOrderStatusOptions,
@@ -16,7 +15,6 @@ import {
} from '@/modules/unified-orders/api/unifiedOrders' } from '@/modules/unified-orders/api/unifiedOrders'
import { useAccessPreviewStore } from '@/core/access/accessPreview' import { useAccessPreviewStore } from '@/core/access/accessPreview'
import { listPlatformTenants, type PlatformTenantResource } from '@/modules/iam' import { listPlatformTenants, type PlatformTenantResource } from '@/modules/iam'
import { listPlatformPartners, type PlatformPartnerResource } from '@/modules/master-data'
import { listAllPlatformWarehouses, type PlatformWarehouseSummaryResource } from '@/modules/warehouses' import { listAllPlatformWarehouses, type PlatformWarehouseSummaryResource } from '@/modules/warehouses'
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http' import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue' import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
@@ -71,22 +69,13 @@ const activeDetailTab = ref('basic')
const approvalOpen = ref(false) const approvalOpen = ref(false)
const approvalLoadingId = ref('') const approvalLoadingId = ref('')
const approvalSaving = ref(false) const approvalSaving = ref(false)
const carrierLoading = ref(false)
const carrierLoadError = ref<ApiErrorDetails | null>(null)
const carriers = ref<PlatformPartnerResource[]>([])
const approvalOrder = ref<UnifiedOrderResource | null>(null) const approvalOrder = ref<UnifiedOrderResource | null>(null)
const carrierFormRef = ref<FormInstance>()
const carrierForm = reactive({ carrierId: '' })
const noteOpen = ref(false) const noteOpen = ref(false)
const noteSaving = ref(false) const noteSaving = ref(false)
const noteContent = ref('') const noteContent = ref('')
const noteOrder = ref<UnifiedOrderResource | null>(null) const noteOrder = ref<UnifiedOrderResource | null>(null)
const showAdvancedFilters = ref(false) const showAdvancedFilters = ref(false)
const filters = ref<FilterModel>(emptyFilters()) const filters = ref<FilterModel>(emptyFilters())
const carrierRules: FormRules = {
carrierId: [{ required: true, message: '请选择承运商', trigger: 'change' }],
}
const orderId = computed(() => typeof route.params.orderId === 'string' ? route.params.orderId : '') const orderId = computed(() => typeof route.params.orderId === 'string' ? route.params.orderId : '')
const detailTabs = [ const detailTabs = [
@@ -270,6 +259,30 @@ function formatNumber(value: number | null | undefined, digits = 2) {
return value === null || value === undefined ? '—' : value.toLocaleString('zh-CN', { maximumFractionDigits: digits }) return value === null || value === undefined ? '—' : value.toLocaleString('zh-CN', { maximumFractionDigits: digits })
} }
function partyValue(parties: Array<Record<string, unknown>>, type: 'SENDER' | 'CONSIGNEE', key: string) {
const party = parties.find((item) => String(item.partyType || '').toUpperCase() === type)
const value = party?.[key]
return value === null || value === undefined || value === '' ? '—' : String(value)
}
function partyAddress(parties: Array<Record<string, unknown>>, type: 'SENDER' | 'CONSIGNEE') {
const party = parties.find((item) => String(item.partyType || '').toUpperCase() === type)
if (!party) return '—'
return ['province', 'city', 'district', 'addressLine'].map((key) => party[key]).filter((value) => typeof value === 'string' && value.trim()).join(' ') || '—'
}
function cargoTotal(cargoes: Array<Record<string, unknown>>, key: string) {
const total = cargoes.reduce((sum, cargo) => {
const value = Number(cargo[key])
return Number.isFinite(value) ? sum + value : sum
}, 0)
return total > 0 ? total : null
}
function displayBoolean(value: boolean | null | undefined) {
return value === null || value === undefined ? '—' : value ? '是' : '否'
}
function displayField(row: UnifiedOrderResource, property: string | undefined) { function displayField(row: UnifiedOrderResource, property: string | undefined) {
if (!property) return '—' if (!property) return '—'
const source = row as unknown as Record<string, unknown> const source = row as unknown as Record<string, unknown>
@@ -297,7 +310,17 @@ function displayField(row: UnifiedOrderResource, property: string | undefined) {
} }
function statusLabel(value: UnifiedOrderMainStatus | string | null) { function statusLabel(value: UnifiedOrderMainStatus | string | null) {
return unifiedOrderStatusOptions.find((item) => item.value === value)?.label || value || '状态更新' return unifiedOrderStatusOptions.find((item) => item.value === value)?.label || orderStatusLabel(value) || '状态更新'
}
function historyActionLabel(value: string | null | undefined) {
return ({
CREATE: '创建订单', CREATED: '创建订单', SUBMITTED: '提交订单', APPROVED: '审核通过',
ORDER_APPROVED: '订单审核通过', REJECTED: '审核驳回', MEASURE_APPROVE: '测量审核通过',
MEASURE_REJECT: '测量审核驳回', INBOUND_BEGIN: '开始入库', INBOUND_COMPLETE: '入库完成',
UI_FIXTURE_MEASURE_APPROVE: '测量数据审核通过', UI_FIXTURE_INBOUND_BEGIN: '开始入库',
STATUS_UPDATE: '状态更新',
} as Record<string, string>)[value || ''] || (value ? value.replaceAll('_', ' ') : '状态更新')
} }
function measurementStatusLabel(value: string | null | undefined) { function measurementStatusLabel(value: string | null | undefined) {
@@ -334,70 +357,34 @@ function openBulkReviewPlaceholder() {
ElMessage.info('批量审核接口待接入,请先逐笔审核') ElMessage.info('批量审核接口待接入,请先逐笔审核')
} }
async function loadCarriers() { function openApproval(row: UnifiedOrderResource) {
carrierLoading.value = true
carrierLoadError.value = null
try {
const records: PlatformPartnerResource[] = []
let currentPage = 1
let pages = 1
do {
const result = await listPlatformPartners({ page: currentPage, size: 200, type: 'CARRIER', status: 'ACTIVE' })
records.push(...result.records)
pages = Math.max(1, result.pages)
currentPage += 1
} while (currentPage <= pages)
carriers.value = records.filter((item) => item.status === 'ACTIVE' && item.types.includes('CARRIER'))
} catch (error) {
carriers.value = []
carrierLoadError.value = getApiErrorDetails(error, '承运商列表加载失败,请稍后重试')
} finally {
carrierLoading.value = false
}
}
async function openApproval(row: UnifiedOrderResource) {
if (!canApprove(row)) return if (!canApprove(row)) return
approvalOrder.value = row approvalOrder.value = row
const currentSnapshot = detail.value?.summary.orderId === row.orderId ? detail.value.order.order : null
carrierForm.carrierId = currentSnapshot?.carrierId == null ? '' : String(currentSnapshot.carrierId)
approvalOpen.value = true approvalOpen.value = true
await nextTick()
carrierFormRef.value?.clearValidate()
await loadCarriers()
} }
function closeApproval() { function closeApproval() {
if (approvalSaving.value) return if (approvalSaving.value) return
approvalOpen.value = false approvalOpen.value = false
approvalOrder.value = null approvalOrder.value = null
carrierForm.carrierId = ''
} }
async function assignCarrierAndApprove() { async function approveOrder() {
if (!approvalOrder.value) return if (!approvalOrder.value) return
const valid = await carrierFormRef.value?.validate().catch(() => false)
if (!valid) return
const carrier = carriers.value.find((item) => String(item.id) === carrierForm.carrierId)
if (!carrier) {
ElMessage.error('所选承运商已停用或不存在,请重新选择')
return
}
const order = approvalOrder.value const order = approvalOrder.value
const orderId = String(order.orderId) const orderId = String(order.orderId)
const tenantId = String(order.tenantId) const tenantId = String(order.tenantId)
approvalSaving.value = true approvalSaving.value = true
approvalLoadingId.value = orderId approvalLoadingId.value = orderId
try { try {
await assignPlatformUnifiedOrderCarrier(tenantId, orderId, String(carrier.id))
await approvePlatformUnifiedOrder(tenantId, orderId) await approvePlatformUnifiedOrder(tenantId, orderId)
approvalOpen.value = false approvalOpen.value = false
await loadOrders() await loadOrders()
if (detail.value?.summary.orderId === order.orderId) await loadDetail() if (detail.value?.summary.orderId === order.orderId) await loadDetail()
approvalOrder.value = null approvalOrder.value = null
ElMessage.success('承运商已指派,订单审核通过') ElMessage.success('订单审核通过')
} catch (error) { } catch (error) {
ElMessage.error(getApiErrorDetails(error, '承运商指派或订单审核失败,请稍后重试').message) ElMessage.error(getApiErrorDetails(error, '订单审核失败,请稍后重试').message)
} finally { } finally {
approvalSaving.value = false approvalSaving.value = false
approvalLoadingId.value = '' approvalLoadingId.value = ''
@@ -636,18 +623,66 @@ onMounted(() => {
<div><dt>公司</dt><dd>{{ customerName(detail.summary) }}</dd></div> <div><dt>公司</dt><dd>{{ customerName(detail.summary) }}</dd></div>
<div><dt>运输方式</dt><dd>{{ detail.order.order?.transportMode || '—' }}</dd></div> <div><dt>运输方式</dt><dd>{{ detail.order.order?.transportMode || '—' }}</dd></div>
<div><dt>当前状态</dt><dd><StatusBadge :status="statusLabel(detail.summary.mainStatus)" /></dd></div> <div><dt>当前状态</dt><dd><StatusBadge :status="statusLabel(detail.summary.mainStatus)" /></dd></div>
<div><dt>内部订单状态</dt><dd>{{ detail.summary.internalOrderStatus || '—' }}</dd></div>
<div><dt>平台收货仓</dt><dd>{{ detail.summary.receivingWarehouseName || '—' }}</dd></div> <div><dt>平台收货仓</dt><dd>{{ detail.summary.receivingWarehouseName || '—' }}</dd></div>
<div><dt>收货仓地址</dt><dd>{{ detail.order.order?.receivingWarehouseAddress || '—' }}</dd></div>
<div><dt>目的仓</dt><dd>{{ detail.summary.destinationWarehouseName || '—' }}</dd></div> <div><dt>目的仓</dt><dd>{{ detail.summary.destinationWarehouseName || '—' }}</dd></div>
<div><dt>目的国家</dt><dd>{{ detail.order.order?.destinationCountryCode || '—' }}</dd></div> <div><dt>目的国家</dt><dd>{{ detail.order.order?.destinationCountryCode || '—' }}</dd></div>
<div><dt>目的仓地址</dt><dd>{{ detail.summary.destinationAddress || '—' }}</dd></div>
<div><dt>目的仓邮编</dt><dd>{{ detail.summary.destinationPostalCode || '—' }}</dd></div>
<div><dt>目的仓联系人</dt><dd>{{ detail.summary.destinationContact || '—' }}</dd></div>
<div><dt>目的仓联系人电话</dt><dd>{{ detail.summary.destinationContactPhone || '—' }}</dd></div>
<div><dt>渠道</dt><dd>{{ detail.summary.channelName || '—' }}</dd></div> <div><dt>渠道</dt><dd>{{ detail.summary.channelName || '—' }}</dd></div>
<div><dt>承运商</dt><dd>{{ detail.order.order?.carrierName || '待平台指派' }}</dd></div> <div><dt>承运商</dt><dd>{{ detail.order.order?.carrierName || '待平台指派' }}</dd></div>
<div><dt>业务类型</dt><dd>{{ detail.order.order?.tradeTerm || '—' }}</dd></div>
<div><dt>贸易方式</dt><dd>{{ detail.order.order?.tradeTerm || detail.summary.businessType || '—' }}</dd></div>
<div><dt>买单 / 退税</dt><dd>{{ detail.order.order?.taxMode || '—' }}</dd></div>
<div><dt>报关方式</dt><dd>{{ detail.summary.businessType || '—' }}</dd></div>
<div><dt>费用审核状态</dt><dd>{{ detail.summary.feeReviewStatus || '—' }}</dd></div>
<div><dt>是否扣货</dt><dd>{{ displayBoolean(detail.summary.holdCargo) }}</dd></div>
<div><dt>扣货理由</dt><dd>{{ detail.summary.holdReason || '—' }}</dd></div>
<div><dt>提货方式</dt><dd>{{ detail.order.order?.pickupMethod || '—' }}</dd></div>
<div><dt>提货时间</dt><dd>{{ formatDate(detail.summary.pickupAt) }}</dd></div>
<div><dt>到仓时间</dt><dd>{{ detail.order.order?.receivingTime || '—' }}</dd></div>
<div><dt>货物到仓时间</dt><dd>{{ formatDate(detail.summary.expectedArrivalAt || detail.summary.inboundCompletedAt) }}</dd></div>
<div><dt>预计出运</dt><dd>{{ formatDate(detail.summary.expectedShipDate) }}</dd></div> <div><dt>预计出运</dt><dd>{{ formatDate(detail.summary.expectedShipDate) }}</dd></div>
<div><dt>预计到达时间</dt><dd>{{ formatDate(detail.summary.expectedArrivalAt) }}</dd></div>
<div><dt>集装箱号</dt><dd>{{ detail.summary.transport?.containerNo || '—' }}</dd></div> <div><dt>集装箱号</dt><dd>{{ detail.summary.transport?.containerNo || '—' }}</dd></div>
<div><dt>柜号</dt><dd>{{ detail.summary.transport?.cabinetNo || '—' }}</dd></div>
<div><dt>船名 / 航次</dt><dd>{{ detail.summary.transport?.vesselName || '—' }} / {{ detail.summary.transport?.voyageNo || '—' }}</dd></div> <div><dt>船名 / 航次</dt><dd>{{ detail.summary.transport?.vesselName || '—' }} / {{ detail.summary.transport?.voyageNo || '—' }}</dd></div>
<div><dt>开船时间</dt><dd>{{ formatDate(detail.summary.transport?.sailingAt) }}</dd></div> <div><dt>开船时间</dt><dd>{{ formatDate(detail.summary.transport?.sailingAt) }}</dd></div>
<div><dt>到港时间</dt><dd>{{ formatDate(detail.summary.transport?.portArrivalAt) }}</dd></div> <div><dt>到港时间</dt><dd>{{ formatDate(detail.summary.transport?.portArrivalAt) }}</dd></div>
<div><dt>下一步</dt><dd>{{ canApprove(detail.summary) ? '选择承运商并审核' : nextActionBoundary(detail.summary) || '—' }}</dd></div> <div><dt>配载状态</dt><dd>{{ detail.summary.transport?.loadingStatus || '—' }}</dd></div>
<div><dt>目的仓类型</dt><dd>{{ detail.order.order?.destinationType || '—' }}</dd></div>
<div><dt>是否整柜</dt><dd>{{ detail.order.order?.containerMode || '—' }}</dd></div>
<div><dt>商检验</dt><dd>{{ displayBoolean(detail.order.order?.customsInspection) }}</dd></div>
<div><dt>总箱数</dt><dd>{{ detail.summary.totalPackageCount ?? '—' }} 箱</dd></div>
<div><dt>总托盘数</dt><dd>{{ detail.summary.totalPalletCount ?? '—' }}</dd></div>
<div><dt>总毛重</dt><dd>{{ formatNumber(cargoTotal(detail.order.cargoes, 'grossWeightKg') ?? detail.summary.measurement?.actualGrossWeightKg) }} kg</dd></div>
<div><dt>总净重</dt><dd>{{ formatNumber(cargoTotal(detail.order.cargoes, 'netWeightKg')) }} kg</dd></div>
<div><dt>总体积</dt><dd>{{ formatNumber(cargoTotal(detail.order.cargoes, 'volumeCbm') ?? detail.summary.measurement?.actualVolumeCbm, 3) }} m³</dd></div>
<div><dt>计费毛重</dt><dd>{{ formatNumber(detail.summary.measurement?.chargeableWeightKg) }} kg</dd></div>
<div><dt>计费体积</dt><dd>{{ formatNumber(detail.summary.chargeableVolumeCbm, 3) }} m³</dd></div>
<div><dt>总材积重</dt><dd>{{ formatNumber(detail.summary.volumetricWeightKg) }} kg</dd></div>
<div><dt>总材积量</dt><dd>{{ formatNumber(detail.summary.volumetricWeightKg) }}</dd></div>
<div><dt>库位位置</dt><dd>{{ detail.summary.location || '—' }}</dd></div>
<div><dt>创建时间</dt><dd>{{ formatDate(detail.summary.createdAt) }}</dd></div> <div><dt>创建时间</dt><dd>{{ formatDate(detail.summary.createdAt) }}</dd></div>
<div><dt>入库完成时间</dt><dd>{{ formatDate(detail.summary.inboundCompletedAt) }}</dd></div>
<div><dt>测量完成时间</dt><dd>{{ formatDate(detail.summary.measurementCompletedAt || detail.summary.measurement?.completedAt) }}</dd></div>
<div class="wide"><dt>提货联系人</dt><dd>{{ partyValue(detail.order.parties, 'SENDER', 'contactName') }} · {{ detail.summary.pickupContactPhone || partyValue(detail.order.parties, 'SENDER', 'phone') }} · {{ detail.summary.pickupContactEmail || partyValue(detail.order.parties, 'SENDER', 'email') }}</dd></div>
<div class="wide"><dt>提货地址</dt><dd>{{ detail.summary.pickupAddress || partyAddress(detail.order.parties, 'SENDER') }} · {{ detail.summary.pickupPostalCode || partyValue(detail.order.parties, 'SENDER', 'postalCode') }}</dd></div>
<div class="wide"><dt>目的地联系人</dt><dd>{{ partyValue(detail.order.parties, 'CONSIGNEE', 'contactName') }} · {{ partyValue(detail.order.parties, 'CONSIGNEE', 'phone') }} · {{ partyValue(detail.order.parties, 'CONSIGNEE', 'email') }}</dd></div>
<div><dt>目的仓联系人邮箱</dt><dd>{{ partyValue(detail.order.parties, 'CONSIGNEE', 'email') }}</dd></div>
<div class="wide"><dt>目的地地址</dt><dd>{{ partyAddress(detail.order.parties, 'CONSIGNEE') }} · {{ partyValue(detail.order.parties, 'CONSIGNEE', 'postalCode') }}</dd></div>
<div class="wide"><dt>备注</dt><dd>{{ detail.order.order?.customerRemark || '—' }}</dd></div>
<div class="wide"><dt>关联文件</dt><dd>{{ detail.summary.associatedFiles?.join('、') || '—' }}</dd></div>
<div class="wide"><dt>唛头文件</dt><dd>{{ detail.summary.markFiles?.join('、') || '—' }}</dd></div>
<div class="wide"><dt>PC 端下单文件</dt><dd>{{ detail.summary.pcOrderFiles?.join('、') || '—' }}</dd></div>
<div><dt>快递公司</dt><dd>{{ detail.summary.expressCompany || detail.order.order?.expressCompany || '—' }}</dd></div>
<div><dt>快递单号</dt><dd>{{ detail.summary.expressTrackingNo || detail.order.order?.expressTrackingNo || '—' }}</dd></div>
<div class="wide"><dt>客服内部备注</dt><dd>{{ detail.summary.internalRemark || '—' }}</dd></div>
<div><dt>创建人</dt><dd>{{ detail.summary.createdBy || '—' }}</dd></div>
<div><dt>下一步</dt><dd>{{ canApprove(detail.summary) ? '审核订单' : nextActionBoundary(detail.summary) || '—' }}</dd></div>
</dl> </dl>
</template> </template>
@@ -667,7 +702,7 @@ onMounted(() => {
<template v-else-if="activeDetailTab === 'history'"> <template v-else-if="activeDetailTab === 'history'">
<div class="panel-title"><div><h2>状态历史</h2><p>共 {{ detail.timeline.length }} 条</p></div><History :size="19" /></div> <div class="panel-title"><div><h2>状态历史</h2><p>共 {{ detail.timeline.length }} 条</p></div><History :size="19" /></div>
<el-empty v-if="detail.timeline.length === 0" description="暂无状态历史" :image-size="64" /> <el-empty v-if="detail.timeline.length === 0" description="暂无状态历史" :image-size="64" />
<ol v-else class="history-list" aria-label="状态历史"><li v-for="(item, index) in detail.timeline" :key="`${item.source || 'event'}-${item.occurredAt || index}`"><span class="history-marker" aria-hidden="true" /><div><div class="history-line"><strong>{{ item.action || item.eventType || '状态更新' }}</strong><StatusBadge :status="statusLabel(item.mainStatus)" /></div><p>{{ item.operatorName || '系统' }} · {{ formatDate(item.occurredAt) }}</p><p v-if="item.remark" class="history-remark">{{ item.remark }}</p></div></li></ol> <ol v-else class="history-list" aria-label="状态历史"><li v-for="(item, index) in detail.timeline" :key="`${item.source || 'event'}-${item.occurredAt || index}`"><span class="history-marker" aria-hidden="true" /><div><div class="history-line"><strong>{{ historyActionLabel(item.action || item.eventType) }}</strong><StatusBadge :status="statusLabel(item.mainStatus)" /></div><p>{{ item.operatorName || '系统' }} · {{ formatDate(item.occurredAt) }}</p><p v-if="item.remark" class="history-remark">{{ item.remark }}</p></div></li></ol>
</template> </template>
<template v-else-if="activeDetailTab === 'measurement'"> <template v-else-if="activeDetailTab === 'measurement'">
@@ -759,20 +794,8 @@ onMounted(() => {
<el-input v-model="noteContent" type="textarea" :rows="5" maxlength="2000" show-word-limit placeholder="请输入仅平台内部可见的备注" aria-label="内部备注内容" /> <el-input v-model="noteContent" type="textarea" :rows="5" maxlength="2000" show-word-limit placeholder="请输入仅平台内部可见的备注" aria-label="内部备注内容" />
</AppDialog> </AppDialog>
<AppDialog v-model="approvalOpen" title="选择承运商并审核" confirm-text="指派并审核通过" confirm-type="success" :loading="carrierLoading" :confirm-loading="approvalSaving" :confirm-disabled="Boolean(carrierLoadError) || carriers.length === 0" destroy-on-close @confirm="assignCarrierAndApprove" @cancel="closeApproval"> <AppDialog v-model="approvalOpen" title="审核订单" confirm-text="审核通过" confirm-type="success" :confirm-loading="approvalSaving" destroy-on-close @confirm="approveOrder" @cancel="closeApproval">
<el-alert title="订单审核通过后将进入履约流程;承运商名称由后端公共主数据生成快照。" type="info" :closable="false" show-icon /> <el-alert title="确认订单资料无误后审核通过,订单将进入待测量阶段。" type="info" :closable="false" show-icon />
<el-alert v-if="carrierLoadError" class="carrier-load-error" type="error" :closable="false" show-icon>
<template #title><div class="api-error-alert-title"><span>{{ carrierLoadError.message }}</span><ApiErrorMeta :details="carrierLoadError" /></div></template>
<el-button link type="danger" :loading="carrierLoading" @click="loadCarriers">重新加载</el-button>
</el-alert>
<el-alert v-else-if="!carrierLoading && carriers.length === 0" class="carrier-load-error" title="暂无启用的承运商,请先到往来单位维护承运商主数据。" type="warning" :closable="false" show-icon />
<el-form ref="carrierFormRef" :model="carrierForm" :rules="carrierRules" label-position="top" class="carrier-form" :disabled="carrierLoading || approvalSaving || Boolean(carrierLoadError)">
<el-form-item label="承运商" prop="carrierId" required>
<el-select v-model="carrierForm.carrierId" filterable :loading="carrierLoading" placeholder="请选择启用的承运商">
<el-option v-for="carrier in carriers" :key="String(carrier.id)" :label="`${carrier.partnerName}(${carrier.partnerCode})`" :value="String(carrier.id)" />
</el-select>
</el-form-item>
</el-form>
</AppDialog> </AppDialog>
</section> </section>
</template> </template>
@@ -796,9 +819,6 @@ onMounted(() => {
.table-card { margin-top: 16px; overflow: hidden; border: 1px solid var(--cc-border); border-radius: 8px; background: #fff; } .table-card { margin-top: 16px; overflow: hidden; border: 1px solid var(--cc-border); border-radius: 8px; background: #fff; }
.row-actions { display: flex; align-items: center; gap: 4px; white-space: nowrap; } .row-actions { display: flex; align-items: center; gap: 4px; white-space: nowrap; }
.row-actions :deep(.el-button) { margin-left: 0; } .row-actions :deep(.el-button) { margin-left: 0; }
.carrier-form { margin-top: 18px; }
.carrier-form :deep(.el-select) { width: 100%; }
.carrier-load-error { margin-top: 14px; }
.primary-cell { display: grid; min-width: 0; gap: 4px; } .primary-cell { display: grid; min-width: 0; gap: 4px; }
.primary-cell strong { overflow-wrap: anywhere; } .primary-cell strong { overflow-wrap: anywhere; }
.primary-cell span { color: var(--cc-muted); font-size: 12px; overflow-wrap: anywhere; } .primary-cell span { color: var(--cc-muted); font-size: 12px; overflow-wrap: anywhere; }
@@ -828,15 +848,16 @@ onMounted(() => {
.detail-tabs button:hover { background: #fafbff; color: #344054; } .detail-tabs button:hover { background: #fafbff; color: #344054; }
.detail-tabs button.active { background: #f7f8ff; color: var(--cc-primary); } .detail-tabs button.active { background: #f7f8ff; color: var(--cc-primary); }
.detail-tabs button.active::after { background: var(--cc-primary); } .detail-tabs button.active::after { background: var(--cc-primary); }
.detail-panel { min-width: 0; min-height: 390px; padding: 22px; border: 1px solid var(--cc-border); border-radius: 0 0 8px 8px; background: #fff; } .detail-panel { min-width: 0; min-height: 390px; padding: 16px; border: 1px solid var(--cc-border); border-radius: 0 0 8px 8px; background: #fff; }
.panel-title { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; margin-bottom: 18px; padding-bottom: 14px; border-bottom: 1px solid var(--cc-border); } .panel-title { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; margin-bottom: 10px; padding-bottom: 8px; border-bottom: 1px solid var(--cc-border); }
.panel-title h2 { margin: 0; color: #344054; font-size: 16px; } .panel-title h2 { margin: 0; color: #344054; font-size: 16px; }
.panel-title p { margin: 5px 0 0; color: var(--cc-muted); font-size: 11px; } .panel-title p { margin: 5px 0 0; color: var(--cc-muted); font-size: 11px; }
.panel-title > svg { color: var(--cc-primary); } .panel-title > svg { color: var(--cc-primary); }
.order-info-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 1px; margin: 0; overflow: hidden; border: 1px solid #edf0f4; background: #edf0f4; } .order-info-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 0; margin: 0; overflow: hidden; border: 1px solid #edf0f4; background: #fff; }
.order-info-grid > div, .measurement-grid > div { min-width: 0; padding: 14px 16px; background: #fff; } .order-info-grid > div, .measurement-grid > div { display: flex; min-width: 0; min-height: 38px; align-items: baseline; gap: 8px; padding: 8px 12px; border-right: 1px solid #edf0f4; border-bottom: 1px solid #edf0f4; background: #fff; }
.order-info-grid dt, .measurement-grid dt { margin-bottom: 6px; color: var(--cc-muted); font-size: 11px; } .order-info-grid > div.wide { grid-column: span 2; }
.order-info-grid dd, .measurement-grid dd { margin: 0; color: #25324a; font-size: 13px; font-weight: 650; line-height: 1.5; overflow-wrap: anywhere; } .order-info-grid dt, .measurement-grid dt { flex: 0 0 auto; margin: 0; color: var(--cc-muted); font-size: 11px; white-space: nowrap; }
.order-info-grid dd, .measurement-grid dd { min-width: 0; margin: 0; color: #25324a; font-size: 12px; font-weight: 650; line-height: 1.35; overflow-wrap: anywhere; }
.detail-table-wrap { overflow-x: auto; } .detail-table-wrap { overflow-x: auto; }
.detail-table-wrap :deep(.el-table) { min-width: 720px; --el-table-header-bg-color: #f8fafc; } .detail-table-wrap :deep(.el-table) { min-width: 720px; --el-table-header-bg-color: #f8fafc; }
.measurement-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 1px; margin: 0; border: 1px solid var(--cc-border); background: var(--cc-border); } .measurement-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 1px; margin: 0; border: 1px solid var(--cc-border); background: var(--cc-border); }
@@ -854,6 +875,6 @@ onMounted(() => {
.notes-list li > p { margin: 0; color: var(--cc-text); font-size: 13px; } .notes-list li > p { margin: 0; color: var(--cc-text); font-size: 13px; }
.notes-list span { display: block; margin-top: 5px; color: var(--cc-muted); font-size: 12px; } .notes-list span { display: block; margin-top: 5px; color: var(--cc-muted); font-size: 12px; }
@media (max-width: 960px) { .detail-overview { grid-template-columns: repeat(2, minmax(0, 1fr)); } .detail-overview article:nth-child(2) { border-right: 0; } .detail-overview article:nth-child(-n+2) { border-bottom: 1px solid var(--cc-border); } .order-info-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } .measurement-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } } @media (max-width: 960px) { .detail-overview { grid-template-columns: repeat(2, minmax(0, 1fr)); } .detail-overview article:nth-child(2) { border-right: 0; } .detail-overview article:nth-child(-n+2) { border-bottom: 1px solid var(--cc-border); } .order-info-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } .measurement-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 720px) { .page-intro { align-items: stretch; flex-direction: column; } .page-intro :deep(.el-button) { width: 100%; min-height: 44px; } .status-tab { min-height: 46px; padding: 0 14px; } .table-actions :deep(.el-button) { width: 100%; min-height: 40px; } .row-actions { flex-wrap: wrap; } .order-detail-header { align-items: stretch; flex-direction: column; } .order-detail-heading { display: grid; grid-template-columns: auto 36px minmax(0, 1fr) auto; } .detail-header-actions { display: grid; grid-template-columns: 1fr; } .detail-header-actions :deep(.el-button) { min-height: 40px; } .detail-overview, .order-info-grid, .measurement-grid { grid-template-columns: 1fr; } .detail-overview article { border-right: 0; border-bottom: 1px solid var(--cc-border); } .detail-overview article:last-child { border-bottom: 0; } .detail-tabs { grid-template-columns: repeat(6, 132px); } .detail-panel { padding: 16px; } } @media (max-width: 720px) { .page-intro { align-items: stretch; flex-direction: column; } .page-intro :deep(.el-button) { width: 100%; min-height: 44px; } .status-tab { min-height: 46px; padding: 0 14px; } .table-actions :deep(.el-button) { width: 100%; min-height: 40px; } .row-actions { flex-wrap: wrap; } .order-detail-header { align-items: stretch; flex-direction: column; } .order-detail-heading { display: grid; grid-template-columns: auto 36px minmax(0, 1fr) auto; } .detail-header-actions { display: grid; grid-template-columns: 1fr; } .detail-header-actions :deep(.el-button) { min-height: 40px; } .detail-overview, .order-info-grid, .measurement-grid { grid-template-columns: 1fr; } .detail-overview article { border-right: 0; border-bottom: 1px solid var(--cc-border); } .detail-overview article:last-child { border-bottom: 0; } .order-info-grid > div.wide { grid-column: auto; } .detail-tabs { grid-template-columns: repeat(6, 132px); } .detail-panel { padding: 16px; } }
@media (max-width: 420px) { .order-detail-heading { grid-template-columns: auto minmax(0, 1fr) auto; } .order-mark { display: none; } } @media (max-width: 420px) { .order-detail-heading { grid-template-columns: auto minmax(0, 1fr) auto; } .order-mark { display: none; } }
</style> </style>