@@ -0,0 +1,305 @@
< script setup lang = "ts" >
import { computed , onBeforeUnmount , onMounted , ref , watch } from 'vue'
import { useRouter } from 'vue-router'
import { ClipboardCheck , Eye , Ruler , SearchCheck } from '@lucide/vue'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
import { listPlatformTenants , type PlatformTenantResource } from '@/modules/iam'
import { listPlatformWarehouses } from '@/modules/warehouses'
import { getApiErrorDetails , type ApiErrorDetails } from '@/shared/api/http'
import AdminPage from '@/shared/components/AdminPage.vue'
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
import DataTable , { type DataTableColumn } from '@/shared/components/DataTable.vue'
import ListPageLayout from '@/shared/components/ListPageLayout.vue'
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
import QueryToolbar , { type QueryField , type QueryModel } from '@/shared/components/QueryToolbar.vue'
import { listPlatformInbounds } from '../core/api'
import type { InboundOrderSummary } from '../core/types'
import InboundMeasurementPanel from '../measurement/InboundMeasurementPanel.vue'
import MeasurementReviewPanel from '../review/MeasurementReviewPanel.vue'
import { inboundProcessStatusLabels , inboundReviewStatusLabels , inboundStatusLabel } from '../status'
import { buildInboundTaskQuery , type InboundTaskMode } from '../task-list'
type FilterModel = QueryModel & {
customerId : string
inboundCode : string
orderNo : string
warehouseId : string
expectedArrivalRange : string [ ]
}
const props = defineProps < { mode : InboundTaskMode } > ( )
const router = useRouter ( )
const access = useAccessPreviewStore ( )
const page = ref ( 1 )
const pageSize = ref ( 20 )
const loading = ref ( false )
const loadError = ref < ApiErrorDetails | null > ( null )
const result = ref ( { records : [ ] as InboundOrderSummary [ ] , total : 0 } )
const filters = ref < FilterModel > ( { customerId : '' , inboundCode : '' , orderNo : '' , warehouseId : '' , expectedArrivalRange : [ ] } )
const appliedFilters = ref < FilterModel > ( { ... filters . value , expectedArrivalRange : [ ] } )
const customers = ref < PlatformTenantResource [ ] > ( [ ] )
const customerLoadError = ref < ApiErrorDetails | null > ( null )
const warehouseOptions = ref < Array < { label : string ; value : string } > > ( [ ] )
const selectedInbound = ref < InboundOrderSummary | null > ( null )
const taskDialogOpen = ref ( false )
let requestSerial = 0
let disposed = false
const isReview = computed ( ( ) => props . mode === 'review' )
const actionPermission = computed ( ( ) => isReview . value
? 'firstleg:inbound:measurement-review'
: 'platform:firstleg:inbound:measure:view' )
const taskLabel = computed ( ( ) => isReview . value ? '审核' : '测量' )
const emptyText = computed ( ( ) => isReview . value ? '暂无待审核的测量数据' : '暂无当前可测量的订单' )
const preferenceKey = computed ( ( ) => isReview . value ? 'inbound-measurement-reviews' : 'inbound-measurements' )
const queryFields = computed < QueryField [ ] > ( ( ) => [
{
key : 'customerId' ,
type : 'select' ,
label : '客户' ,
width : 260 ,
placeholder : customerLoadError . value ? '客户列表加载失败' : '全部客户' ,
options : customers . value . map ( ( customer ) => ( {
value : String ( customer . id ) ,
label : ` ${ customer . legalName ? . trim ( ) || customer . tenantName } ( ${ customer . tenantCode || '无客户编码' } ) ` ,
} ) ) ,
props : { filterable : true , disabled : Boolean ( customerLoadError . value ) } ,
} ,
{ key : 'warehouseId' , type : 'select' , label : '定位仓库' , width : 220 , placeholder : '全部仓库' , options : warehouseOptions . value , props : { filterable : true } } ,
{ key : 'inboundCode' , type : 'input' , label : '入仓码' , width : 220 , placeholder : '请输入入仓码' } ,
{ key : 'orderNo' , type : 'input' , label : '订单号' , width : 220 , placeholder : '请输入订单号' } ,
{ key : 'expectedArrivalRange' , type : 'daterange' , label : '预计到达时间' , width : 320 , placeholder : '请选择日期范围' } ,
] )
const columns : DataTableColumn [ ] = [
{ key : 'actions' , label : '操作' , width : 190 , fixed : 'left' , slotName : 'actions' } ,
{ key : 'inboundCode' , label : '入仓码' , minWidth : 170 , slotName : 'inboundCode' } ,
{ key : 'processStatus' , label : '流程状态' , minWidth : 132 , slotName : 'processStatus' } ,
{ key : 'measureReviewStatus' , label : '测量审核状态' , minWidth : 142 , slotName : 'measureReviewStatus' } ,
{ key : 'expectedArrivalAt' , label : '预计到达时间' , minWidth : 164 , slotName : 'expectedArrivalAt' } ,
{ key : 'customerName' , label : '客户' , minWidth : 160 , slotName : 'customerName' } ,
{ key : 'warehouseName' , label : '仓库' , minWidth : 140 , slotName : 'warehouseName' } ,
{ key : 'deliveryWarehouseName' , label : '交货仓' , minWidth : 140 , slotName : 'deliveryWarehouseName' } ,
{ key : 'businessType' , label : '业务类型' , minWidth : 120 , slotName : 'businessType' } ,
{ key : 'channelName' , label : '渠道' , minWidth : 140 , slotName : 'channelName' } ,
{ key : 'totalPackageCount' , label : '总箱数' , width : 92 , slotName : 'totalPackageCount' } ,
{ key : 'actualGrossWeightKg' , label : '实测毛重(kg)' , minWidth : 128 , slotName : 'actualGrossWeightKg' } ,
{ key : 'actualVolumeCbm' , label : '实测体积(m³)' , minWidth : 128 , slotName : 'actualVolumeCbm' } ,
{ key : 'measureCompletedAt' , label : '测量完成时间' , minWidth : 164 , slotName : 'measureCompletedAt' } ,
]
function text ( value : unknown ) {
return value === null || value === undefined || value === '' ? '—' : String ( value )
}
function formatNumber ( value : number | null | undefined ) {
return value === null || value === undefined ? '—' : value . toLocaleString ( 'zh-CN' , { maximumFractionDigits : 3 } )
}
function formatDate ( value : string | null | undefined ) {
if ( ! value ) return '—'
const date = new Date ( value )
return Number . isNaN ( date . getTime ( ) ) ? value : date . toLocaleString ( 'zh-CN' , { hour12 : false } )
}
function dateParam ( value : string | undefined , end = false ) {
return value ? ` ${ value } T ${ end ? '23:59:59' : '00:00:00' } ` : undefined
}
async function loadOptions ( ) {
customerLoadError . value = null
try {
const [ customerPage , warehousePage ] = await Promise . all ( [
listPlatformTenants ( { page : 1 , size : 200 } ) ,
listPlatformWarehouses ( 1 , 200 ) ,
] )
if ( disposed ) return
customers . value = customerPage . records
warehouseOptions . value = warehousePage . records . map ( ( warehouse ) => ( {
label : ` ${ warehouse . warehouseName } · ${ warehouse . warehouseCode } ` ,
value : String ( warehouse . id ) ,
} ) )
} catch ( error ) {
if ( disposed ) return
customerLoadError . value = getApiErrorDetails ( error , '客户或仓库筛选项加载失败,不影响查看全部待办' )
}
}
async function loadTasks ( ) {
const serial = ++ requestSerial
loading . value = true
loadError . value = null
const applied = appliedFilters . value
try {
const response = await listPlatformInbounds ( applied . customerId || undefined , buildInboundTaskQuery ( props . mode , {
page : page . value ,
size : pageSize . value ,
warehouseId : applied . warehouseId || undefined ,
inboundCode : applied . inboundCode . trim ( ) || undefined ,
orderNo : applied . orderNo . trim ( ) || undefined ,
expectedArrivalFrom : dateParam ( applied . expectedArrivalRange [ 0 ] ) ,
expectedArrivalTo : dateParam ( applied . expectedArrivalRange [ 1 ] , true ) ,
} ) )
if ( disposed || serial !== requestSerial ) return
result . value = { records : response . records , total : response . total }
} catch ( error ) {
if ( disposed || serial !== requestSerial ) return
loadError . value = getApiErrorDetails ( error , ` ${ taskLabel . value } 待办加载失败 ` )
} finally {
if ( ! disposed && serial === requestSerial ) loading . value = false
}
}
function applyFilters ( ) {
appliedFilters . value = { ... filters . value , expectedArrivalRange : [ ... filters . value . expectedArrivalRange ] }
if ( page . value === 1 ) void loadTasks ( )
else page . value = 1
}
function resetFilters ( ) {
filters . value = { customerId : '' , inboundCode : '' , orderNo : '' , warehouseId : '' , expectedArrivalRange : [ ] }
appliedFilters . value = { ... filters . value , expectedArrivalRange : [ ] }
if ( page . value === 1 ) void loadTasks ( )
else page . value = 1
}
function openTask ( row : InboundOrderSummary ) {
selectedInbound . value = row
taskDialogOpen . value = true
}
function openDetail ( row : InboundOrderSummary ) {
void router . push ( { name : 'inbound-order-detail' , params : { inboundId : row . id } , query : { tenantId : row . tenantId } } )
}
function handleTaskChanged ( ) {
taskDialogOpen . value = false
selectedInbound . value = null
void loadTasks ( )
}
watch ( [ page , pageSize ] , ( ) => { void loadTasks ( ) } )
onMounted ( ( ) => { void loadOptions ( ) ; void loadTasks ( ) } )
onBeforeUnmount ( ( ) => { disposed = true ; requestSerial += 1 } )
< / script >
< template >
< AdminPage as = "div" class = "inbound-task-page" >
< ListPageLayout :aria-label = "`${taskLabel}待办`" >
< template # toolbar >
< QueryToolbar
v-model = "filters"
class = "surface-panel"
:ariaLabel = "`${taskLabel}待办筛选`"
:fields = "queryFields"
:preference-key = "`${preferenceKey}-query`"
:search-on-reset = "false"
@search ="applyFilters"
@reset ="resetFilters"
/ >
< / template >
< template v-if = "customerLoadError || loadError" # state >
< div class = "state-stack" >
< el-alert v-if = "customerLoadError" type="warning" :closable="false" show -icon >
< template # title > < span > { { customerLoadError . message } } < / span > < / template >
< / el-alert >
< LoadErrorState v-if = "loadError" :title="`${taskLabel}待办暂时无法加载`" :message="loadError.message" @retry="loadTasks" >
< template # details > < ApiErrorMeta :details = "loadError" / > < / template >
< / LoadErrorState >
< / div >
< / template >
< section v-if = "!loadError" class="surface-panel table-panel" :aria-busy="loading" >
< DataTable
v -model :page = "page"
v -model :page-size = "pageSize"
:data = "result.records"
:columns = "columns"
:preference-key = "`${preferenceKey}-table`"
:loading = "loading"
:total = "result.total"
: page -sizes = " [ 20 , 50 , 100 ] "
row -key = " id "
:empty-text = "emptyText"
:aria-label = "`${taskLabel}待办列表`"
>
< template # pagination -left > 当前显示 { { result . records . length } } 条 · 共 { { result . total } } 条 < / template >
< template # actions = "{ row }" >
< div class = "row-actions" >
< el-button v-if = "access.can(actionPermission)" size="small" type="primary" @click="openTask(row as InboundOrderSummary)" >
< SearchCheck v-if = "isReview" :size="15" / >
< Ruler v-else :size = "15" / >
{ { taskLabel } }
< / el-button >
< el-button v-if = "access.can('platform:firstleg:inbound:view')" size="small" plain @click="openDetail(row as InboundOrderSummary)" >
< Eye :size = "15" / > 查看
< / el-button >
< span v-if = "!access.can(actionPermission) && !access.can('platform:firstleg:inbound:view')" class="muted-action" > 无操作权限 < / span >
< / div >
< / template >
< template # inboundCode = "{ row }" > < div class = "primary-cell" > < strong > { { text ( ( row as InboundOrderSummary ) . inboundCode ) } } < / strong > < span > 订单 { { text ( ( row as InboundOrderSummary ) . orderNo ) } } < / span > < / div > < / template >
< template # processStatus = "{ row }" > < el-tag effect = "light" > { { inboundStatusLabel ( ( row as InboundOrderSummary ) . processStatus , inboundProcessStatusLabels ) } } < / el-tag > < / template >
< template # measureReviewStatus = "{ row }" > { { inboundStatusLabel ( ( row as InboundOrderSummary ) . measureReviewStatus , inboundReviewStatusLabels ) } } < / template >
< template # expectedArrivalAt = "{ row }" > { { formatDate ( ( row as InboundOrderSummary ) . expectedArrivalAt ) } } < / template >
< template # customerName = "{ row }" > { { text ( ( row as InboundOrderSummary ) . customerName ) } } < / template >
< template # warehouseName = "{ row }" > { { text ( ( row as InboundOrderSummary ) . warehouseName ) } } < / template >
< template # deliveryWarehouseName = "{ row }" > { { text ( ( row as InboundOrderSummary ) . deliveryWarehouseName ) } } < / template >
< template # businessType = "{ row }" > { { text ( ( row as InboundOrderSummary ) . businessType ) } } < / template >
< template # channelName = "{ row }" > { { text ( ( row as InboundOrderSummary ) . channelName || ( row as InboundOrderSummary ) . channelCode ) } } < / template >
< template # totalPackageCount = "{ row }" > { { text ( ( row as InboundOrderSummary ) . totalPackageCount ) } } < / template >
< template # actualGrossWeightKg = "{ row }" > { { formatNumber ( ( row as InboundOrderSummary ) . actualGrossWeightKg ) } } < / template >
< template # actualVolumeCbm = "{ row }" > { { formatNumber ( ( row as InboundOrderSummary ) . actualVolumeCbm ) } } < / template >
< template # measureCompletedAt = "{ row }" > { { formatDate ( ( row as InboundOrderSummary ) . measureCompletedAt ) } } < / template >
< template # empty > < el-empty :description = "emptyText" / > < / template >
< / DataTable >
< / section >
< / ListPageLayout >
< el-dialog
v-model = "taskDialogOpen"
: title = "selectedInbound ? `${taskLabel} · ${selectedInbound.inboundCode || selectedInbound.id}` : taskLabel"
width = "min(1500px, calc(100vw - 32px))"
top = "4vh"
destroy -on -close
append -to -body
class = "inbound-task-dialog"
>
< MeasurementReviewPanel
v-if = "isReview && selectedInbound"
: customer -id = " selectedInbound.customerId | | selectedInbound.tenantId "
:inbound-id = "selectedInbound.id"
@changed ="handleTaskChanged"
/ >
< InboundMeasurementPanel
v-else-if = "selectedInbound"
: customer -id = " selectedInbound.customerId | | selectedInbound.tenantId "
:inbound-id = "selectedInbound.id"
@changed ="handleTaskChanged"
/ >
< / el-dialog >
< / AdminPage >
< / template >
< style scoped >
. inbound - task - page { min - width : 0 ; }
. state - stack { display : grid ; gap : 12 px ; }
. row - actions { display : flex ; align - items : center ; gap : 8 px ; white - space : nowrap ; }
. row - actions . el - button { margin : 0 ; }
. primary - cell { display : grid ; gap : 3 px ; min - width : 0 ; }
. primary - cell strong , . primary - cell span { overflow - wrap : anywhere ; }
. primary - cell span , . muted - action { color : var ( -- cc - muted ) ; font - size : 12 px ; }
@ media ( max - width : 520 px ) {
. row - actions { align - items : stretch ; flex - direction : column ; }
. row - actions : deep ( . el - button ) { width : 100 % ; }
}
< / style >
< style >
. inbound - task - dialog { max - height : 92 vh ; overflow : hidden ; }
. inbound - task - dialog . el - dialog _ _body { max - height : calc ( 92 vh - 62 px ) ; overflow : auto ; padding : 20 px 24 px 24 px ; }
@ media ( max - width : 520 px ) {
. inbound - task - dialog . el - dialog _ _body { padding : 14 px ; }
}
< / style >