This commit is contained in:
2026-08-20 18:02:52 +08:00
parent 4ae5e7c672
commit 80947cd9ce
65 changed files with 1386 additions and 1083 deletions
@@ -1,88 +1,28 @@
<script setup lang="ts">
import { computed } from 'vue'
import { computed, onMounted, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
import OverseasDetailShell from '../components/OverseasDetailShell.vue'
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 { formatOverseasDateTime } from '../lastMilePresentation'; import OverseasDetailShell from '../components/OverseasDetailShell.vue'
const route = useRoute()
const router = useRouter()
const inventoryId = computed(() => String(route.params.inventoryId || '—'))
const productFields = ['客户', '海外仓', 'SKU编码', '中文品名', '英文品名', '更新时间']
const stockFields = ['在库总量', '可用库存', '暂扣库存', '美本土散版', '在途库存', '采购库存', '预计总量']
const locationColumns: DataTableColumn[] = [
{ key: 'location', label: '库位', minWidth: 130 },
{ key: 'status', label: '库存状态', width: 120 },
{ key: 'palletNo', label: '托盘号', minWidth: 150 },
{ key: 'cartons', label: '箱数', width: 90, align: 'right' },
{ key: 'quantity', label: '库存数量', width: 110, align: 'right' },
{ key: 'available', label: '可用数量', width: 110, align: 'right' },
{ key: 'inboundDate', label: '入库日期', width: 130 },
{ key: 'ageDays', label: '库龄(天)', width: 100, align: 'right' },
]
const palletColumns: DataTableColumn[] = [
{ key: 'palletNo', label: '托盘号', minWidth: 150 },
{ key: 'cartons', label: '关联箱数', width: 110, align: 'right' },
{ key: 'cartonRange', label: '箱号范围', minWidth: 200 },
{ key: 'location', label: '库位', minWidth: 130 },
{ key: 'quantity', label: '库存数量', width: 110, align: 'right' },
]
const ledgerColumns: DataTableColumn[] = [
{ key: 'occurredAt', label: '发生时间', minWidth: 160 },
{ key: 'businessType', label: '业务类型', width: 120 },
{ key: 'businessOrderNo', label: '业务订单号', minWidth: 170 },
{ key: 'inboundOrderNo', label: '入库单号', minWidth: 160 },
{ key: 'outboundOrderNo', label: '出库单号', minWidth: 160 },
{ key: 'location', label: '库位', minWidth: 120 },
{ key: 'inboundQuantity', label: '入库数量', width: 110, align: 'right' },
{ key: 'outboundQuantity', label: '出库数量', width: 110, align: 'right' },
{ key: 'balance', label: '结存', width: 100, align: 'right' },
{ key: 'operator', label: '操作人', minWidth: 120 },
{ key: 'remark', label: '备注', minWidth: 180 },
]
function back() {
void router.push({ name: 'overseas-inventory' })
}
const route = useRoute(); const router = useRouter(); const inventoryId = computed(() => String(route.params.inventoryId || '')); const customerId = computed(() => String(route.query.customerId || ''))
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 productValues = computed(() => ({ 客户: text(balance.value?.customerName), 海外仓: text(balance.value?.overseasWarehouseName), SKU编码: text(balance.value?.skuCode), 中文品名: text(balance.value?.productNameZh), 英文品名: text(balance.value?.productNameEn), 更新时间: 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: text(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: text(item.operatorId), 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 || !customerId.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({ customerId: customerId.value, 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, customerId.value), listInventoryLedger({ customerId: customerId.value, 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 title="库存详情" reference-label="库存记录" :reference-value="inventoryId" back-label="返回库存管理" domain="库存详情" @back="back">
<section 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>—</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>—</dd></div></dl>
</section>
</section>
<section class="ledger-section" aria-labelledby="inventory-location-heading">
<h2 id="inventory-location-heading">库位库存</h2>
<DataTable :data="[]" :columns="locationColumns" column-settings-mode="hide" empty-text="海外区域库位库存契约待接入" />
</section>
<section class="ledger-section" aria-labelledby="inventory-pallet-heading">
<h2 id="inventory-pallet-heading">托盘箱号信息</h2>
<DataTable :data="[]" :columns="palletColumns" column-settings-mode="hide" empty-text="海外区域托盘箱号契约待接入" />
</section>
<section class="ledger-section" aria-labelledby="inventory-ledger-heading">
<h2 id="inventory-ledger-heading">进销存单据明细</h2>
<DataTable :data="[]" :columns="ledgerColumns" column-settings-mode="hide" empty-text="海外区域进销存契约待接入" />
</section>
</OverseasDetailShell>
</template>
<style scoped>
.overview-panel { display: grid; min-width: 0; grid-template-columns: repeat(2, minmax(0, 1fr)); border: 1px solid var(--cc-border); background: #fff; }
.overview-panel > section { min-width: 0; padding: 16px 18px 18px; border-right: 1px solid var(--cc-border); }
.overview-panel > section:last-child { border-right: 0; }
.overview-panel h2, .ledger-section h2 { margin: 0 0 12px; color: var(--cc-text); font-size: 15px; }
.detail-fields { margin: 0; border-top: 1px solid var(--cc-border); border-left: 1px solid var(--cc-border); }
.detail-fields > div { display: grid; min-width: 0; min-height: 46px; grid-template-columns: 120px minmax(0, 1fr); border-right: 1px solid var(--cc-border); border-bottom: 1px solid var(--cc-border); }
.detail-fields dt, .detail-fields dd { display: flex; min-width: 0; align-items: center; padding: 8px 10px; }
.detail-fields dt { background: #f8fafc; color: var(--cc-muted); font-size: 12px; }
.detail-fields dd { margin: 0; color: var(--cc-text); }
.ledger-section { min-width: 0; overflow: hidden; padding-top: 16px; border: 1px solid var(--cc-border); background: #fff; }
.ledger-section h2 { margin-right: 18px; margin-left: 18px; }
</style>
<template><OverseasDetailShell title="库存详情" reference-label="库存记录" :reference-value="inventoryId" 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"><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>
<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">当前流水接口未返回 SKU,按该库存记录的入库单范围展示,出库字段仅为只读事实。</p><DataTable :data="ledgerRows" :columns="ledgerColumns" column-settings-mode="hide" row-key="id" empty-text="暂无进销存明细" /></section></template>
</OverseasDetailShell></template>
<style scoped>.overview-panel{display:grid;min-width:0;grid-template-columns:repeat(2,minmax(0,1fr));border:1px solid var(--cc-border);background:#fff}.overview-panel>section{min-width:0;padding:16px 18px 18px;border-right:1px solid var(--cc-border)}.overview-panel>section:last-child{border-right:0}.overview-panel h2,.ledger-section h2{margin:0 0 12px;color:var(--cc-text);font-size:15px}.detail-fields{margin:0;border-top:1px solid var(--cc-border);border-left:1px solid var(--cc-border)}.detail-fields>div{display:grid;min-width:0;min-height:46px;grid-template-columns:120px minmax(0,1fr);border-right:1px solid var(--cc-border);border-bottom:1px solid var(--cc-border)}.detail-fields dt,.detail-fields dd{display:flex;min-width:0;align-items:center;padding:8px 10px}.detail-fields dt{background:#f8fafc;color:var(--cc-muted);font-size:12px}.detail-fields dd{margin:0;color:var(--cc-text)}.ledger-section{min-width:0;overflow:hidden;padding-top:16px;border:1px solid var(--cc-border);background:#fff}.ledger-section h2,.ledger-note{margin-right:18px;margin-left:18px}.ledger-note{color:var(--cc-muted);font-size:12px}</style>