121 lines
9.9 KiB
Vue
121 lines
9.9 KiB
Vue
<script setup lang="ts">
|
||
import { useExportTask } from '@/modules/downloads'
|
||
import '@/styles/adminDensity.css'
|
||
import { computed, onMounted, ref } from 'vue'
|
||
import { BookOpen, Download } from '@lucide/vue'
|
||
import { useRouter } from 'vue-router'
|
||
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'
|
||
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
|
||
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 InventoryDashboardDto } from '../api/inventory'
|
||
import { ElMessage } from 'element-plus'
|
||
import { listReceivingOverseasWarehouses, type ReceivingOverseasWarehouseDto } from '../api/pallets'
|
||
import { toInventoryListItem } from '../inventoryPresentation'
|
||
import { overseasWarehousePermissions } from '../permissions'
|
||
import type { InventoryListItem } from '../types'
|
||
|
||
const submitExport = useExportTask()
|
||
const router = useRouter()
|
||
const access = useAccessPreviewStore()
|
||
const warehouses = ref<ReceivingOverseasWarehouseDto[]>([])
|
||
const rows = ref<InventoryListItem[]>([])
|
||
const summary = ref<InventoryDashboardDto | null>(null)
|
||
const loading = ref(false)
|
||
const contextLoading = ref(false)
|
||
const loadError = ref<ApiErrorDetails | null>(null)
|
||
const page = ref(1)
|
||
const pageSize = ref(10)
|
||
const total = ref(0)
|
||
const exportLoading = ref(false)
|
||
const filters = ref<QueryModel>({ warehouseId: '', owner: '', sku: '', productName: '', batchNo: '', location: '', status: '' })
|
||
const fields = computed<QueryField[]>(() => [
|
||
...(access.can(overseasWarehousePermissions.warehouseList) ? [{ key: 'warehouseId', type: 'select' as const, label: '海外仓', width: 200, 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: '客户/货主', width: 190, placeholder: '请输入客户或货主' },
|
||
{ key: 'sku', type: 'input', label: 'SKU编码', width: 180, placeholder: '请输入SKU编码' },
|
||
{ key: 'productName', type: 'input', label: '中文品名', width: 190, placeholder: '请输入中文品名' },
|
||
{ key: 'batchNo', type: 'input', label: '批次号', width: 170, placeholder: '请输入批次号' },
|
||
{ key: 'location', type: 'input', label: '库位', width: 200, placeholder: '请输入库位(可空)' },
|
||
{ key: 'status', type: 'select', label: '库存状态', width: 170, placeholder: '全部库存状态', options: [{ label: '有可用库存', value: 'AVAILABLE' }, { label: '存在暂扣', value: 'HOLD' }, { label: '已分配', value: 'ALLOCATED' }, { label: '在途', value: 'IN_TRANSIT' }, { label: '收货中', value: 'RECEIVING' }, { label: '采购库存', value: 'PROCUREMENT' }] },
|
||
])
|
||
const metrics = computed(() => [
|
||
['在库总量', summary.value?.totalPackages ?? 0, 'primary'], ['可用库存', summary.value?.availablePackages ?? 0, 'success'], ['暂扣库存', summary.value?.holdPackages ?? 0, 'danger'],
|
||
['美本土散板', '—', 'neutral'], ['在途库存', summary.value?.inTransitPackages ?? 0, 'warning'], ['采购库存', summary.value?.procurementPackages ?? 0, 'neutral'],
|
||
] as const)
|
||
const columns: DataTableColumn[] = [
|
||
{ key: 'actions', label: '操作', width: 110, fixed: 'left', slotName: 'actions' },
|
||
{ key: 'owner', prop: 'owner', label: '客户', minWidth: 160 },
|
||
{ key: 'warehouse', prop: 'warehouse', label: '海外仓', minWidth: 170 },
|
||
{ key: 'sku', prop: 'sku', label: 'SKU编码', minWidth: 150 },
|
||
{ key: 'productName', prop: 'productName', label: '中文品名', minWidth: 180 },
|
||
{ key: 'location', prop: 'location', label: '库位', minWidth: 140, defaultVisible: false },
|
||
{ key: 'onHand', prop: 'onHand', label: '在库总量', width: 105, align: 'right' },
|
||
{ key: 'available', prop: 'available', label: '可用库存', width: 105, align: 'right' },
|
||
{ key: 'held', prop: 'held', label: '暂扣库存', width: 105, align: 'right' },
|
||
{ key: 'looseboard', prop: 'looseboard', label: '美本土散板', width: 115, align: 'right' },
|
||
{ key: 'inTransit', prop: 'inTransit', label: '在途库存', width: 105, align: 'right' },
|
||
{ key: 'updatedAt', prop: 'updatedAt', label: '更新时间', minWidth: 150 },
|
||
]
|
||
|
||
async function loadWarehouses() {
|
||
if (!access.can(overseasWarehousePermissions.warehouseList)) return
|
||
contextLoading.value = true; loadError.value = null
|
||
try { warehouses.value = (await listReceivingOverseasWarehouses()).records }
|
||
catch (error) { loadError.value = getApiErrorDetails(error, '海外仓列表加载失败,请重试') }
|
||
finally { contextLoading.value = false }
|
||
}
|
||
|
||
function query() {
|
||
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)
|
||
loading.value = true; loadError.value = null
|
||
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: { overseasWarehouseId: row.warehouseId, inboundOrderNo: row.inboundOrderNo, inboundCode: row.inboundCode, skuCode: row.sku === '—' ? undefined : row.sku.split('、')[0] } }) }
|
||
async function exportBalances() { exportLoading.value = true; try { const q = query(); const { page: _page, size: _size, ...params } = q; await submitExport('OVERSEAS_INVENTORY_BALANCE', '库存余额', params) } catch { ElMessage.error('库存导出失败,请重试') } finally { exportLoading.value = false } }
|
||
onMounted(() => { void loadWarehouses(); void load() })
|
||
</script>
|
||
|
||
<template>
|
||
<AdminPage class="admin-density"><ListPageLayout aria-label="海外仓库存管理">
|
||
|
||
|
||
|
||
<section><DataTable appearance="integrated" :stripe="false" column-settings-mode="show" :results-visible="!loadError" 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-start><h1>库存管理</h1></template>
|
||
<template #filters><QueryToolbar v-model="filters" :fields="fields" :loading="loading || contextLoading" ariaLabel="库存筛选" preference-key="overseas-inventory-query" @search="search" @reset="page = 1; rows = []; total = 0; summary = null" /></template>
|
||
<template #notices><template v-if="loadError"><LoadErrorState title="库存数据加载失败" :message="loadError.message" :loading="loading || contextLoading" @retry="load"><template #details><ApiErrorMeta :details="loadError" /></template></LoadErrorState></template></template>
|
||
<template #before-results><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 #toolbar-actions><el-button v-if="access.can(overseasWarehousePermissions.inventoryExport) && access.can('platform:download:list')" plain :loading="exportLoading" @click="exportBalances"><Download :size="16" />导出库存</el-button><el-button @click="router.push({ name: 'overseas-inventory-transactions' })"><BookOpen :size="16" />进销存记录</el-button></template>
|
||
<template #actions="{ row }"><div class="density-actions row-actions admin-text-actions"><el-button class="density-action" data-tone="neutral" size="small" @click="openDetail(row)">查看</el-button></div></template>
|
||
<template #pagination-left>共 {{ total }} 条库存记录</template>
|
||
</DataTable></section>
|
||
</ListPageLayout></AdminPage>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.inventory-metrics { display: grid; grid-template-columns: repeat(6, minmax(130px, 1fr)); overflow: hidden; border: 1px solid var(--cc-border); border-radius: var(--admin-radius); background: #fff; }
|
||
.inventory-metric:last-child { border-right: 0; }
|
||
.inventory-metric span { color: #64748b; font-size: var(--admin-font-caption); }
|
||
.inventory-metric.is-primary strong { color: var(--cc-primary); }
|
||
.inventory-metric.is-success strong { color: #16a34a; }
|
||
.inventory-metric.is-danger strong { color: #dc2626; }
|
||
.inventory-metric.is-warning strong { color: #d97706; }
|
||
|
||
.inventory-metric { display: flex; min-width: 0; flex-direction: row; gap: var(--admin-space-3); padding: var(--admin-space-3) var(--admin-space-5); border-right: 1px solid var(--cc-border); align-items: baseline; }
|
||
.inventory-metric strong { color: #334155; font-size: var(--admin-font-body); line-height: 20px; overflow-wrap: anywhere; }
|
||
|
||
</style>
|