@@ -0,0 +1,28 @@
< script setup lang = "ts" >
import { onMounted , reactive , ref } from 'vue'
import { ElMessage , ElMessageBox } from 'element-plus'
import { Eye , RotateCcw , Send } from '@lucide/vue'
import QueryToolbar , { type QueryField , type QueryModel } from '@/shared/components/QueryToolbar.vue'
import DataTable from '@/shared/components/DataTable.vue'
import AppDialog from '@/shared/components/AppDialog.vue'
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
import AdminPage from '@/shared/components/AdminPage.vue'
import ListPageLayout from '@/shared/components/ListPageLayout.vue'
import StatusBadge from '@/shared/components/StatusBadge.vue'
import { getApiErrorDetails , type ApiErrorDetails } from '@/shared/api/http'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
import { changeOutboundDispatch , getOutboundDispatchDetail , listOutboundDispatches } from '../api/platformOutboundDispatch'
import type { OutboundDispatchDetail , OutboundDispatchResource } from '../types'
const access = useAccessPreviewStore ( ) ; const rows = ref < OutboundDispatchResource [ ] > ( [ ] ) ; const loading = ref ( false ) ; const error = ref < ApiErrorDetails | null > ( null ) ; const filters = reactive < QueryModel > ( { orderId : '' , inboundId : '' , containerBookingId : '' } )
const fields : QueryField [ ] = [ { key : 'orderId' , type : 'input' , label : '订单 ID' } , { key : 'inboundId' , type : 'input' , label : '入库单 ID' } , { key : 'containerBookingId' , type : 'input' , label : '柜次 ID' } ]
const columns = [ { key : 'actions' , label : '操作' , fixed : 'left' , width : 240 , slotName : 'actions' , hideable : false , movable : false } , { prop : 'orderNo' , label : '订单号' , minWidth : 145 } , { prop : 'inboundCode' , label : '入库单号' , minWidth : 145 } , { prop : 'customerName' , label : '客户' , minWidth : 140 } , { prop : 'containerNo' , label : '柜号' , minWidth : 130 } , { prop : 'oceanBookingStatus' , label : '订舱' , width : 105 , slotName : 'booking' } , { prop : 'truckAppointmentStatus' , label : '拖车' , width : 105 , slotName : 'truck' } , { prop : 'pickTaskStatus' , label : '拣货' , width : 105 , slotName : 'pick' } , { prop : 'dispatchStatus' , label : '出仓状态' , width : 115 , slotName : 'status' } , { key : 'gate' , label : '门禁' , width : 90 , slotName : 'gate' } ]
function query ( ) { return { orderId : String ( filters . orderId || '' ) || undefined , inboundId : String ( filters . inboundId || '' ) || undefined , containerBookingId : String ( filters . containerBookingId || '' ) || undefined } }
async function load ( ) { loading . value = true ; error . value = null ; try { rows . value = await listOutboundDispatches ( query ( ) ) } catch ( cause ) { error . value = getApiErrorDetails ( cause ) } finally { loading . value = false } }
const detailOpen = ref ( false ) ; const detail = ref < OutboundDispatchDetail | null > ( null ) ; const actionError = ref < ApiErrorDetails | null > ( null )
async function open ( value : unknown ) { const row = value as OutboundDispatchResource ; detailOpen . value = true ; actionError . value = null ; try { detail . value = await getOutboundDispatchDetail ( { inboundId : row . inboundId , containerBookingId : row . containerBookingId || undefined } ) } catch ( cause ) { actionError . value = getApiErrorDetails ( cause ) } }
async function action ( value : unknown , name : 'confirm-dispatch' | 'reverse-dispatch' ) { const row = value as OutboundDispatchResource ; try { await ElMessageBox . confirm ( name === 'confirm-dispatch' ? '确认国内出仓?系统将校验全部装柜门禁。' : '确认撤销国内出仓?' , '出仓状态确认' , { type : name === 'reverse-dispatch' ? 'warning' : 'info' } ) ; await changeOutboundDispatch ( name , { inboundId : row . inboundId , containerBookingId : row . containerBookingId , expectedVersion : row . version , remark : name === 'confirm-dispatch' ? '后台确认出仓' : '后台撤销出仓' } ) ; ElMessage . success ( '出仓状态已更新' ) ; await load ( ) } catch ( cause ) { if ( cause !== 'cancel' && ! ( cause instanceof Error && cause . message === 'cancel' ) ) error . value = getApiErrorDetails ( cause ) ; if ( error . value ? . code === 'A0409' ) await load ( ) } }
onMounted ( load )
< / script >
< template > < AdminPage as = "div" > < ListPageLayout aria -label = " 国内出仓列表 " > < template # toolbar > < QueryToolbar v-model = "filters" class="surface-panel" ariaLabel="国内出仓筛选" :fields="fields" @search="load" /></template><template v-if="error" #state><LoadErrorState title="国内出仓加载失败" :message="error.message" :loading="loading" @retry="load"><template #details><ApiErrorMeta :details="error" /></template></LoadErrorState></template><section v-if="!error" class="surface-panel table-panel"><DataTable :data="rows" :columns="columns" :loading="loading" row-key="inboundId" preference-key="outbound-dispatches" empty-text="暂无符合条件的出仓记录"><template #actions="{ row }"><el-button v-if="access.can('platform:firstleg:outbound-dispatch:view')" link type="primary" @click="open(row)"><Eye :size="15" />查看</el-button><el-button v-if="access.can('platform:firstleg:outbound-dispatch:confirm') && row.dispatchStatus !== 'DISPATCHED'" link type="primary" @click="action(row, 'confirm-dispatch')"><Send :size="15" />确认</el-button><el-button v-if="access.can('platform:firstleg:outbound-dispatch:reverse') && row.dispatchStatus === 'DISPATCHED'" link type="danger" @click="action(row, 'reverse-dispatch')"><RotateCcw :size="15" />撤销</el-button></template><template #booking="{ row }"><StatusBadge :status="row.oceanBookingStatus || '未关联'" /></template><template #truck="{ row }"><StatusBadge :status="row.truckAppointmentStatus || '未关联'" /></template><template #pick="{ row }"><StatusBadge :status="row.pickTaskStatus || '未关联'" /></template><template #status="{ row }"><StatusBadge :status="row.dispatchStatus || '待处理'" /></template><template #gate="{ row }"><el-tag :type="row.gate?.passed ? 'success' : 'danger'">{{ row.gate?.passed ? '通过' : '未通过' }}</el-tag></template></DataTable></section></ListPageLayout><AppDialog v-model="detailOpen" title="国内出仓详情" width="900px" :show-confirm="false"><LoadErrorState v-if="actionError" title="出仓详情加载失败" :message="actionError.message" :show-retry="false"><template #details><ApiErrorMeta :details="actionError" /></template></LoadErrorState><template v-else-if="detail"><el-descriptions :column="2" border><el-descriptions-item label="订单号">{{ detail.aggregate.orderNo }}</el-descriptions-item><el-descriptions-item label="入库单号">{{ detail.aggregate.inboundCode }}</el-descriptions-item><el-descriptions-item label="柜号">{{ detail.aggregate.containerNo || '—' }}</el-descriptions-item><el-descriptions-item label="出仓状态"><StatusBadge :status="detail.aggregate.dispatchStatus || '待处理'" /></el-descriptions-item></el-descriptions><h3>统一门禁</h3><el-table :data="detail.aggregate.gate?.items || []"><el-table-column label="结果" width="80"><template #default="scope"><el-tag :type="scope.row.passed ? 'success' : 'danger'">{{ scope.row.passed ? '通过' : '阻断' }}</el-tag></template></el-table-column><el-table-column prop="name" label="门禁" min-width="140" /><el-table-column prop="actual" label="当前情况" min-width="180" /><el-table-column prop="requirement" label="要求" min-width="180" /></el-table><h3>整体时间线</h3><el-timeline><el-timeline-item v-for="entry in detail.timeline" :key="`${entry.module}-${entry.occurredAt}-${entry.action}`" :timestamp="entry.occurredAt" > {{ entry.module }} · {{ entry.action }} : {{ entry.fromStatus | | ' — ' }} → {{ entry.toStatus | | ' — ' }} < small > { { entry . operatorName || '系统' } } { { entry . remark || '' } } < / small > < / el-timeline-item > < / el-timeline > < / template > < / AppDialog > < / AdminPage > < / template >
< style scoped > h3 { margin : 20 px 0 12 px } small { display : block ; color : var ( -- cc - muted ) } < / style >