@@ -1,6 +1,6 @@
< script setup lang = "ts" >
import { computed , onMounted , reactive , ref , watch } from 'vue'
import { ClipboardList , Eye , RefreshCw } from '@lucide/vue'
import { ChevronDown , ChevronUp , ClipboardList , Eye , RefreshCw , SlidersHorizontal } from '@lucide/vue'
import { useRoute , useRouter } from 'vue-router'
import {
getPlatformUnifiedOrder ,
@@ -12,6 +12,7 @@ import {
} from '@/modules/unified-orders/api/unifiedOrders'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
import { listPlatformTenants , type PlatformTenantResource } from '@/modules/iam'
import { listAllPlatformWarehouses , type PlatformWarehouseSummaryResource } from '@/modules/warehouses'
import { getApiErrorDetails , type ApiErrorDetails } from '@/shared/api/http'
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
import AppDrawer from '@/shared/components/AppDrawer.vue'
@@ -23,7 +24,22 @@ import StatusBadge from '@/shared/components/StatusBadge.vue'
type FilterModel = QueryModel & {
tenantId : string
keyword : string
orderNo : string
mainStatus : UnifiedOrderMainStatus | ''
receivingWarehouseId : string
destinationWarehouseId : string
inboundCode : string
internalOrderStatus : string
expectedArrivalDate : string
feeReviewStatus : string
channelName : string
containerNo : string
cabinetNo : string
sailingDate : string
loaded : string
fullContainer : string
measurementReviewStatus : string
fbaCode : string
createdRange : string [ ]
}
@@ -32,6 +48,7 @@ const router = useRouter()
const access = useAccessPreviewStore ( )
const records = ref < UnifiedOrderResource [ ] > ( [ ] )
const customers = ref < PlatformTenantResource [ ] > ( [ ] )
const warehouses = ref < PlatformWarehouseSummaryResource [ ] > ( [ ] )
const page = ref ( 1 )
const pageSize = ref ( 20 )
const total = ref ( 0 )
@@ -39,19 +56,36 @@ const loading = ref(false)
const loadError = ref < ApiErrorDetails | null > ( null )
const customerLoading = ref ( false )
const customerLoadError = ref < ApiErrorDetails | null > ( null )
const warehouseLoading = ref ( false )
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 filters = ref < FilterModel > ( { tenantId : '' , keyword : '' , mainStatus : '' , createdRange : [ ] } )
const showAdvancedFilters = ref ( false )
const filters = ref < FilterModel > ( emptyFilters ( ) )
const orderId = computed ( ( ) => typeof route . params . orderId === 'string' ? route . params . orderId : '' )
const queryFields = computed < QueryField [ ] > ( ( ) => [
const statusTabs = computed ( ( ) => [
{ value : '' as const , label : '全部' } ,
... unifiedOrderStatusOptions ,
] )
const warehouseOptions = computed ( ( ) => warehouses . value . map ( ( warehouse ) => ( {
value : String ( warehouse . id ) ,
label : ` ${ warehouse . warehouseName } ( ${ warehouse . warehouseCode } ) ` ,
} ) ) )
const receivingWarehouseOptions = computed ( ( ) => warehouses . value
. filter ( ( warehouse ) => warehouse . warehouseType === 'DOMESTIC' )
. map ( ( warehouse ) => ( { value : String ( warehouse . id ) , label : ` ${ warehouse . warehouseName } ( ${ warehouse . warehouseCode } ) ` } ) ) )
const primaryQueryFields = computed < QueryField [ ] > ( ( ) => [
{
key : 'tenantId' ,
type : 'select' ,
width : 220 ,
label : '公司' ,
placeholder : customerLoadError . value ? '客户列表加载失败' : customerLoading . value ? '正在加载客户' : '全部客户' ,
options : customers . value . map ( ( customer ) => ( {
value : String ( customer . id ) ,
@@ -59,18 +93,39 @@ const queryFields = computed<QueryField[]>(() => [
} ) ) ,
props : { 'aria-label' : '客户' , filterable : true , loading : customerLoading . value , disabled : Boolean ( customerLoadError . value ) } ,
} ,
{
key : 'mainStatus' ,
type : 'select' ,
width : 160 ,
placeholder : '全部主状态' ,
options : unifiedOrderStatusOptions ,
props : { 'aria-label' : '订单主状态' } ,
} ,
{ key : 'keyword' , type : 'input' , width : 260 , placeholder : '订单号、入仓码或客户名称' , props : { 'aria-label' : '订单关键词' } } ,
{ key : 'createdRange' , type : 'daterange' , width : 300 , placeholder : '创建日期范围' , props : { 'aria-label' : '创建日期范围' } } ,
{ key : 'keyword' , type : 'input' , width : 260 , label : '综合搜索' , placeholder : '订单号、入仓码或客户名称' , props : { 'aria-label' : '订单关键词' } } ,
{ key : 'orderNo' , type : 'input' , width : 220 , label : '订单号' , placeholder : '请输入订单号' , props : { 'aria-label' : '订单号' } } ,
{ key : 'createdRange' , type : 'daterange' , width : 300 , label : '创建日期' , placeholder : '创建日期范围' , props : { 'aria-label' : '创建日期范围' } } ,
] )
const advancedQueryFields = computed < QueryField [ ] > ( ( ) => [
{ key : 'receivingWarehouseId' , type : 'select' , width : 220 , label : '定位仓库' , placeholder : warehouseLoadError . value ? '仓库加载失败' : '请选择定位仓库' , options : receivingWarehouseOptions . value , props : { 'aria-label' : '定位仓库' , filterable : true , loading : warehouseLoading . value , disabled : Boolean ( warehouseLoadError . value ) } } ,
{ key : 'destinationWarehouseId' , type : 'select' , width : 220 , label : '交货仓' , placeholder : warehouseLoadError . value ? '仓库加载失败' : '请选择交货仓' , options : warehouseOptions . value , props : { 'aria-label' : '交货仓' , filterable : true , loading : warehouseLoading . value , disabled : Boolean ( warehouseLoadError . value ) } } ,
{ key : 'inboundCode' , type : 'input' , width : 220 , label : '入仓码' , placeholder : '请输入入仓码' , props : { 'aria-label' : '入仓码' } } ,
{ key : 'internalOrderStatus' , type : 'select' , width : 190 , label : '订单状态' , placeholder : '请选择订单状态' , options : [
{ value : 'DRAFT' , label : '草稿' } , { value : 'SUBMITTED' , label : '待审核' } , { value : 'APPROVED' , label : '已审核' } ,
{ value : 'ACCEPTED' , label : '已受理' } , { value : 'PROCESSING' , label : '处理中' } , { value : 'COMPLETED' , label : '已完成' } , { value : 'CANCELLED' , label : '已取消' } ,
] , props : { 'aria-label' : '订单状态' } } ,
{ key : 'expectedArrivalDate' , type : 'date' , width : 190 , label : '预计到达时间' , placeholder : '请选择预计到达日期' , props : { 'aria-label' : '预计到达时间' } } ,
{ key : 'feeReviewStatus' , type : 'select' , width : 190 , label : '费用审核状态' , placeholder : '请选择费用审核状态' , options : [
{ value : 'PENDING' , label : '待审核' } , { value : 'APPROVED' , label : '审核通过' } , { value : 'REJECTED' , label : '审核驳回' } ,
] , props : { 'aria-label' : '费用审核状态' } } ,
{ key : 'containerNo' , type : 'input' , width : 210 , label : '集装箱号' , placeholder : '请输入集装箱号' , props : { 'aria-label' : '集装箱号' } } ,
{ key : 'cabinetNo' , type : 'input' , width : 190 , label : '柜号' , placeholder : '请输入柜号' , props : { 'aria-label' : '柜号' } } ,
{ key : 'channelName' , type : 'input' , width : 210 , label : '渠道' , placeholder : '请输入渠道名称' , props : { 'aria-label' : '渠道' } } ,
{ key : 'sailingDate' , type : 'date' , width : 190 , label : '船期' , placeholder : '请选择船期' , props : { 'aria-label' : '船期' } } ,
{ key : 'loaded' , type : 'select' , width : 180 , label : '是否已配载' , placeholder : '请选择配载状态' , options : [ { value : 'true' , label : '已配载' } , { value : 'false' , label : '未配载' } ] , props : { 'aria-label' : '是否已配载' } } ,
{ key : 'fullContainer' , type : 'select' , width : 180 , label : '是否整柜' , placeholder : '请选择装柜方式' , options : [ { value : 'true' , label : '整柜' } , { value : 'false' , label : '拼箱' } ] , props : { 'aria-label' : '是否整柜' } } ,
{ key : 'measurementReviewStatus' , type : 'select' , width : 220 , label : '测量数据审核状态' , placeholder : '请选择测量审核状态' , options : [
{ value : 'PENDING' , label : '待审核' } , { value : 'APPROVED' , label : '审核通过' } , { value : 'REJECTED' , label : '审核驳回' } ,
] , props : { 'aria-label' : '测量数据审核状态' } } ,
{ key : 'fbaCode' , type : 'input' , width : 220 , label : '商品 FBA 编码' , placeholder : '请输入商品 FBA 编码' , props : { 'aria-label' : '商品 FBA 编码' } } ,
] )
const queryFields = computed ( ( ) => showAdvancedFilters . value
? [ ... primaryQueryFields . value , ... advancedQueryFields . value ]
: primaryQueryFields . value )
const columns : DataTableColumn [ ] = [
{ key : 'actions' , label : '操作' , width : 180 , fixed : 'left' , slotName : 'actions' } ,
{ key : 'order' , label : '订单信息' , minWidth : 220 , slotName : 'order' } ,
@@ -84,6 +139,43 @@ const columns: DataTableColumn[] = [
{ key : 'updatedAt' , label : '最近更新' , minWidth : 150 , slotName : 'updatedAt' } ,
]
function emptyFilters ( ) : FilterModel {
return {
tenantId : '' , keyword : '' , orderNo : '' , mainStatus : '' , receivingWarehouseId : '' , destinationWarehouseId : '' ,
inboundCode : '' , internalOrderStatus : '' , expectedArrivalDate : '' , feeReviewStatus : '' , channelName : '' ,
containerNo : '' , cabinetNo : '' , sailingDate : '' , loaded : '' , fullContainer : '' , measurementReviewStatus : '' ,
fbaCode : '' , createdRange : [ ] ,
}
}
function trimmed ( value : string ) {
return value . trim ( ) || undefined
}
function optionalBoolean ( value : string ) {
return value === '' ? undefined : value === 'true'
}
function selectStatus ( status : UnifiedOrderMainStatus | '' ) {
filters . value = { ... filters . value , mainStatus : status }
search ( )
}
function handleTabKeydown ( event : KeyboardEvent , index : number ) {
let nextIndex = index
if ( event . key === 'ArrowRight' ) nextIndex = ( index + 1 ) % statusTabs . value . length
else if ( event . key === 'ArrowLeft' ) nextIndex = ( index - 1 + statusTabs . value . length ) % statusTabs . value . length
else if ( event . key === 'Home' ) nextIndex = 0
else if ( event . key === 'End' ) nextIndex = statusTabs . value . length - 1
else return
event . preventDefault ( )
const tab = statusTabs . value [ nextIndex ]
if ( ! tab ) return
selectStatus ( tab . value )
const buttons = ( event . currentTarget as HTMLElement ) . parentElement ? . querySelectorAll < HTMLElement > ( '[role="tab"]' )
buttons ? . [ nextIndex ] ? . focus ( )
}
function can ( permission : string ) {
return access . can ( permission )
}
@@ -140,6 +232,19 @@ async function loadCustomers() {
}
}
async function loadWarehouses ( ) {
warehouseLoading . value = true
warehouseLoadError . value = null
try {
warehouses . value = await listAllPlatformWarehouses ( { status : 'ACTIVE' } )
} catch ( error ) {
warehouses . value = [ ]
warehouseLoadError . value = getApiErrorDetails ( error , '平台仓库加载失败,暂时无法按仓库筛选' )
} finally {
warehouseLoading . value = false
}
}
async function loadOrders ( ) {
loading . value = true
loadError . value = null
@@ -147,8 +252,23 @@ async function loadOrders() {
const range = filters . value . createdRange
const result = await listPlatformUnifiedOrders ( {
tenantId : filters . value . tenantId || undefined ,
keyword : filters . value . keyword . trim ( ) || undefined ,
keyword : trimmed ( filters . value . keyword ) ,
orderNo : trimmed ( filters . value . orderNo ) ,
mainStatus : filters . value . mainStatus || undefined ,
receivingWarehouseId : filters . value . receivingWarehouseId || undefined ,
destinationWarehouseId : filters . value . destinationWarehouseId || undefined ,
inboundCode : trimmed ( filters . value . inboundCode ) ,
internalOrderStatus : filters . value . internalOrderStatus || undefined ,
expectedArrivalDate : filters . value . expectedArrivalDate || undefined ,
feeReviewStatus : filters . value . feeReviewStatus || undefined ,
channelName : trimmed ( filters . value . channelName ) ,
containerNo : trimmed ( filters . value . containerNo ) ,
cabinetNo : trimmed ( filters . value . cabinetNo ) ,
sailingDate : filters . value . sailingDate || undefined ,
loaded : optionalBoolean ( filters . value . loaded ) ,
fullContainer : optionalBoolean ( filters . value . fullContainer ) ,
measurementReviewStatus : filters . value . measurementReviewStatus || undefined ,
fbaCode : trimmed ( filters . value . fbaCode ) ,
createdFrom : range . length === 2 ? ` ${ range [ 0 ] } T00:00:00 ` : undefined ,
createdTo : range . length === 2 ? ` ${ range [ 1 ] } T23:59:59 ` : undefined ,
page : page . value ,
@@ -174,7 +294,7 @@ function search() {
}
function reset ( ) {
filters . value = { tenantId : '' , keyword : '' , mainStatus : '' , createdRange : [ ] }
filters . value = emptyFilters ( )
page . value = 1
void loadOrders ( )
}
@@ -236,6 +356,7 @@ watch(orderId, (value) => {
} , { immediate : true } )
onMounted ( ( ) => {
void loadCustomers ( )
void loadWarehouses ( )
void loadOrders ( )
} )
< / script >
@@ -250,12 +371,34 @@ onMounted(() => {
< el-button :loading = "loading" @click ="loadOrders" > < RefreshCw :size = "16" / > 刷新 < / el-button >
< / div >
< QueryToolbar v-model = "filters" ariaLabel="订单列表筛选" :fields="queryFields" :search-on-reset="false" @search="search" @reset="reset" / >
< 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" :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 = "loadError" class="orders-load-error" title="订单列表暂时无法加载" :message="loadError.message" :loading="loading" @retry="loadOrders" >
< template # details > < ApiErrorMeta :details = "loadError" / > < / template >
< / LoadErrorState >
< div v-else class = "table-card ">
< 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 = " 统一订单列表 " >
< template # actions = "{ row }" >
< div class = "row-actions" >
@@ -297,6 +440,14 @@ 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 ; }
. 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 : '' ; }
. status - tab : hover { color : var ( -- cc - primary ) ; background : # f6f9fd ; }
. 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 - 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 ; }
@@ -323,5 +474,5 @@ onMounted(() => {
. notes - list li { padding - bottom : 12 px ; border - bottom : 1 px solid # edf0f4 ; }
. 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 ; } . 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 : 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 ; } }
< / style >