This commit is contained in:
2026-08-21 11:20:09 +08:00
parent 595c998214
commit 6e9bb6e00c
45 changed files with 1051 additions and 329 deletions
@@ -1,8 +1,8 @@
<script setup lang="ts">
import { computed, onMounted, ref, watch } from 'vue'
import { computed, onMounted, ref } from 'vue'
import { BookOpen, Eye } from '@lucide/vue'
import { useRouter } from 'vue-router'
import { listPlatformTenants, type PlatformTenantResource } from '@/modules/iam'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
import AdminPage from '@/shared/components/AdminPage.vue'
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
@@ -10,12 +10,14 @@ import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.v
import ListPageLayout from '@/shared/components/ListPageLayout.vue'
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
import { getInventorySummary, listInventoryBalances, type InventoryBalanceDto, type InventoryDashboardDto } from '../api/inventory'
import { getInventorySummary, listInventoryBalances, type InventoryDashboardDto } from '../api/inventory'
import { listCustomerWarehouses, type CustomerWarehouseDto } from '../api/pallets'
import { toInventoryListItem } from '../inventoryPresentation'
import { overseasWarehousePermissions } from '../permissions'
import type { InventoryListItem } from '../types'
const router = useRouter()
const customers = ref<PlatformTenantResource[]>([])
const access = useAccessPreviewStore()
const warehouses = ref<CustomerWarehouseDto[]>([])
const rows = ref<InventoryListItem[]>([])
const summary = ref<InventoryDashboardDto | null>(null)
@@ -25,10 +27,9 @@ const loadError = ref<ApiErrorDetails | null>(null)
const page = ref(1)
const pageSize = ref(20)
const total = ref(0)
const filters = ref<QueryModel>({ customerId: '', warehouseId: '', owner: '', sku: '', productName: '', batchNo: '', location: '', status: '' })
const filters = ref<QueryModel>({ warehouseId: '', owner: '', sku: '', productName: '', batchNo: '', location: '', status: '' })
const fields = computed<QueryField[]>(() => [
{ key: 'customerId', type: 'select', label: '客户', width: 230, placeholder: '请选择客户', options: customers.value.map((item) => ({ label: item.tenantName, value: String(item.id) })), props: { filterable: true, clearable: true } },
{ key: 'warehouseId', type: 'select', label: '海外仓', width: 230, placeholder: '全部海外仓', options: warehouses.value.map((item) => ({ label: `${item.warehouseName}(${item.warehouseCode})`, value: String(item.id) })), props: { filterable: true, clearable: true, disabled: !filters.value.customerId || contextLoading.value } },
...(access.can(overseasWarehousePermissions.warehouseList) ? [{ key: 'warehouseId', type: 'select' as const, label: '海外仓', width: 230, placeholder: '全部海外仓', options: warehouses.value.map((item) => ({ label: `${item.warehouseName}(${item.warehouseCode})`, value: String(item.id) })), props: { filterable: true, clearable: true, disabled: contextLoading.value } }] : []),
{ key: 'owner', type: 'input', label: '客户/货主', placeholder: '请输入客户或货主' },
{ key: 'sku', type: 'input', label: 'SKU编码', placeholder: '请输入SKU编码' },
{ key: 'productName', type: 'input', label: '中文品名', placeholder: '请输入中文品名' },
@@ -57,44 +58,29 @@ const columns: DataTableColumn[] = [
{ key: 'updatedAt', prop: 'updatedAt', label: '更新时间', minWidth: 150 },
]
function text(value: unknown) { const normalized = String(value ?? '').trim(); return normalized || '—' }
function number(value: unknown) { const normalized = Number(value); return Number.isFinite(normalized) ? normalized : 0 }
function status(dto: InventoryBalanceDto) { if (number(dto.holdPackages) > 0) return '存在暂扣'; if (number(dto.availablePackages) <= 0) return '无可用库存'; return '正常' }
function toRow(dto: InventoryBalanceDto): InventoryListItem { return { id: String(dto.id), customerId: String(dto.customerId), warehouseId: String(dto.overseasWarehouseId || ''), inboundOrderNo: text(dto.inboundOrderNo) === '—' ? '' : String(dto.inboundOrderNo), inboundCode: text(dto.inboundCode) === '—' ? '' : String(dto.inboundCode), owner: text(dto.customerName), warehouse: text(dto.overseasWarehouseName || dto.overseasWarehouseCode), sku: dto.skuCodes?.length ? dto.skuCodes.join('、') : '—', productName: dto.productNames?.length ? dto.productNames.join('、') : '—', location: dto.warehouseLocationCodes?.length ? dto.warehouseLocationCodes.join('、') : '—', onHand: number(dto.totalPackages), available: number(dto.availablePackages), held: number(dto.holdPackages), looseboard: '—', inTransit: number(dto.inTransitPackages), purchase: number(dto.procurementPackages), status: status(dto), updatedAt: text(dto.updatedAt) } }
async function loadCustomers() {
async function loadWarehouses() {
if (!access.can(overseasWarehousePermissions.warehouseList)) return
contextLoading.value = true; loadError.value = null
try { customers.value = (await listPlatformTenants({ page: 1, size: 200, status: 'ACTIVE' })).records }
catch (error) { loadError.value = getApiErrorDetails(error, '客户列表加载失败,请重试') }
finally { contextLoading.value = false }
}
async function loadWarehouses(customerId: string) {
warehouses.value = []; filters.value.warehouseId = ''; rows.value = []; total.value = 0; summary.value = null
if (!customerId) return
contextLoading.value = true; loadError.value = null
try { warehouses.value = (await listCustomerWarehouses(customerId)).records }
try { warehouses.value = (await listCustomerWarehouses()).records }
catch (error) { loadError.value = getApiErrorDetails(error, '海外仓列表加载失败,请重试') }
finally { contextLoading.value = false }
}
function query() {
return { customerId: String(filters.value.customerId || ''), page: page.value, size: pageSize.value, customerName: String(filters.value.owner || '').trim() || undefined, overseasWarehouseId: String(filters.value.warehouseId || '') || undefined, skuCode: String(filters.value.sku || '').trim() || undefined, productName: String(filters.value.productName || '').trim() || undefined, batchNo: String(filters.value.batchNo || '').trim() || undefined, warehouseLocationCode: String(filters.value.location || '').trim() || undefined, inventoryStatus: String(filters.value.status || '') || undefined }
return { page: page.value, size: pageSize.value, customerName: String(filters.value.owner || '').trim() || undefined, overseasWarehouseId: String(filters.value.warehouseId || '') || undefined, skuCode: String(filters.value.sku || '').trim() || undefined, productName: String(filters.value.productName || '').trim() || undefined, batchNo: String(filters.value.batchNo || '').trim() || undefined, warehouseLocationCode: String(filters.value.location || '').trim() || undefined, inventoryStatus: String(filters.value.status || '') || undefined }
}
async function load(model: QueryModel = filters.value) {
Object.assign(filters.value, model)
if (!filters.value.customerId) { rows.value = []; total.value = 0; summary.value = null; return }
loading.value = true; loadError.value = null
try { const [pageResult, summaryResult] = await Promise.all([listInventoryBalances(query()), getInventorySummary(query())]); rows.value = pageResult.records.map(toRow); total.value = pageResult.total; summary.value = summaryResult }
try { const [pageResult, summaryResult] = await Promise.all([listInventoryBalances(query()), getInventorySummary(query())]); rows.value = pageResult.records.map(toInventoryListItem); total.value = pageResult.total; summary.value = summaryResult }
catch (error) { rows.value = []; total.value = 0; summary.value = null; loadError.value = getApiErrorDetails(error, '库存数据加载失败,请重试') }
finally { loading.value = false }
}
function search(model: QueryModel) { page.value = 1; void load(model) }
function openDetail(source: object) { const row = source as InventoryListItem; void router.push({ name: 'overseas-inventory-detail', params: { inventoryId: row.id }, query: { customerId: row.customerId, overseasWarehouseId: row.warehouseId, inboundOrderNo: row.inboundOrderNo, inboundCode: row.inboundCode, skuCode: row.sku === '—' ? undefined : row.sku.split('、')[0] } }) }
watch(() => String(filters.value.customerId || ''), (value, oldValue) => { if (value !== oldValue) void loadWarehouses(value) })
onMounted(loadCustomers)
function openDetail(source: object) { const row = source as InventoryListItem; void router.push({ name: 'overseas-inventory-detail', params: { inventoryId: row.id }, query: { overseasWarehouseId: row.warehouseId, inboundOrderNo: row.inboundOrderNo, inboundCode: row.inboundCode, skuCode: row.sku === '—' ? undefined : row.sku.split('、')[0] } }) }
onMounted(() => { void loadWarehouses(); void load() })
</script>
<template>
@@ -102,7 +88,7 @@ onMounted(loadCustomers)
<template #toolbar><QueryToolbar v-model="filters" class="surface-panel" :fields="fields" :loading="loading || contextLoading" ariaLabel="库存筛选" preference-key="overseas-inventory-query" @search="search" @reset="page = 1; rows = []; total = 0; summary = null" /></template>
<template #metrics><section class="inventory-metrics" aria-label="库存汇总"><div v-for="metric in metrics" :key="metric[0]" class="inventory-metric" :class="`is-${metric[2]}`"><span>{{ metric[0] }}</span><strong>{{ metric[1] }}</strong></div></section></template>
<template v-if="loadError" #state><LoadErrorState title="库存数据加载失败" :message="loadError.message" :loading="loading || contextLoading" @retry="load"><template #details><ApiErrorMeta :details="loadError" /></template></LoadErrorState></template>
<section v-if="!loadError" class="surface-panel table-panel"><DataTable v-model:page="page" v-model:page-size="pageSize" :data="rows" :columns="columns" :loading="loading" :total="total" preference-key="overseas-inventory" row-key="id" :empty-text="filters.customerId ? '暂无库存记录' : '请先选择客户后查询'" @update:page="load()" @update:page-size="page = 1; load()">
<section v-if="!loadError" class="surface-panel table-panel"><DataTable v-model:page="page" v-model:page-size="pageSize" :data="rows" :columns="columns" :loading="loading" :total="total" preference-key="overseas-inventory" row-key="id" empty-text="暂无库存记录" @update:page="load()" @update:page-size="page = 1; load()">
<template #toolbar-actions><el-button @click="router.push({ name: 'overseas-inventory-transactions' })"><BookOpen :size="16" />进销存记录</el-button></template>
<template #actions="{ row }"><el-button size="small" plain type="primary" @click="openDetail(row)"><Eye :size="15" />查看</el-button></template>
<template #pagination-left>共 {{ total }} 条库存记录</template>