@@ -1,12 +1,13 @@
< script setup lang = "ts" >
import { computed , nextTick , onMounted , reactive , ref , watch } from 'vue'
import { ElMessage , type FormInstance , type FormRules } from 'element-plus'
import { ChevronDown , ChevronUp , CircleCheck , ClipboardList , Eye , RefreshCw , SlidersHorizontal } from '@lucide/vue'
import { ArrowLeft , Box , ChevronDown , ChevronUp , CircleCheck , ClipboardList , Eye , History , ListChecks , PackageOpen , RefreshCw , Ruler , SlidersHorizontal , StickyNote , WalletCards } from '@lucide/vue'
import { useRoute , useRouter } from 'vue-router'
import {
getPlatformUnifiedOrder ,
approvePlatformUnifiedOrder ,
assignPlatformUnifiedOrderCarrier ,
addPlatformUnifiedOrderNote ,
listPlatformUnifiedOrders ,
unifiedOrderStatusOptions ,
type UnifiedOrderDetailResource ,
@@ -20,7 +21,6 @@ import { listAllPlatformWarehouses, type PlatformWarehouseSummaryResource } from
import { getApiErrorDetails , type ApiErrorDetails } from '@/shared/api/http'
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
import AppDialog from '@/shared/components/AppDialog.vue'
import AppDrawer from '@/shared/components/AppDrawer.vue'
import DataTable , { type DataTableColumn } from '@/shared/components/DataTable.vue'
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
import QueryToolbar , { type QueryField , type QueryModel } from '@/shared/components/QueryToolbar.vue'
@@ -52,6 +52,7 @@ const route = useRoute()
const router = useRouter ( )
const access = useAccessPreviewStore ( )
const records = ref < UnifiedOrderResource [ ] > ( [ ] )
const selectedOrders = ref < UnifiedOrderResource [ ] > ( [ ] )
const customers = ref < PlatformTenantResource [ ] > ( [ ] )
const warehouses = ref < PlatformWarehouseSummaryResource [ ] > ( [ ] )
const page = ref ( 1 )
@@ -66,7 +67,7 @@ const warehouseLoadError = ref<ApiErrorDetails | null>(null)
const detail = ref < UnifiedOrderDetailResource | null > ( null )
const detailLoading = ref ( false )
const detailError = ref < ApiErrorDetails | null > ( null )
const detailOpen = ref ( false )
const activeDetailTab = ref ( 'basic' )
const approvalOpen = ref ( false )
const approvalLoadingId = ref ( '' )
const approvalSaving = ref ( false )
@@ -76,6 +77,9 @@ const carriers = ref<PlatformPartnerResource[]>([])
const approvalOrder = ref < UnifiedOrderResource | null > ( null )
const carrierFormRef = ref < FormInstance > ( )
const carrierForm = reactive ( { carrierId : '' } )
const noteOpen = ref ( false )
const noteSaving = ref ( false )
const noteContent = ref ( '' )
const showAdvancedFilters = ref ( false )
const filters = ref < FilterModel > ( emptyFilters ( ) )
const carrierRules : FormRules = {
@@ -84,6 +88,15 @@ const carrierRules: FormRules = {
const orderId = computed ( ( ) => typeof route . params . orderId === 'string' ? route . params . orderId : '' )
const detailTabs = [
{ key : 'basic' , label : '基本信息' , icon : Box } ,
{ key : 'cargo' , label : '货品信息' , icon : PackageOpen } ,
{ key : 'fees' , label : '费用信息' , icon : WalletCards } ,
{ key : 'history' , label : '状态历史' , icon : History } ,
{ key : 'measurement' , label : '测量数据' , icon : Ruler } ,
{ key : 'other' , label : '其他' , icon : StickyNote } ,
]
const statusTabs = computed ( ( ) => [
{ value : '' as const , label : '全部' } ,
... unifiedOrderStatusOptions ,
@@ -141,21 +154,66 @@ const advancedQueryFields = computed<QueryField[]>(() => [
{ key : 'fbaCode' , type : 'input' , width : 220 , label : '商品 FBA 编码' , placeholder : '请输入商品 FBA 编码' , props : { 'aria-label' : '商品 FBA 编码' } } ,
] )
const allQueryFields = computed ( ( ) => [ ... primaryQueryFields . value , ... advancedQueryFields . value ] )
const queryFields = computed ( ( ) => showAdvancedFilters . value
? [ ... primaryQueryFields . value , ... advancedQueryFields . value ]
? allQueryFields . value
: primaryQueryFields . value )
const columns : DataTableColumn [ ] = [
{ key : 'actions' , label : '操作' , width : 180 , fixed : 'left' , slotName : 'actions' } ,
{ key : 'order ' , label : '订单信息' , minWidth : 220 , slotName : 'order' } ,
{ key : 'mainStatus ' , label : '主状态 ' , width : 132 , slotName : 'mainStatus ' } ,
{ key : 'customer ' , label : '客户 ' , minWidth : 180 , slotName : 'customer ' } ,
{ key : 'receivingWarehouseName ' , label : '平台收货仓 ' , minWidth : 160 , slotName : 'receivingWarehouseName ' } ,
{ key : 'destinationWarehouseName ' , label : '目的仓 ' , minWidth : 160 , slotName : 'destinationWarehouseName ' } ,
{ key : 'transport ' , label : '运输/装柜 ' , minWidth : 180 , slotName : 'transport ' } ,
{ key : 'cargo ' , label : '货物数据 ' , minWidth : 180 , slotName : 'cargo ' } ,
{ key : 'expectedShipDat e' , label : '预计出运 ' , minWidth : 150 , slotName : 'expectedShipDate ' } ,
{ key : 'updatedAt ' , label : '最近更新 ' , minWidth : 150 , slotName : 'updatedAt ' } ,
{ key : 'selection ' , type : 'selection' , width : 52 , fixed : 'left' , align : 'center' , selectable : canBulkReview } ,
{ key : 'inboundCode' , prop : 'inboundCode ', label : '入仓码 ' , minWidth : 140 , slotName : 'field ' } ,
{ key : 'internalOrderStatus' , prop : 'internalOrderStatus ', label : '订单状态 ' , minWidth : 130 , slotName : 'field ' } ,
{ key : 'measurementReviewStatus' , prop : 'measurementReviewStatus ', label : '测量数据审核状态 ' , minWidth : 170 , slotName : 'field ' } ,
{ key : 'expectedArrivalAt' , prop : 'expectedArrivalAt ', label : '预计到达时间 ' , minWidth : 150 , slotName : 'field ' } ,
{ key : 'feeReviewStatus' , prop : 'feeReviewStatus ', label : '费用审核状态 ' , minWidth : 140 , slotName : 'field ' } ,
{ key : 'customerName' , prop : 'customerName ', label : '客户 ' , minWidth : 150 , slotName : 'field ' } ,
{ key : 'receivingWarehouseName' , prop : 'receivingWarehouseNam e', label : '仓库 ' , minWidth : 150 , slotName : 'field ' } ,
{ key : 'destinationWarehouseName' , prop : 'destinationWarehouseName ', label : '交货仓 ' , minWidth : 150 , slotName : 'field ' } ,
{ key : 'loadingStatus' , prop : 'loadingStatus' , label : '配载状态' , minWidth : 130 , slotName : 'field' } ,
{ key : 'holdCargo' , prop : 'holdCargo' , label : '是否扣货' , minWidth : 110 , slotName : 'field' } ,
{ key : 'holdReason' , prop : 'holdReason' , label : '扣货理由' , minWidth : 160 , slotName : 'field' } ,
{ key : 'containerNo' , prop : 'containerNo' , label : '集装箱号' , minWidth : 150 , slotName : 'field' } ,
{ key : 'cabinetNo' , prop : 'cabinetNo' , label : '柜号' , minWidth : 130 , slotName : 'field' } ,
{ key : 'companyName' , prop : 'companyName' , label : '公司' , minWidth : 150 , slotName : 'field' } ,
{ key : 'businessType' , prop : 'businessType' , label : '业务类型' , minWidth : 130 , slotName : 'field' } ,
{ key : 'pickupMethod' , prop : 'pickupMethod' , label : '提货方式' , minWidth : 130 , slotName : 'field' } ,
{ key : 'pickupAt' , prop : 'pickupAt' , label : '提货时间' , minWidth : 150 , slotName : 'field' } ,
{ key : 'pickupContactPhone' , prop : 'pickupContactPhone' , label : '提货联系人电话' , minWidth : 150 , slotName : 'field' } ,
{ key : 'pickupAddress' , prop : 'pickupAddress' , label : '提货地址' , minWidth : 200 , slotName : 'field' } ,
{ key : 'pickupPostalCode' , prop : 'pickupPostalCode' , label : '提货邮编' , minWidth : 110 , slotName : 'field' } ,
{ key : 'pickupContactEmail' , prop : 'pickupContactEmail' , label : '提货联系人邮箱' , minWidth : 190 , slotName : 'field' } ,
{ key : 'channelName' , prop : 'channelName' , label : '渠道' , minWidth : 130 , slotName : 'field' } ,
{ key : 'taxMode' , prop : 'taxMode' , label : '买单/退税' , minWidth : 130 , slotName : 'field' } ,
{ key : 'fullContainer' , prop : 'fullContainer' , label : '是否整柜' , minWidth : 110 , slotName : 'field' } ,
{ key : 'customsInspection' , prop : 'customsInspection' , label : '商检验' , minWidth : 110 , slotName : 'field' } ,
{ key : 'sailingAt' , prop : 'sailingAt' , label : '开船时间' , minWidth : 150 , slotName : 'field' } ,
{ key : 'portArrivalAt' , prop : 'portArrivalAt' , label : '到港时间' , minWidth : 150 , slotName : 'field' } ,
{ key : 'totalPackageCount' , prop : 'totalPackageCount' , label : '总箱数' , minWidth : 100 , slotName : 'field' } ,
{ key : 'totalGrossWeightKg' , prop : 'totalGrossWeightKg' , label : '总毛重(T.K.GW)' , minWidth : 140 , slotName : 'field' } ,
{ key : 'chargeableWeightKg' , prop : 'chargeableWeightKg' , label : '计费毛重' , minWidth : 120 , slotName : 'field' } ,
{ key : 'totalVolumeCbm' , prop : 'totalVolumeCbm' , label : '总体积(m³)' , minWidth : 120 , slotName : 'field' } ,
{ key : 'chargeableVolumeCbm' , prop : 'chargeableVolumeCbm' , label : '计费体积(m³)' , minWidth : 130 , slotName : 'field' } ,
{ key : 'totalPalletCount' , prop : 'totalPalletCount' , label : '总托盘数' , minWidth : 110 , slotName : 'field' } ,
{ key : 'location' , prop : 'location' , label : '库位位置' , minWidth : 130 , slotName : 'field' } ,
{ key : 'volumetricWeightKg' , prop : 'volumetricWeightKg' , label : '总材积重' , minWidth : 120 , slotName : 'field' } ,
{ key : 'totalNetWeightKg' , prop : 'totalNetWeightKg' , label : '总净重(T.K.NW)' , minWidth : 140 , slotName : 'field' } ,
{ key : 'destinationType' , prop : 'destinationType' , label : '目的仓类型' , minWidth : 130 , slotName : 'field' } ,
{ key : 'destinationAddress' , prop : 'destinationAddress' , label : '地址' , minWidth : 200 , slotName : 'field' } ,
{ key : 'destinationPostalCode' , prop : 'destinationPostalCode' , label : '邮编' , minWidth : 100 , slotName : 'field' } ,
{ key : 'destinationContact' , prop : 'destinationContact' , label : '联系人' , minWidth : 120 , slotName : 'field' } ,
{ key : 'destinationContactPhone' , prop : 'destinationContactPhone' , label : '联系人电话' , minWidth : 140 , slotName : 'field' } ,
{ key : 'measurementCompletedAt' , prop : 'measurementCompletedAt' , label : '测量完成时间' , minWidth : 150 , slotName : 'field' } ,
{ key : 'inboundCompletedAt' , prop : 'inboundCompletedAt' , label : '入库完成时间' , minWidth : 150 , slotName : 'field' } ,
{ key : 'associatedFiles' , prop : 'associatedFiles' , label : '关联文件' , minWidth : 120 , slotName : 'field' } ,
{ key : 'markFiles' , prop : 'markFiles' , label : '唛头文件' , minWidth : 120 , slotName : 'field' } ,
{ key : 'pcOrderFiles' , prop : 'pcOrderFiles' , label : 'PC端下单文件' , minWidth : 140 , slotName : 'field' } ,
{ key : 'expressCompany' , prop : 'expressCompany' , label : '快递公司' , minWidth : 130 , slotName : 'field' } ,
{ key : 'expressTrackingNo' , prop : 'expressTrackingNo' , label : '快递单号' , minWidth : 150 , slotName : 'field' } ,
{ key : 'remark' , prop : 'remark' , label : '备注' , minWidth : 180 , slotName : 'field' } ,
{ key : 'internalRemark' , prop : 'internalRemark' , label : '客服内部备注' , minWidth : 180 , slotName : 'field' } ,
{ key : 'createdBy' , prop : 'createdBy' , label : '创建人' , minWidth : 120 , slotName : 'field' } ,
{ key : 'createdAt' , prop : 'createdAt' , label : '创建日期' , minWidth : 150 , slotName : 'field' } ,
]
function emptyFilters ( ) : FilterModel {
@@ -211,10 +269,42 @@ function formatNumber(value: number | null | undefined, digits = 2) {
return value === null || value === undefined ? '—' : value . toLocaleString ( 'zh-CN' , { maximumFractionDigits : digits } )
}
function displayField ( row : UnifiedOrderResource , property : string | undefined ) {
if ( ! property ) return '—'
const source = row as unknown as Record < string , unknown >
const nested : Record < string , string > = {
measurementReviewStatus : 'measurement.reviewStatus' , expectedArrivalAt : 'expectedArrivalAt' ,
loadingStatus : 'transport.loadingStatus' , containerNo : 'transport.containerNo' , cabinetNo : 'transport.cabinetNo' ,
sailingAt : 'transport.sailingAt' , portArrivalAt : 'transport.portArrivalAt' ,
chargeableWeightKg : 'measurement.chargeableWeightKg' , measurementCompletedAt : 'measurement.completedAt' ,
totalGrossWeightKg : 'measurement.actualGrossWeightKg' , totalVolumeCbm : 'measurement.actualVolumeCbm' ,
expressTrackingNo : 'delivery.trackingNo' ,
}
const path = nested [ property ] || property
let value = path . split ( '.' ) . reduce < unknown > ( ( current , key ) => (
current && typeof current === 'object' ? ( current as Record < string , unknown > ) [ key ] : undefined
) , source )
if ( ( value === null || value === undefined || value === '' ) && property === 'internalOrderStatus' ) value = source . mainStatus
if ( value === null || value === undefined || value === '' ) return '—'
if ( typeof value === 'boolean' ) return value ? '是' : '否'
if ( Array . isArray ( value ) ) return value . length ? ` ${ value . length } 个文件 ` : '—'
if ( typeof value === 'number' ) return formatNumber ( value )
if ( property . endsWith ( 'At' ) || property . endsWith ( 'Date' ) ) return formatDate ( String ( value ) )
if ( property === 'measurementReviewStatus' ) return measurementStatusLabel ( String ( value ) )
if ( property === 'internalOrderStatus' ) {
return ( { SUBMITTED : '待审核' , APPROVED : '已审核' , ACCEPTED : '已受理' , PROCESSING : '处理中' , COMPLETED : '已完成' , CANCELLED : '已取消' } as Record < string , string > ) [ String ( value ) ] || String ( value )
}
return String ( value )
}
function statusLabel ( value : UnifiedOrderMainStatus | string | null ) {
return unifiedOrderStatusOptions . find ( ( item ) => item . value === value ) ? . label || value || '状态更新'
}
function measurementStatusLabel ( value : string | null | undefined ) {
return ( { UNRECORDED : '未录入' , PENDING : '待审核' , APPROVED : '审核通过' , REJECTED : '审核驳回' } as Record < string , string > ) [ value || '' ] || value || '未录入'
}
const nextActionLabels : Record < string , string > = {
APPROVE _ORDER : '审核通过' ,
CONFIRM _MEASUREMENT : '完成测量' ,
@@ -234,6 +324,19 @@ function canApprove(row: UnifiedOrderResource) {
return row . mainStatus === 'PENDING_REVIEW' && row . nextAction === 'APPROVE_ORDER' && can ( 'platform:order:approve' )
}
function canBulkReview ( row : UnifiedOrderResource ) {
return canApprove ( row )
}
function handleSelectionChange ( rows : UnifiedOrderResource [ ] ) {
selectedOrders . value = rows
}
function openBulkReviewPlaceholder ( ) {
if ( selectedOrders . value . length === 0 ) return
ElMessage . info ( '批量审核接口待接入,请先逐笔审核' )
}
async function loadCarriers ( ) {
carrierLoading . value = true
carrierLoadError . value = null
@@ -304,6 +407,31 @@ async function assignCarrierAndApprove() {
}
}
function openNoteDialog ( ) {
if ( ! detail . value || ! can ( 'platform:order:note' ) ) return
noteContent . value = ''
noteOpen . value = true
}
async function saveNote ( ) {
const content = noteContent . value . trim ( )
if ( ! detail . value || ! content ) {
ElMessage . warning ( '请填写内部备注' )
return
}
noteSaving . value = true
try {
await addPlatformUnifiedOrderNote ( String ( detail . value . summary . tenantId ) , String ( detail . value . summary . orderId ) , content )
noteOpen . value = false
await loadDetail ( )
ElMessage . success ( '内部备注已保存' )
} catch ( error ) {
ElMessage . error ( getApiErrorDetails ( error , '内部备注保存失败,请稍后重试' ) . message )
} finally {
noteSaving . value = false
}
}
async function loadCustomers ( ) {
customerLoading . value = true
customerLoadError . value = null
@@ -369,6 +497,7 @@ async function loadOrders() {
size : pageSize . value ,
} )
records . value = result . records
selectedOrders . value = [ ]
total . value = result . total
} catch ( error ) {
records . value = [ ]
@@ -393,8 +522,10 @@ function reset() {
void loadOrders ( )
}
function openDetail ( row : UnifiedOrderResource ) {
void router . push ( { name : 'unified-order-detail' , params : { orderId : String ( row . orderId ) } , query : { tenantId : String ( row . tenantId ) } } )
async function openDetail ( row : UnifiedOrderResource ) {
await router . push ( { name : 'unified-order-detail' , params : { orderId : String ( row . orderId ) } , query : { tenantId : String ( row . tenantId ) } } )
await nextTick ( )
window . scrollTo ( { top : 0 } )
}
function openMeasurement ( row : UnifiedOrderResource ) {
@@ -416,7 +547,6 @@ function openWorkflow(row: UnifiedOrderResource) {
}
function closeDetail ( ) {
detailOpen . value = false
detail . value = null
detailError . value = null
void router . push ( { name : 'unified-orders' } )
@@ -427,12 +557,10 @@ async function loadDetail() {
const tenantId = typeof route . query . tenantId === 'string' ? route . query . tenantId : ''
if ( ! tenantId ) {
detailError . value = { message : '订单详情缺少客户标识,请从订单列表重新打开' }
detailOpen . value = true
return
}
detailLoading . value = true
detailError . value = null
detailOpen . value = true
try {
detail . value = await getPlatformUnifiedOrder ( tenantId , orderId . value )
} catch ( error ) {
@@ -445,8 +573,10 @@ async function loadDetail() {
watch ( [ page , pageSize ] , ( ) => { void loadOrders ( ) } )
watch ( orderId , ( value ) => {
if ( value ) void loadDetail ( )
else detailOpen . value = false
if ( value ) {
activeDetailTab . value = 'basic'
void loadDetail ( )
}
} , { immediate : true } )
onMounted ( ( ) => {
void loadCustomers ( )
@@ -457,35 +587,132 @@ onMounted(() => {
< template >
< section class = "unified-orders-page" >
< div class = "status-tabs" role = "tablist" aria -label = " 订单主状态 " >
< button
v-for = "(tab, index) in statusTabs"
: key = "tab.value || 'ALL'"
type = "button"
role = "tab"
: aria -selected = " filters.mainStatus = = = tab.value "
aria -controls = " unified -orders -results "
: tabindex = "filters.mainStatus === tab.value ? 0 : -1"
: class = "['status-tab', { 'is-active': filters.mainStatus === tab.value }]"
@click ="selectStatus(tab.value)"
@keydown ="handleTabKeydown($event, index)"
> { { tab . label } } < / button >
< / div >
< template v-if = "orderId" >
< header class = "order-detail-header" >
< div class = "order-detail-heading" >
< el-button class = "back-button" @click ="closeDetail" > < ArrowLeft :size = "17" / > 返回订单列表 < / el-button >
< span class = "order-mark" aria -hidden = " true " > < Box :size = "23" / > < / span >
< div class = "order-title" >
< span > 入仓码 < / span >
< h1 > { { detail ? . summary . inboundCode || '暂未生成' } } < / h1 >
< p > { { detail ? . summary . orderNo || ` 订单 ${ orderId } ` } } < / p >
< / div >
< StatusBadge v-if = "detail" :status="statusLabel(detail.summary.mainStatus)" / >
< / div >
< div v-if = "detail" class="detail-header-actions" >
< el -button v-if = "canApprove(detail.summary)" type="success" plain :loading="approvalLoadingId === String(detail.summary.orderId)" @click="openApproval(detail.summary)"><CircleCheck :size="15" / > 审核通过 < / el-button >
< el-button v-if = "workflowEligible(detail.summary) && can('platform:firstleg:inbound:view')" type="primary" @click="openWorkflow(detail.summary)"><ClipboardList :size="15" / > 后半程作业 < / el-button >
< / div >
< / header >
< QueryToolbar id = "unified-orders-advanced-filters" v-model = "filters" ariaLabel="订单列表筛选" :fields="queryFields" :search-on-reset="false" @search="search" @reset="reset ">
< template # actions >
< el-button :aria-expanded = "showAdvancedFilters" aria -controls = " unified -orders -advanced -filters " @click ="showAdvancedFilters = !showAdvancedFilters" >
< SlidersHorizontal :size = "16" / > { { showAdvancedFilters ? '收起筛选' : '高级筛选' } }
< ChevronUp v-if = "showAdvancedFilters" :size="15" /><ChevronDown v-else :size="15" / >
< / el-button >
< / template >
< / QueryToolbar >
< LoadErrorState v-if = "detailError" class="detail-load-error" title="订单详情暂时无法加载" :message="detailError.message" :loading="detailLoading" @retry="loadDetail " >
< template # details > < ApiErrorMeta :details = "detailError" / > < / template >
< / LoadErrorState >
< div v-else v-loading = "detailLoading" class="order-detail-content" >
< template v-if = "detail" >
< section class = "detail-overview" aria -label = " 订单概览 " >
< article > < span > 客户 < / span > < strong > { { customerName ( detail . summary ) } } < / strong > < small > { { detail . summary . channelName || '未配置渠道' } } < / small > < / article >
< article > < span > 总箱数 < / span > < strong > { { detail . summary . totalPackageCount ? ? '—' } } < small > 箱 < / small > < / strong > < small > { { detail . order . cargoes . length } } 条货品明细 < / small > < / article >
< article > < span > 实测毛重 < / span > < strong > { { formatNumber ( detail . summary . measurement ? . actualGrossWeightKg ) } } < small > kg < / small > < / strong > < small > { { measurementStatusLabel ( detail . summary . measurement ? . reviewStatus ) } } < / small > < / article >
< article > < span > 实测体积 < / span > < strong > { { formatNumber ( detail . summary . measurement ? . actualVolumeCbm , 3 ) } } < small > m³ < / small > < / strong > < small > 最近更新 { { formatDate ( detail . summary . updatedAt ) } } < / small > < / article >
< / section >
< div class = "detail-tabs" role = "tablist" aria -label = " 订单详情模块 " >
< button v-for = "tab in detailTabs" :id="`order-detail-tab-${tab.key}`" :key="tab.key" type="button" role="tab" :class="{ active: activeDetailTab === tab.key }" :aria-selected="activeDetailTab === tab.key" :aria-controls="`order-detail-panel-${tab.key}`" :tabindex="activeDetailTab === tab.key ? 0 : -1" @click="activeDetailTab = tab.key"><component :is="tab.icon" :size="17" / > < span > { { tab . label } } < / span > < / button >
< / div >
< section :id = "`order-detail-panel-${activeDetailTab}`" class = "detail-panel" role = "tabpanel" :aria-labelledby = "`order-detail-tab-${activeDetailTab}`" tabindex = "0" >
< template v-if = "activeDetailTab === 'basic'" >
< div class = "panel-title" > < div > < h2 > 订单基本信息 < / h2 > < p > { { detail . summary . orderNo || ` 订单 ${ detail . summary . orderId } ` } } < / p > < / div > < Box :size = "19" / > < / div >
< dl class = "order-info-grid" >
< div > < dt > 公司 < / dt > < dd > { { customerName ( detail . summary ) } } < / 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 > { { detail . summary . receivingWarehouseName || '—' } } < / 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 . summary . channelName || '—' } } < / dd > < / div >
< div > < dt > 承运商 < / dt > < dd > { { detail . order . order ? . carrierName || '待平台指派' } } < / dd > < / div >
< div > < dt > 预计出运 < / dt > < dd > { { formatDate ( detail . summary . expectedShipDate ) } } < / dd > < / div >
< div > < dt > 集装箱号 < / dt > < dd > { { detail . summary . transport ? . containerNo || '—' } } < / 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 ? . portArrivalAt ) } } < / dd > < / div >
< div > < dt > 下一步 < / dt > < dd > { { canApprove ( detail . summary ) ? '选择承运商并审核' : nextActionBoundary ( detail . summary ) || '—' } } < / dd > < / div >
< div > < dt > 创建时间 < / dt > < dd > { { formatDate ( detail . summary . createdAt ) } } < / dd > < / div >
< / dl >
< / template >
< template v-else-if = "activeDetailTab === 'cargo'" >
< div class = "panel-title" > < div > < h2 > 货品信息 < / h2 > < p > 共 { { detail . order . cargoes . length } } 条 < / p > < / div > < PackageOpen :size = "19" / > < / div >
< div class = "detail-table-wrap" >
< el-table :data = "detail.order.cargoes" empty -text = " 暂无货品信息 " >
< el-table-column prop = "itemNo" label = "#" width = "64" / > < el-table-column prop = "cargoName" label = "品名" min -width = " 170 " / > < el-table-column prop = "packageType" label = "包装" min -width = " 110 " / > < el-table-column prop = "packageCount" label = "箱数" width = "90" / > < el-table-column prop = "grossWeightKg" label = "毛重(kg)" min -width = " 110 " / > < el-table-column prop = "volumeCbm" label = "体积(m³)" min -width = " 110 " / >
< / el-table >
< / div >
< / template >
< template v-else-if = "activeDetailTab === 'fees'" >
< div class = "panel-title" > < div > < h2 > 费用信息 < / h2 > < p > 订单费用与审核结果 < / p > < / div > < WalletCards :size = "19" / > < / div > < el-empty description = "暂无费用数据" :image-size = "64" / >
< / template >
< template v-else-if = "activeDetailTab === 'history'" >
< 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" / >
< 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 >
< / template >
< template v-else-if = "activeDetailTab === 'measurement'" >
< div class = "panel-title" > < div > < h2 > 测量数据 < / h2 > < p > { { detail . summary . measurement ? . completedAt ? ` 完成于 ${ formatDate ( detail . summary . measurement . completedAt ) } ` : '尚未完成测量' } } < / p > < / div > < Ruler :size = "19" / > < / div >
< dl class = "measurement-grid" > < div > < dt > 审核状态 < / dt > < dd > { { measurementStatusLabel ( detail . summary . measurement ? . reviewStatus ) } } < / dd > < / div > < div > < dt > 实测毛重 < / dt > < dd > { { formatNumber ( detail . summary . measurement ? . actualGrossWeightKg ) } } kg < / dd > < / div > < div > < dt > 实测体积 < / dt > < dd > { { formatNumber ( detail . summary . measurement ? . actualVolumeCbm , 3 ) } } m³ < / dd > < / div > < div > < dt > 计费重量 < / dt > < dd > { { formatNumber ( detail . summary . measurement ? . chargeableWeightKg ) } } kg < / dd > < / div > < / dl >
< / template >
< template v-else >
< div class = "panel-title" > < div > < h2 > 内部备注 < / h2 > < p > 仅平台内部可见 < / p > < / div > < el-button v-if = "can('platform:order:note')" type="primary" size="small" @click="openNoteDialog"><StickyNote :size="15" / > 添加备注 < / el-button > < / div >
< el-empty v-if = "detail.order.internalNotes.length === 0" description="暂无内部备注" :image-size="64" / >
< ul v-else class = "notes-list" aria -label = " 内部备注 " > < li v-for = "item in detail.order.internalNotes" :key="String(item.id)" > < p > { { item . content } } < / p > < span > 操作人 { { item . createdBy || '系统' } } · { { formatDate ( item . createdAt ) } } < / span > < / li > < / ul >
< / template >
< / section >
< / template >
< / div >
< / template >
< template v-else >
< div class = "orders-query-panel" >
< div class = "status-tabs" role = "tablist" aria -label = " 订单主状态 " >
< button
v-for = "(tab, index) in statusTabs"
: key = "tab.value || 'ALL'"
type = "button"
role = "tab"
: aria -selected = " filters.mainStatus = = = tab.value "
aria -controls = " unified -orders -results "
: tabindex = "filters.mainStatus === tab.value ? 0 : -1"
: class = "['status-tab', { 'is-active': filters.mainStatus === tab.value }]"
@click ="selectStatus(tab.value)"
@keydown ="handleTabKeydown($event, index)"
> { { tab . label } } < / button >
< / div >
< QueryToolbar id = "unified-orders-advanced-filters" v-model = "filters" ariaLabel="订单列表筛选" :fields="queryFields" :available-fields="allQueryFields" preference-key="unified-orders-query" :search-on-reset="false" @search="search" @reset="reset" >
< template # actions >
< el-button :aria-expanded = "showAdvancedFilters" aria -controls = " unified -orders -advanced -filters " @click ="showAdvancedFilters = !showAdvancedFilters" >
< SlidersHorizontal :size = "16" / > { { showAdvancedFilters ? '收起筛选' : '高级筛选' } }
< ChevronUp v-if = "showAdvancedFilters" :size="15" /><ChevronDown v-else :size="15" / >
< / el-button >
< / template >
< / QueryToolbar >
< / div >
< LoadErrorState v-if = "loadError" class="orders-load-error" title="订单列表暂时无法加载" :message="loadError.message" :loading="loading" @retry="loadOrders" >
< template # details > < ApiErrorMeta :details = "loadError" / > < / template >
< / LoadErrorState >
< div v-else id = "unified-orders-results" class = "table-card" role = "tabpanel" >
< DataTable v -model :page = "page" v -model :page-size = "pageSize" :data = "records" :columns = "columns" :loading = "loading" :total = "total" : page -sizes = " [ 20 , 50 , 100 ] " row -key = " orderId " empty -text = " 暂无符合条件的订单 " aria -label = " 统一订单列表 " >
< DataTable v -model :page = "page" v -model :page-size = "pageSize" :data = "records" :columns = "columns" preference -key = " unified -orders -table " :loading = "loading" :total = "total" : page -sizes = " [ 20 , 50 , 100 ] " row -key = " orderId " empty -text = " 暂无符合条件的订单 " aria -label = " 统一订单列表 " @ selection -change = " handleSelectionChange " >
< template # toolbar -start >
< el-button type = "primary" plain : disabled = "selectedOrders.length === 0" title = "批量审核接口待接入" @click ="openBulkReviewPlaceholder" > < ListChecks :size = "16" / > 批量审核 < span v-if = "selectedOrders.length" > ( {{ selectedOrders.length }} ) < / span > < / el -button >
< / template >
< template # actions = "{ row }" >
< div class = "row-actions" >
< el-button link type = "primary" @click ="openDetail(row as UnifiedOrderResource)" > < Eye :size = "15" / > 详情 < / el-button >
@@ -495,6 +722,7 @@ onMounted(() => {
< span v-if = "nextActionBoundary(row as UnifiedOrderResource)" class="action-boundary" > {{ nextActionBoundary ( row as UnifiedOrderResource ) }} < / span >
< / div >
< / template >
< template # field = "{ row, column }" > { { displayField ( row as UnifiedOrderResource , column . property ) } } < / template >
< template # order = "{ row }" > < div class = "primary-cell" > < strong > { { ( row as UnifiedOrderResource ) . orderNo || ` 订单 ${ ( row as UnifiedOrderResource ) . orderId } ` } } < / strong > < span v-if = "(row as UnifiedOrderResource).inboundCode" > 入仓码 {{ ( row as UnifiedOrderResource ) .inboundCode }} < / span > < / div > < / template >
< template # mainStatus = "{ row }" > < StatusBadge : status = "statusLabel((row as UnifiedOrderResource).mainStatus)" / > < / template >
< template # customer = "{ row }" > < div class = "primary-cell" > < strong > { { customerName ( row as UnifiedOrderResource ) } } < / strong > < span > 客户 ID { { ( row as UnifiedOrderResource ) . tenantId } } < / span > < / div > < / template >
@@ -507,17 +735,11 @@ onMounted(() => {
< / DataTable >
< / div >
< AppDrawer v-model = "detailOpen" :title="detail ? `订单详情 · ${detail.summary.orderNo || detail.summary.orderId}` : '订单详情'" size="min(760px, 94vw)" :loading="detailLoading" :show-footer="false" destroy-on-close @closed="closeDetail" >
< LoadErrorState v-if = "detailError" title="订单详情暂时无法加载" :message="detailError.message" :loading="detailLoading" @retry="loadDetail"><template #details><ApiErrorMeta :details="detailError" / > < / template > < / LoadErrorState >
< div v-else-if = "detail" class="detail-content " >
< section class = "detail-summary" aria -label = " 订单概览 " >
< div class = "detail-summary__header" > < div > < span class = "eyebrow" > 订单主状态 < / span > < h2 > { { detail . summary . orderNo || ` 订单 ${ detail . summary . orderId } ` } } < / h2 > < / div > < div class = "detail-summary__actions" > < StatusBadge :status = "statusLabel(detail.summary.mainStatus)" / > < el-button v-if = "canApprove(detail.summary)" type="success" plain :loading="approvalLoadingId === String(detail.summary.orderId)" @click="openApproval(detail.summary)"><CircleCheck :size="15" />审核通过</el-button><el-button v-if="workflowEligible(detail.summary) && can('platform:firstleg:inbound:view')" type="primary" plain @click="openWorkflow(detail.summary)"><ClipboardList :size="15" / > 后半程作业 < / el-button > < / div > < / div >
< dl class = "detail-grid" > < div > < dt > 客户 < / dt > < dd > { { customerName ( detail . summary ) } } < / dd > < / div > < div > < dt > 入仓码 < / dt > < dd > { { detail . summary . inboundCode || '—' } } < / dd > < / div > < div > < dt > 平台收货仓 < / dt > < dd > { { detail . summary . receivingWarehouseName || '—' } } < / dd > < / div > < div > < dt > 目的仓 < / dt > < dd > { { detail . summary . destinationWarehouseName || '—' } } < / dd > < / div > < div > < dt > 承运商 < / dt > < dd > { { detail . order . order ? . carrierName || '待平台指派' } } < / dd > < / div > < div > < dt > 下一步 < / dt > < dd > { { canApprove ( detail . summary ) ? '选择承运商并审核' : nextActionBoundary ( detail . summary ) || '—' } } < / dd > < / div > < div > < dt > 最近更新 < / dt > < dd > { { formatDate ( detail . summary . updatedAt ) } } < / dd > < / div > < / dl >
< / section >
< section class = "detail-section" aria -label = " 状态历史 " > < h3 > 状态历史 < / h3 > < el-empty v-if = "detail.timeline.length === 0" description="暂无状态历史" :image-size="56" /><ol v-else class="history-list"><li v-for="(item, index) in detail.timeline" :key="`${item.source || 'event'}-${item.occurredAt || index}`"><StatusBadge :status="statusLabel(item.mainStatus)" /><div><strong>{{ item.action || item.eventType || '状态更新' }}</strong><p>{{ item.operatorName || '系统' }} · {{ formatDate(item.occurredAt) }}</p><p v-if="item.remark" > {{ item.remark }} < / p > < / div > < / li > < / ol > < / section >
< section class = "detail-section" aria -label = " 内部备注 " > < h3 > 内部备注 < / h3 > < el-empty v-if = "detail.order.internalNotes.length === 0" description="暂无内部备注" :image-size="56" /><ul v-else class="notes-list"><li v-for="item in detail.order.internalNotes" :key="String(item.id)" > < p > { { item . content } } < / p > < span > 操作人 { { item . createdBy || '系统' } } · { { formatDate ( item . createdAt ) } } < / span > < / li > < / ul > < / section >
< / div >
< / AppDrawer >
< / template >
< AppDialog v-model = "noteOpen" title="添加内部备注" confirm-text="保存备注" :confirm-loading="noteSaving" @confirm="saveNote " >
< el -input v-model = "noteContent" type="textarea" :rows="5" maxlength="2000" show-word-limit placeholder="请输入仅平台内部可见的备注" aria-label="内部备注内容" / >
< / 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" >
< el -alert title = "订单审核通过后将进入履约流程;承运商名称由后端公共主数据生成快照。" type = "info" :closable = "false" show -icon / >
@@ -543,6 +765,7 @@ onMounted(() => {
. page - intro h1 { margin : 0 ; color : var ( -- cc - text ) ; font - size : 22 px ; line - height : 1.35 ; }
. page - intro p { margin : 5 px 0 0 ; color : var ( -- cc - muted ) ; font - size : 13 px ; line - height : 1.6 ; }
. page - intro : deep ( . el - button ) { flex : 0 0 auto ; }
. orders - query - panel { overflow : hidden ; border : 1 px solid var ( -- cc - border ) ; border - radius : 8 px ; background : # fff ; }
. status - tabs { display : flex ; max - width : 100 % ; overflow - x : auto ; margin - bottom : 0 ; border - bottom : 1 px solid var ( -- cc - border ) ; scrollbar - width : thin ; }
. status - tab { position : relative ; flex : 0 0 auto ; min - height : 44 px ; padding : 0 18 px ; border : 0 ; background : transparent ; color : var ( -- cc - muted ) ; font : inherit ; font - size : 14 px ; cursor : pointer ; }
. status - tab : : after { position : absolute ; right : 12 px ; bottom : 0 ; left : 12 px ; height : 3 px ; background : transparent ; content : '' ; }
@@ -550,7 +773,7 @@ onMounted(() => {
. status - tab : focus - visible { outline : 2 px solid var ( -- cc - primary ) ; outline - offset : - 2 px ; }
. status - tab . is - active { color : var ( -- cc - primary ) ; font - weight : 600 ; }
. status - tab . is - active : : after { background : var ( -- cc - primary ) ; }
. status - tabs + : deep ( . query - toolbar ) { border - top : 0 ; }
. orders - query - panel : deep ( . query - toolbar ) { border : 0 ; }
. orders - load - error { margin - top : 12 px ; }
. table - card { margin - top : 16 px ; overflow : hidden ; border : 1 px solid var ( -- cc - border ) ; border - radius : 8 px ; background : # fff ; }
. row - actions { display : flex ; align - items : center ; gap : 4 px ; white - space : nowrap ; }
@@ -561,24 +784,57 @@ onMounted(() => {
. primary - cell { display : grid ; min - width : 0 ; gap : 4 px ; }
. primary - cell strong { overflow - wrap : anywhere ; }
. primary - cell span { color : var ( -- cc - muted ) ; font - size : 12 px ; overflow - wrap : anywhere ; }
. detail - content { display : grid ; gap : 22 px ; min - width : 0 ; }
. detail - summary , . detail - section { padding : 16 px ; border : 1 px solid var ( -- cc - border ) ; border - radius : 8 px ; background : # fff ; }
. detail - summary _ _header { display : flex ; align - items : flex - start ; justify - content : space - between ; gap : 12 px ; }
. detail - summary _ _actions { display : flex ; align - items : center ; flex - wrap : wrap ; justify - content : flex - end ; gap : 8 px ; }
. eyebrow { color : var ( -- cc - muted ) ; font - size : 12 px ; }
. detail - summary h2 { margin : 4 px 0 0 ; color : var ( -- cc - text ) ; font - size : 18 px ; }
. detail - grid { display : grid ; grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr ) ) ; gap : 14 px 20 px ; margin : 20 px 0 0 ; }
. detail - grid div { min - width : 0 ; }
. detail - grid dt { color : var ( -- cc - muted ) ; font - size : 12 px ; }
. detail - grid dd { margin : 4 px 0 0 ; color : var ( -- cc - text ) ; overflow - wrap : anywhere ; }
. detail - section h3 { margin : 0 0 12 px ; color : var ( -- cc - text ) ; font - size : 15 px ; }
. history - list , . notes - list { display : grid ; gap : 12 px ; margin : 0 ; padding : 0 ; list - style : none ; }
. history - list li { display : flex ; align - items : flex - start ; gap : 10 px ; padding - bottom : 12 px ; border - bottom : 1 px solid # edf0f4 ; }
. history - list li : last - child , . notes - list li : last - child { padding - bottom : 0 ; border - bottom : 0 ; }
. history - list strong { font - size : 13 px ; }
. history - list p , . notes - list p { margin : 3 px 0 0 ; color : var ( -- cc - muted ) ; font - size : 12 px ; line - height : 1.5 ; overflow - wrap : anywhere ; }
. notes - list li { padding - bottom : 12 px ; border - bottom : 1 px solid # edf0f4 ; }
. order - detail - header { display : flex ; min - width : 0 ; align - items : center ; justify - content : space - between ; gap : 20 px ; margin - bottom : 18 px ; }
. order - detail - heading { display : flex ; min - width : 0 ; align - items : center ; gap : 12 px ; }
. back - button { flex : 0 0 auto ; }
. order - mark { display : grid ; width : 44 px ; height : 44 px ; flex : 0 0 44 px ; place - items : center ; border - radius : 8 px ; background : # 1 d2939 ; color : # fff ; }
. order - title { min - width : 0 ; }
. order - title > span { display : block ; margin - bottom : 2 px ; color : var ( -- cc - muted ) ; font - size : 11 px ; }
. order - title h1 { margin : 0 ; overflow : hidden ; color : # 172033 ; font - size : 22 px ; line - height : 1.25 ; text - overflow : ellipsis ; white - space : nowrap ; }
. order - title p { margin : 4 px 0 0 ; color : var ( -- cc - muted ) ; font - size : 12 px ; }
. detail - header - actions { display : flex ; flex : 0 0 auto ; gap : 8 px ; }
. detail - header - actions : deep ( . el - button + . el - button ) { margin - left : 0 ; }
. detail - load - error { min - height : 280 px ; }
. order - detail - content { min - height : 520 px ; }
. detail - overview { display : grid ; grid - template - columns : repeat ( 4 , minmax ( 0 , 1 fr ) ) ; overflow : hidden ; margin - bottom : 16 px ; border : 1 px solid var ( -- cc - border ) ; border - radius : 8 px ; background : # fff ; }
. detail - overview article { min - width : 0 ; padding : 18 px 20 px ; border - right : 1 px solid var ( -- cc - border ) ; }
. detail - overview article : last - child { border - right : 0 ; }
. detail - overview article > span , . detail - overview article > small { display : block ; overflow : hidden ; color : var ( -- cc - muted ) ; font - size : 11 px ; text - overflow : ellipsis ; white - space : nowrap ; }
. detail - overview strong { display : block ; margin : 7 px 0 5 px ; overflow : hidden ; color : # 172033 ; font - size : 20 px ; line - height : 1.2 ; text - overflow : ellipsis ; white - space : nowrap ; }
. detail - overview strong small { margin - left : 5 px ; color : # 475467 ; font - size : 11 px ; }
. detail - tabs { display : grid ; grid - template - columns : repeat ( 6 , minmax ( 124 px , 1 fr ) ) ; overflow - x : auto ; border : 1 px solid var ( -- cc - border ) ; border - bottom : 0 ; border - radius : 8 px 8 px 0 0 ; background : # fff ; }
. detail - tabs button { position : relative ; display : flex ; min - height : 56 px ; align - items : center ; justify - content : center ; gap : 7 px ; padding : 0 14 px ; border : 0 ; border - right : 1 px solid # edf0f4 ; background : transparent ; color : # 667085 ; font - weight : 650 ; cursor : pointer ; }
. detail - tabs button : last - child { border - right : 0 ; }
. detail - tabs button : : after { position : absolute ; right : 14 px ; bottom : 0 ; left : 14 px ; height : 3 px ; background : transparent ; content : '' ; }
. detail - tabs button : hover { background : # fafbff ; color : # 344054 ; }
. detail - tabs button . active { background : # f7f8ff ; color : var ( -- cc - primary ) ; }
. detail - tabs button . active : : after { background : var ( -- cc - primary ) ; }
. detail - panel { min - width : 0 ; min - height : 390 px ; padding : 22 px ; border : 1 px solid var ( -- cc - border ) ; border - radius : 0 0 8 px 8 px ; background : # fff ; }
. panel - title { display : flex ; align - items : flex - start ; justify - content : space - between ; gap : 16 px ; margin - bottom : 18 px ; padding - bottom : 14 px ; border - bottom : 1 px solid var ( -- cc - border ) ; }
. panel - title h2 { margin : 0 ; color : # 344054 ; font - size : 16 px ; }
. panel - title p { margin : 5 px 0 0 ; color : var ( -- cc - muted ) ; font - size : 11 px ; }
. panel - title > svg { color : var ( -- cc - primary ) ; }
. order - info - grid { display : grid ; grid - template - columns : repeat ( 3 , minmax ( 0 , 1 fr ) ) ; gap : 1 px ; margin : 0 ; overflow : hidden ; border : 1 px solid # edf0f4 ; background : # edf0f4 ; }
. order - info - grid > div , . measurement - grid > div { min - width : 0 ; padding : 14 px 16 px ; background : # fff ; }
. order - info - grid dt , . measurement - grid dt { margin - bottom : 6 px ; color : var ( -- cc - muted ) ; font - size : 11 px ; }
. order - info - grid dd , . measurement - grid dd { margin : 0 ; color : # 25324 a ; font - size : 13 px ; font - weight : 650 ; line - height : 1.5 ; overflow - wrap : anywhere ; }
. detail - table - wrap { overflow - x : auto ; }
. detail - table - wrap : deep ( . el - table ) { min - width : 720 px ; -- el - table - header - bg - color : # f8fafc ; }
. measurement - grid { display : grid ; grid - template - columns : repeat ( 4 , minmax ( 0 , 1 fr ) ) ; gap : 1 px ; margin : 0 ; border : 1 px solid var ( -- cc - border ) ; background : var ( -- cc - border ) ; }
. history - list , . notes - list { display : grid ; gap : 0 ; margin : 0 ; padding : 0 ; list - style : none ; }
. history - list li { position : relative ; display : grid ; grid - template - columns : 16 px minmax ( 0 , 1 fr ) ; gap : 12 px ; padding - bottom : 20 px ; }
. history - list li : not ( : last - child ) : : after { position : absolute ; top : 15 px ; bottom : 0 ; left : 7 px ; width : 1 px ; background : var ( -- cc - border ) ; content : '' ; }
. history - marker { z - index : 1 ; width : 14 px ; height : 14 px ; margin - top : 3 px ; border : 3 px solid var ( -- cc - primary ) ; border - radius : 50 % ; background : # fff ; }
. history - line { display : flex ; align - items : center ; justify - content : space - between ; gap : 12 px ; }
. history - list strong { color : # 344054 ; font - size : 13 px ; }
. history - list p , . notes - list p { margin : 4 px 0 0 ; color : var ( -- cc - muted ) ; font - size : 12 px ; line - height : 1.55 ; overflow - wrap : anywhere ; }
. history - list . history - remark { margin - top : 8 px ; padding : 10 px 12 px ; background : # f8fafc ; color : # 475467 ; }
. notes - list li { padding : 14 px 0 ; border - bottom : 1 px solid # edf0f4 ; }
. notes - list li : first - child { padding - top : 0 ; }
. notes - list li : last - child { padding - bottom : 0 ; border - bottom : 0 ; }
. notes - list li > p { margin : 0 ; color : var ( -- cc - text ) ; font - size : 13 px ; }
. notes - list span { display : block ; margin - top : 5 px ; color : var ( -- cc - muted ) ; font - size : 12 px ; }
@ media ( max - width : 720 px ) { . page - intro { align - items : stretch ; flex - direction : column ; } . page - intro : deep ( . el - button ) { width : 100 % ; min - height : 44 px ; } . status - tab { min - height : 46 px ; padding : 0 14 px ; } . detail - grid { grid - template - columns : 1 fr ; } . detail - summary _ _header { flex - direction : column ; } . detail - summary _ _actions { justify - content : flex - start ; } . row - actions { flex - wrap : wrap ; } }
@ media ( max - width : 960 px ) { . detail - overview { grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr ) ) ; } . detail - overview article : nth - child ( 2 ) { border - right : 0 ; } . detail - overview article : nth - child ( - n + 2 ) { border - bottom : 1 px solid var ( -- cc - border ) ; } . order - info - grid { grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr ) ) ; } . measurement - grid { grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr ) ) ; } }
@ media ( max - width : 720 px ) { . page - intro { align - items : stretch ; flex - direction : column ; } . page - intro : deep ( . el - button ) { width : 100 % ; min - height : 44 px ; } . status - tab { min - height : 46 px ; padding : 0 14 px ; } . table - actions : deep ( . el - button ) { width : 100 % ; min - height : 40 px ; } . row - actions { flex - wrap : wrap ; } . order - detail - header { align - items : stretch ; flex - direction : column ; } . order - detail - heading { display : grid ; grid - template - columns : auto 40 px minmax ( 0 , 1 fr ) auto ; } . order - mark { width : 40 px ; height : 40 px ; } . detail - header - actions { display : grid ; grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr ) ) ; } . detail - header - actions : deep ( . el - button ) { min - height : 44 px ; } . detail - overview , . order - info - grid , . measurement - grid { grid - template - columns : 1 fr ; } . detail - overview article { border - right : 0 ; border - bottom : 1 px solid var ( -- cc - border ) ; } . detail - overview article : last - child { border - bottom : 0 ; } . detail - tabs { grid - template - columns : repeat ( 6 , 132 px ) ; } . detail - panel { padding : 16 px ; } }
@ media ( max - width : 420 px ) { . order - detail - heading { grid - template - columns : 1 fr auto ; } . back - button { grid - column : 1 / - 1 ; justify - self : start ; } . order - mark { display : none ; } . detail - header - actions { grid - template - columns : 1 fr ; } }
< / style >