优化
This commit is contained in:
@@ -1,75 +1,47 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { BookOpen, Eye } from '@lucide/vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import AdminPage from '@/shared/components/AdminPage.vue'
|
||||
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
|
||||
import ListPageLayout from '@/shared/components/ListPageLayout.vue'
|
||||
import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
|
||||
import ContractPendingNotice from '../components/ContractPendingNotice.vue'
|
||||
import { listPlatformTenants, type PlatformTenantResource } from '@/modules/iam'
|
||||
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 { formatOverseasDateTime } from '../lastMilePresentation'
|
||||
import type { InventoryListItem } from '../types'
|
||||
|
||||
const rows = ref<InventoryListItem[]>([])
|
||||
const router = useRouter()
|
||||
const page = ref(1)
|
||||
const pageSize = ref(20)
|
||||
const filters = ref<QueryModel>({ warehouse: '', owner: '', sku: '', productName: '', batchNo: '', location: '', status: '' })
|
||||
const fields: QueryField[] = [
|
||||
{ key: 'warehouse', type: 'select', label: '海外仓', placeholder: '全部海外仓', options: [] },
|
||||
{ key: 'owner', type: 'input', label: '客户/货主', placeholder: '请输入客户或货主' },
|
||||
{ key: 'sku', type: 'input', label: 'SKU编码', placeholder: '请输入SKU编码' },
|
||||
{ key: 'productName', type: 'input', label: '中文品名', placeholder: '请输入中文品名' },
|
||||
{ key: 'batchNo', type: 'input', label: '批次号', placeholder: '请输入批次号' },
|
||||
{ key: 'location', type: 'input', label: '库位', placeholder: '请输入库位' },
|
||||
{ key: 'status', type: 'select', label: '库存状态', placeholder: '全部库存状态', options: ['正常', '库存不足', '无可用库存', '存在暂扣', '存在美本土散版'].map((value) => ({ label: value, value })) },
|
||||
]
|
||||
const metrics = [
|
||||
['在库总量', 0, 'primary'], ['可用库存', 0, 'success'], ['暂扣库存', 0, 'danger'],
|
||||
['美本土散版', 0, 'neutral'], ['在途库存', 0, 'warning'], ['采购库存', 0, 'neutral'],
|
||||
] as const
|
||||
const rows = ref<InventoryListItem[]>([]); const customers = ref<PlatformTenantResource[]>([]); const router = useRouter(); const page = ref(1); const pageSize = ref(20); const total = ref(0)
|
||||
const loading = ref(false); const customersLoading = ref(false); const loadError = ref<ApiErrorDetails | null>(null); const customersError = ref<ApiErrorDetails | null>(null); const summary = ref<InventoryDashboardDto | null>(null)
|
||||
const filters = ref<QueryModel>({ customerId: '', warehouse: '', owner: '', sku: '', productName: '', batchNo: '', location: '', status: '' })
|
||||
const warehouseOptions = computed(() => { const values = new Map<string, string>(); for (const row of rows.value) if (row.warehouseId && row.warehouse !== '—') values.set(row.warehouseId, row.warehouse); return [...values].map(([value, label]) => ({ label, value })) })
|
||||
const fields = computed<QueryField[]>(() => [
|
||||
{ key: 'customerId', type: 'select', label: '客户名称', placeholder: '请选择客户', width: 240, props: { filterable: true, clearable: true, loading: customersLoading.value }, options: customers.value.map((item) => ({ label: item.tenantName, value: String(item.id) })) },
|
||||
{ key: 'warehouse', type: 'select', label: '海外仓', placeholder: '全部海外仓', options: warehouseOptions.value }, { key: 'owner', type: 'input', label: '客户/货主', placeholder: '请输入客户或货主' },
|
||||
{ key: 'sku', type: 'input', label: 'SKU编码', placeholder: '请输入SKU编码' }, { key: 'productName', type: 'input', label: '中文品名', placeholder: '请输入中文品名' },
|
||||
{ key: 'batchNo', type: 'input', label: '批次号', placeholder: '详情接口支持', props: { disabled: true } }, { key: 'location', type: 'input', label: '库位', placeholder: '详情接口支持', props: { disabled: true } },
|
||||
{ key: 'status', type: 'select', label: '库存状态', placeholder: '全部库存状态', options: [{ label: '可用', value: 'AVAILABLE' }, { label: '暂扣', value: 'HOLD' }, { label: '已分配', value: 'ALLOCATED' }, { label: '已拣货', value: 'PICKED' }, { 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: '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: 'purchase', prop: 'purchase', label: '采购库存', width: 105, align: 'right' },
|
||||
{ key: 'status', prop: 'status', label: '库存状态', width: 110 },
|
||||
{ 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: '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: 'purchase', prop: 'purchase', label: '采购库存', width: 105, align: 'right' }, { key: 'status', prop: 'status', label: '库存状态', width: 110 },
|
||||
{ key: 'updatedAt', prop: 'updatedAt', label: '更新时间', minWidth: 150 },
|
||||
]
|
||||
|
||||
function openDetail(source: object) {
|
||||
const row = source as InventoryListItem
|
||||
void router.push({ name: 'overseas-inventory-detail', params: { inventoryId: row.id } })
|
||||
}
|
||||
function text(value: unknown) { return String(value ?? '').trim() || '—' }
|
||||
function inventoryStatus(row: { holdPackages: number; availablePackages: number; totalPackages: number }) { if (row.holdPackages > 0) return '存在暂扣'; if (row.availablePackages <= 0) return '无可用库存'; return row.totalPackages > 0 ? '正常' : '库存不足' }
|
||||
async function loadCustomers() { customersLoading.value = true; customersError.value = null; try { const records: PlatformTenantResource[] = []; let current = 1; let pages = 1; do { const result = await listPlatformTenants({ page: current, size: 200, status: 'ACTIVE' }); records.push(...result.records); pages = Math.max(1, result.pages); current += 1 } while (current <= pages); customers.value = records } catch (error) { customersError.value = getApiErrorDetails(error, '客户列表加载失败,请重试') } finally { customersLoading.value = false } }
|
||||
function query() { return { customerId: String(filters.value.customerId || '').trim(), customerName: String(filters.value.owner || '').trim() || undefined, overseasWarehouseId: String(filters.value.warehouse || '').trim() || undefined, skuCode: String(filters.value.sku || '').trim() || undefined, productName: String(filters.value.productName || '').trim() || undefined, inventoryStatus: String(filters.value.status || '').trim() || undefined } }
|
||||
async function load(model: QueryModel = filters.value) { Object.assign(filters.value, model); const params = query(); if (!params.customerId) { rows.value = []; total.value = 0; summary.value = null; loadError.value = null; return }; loading.value = true; loadError.value = null; try { const [result, dashboard] = await Promise.all([listInventoryBalances({ ...params, page: page.value, size: pageSize.value }), getInventorySummary(params)]); rows.value = result.records.map((item) => ({ id: String(item.id), customerId: params.customerId, warehouseId: text(item.overseasWarehouseId) === '—' ? '' : String(item.overseasWarehouseId), inboundOrderNo: text(item.inboundOrderNo), inboundCode: text(item.inboundCode), owner: text(item.customerName), warehouse: text(item.overseasWarehouseName), sku: text(item.skuCode), productName: text(item.productNameZh), onHand: Number(item.totalPackages || 0), available: Number(item.availablePackages || 0), held: Number(item.holdPackages || 0), looseboard: null, inTransit: Number(item.inTransitPackages || 0), purchase: Number(item.procurementPackages || 0), status: inventoryStatus(item), updatedAt: formatOverseasDateTime(item.updatedAt) })); total.value = result.total; summary.value = dashboard } 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 reset(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, inboundOrderNo: row.inboundOrderNo, inboundCode: row.inboundCode, skuCode: row.sku, overseasWarehouseId: row.warehouseId } }) }
|
||||
function openTransactions() { const customerId = String(filters.value.customerId || ''); void router.push({ name: 'overseas-inventory-transactions', query: customerId ? { customerId } : {} }) }
|
||||
onMounted(loadCustomers)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AdminPage><ListPageLayout aria-label="海外仓库存管理">
|
||||
<template #toolbar><QueryToolbar v-model="filters" class="surface-panel" :fields="fields" ariaLabel="库存筛选" preference-key="overseas-inventory-query" @search="page = 1" /></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 #state><ContractPendingNotice domain="库存管理" /></template>
|
||||
<section class="surface-panel table-panel"><DataTable v-model:page="page" v-model:page-size="pageSize" :data="rows" :columns="columns" :total="0" preference-key="overseas-inventory" row-key="id" empty-text="海外区域库存查询契约待接入">
|
||||
<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>共 0 条库存记录</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: 8px; background: #fff; }
|
||||
.inventory-metric { display: flex; min-width: 0; flex-direction: column; gap: 5px; padding: 13px 16px; border-right: 1px solid var(--cc-border); }
|
||||
.inventory-metric:last-child { border-right: 0; }
|
||||
.inventory-metric span { color: #64748b; font-size: 12px; }
|
||||
.inventory-metric strong { color: #334155; font-size: 22px; line-height: 1; }
|
||||
.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; }
|
||||
</style>
|
||||
<template><AdminPage><ListPageLayout aria-label="海外仓库存管理"><template #toolbar><QueryToolbar v-model="filters" class="surface-panel" :fields="fields" :loading="loading" ariaLabel="库存筛选" preference-key="overseas-inventory-query" @search="search" @reset="reset" /></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="customersError || loadError" #state><LoadErrorState title="库存加载失败" :message="(customersError || loadError)?.message" :loading="customersLoading || loading" @retry="customersError ? loadCustomers() : load()"><template #details><ApiErrorMeta :details="(customersError || loadError)!" /></template></LoadErrorState></template>
|
||||
<section v-if="!customersError && !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()"><template #toolbar-actions><el-button @click="openTransactions"><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></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:8px;background:#fff}.inventory-metric{display:flex;min-width:0;flex-direction:column;gap:5px;padding:13px 16px;border-right:1px solid var(--cc-border)}.inventory-metric:last-child{border-right:0}.inventory-metric span{color:#64748b;font-size:12px}.inventory-metric strong{color:#334155;font-size:22px;line-height:1}.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}</style>
|
||||
|
||||
Reference in New Issue
Block a user