@@ -7,23 +7,30 @@ import { BusinessDocumentFees } from '@/modules/overseas-business'
import { computed , onMounted , ref } from 'vue'
import { ElMessage } from 'element-plus'
import { useRoute , useRouter } from 'vue-router'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
import { getApiErrorDetails , type ApiErrorDetails } from '@/shared/api/http'
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
import DataTable , { type DataTableColumn } from '@/shared/components/DataTable.vue'
import AppDialog from '@/shared/components/AppDialog.vue'
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
import RowActions , { type RowActionItem } from '@/shared/components/RowActions.vue'
import { getInboundPlan , moveInboundPallet , type InboundPlanDetailDto } from '../api/inboundPlans'
import { listOverseasLocations , type OverseasLocationDto } from '../api/locations'
import { getPalletLabelPdf } from '../api/pallets'
import { overseasWarehousePermissions } from '../permissions'
import OverseasDetailShell from '../components/OverseasDetailShell.vue'
import InboundShortageConfirmation from '../components/InboundShortageConfirmation.vue'
import { formatOverseasDateTime , inboundPlanStatusLabel , lastMileOrderTypeLabel } from '../lastMilePresentation'
const route = useRoute ( ) ; const router = useRouter ( )
const access = useAccessPreviewStore ( )
const planNo = computed ( ( ) => String ( route . params . orderId || '' ) )
const customerId = computed ( ( ) => String ( route . query . customerId || '' ) )
const detail = ref < InboundPlanDetailDto | null > ( null ) ; const loading = ref ( false ) ; const loadError = ref < ApiErrorDetails | null > ( null )
const moveOpen = ref ( false ) ; const moveSaving = ref ( false ) ; const moveRow = ref < { palletNo : string ; palletVersion : number } | null > ( null ) ; const targetLocationCode = ref ( '' ) ; const moveReason = ref ( '入库托盘上架' ) ; const locationOptions = ref < OverseasLocationDto [ ] > ( [ ] )
const moveLocationsLoading = ref ( false ) ; const moveLocationError = ref < ApiErrorDetails | null > ( null ) ; const moveError = ref < ApiErrorDetails | null > ( null )
const printingPalletId = ref ( '' )
const printError = ref < ApiErrorDetails | null > ( null )
const referenceValue = computed ( ( ) => detail . value ? . planNo || ( /^\d+$/ . test ( planNo . value ) ? '—' : planNo . value ) )
function text ( value : unknown ) { return String ( value ? ? '' ) . trim ( ) || '—' }
function receivingWarehouse ( code : unknown , name : unknown ) {
@@ -47,7 +54,7 @@ const summaryGroups = [
]
const appointmentFields = [ '预约状态' , '预约送仓时间' , '预计装车时间' , '排车状态' , '创建时间' , '更新时间' , '备注' ]
const palletColumns : DataTableColumn [ ] = [
{ key : 'actions' , label : '操作' , width : 100 , fixed : 'left' , slotName : 'actions' } ,
{ key : 'actions' , label : '操作' , width : 180 , fixed : 'left' , slotName : 'actions' } ,
{ key : 'inboundCode' , prop : 'inboundCode' , label : '入仓码' , minWidth : 150 } ,
{ key : 'palletNo' , prop : 'palletNo' , label : '托盘号' , minWidth : 150 } ,
{ key : 'putawayStatus' , label : '上架状态' , width : 110 , prop : 'putawayStatus' , afterColumn : 'palletNo' } ,
@@ -67,11 +74,34 @@ const palletRows = computed(() => (detail.value?.allocations || []).map((allocat
palletNo : text ( allocation . palletNo ) ,
palletOwner : allocation . palletOwnedByCurrentPlan ? ` ${ allocation . palletOwnerPlanNo } (本计划) ` : ` ${ allocation . palletOwnerPlanNo } (跨计划合托) ` ,
packageCount : Number ( allocation . pickedPackageCount ? ? allocation . countedPackageCount ? ? allocation . plannedPackageCount ? ? 0 ) ,
palletId : String ( allocation . palletId ) , palletVersion : pallet ? . version ? ? null , sourceLocationId : pallet ? . warehouseLocationId || null , sourceLocationCode : locationCode ,
palletId : String ( allocation . palletId ) , warehousePalletId : pallet ? . warehousePalletId || null , palletVersion : pallet ? . version ? ? null , sourceLocationId : pallet ? . warehouseLocationId || null , sourceLocationCode : locationCode ,
sourceLocationVersion : null , location : locationCode || ( pallet ? . warehouseLocationId ? '—' : '虚拟收货位' ) , locationType : locationType === 'SHARED_OVERFLOW' ? 'A0共享溢出位' : locationType === 'NORMAL' ? '普通库位' : '虚拟收货位' , putawayStatus : locationType === 'NORMAL' || locationType === 'SHARED_OVERFLOW' ? '已上架' : '待上架' ,
}
} ) )
const availableLocations = computed ( ( ) => locationOptions . value . filter ( item => item . status !== 'DISABLED' ) )
function palletActions ( row : typeof palletRows . value [ number ] ) : RowActionItem [ ] {
return [
{ key : 'print' , label : '打印托盘码' , visible : access . can ( overseasWarehousePermissions . palletList ) ,
disabled : ! ! printingPalletId . value || ! row . warehousePalletId || ! detail . value ? . overseasWarehouseId ,
loading : ! ! row . warehousePalletId && printingPalletId . value === row . warehousePalletId } ,
{ key : 'move' , label : '移库' , disabled : moveSaving . value || moveLocationsLoading . value || row . palletNo === '—' || row . palletVersion == null } ,
]
}
async function printPalletLabel ( row : typeof palletRows . value [ number ] ) {
const warehouseId = detail . value ? . overseasWarehouseId
if ( ! access . can ( overseasWarehousePermissions . palletList ) || printingPalletId . value || ! warehouseId || ! row . warehousePalletId ) return
printingPalletId . value = row . warehousePalletId ; printError . value = null
try {
const blob = await getPalletLabelPdf ( warehouseId , row . warehousePalletId )
const url = URL . createObjectURL ( blob )
try {
const link = document . createElement ( 'a' )
link . href = url ; link . download = ` ${ row . palletNo } -托盘码.pdf ` ; link . click ( )
} finally { window . setTimeout ( ( ) => URL . revokeObjectURL ( url ) , 0 ) }
ElMessage . success ( '已生成 1 张托盘标签 PDF' )
} catch ( cause ) { printError . value = getApiErrorDetails ( cause , '托盘标签打印失败,请重试' ) }
finally { printingPalletId . value = '' }
}
async function openMove ( row : typeof palletRows . value [ number ] ) {
if ( moveSaving . value || moveLocationsLoading . value || row . palletVersion == null || row . palletNo === '—' ) return
if ( ! detail . value ? . overseasWarehouseCode ) { ElMessage . warning ( '入库单未返回海外仓范围,暂不能移库' ) ; return }
@@ -99,7 +129,9 @@ onMounted(load)
< template > < OverseasDetailShell workbench class = "admin-density" dense title = "入库单详情" reference -label = " 入库单号 " :reference-value = "referenceValue" back -label = " 返回入库单 " : back -text = " appointmentReturnTarget ( route.query ) ? ' 返回预约工作 ' : ' 返回列表 ' " domain = "入库单详情" :pending = "false" : status -text = " detail ? status ( detail.status ) : ' 加载中 ' " @back ="back" >
< LoadErrorState v-if = "loadError" title="入库单详情加载失败" :message="loadError.message" :loading="loading" @retry="load"><template #details><ApiErrorMeta :details="loadError" / > < / template > < / LoadErrorState >
< template v-else > < section v-loading = "loading" class="detail-section" aria-label="入库单基本信息"><div class="detail-groups"><section v-for="group in summaryGroups" :key="group.title" class="detail-group" :aria-label="group.title"><h2>{{ group.title }}</h2><dl class="detail-fields density-fields"><div v-for="label in group.fields" :key="label" > < dt > { { label } } < / dt > < dd > { { fieldValues [ label ] } } < / dd > < / div > < / dl > < / section > < / div > < / section >
< section class = "detail-section pallet-section" aria -labelledby = " inbound -pallet -heading " > < div class = "section-heading" > < div > < h2 id = "inbound-pallet-heading" > 入库单托盘明细 < InlineHelp title = "托盘明细说明" > < p > PDA 合托后 , 参与合托的入库单会分别展示同一托盘及本单对应箱数 ; A0 为系统共享溢出位 。 < / p > < / InlineHelp > < / h2 > < / div > < InboundShortageConfirmation v-if = "detail" :detail="detail" @refresh="load" /><span class="section-count">{{ palletRows.length }} 条</span></div><div class="detail-table-wrap"><div class="workbench-table"><DataTable :data="palletRows" :columns="palletColumns" column-settings-mode="hide" row-key="id" empty-text="暂无托盘明细"><template #actions="{ row }"><el-button class="density-action" size="small" :disabled="moveSaving || moveLocationsLoading || !(row as typeof palletRows[number]).palletNo || (row as typeof palletRows[number]).palletNo === '—' || (row as typeof palletRows[number]).palletVersion == null" @click="openMove(row as typeof palletRows[number])" > 移库 < / el -button > < / template > < / DataTable > < / div > < / div > < / section >
< section class = "detail-section pallet-section" aria -labelledby = " inbound -pallet -heading " > < div class = "section-heading" > < div > < h2 id = "inbound-pallet-heading" > 入库单托盘明细 < InlineHelp title = "托盘明细说明" > < p > PDA 合托后 , 参与合托的入库单会分别展示同一托盘及本单对应箱数 ; A0 为系统共享溢出位 。 < / p > < / InlineHelp > < / h2 > < / div > < InboundShortageConfirmation v-if = "detail" :detail="detail" @refresh="load" /><span class="section-count">{{ palletRows.length }} 条</span></div><div class="detail-table-wrap" >
< LoadErrorState v-if = "printError" compact title="托盘标签打印失败" :message="printError.message" :show-retry="false"><template #details><ApiErrorMeta :details="printError" / > < / template > < / LoadErrorState >
< div class = "workbench-table" > < DataTable :data = "palletRows" :columns = "palletColumns" column -settings -mode = " hide " row -key = " id " empty -text = " 暂无托盘明细 " > < template # actions = "{ row }" > < RowActions : actions = "palletActions(row as typeof palletRows[number])" text -actions @action ="action => action === 'print' ? printPalletLabel(row as typeof palletRows[number]) : openMove(row as typeof palletRows[number])" / > < / template > < / DataTable > < / div > < / div > < / section >
< section class = "detail-section appointment-section" aria -labelledby = " inbound -appointment -heading " > < div class = "compact-section-heading" > < h2 id = "inbound-appointment-heading" > 预约与排车 < / h2 > < / div > < dl class = "detail-fields appointment-fields density-fields" > < div v-for = "label in appointmentFields" :key="label" :class="{ 'is-wide': label === '备注' }" > < dt > { { label } } < / dt > < dd > { { fieldValues [ label ] } } < / dd > < / div > < / dl > < / section >
< section v-if = "detail" class="detail-section fee-section" aria-label="入库单费用"><BusinessDocumentFees business-type="OVERSEAS_INBOUND" :business-no="detail.planNo" :customer-id="detail.customerId" :business-id="detail.id" / > < / section > < / template >
< / OverseasDetailShell > < AppDialog v-model = "moveOpen" class="admin-density-dialog admin-workbench is-dialog" :title="`托盘移库 · ${moveRow?.palletNo || '—'}`" width="min(560px, calc(100vw - 28px))" fullscreenable confirm-text="提交移库" :confirm-loading="moveSaving" :confirm-disabled="moveLocationsLoading || !!moveLocationError || !targetLocationCode || !moveReason.trim()" @confirm="submitMove"><div class="density-dialog-sections" >