Files
admin-web/src/modules/overseas-warehouse/pages/InventoryDetailView.vue
T
2026-09-20 19:57:51 +08:00

43 lines
11 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import '@/styles/adminDensity.css'
import { computed, onMounted, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
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'
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'
const route = useRoute(); const router = useRouter(); const inventoryId = computed(() => String(route.params.inventoryId || ''))
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() || '—' }
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(' / ') || '—')
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) }))
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 = ['在库总量', '可用库存', '暂扣库存', '美本土散板', '在途库存', '采购库存', '预计总量']
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) })))
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) })))
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 }]
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 } }
function back() { void router.push({ name: 'overseas-inventory' }) }; onMounted(load)
</script>
<template><OverseasDetailShell class="admin-density" dense 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>
<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 density-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 density-fields"><div v-for="label in stockFields" :key="label"><dt>{{ label }}</dt><dd>{{ stockValues[label as keyof typeof stockValues] }}</dd></div></dl></section></section>
<section class="ledger-section" aria-labelledby="inventory-location-heading"><h2 id="inventory-location-heading">库位库存</h2><div class="inventory-table-wrap"><DataTable :data="locationRows" :columns="locationColumns" column-settings-mode="hide" row-key="id" empty-text="暂无库位库存" /></div></section><section class="ledger-section" aria-labelledby="inventory-pallet-heading"><h2 id="inventory-pallet-heading">托盘箱号信息</h2><div class="inventory-table-wrap"><DataTable :data="palletRows" :columns="palletColumns" column-settings-mode="hide" row-key="id" empty-text="暂无托盘库存" /></div></section><section class="ledger-section" aria-labelledby="inventory-ledger-heading"><h2 id="inventory-ledger-heading">进销存单据明细</h2><p class="ledger-note">按该库存记录的入库单范围展示,出库字段仅为只读事实。</p><div class="inventory-table-wrap"><DataTable :data="ledgerRows" :columns="ledgerColumns" column-settings-mode="hide" row-key="id" empty-text="暂无进销存明细" /></div></section></template>
</OverseasDetailShell></template>
<style scoped>.overview-panel>section{min-width:0;padding:var(--admin-space-5) var(--admin-space-5) var(--admin-space-5);border-right:1px solid var(--cc-border)}.overview-panel>section:last-child{border-right:0}.overview-panel h2,.ledger-section h2{margin:0 0 var(--admin-space-5);color:var(--cc-text);font-size:var(--admin-font-section)}.ledger-section h2,.ledger-note{margin-right:var(--admin-space-5);margin-left:var(--admin-space-5)}.ledger-note{color:var(--cc-muted);font-size:var(--admin-font-caption)}@media(max-width:1100px){.overview-panel,.detail-fields{grid-template-columns:1fr}.overview-panel>section{border-right:0;border-bottom:1px solid var(--cc-border)}}
.overview-panel {display:grid;min-width:0; grid-template-columns: minmax(0, 1fr); border: 0;background:#fff; }
.overview-panel > section { border-right: 0; }
.ledger-section {min-width:0;overflow:hidden;padding-top:var(--admin-space-5); border: 0;background:#fff; border-top: 1px solid var(--cc-border); }
.inventory-table-wrap { padding:0 var(--admin-space-5) var(--admin-space-4); }
</style>