2026-08-19 16:45:51 +08:00
< script setup lang = "ts" >
2026-08-20 18:02:52 +08:00
import { computed , onMounted , ref } from 'vue'
2026-08-19 16:45:51 +08:00
import { useRoute , useRouter } from 'vue-router'
2026-08-20 18:02:52 +08:00
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 LoadErrorState from '@/shared/components/LoadErrorState.vue'
2026-09-04 17:46:15 +08:00
import { listInventoryBalances , listInventoryLedger , listInventoryPositions , type InventoryBalanceDto , type InventoryLedgerDto , type InventoryPositionDto } from '../api/inventory' ; import { inventoryProductName } from '../inventoryPresentation' ; import { formatOverseasDateTime , inventoryBusinessTypeLabel } from '../lastMilePresentation' ; import OverseasDetailShell from '../components/OverseasDetailShell.vue'
2026-08-19 16:45:51 +08:00
2026-08-21 11:20:09 +08:00
const route = useRoute (); const router = useRouter (); const inventoryId = computed (() => String ( route . params . inventoryId || '' ))
2026-08-20 18:02:52 +08:00
const balance = ref < InventoryBalanceDto | null >( null ); const positions = ref < InventoryPositionDto [] >([]); const ledger = ref < InventoryLedgerDto [] >([]); const loading = ref ( false ); const loadError = ref < ApiErrorDetails | null >( null )
function text ( value : unknown ) { return String ( value ?? '' ). trim () || '—' }
2026-09-08 18:16:33 +08:00
const businessReferences = computed (() => [
[ '入库单号' , balance . value ? . inboundOrderNo || route . query . inboundOrderNo ],
[ '入仓码' , balance . value ? . inboundCode || route . query . inboundCode ],
[ 'SKU' , balance . value ? . skuCode || route . query . skuCode ],
]. map (([ label , value ]) => [ String ( label ), String ( value ?? '' ). trim ()] as const ). filter (([, value ]) => value ))
const referenceLabel = computed (() => businessReferences . value . map (([ label ]) => label ). join ( ' / ' ) || '业务标识' )
const referenceValue = computed (() => businessReferences . value . map (([, value ]) => value ). join ( ' / ' ) || '—' )
2026-08-21 11:20:09 +08:00
const productValues = computed (() => ({ 客户 : text ( balance . value ? . customerName ), 海外仓 : text ( balance . value ? . overseasWarehouseName ), SKU编码 : text ( balance . value ? . skuCode ), 产品名称 : balance . value ? inventoryProductName ( balance . value ) : '—' , 更新时间 : formatOverseasDateTime ( balance . value ? . updatedAt ) }))
2026-09-08 17:56:17 +08:00
const stockValues = computed (() => ({ 在库总量 : Number ( balance . value ? . totalPackages || 0 ), 可用库存 : Number ( balance . value ? . availablePackages || 0 ), 暂扣库存 : Number ( balance . value ? . holdPackages || 0 ), 美本土散板 : '—' , 在途库存 : Number ( balance . value ? . inTransitPackages || 0 ), 采购库存 : Number ( balance . value ? . procurementPackages || 0 ), 预计总量 : Number ( balance . value ? . totalPackages || 0 ) + Number ( balance . value ? . inTransitPackages || 0 ) + Number ( balance . value ? . procurementPackages || 0 ) }))
const productFields = [ '客户' , '海外仓' , 'SKU编码' , '产品名称' , '更新时间' ]; const stockFields = [ '在库总量' , '可用库存' , '暂扣库存' , '美本土散板' , '在途库存' , '采购库存' , '预计总量' ]
2026-08-20 18:02:52 +08:00
const locationRows = computed (() => positions . value . map (( item ) => ({ id : String ( item . id ), location : text ( item . warehouseLocationCode ), status : text ( item . status ), palletNo : text ( item . palletNo ), cartons : Number ( item . totalPackages || 0 ), quantity : Number ( item . totalPackages || 0 ), available : Number ( item . availablePackages || 0 ), inboundDate : formatOverseasDateTime ( item . updatedAt ), ageDays : '—' })))
const palletRows = computed (() => positions . value . map (( item ) => ({ id : String ( item . id ), palletNo : text ( item . palletNo ), cartons : Number ( item . totalPackages || 0 ), cartonRange : '—' , location : text ( item . warehouseLocationCode ), quantity : Number ( item . totalPackages || 0 ) })))
2026-09-04 17:46:15 +08:00
const ledgerRows = computed (() => ledger . value . map (( item ) => ({ id : String ( item . id ), occurredAt : formatOverseasDateTime ( item . occurredAt ), businessType : inventoryBusinessTypeLabel ( item . businessType ), businessOrderNo : text ( item . businessOrderNo ), inboundOrderNo : text ( item . inboundOrderNo ), outboundOrderNo : text ( item . outboundOrderNo ), location : text ( item . warehouseLocationCode ), inboundQuantity : item . deltaPackages > 0 ? item . deltaPackages : 0 , outboundQuantity : item . deltaPackages < 0 ? Math . abs ( item . deltaPackages ) : 0 , balance : Number ( item . afterPackages || 0 ), operator : '—' , remark : text ( item . remark ) })))
2026-08-20 18:02:52 +08:00
const locationColumns : DataTableColumn [] = [{ key : 'location' , prop : 'location' , label : '库位' , minWidth : 130 }, { key : 'status' , prop : 'status' , label : '库存状态' , width : 120 }, { key : 'palletNo' , prop : 'palletNo' , label : '托盘号' , minWidth : 150 }, { key : 'cartons' , prop : 'cartons' , label : '箱数' , width : 90 , align : 'right' }, { key : 'quantity' , prop : 'quantity' , label : '库存数量' , width : 110 , align : 'right' }, { key : 'available' , prop : 'available' , label : '可用数量' , width : 110 , align : 'right' }, { key : 'inboundDate' , prop : 'inboundDate' , label : '最近更新' , width : 160 }, { key : 'ageDays' , prop : 'ageDays' , label : '库龄(天)' , width : 100 , align : 'right' }]
const palletColumns : DataTableColumn [] = [{ key : 'palletNo' , prop : 'palletNo' , label : '托盘号' , minWidth : 150 }, { key : 'cartons' , prop : 'cartons' , label : '关联箱数' , width : 110 , align : 'right' }, { key : 'cartonRange' , prop : 'cartonRange' , label : '箱号范围' , minWidth : 200 }, { key : 'location' , prop : 'location' , label : '库位' , minWidth : 130 }, { key : 'quantity' , prop : 'quantity' , label : '库存数量' , width : 110 , align : 'right' }]
const ledgerColumns : DataTableColumn [] = [{ key : 'occurredAt' , prop : 'occurredAt' , label : '发生时间' , minWidth : 160 }, { key : 'businessType' , prop : 'businessType' , label : '业务类型' , width : 120 }, { key : 'businessOrderNo' , prop : 'businessOrderNo' , label : '业务订单号' , minWidth : 170 }, { key : 'inboundOrderNo' , prop : 'inboundOrderNo' , label : '入库单号' , minWidth : 160 }, { key : 'outboundOrderNo' , prop : 'outboundOrderNo' , label : '出库单号' , minWidth : 160 }, { key : 'location' , prop : 'location' , label : '库位' , minWidth : 120 }, { key : 'inboundQuantity' , prop : 'inboundQuantity' , label : '入库数量' , width : 110 , align : 'right' }, { key : 'outboundQuantity' , prop : 'outboundQuantity' , label : '出库数量' , width : 110 , align : 'right' }, { key : 'balance' , prop : 'balance' , label : '结存' , width : 100 , align : 'right' }, { key : 'operator' , prop : 'operator' , label : '操作人' , minWidth : 120 }, { key : 'remark' , prop : 'remark' , label : '备注' , minWidth : 180 }]
2026-08-21 11:20:09 +08:00
async function load () { if ( ! inventoryId . value ) { loadError . value = { message : '详情缺少库存记录编号,请从库存列表重新进入' }; return }; loading . value = true ; loadError . value = null ; try { const inboundOrderNo = String ( route . query . inboundOrderNo || '' ); const inboundCode = String ( route . query . inboundCode || '' ); const skuCode = String ( route . query . skuCode || '' ); const overseasWarehouseId = String ( route . query . overseasWarehouseId || '' ); const result = await listInventoryBalances ({ page : 1 , size : 200 , inboundOrderNo : inboundOrderNo || undefined , inboundCode : inboundCode || undefined , skuCode : skuCode || undefined , overseasWarehouseId : overseasWarehouseId || undefined }); balance . value = result . records . find (( item ) => String ( item . id ) === inventoryId . value ) || null ; if ( ! balance . value ) throw new Error ( 'Inventory balance not found' ); const [ positionRows , ledgerPage ] = await Promise . all ([ listInventoryPositions ( inventoryId . value ), listInventoryLedger ({ page : 1 , size : 200 , inboundOrderNo : balance . value . inboundOrderNo || undefined , inboundCode : balance . value . inboundCode || undefined })]); positions . value = positionRows ; ledger . value = ledgerPage . records } catch ( error ) { loadError . value = getApiErrorDetails ( error , '库存详情加载失败,请重试' ) } finally { loading . value = false } }
2026-08-20 18:02:52 +08:00
function back () { void router . push ({ name : 'overseas-inventory' }) }; onMounted ( load )
2026-08-19 16:45:51 +08:00
</ script >
2026-09-08 17:56:17 +08:00
< template >< OverseasDetailShell title = "库存详情" :reference-label = "referenceLabel" :reference-value = "referenceValue" back -label =" 返回库存管理 " domain = "库存详情" :pending = "false" : status -text =" balance ? ' 库存事实 ' : ' 加载中 '" @back ="back" >< LoadErrorState v-if = "loadError" title="库存详情加载失败" :message="loadError.message" :loading="loading" @retry="load"><template #details><ApiErrorMeta :details="loadError" /></ template ></ LoadErrorState >
2026-08-20 18:02:52 +08:00
< template v-else >< section v-loading = "loading" class="overview-panel" aria-label="库存概览"><section aria-labelledby="inventory-product-heading"><h2 id="inventory-product-heading">商品信息</h2><dl class="detail-fields"><div v-for="label in productFields" :key="label"><dt>{{ label }}</dt><dd>{{ productValues[label as keyof typeof productValues] }}</dd></div></dl></section><section aria-labelledby="inventory-summary-heading"><h2 id="inventory-summary-heading">库存状态汇总</h2><dl class="detail-fields"><div v-for="label in stockFields" :key="label" >< dt >{{ label }}</ dt >< dd >{{ stockValues [ label as keyof typeof stockValues ] }}</ dd ></ div ></ dl ></ section ></ section >
2026-08-20 18:27:30 +08:00
< section class = "ledger-section" aria -labelledby =" inventory -location-heading " >< h2 id = "inventory-location-heading" > 库位库存 </ h2 >< DataTable :data = "locationRows" :columns = "locationColumns" column -settings-mode =" hide " row -key =" id " empty -text =" 暂无库位库存 " /></ section >< section class = "ledger-section" aria -labelledby =" inventory -pallet-heading " >< h2 id = "inventory-pallet-heading" > 托盘箱号信息 </ h2 >< DataTable :data = "palletRows" :columns = "palletColumns" column -settings-mode =" hide " row -key =" id " empty -text =" 暂无托盘库存 " /></ section >< section class = "ledger-section" aria -labelledby =" inventory -ledger-heading " >< h2 id = "inventory-ledger-heading" > 进销存单据明细 </ h2 >< p class = "ledger-note" > 按该库存记录的入库单范围展示 , 出库字段仅为只读事实 。 </ p >< DataTable :data = "ledgerRows" :columns = "ledgerColumns" column -settings-mode =" hide " row -key =" id " empty -text =" 暂无进销存明细 " /></ section ></ template >
2026-08-20 18:02:52 +08:00
</ OverseasDetailShell ></ template >
2026-09-02 14:39:34 +08:00
< style scoped >. overview - panel { display : grid ; min - width : 0 ; grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr )); border : 1 px solid var ( -- cc - border ); background : # fff }. overview - panel > section { min - width : 0 ; padding : 16 px 18 px 18 px ; border - right : 1 px solid var ( -- cc - border )}. overview - panel > section : last - child { border - right : 0 }. overview - panel h2 ,. ledger - section h2 { margin : 0 0 12 px ; color : var ( -- cc - text ); font - size : 15 px }. detail - fields { display : grid ; grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr )); margin : 0 ; border - top : 1 px solid var ( -- cc - border ); border - left : 1 px solid var ( -- cc - border )}. detail - fields > div { display : grid ; min - width : 0 ; min - height : 46 px ; grid - template - columns : 110 px minmax ( 0 , 1 fr ); border - right : 1 px solid var ( -- cc - border ); border - bottom : 1 px solid var ( -- cc - border )}. detail - fields dt ,. detail - fields dd { display : flex ; min - width : 0 ; align - items : center ; padding : 8 px 10 px }. detail - fields dt { background : # f8fafc ; color : var ( -- cc - muted ); font - size : 12 px }. detail - fields dd { margin : 0 ; color : var ( -- cc - text ); overflow - wrap : anywhere }. ledger - section { min - width : 0 ; overflow : hidden ; padding - top : 16 px ; border : 1 px solid var ( -- cc - border ); background : # fff }. ledger - section h2 ,. ledger - note { margin - right : 18 px ; margin - left : 18 px }. ledger - note { color : var ( -- cc - muted ); font - size : 12 px } @ media ( max - width : 1100 px ){. overview - panel ,. detail - fields { grid - template - columns : 1 fr }. overview - panel > section { border - right : 0 ; border - bottom : 1 px solid var ( -- cc - border )}}</ style >