feat(wms):完成 inventory index 的迁移

pull/345/head
YunaiV 2026-05-18 23:15:34 +08:00
parent 0280df114f
commit 3d0917f1a9
4 changed files with 882 additions and 0 deletions

View File

@ -0,0 +1,249 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import { getWarehouseSimpleList } from '#/api/wms/md/warehouse';
export const INVENTORY_DIMENSION = {
ITEM: 'item',
WAREHOUSE: 'warehouse',
} as const;
export type InventoryDimension =
(typeof INVENTORY_DIMENSION)[keyof typeof INVENTORY_DIMENSION];
const dimensionOptions = [
{ label: '仓库', value: INVENTORY_DIMENSION.WAREHOUSE },
{ label: '商品', value: INVENTORY_DIMENSION.ITEM },
];
interface DimensionChangeEvent {
target?: {
value?: InventoryDimension;
};
}
type DimensionChangeHandler = (
dimension: InventoryDimension,
) => Promise<void> | void;
/** 统一解析 antd 事件对象和原始维度值 */
function getDimensionChangeValue(
value?: DimensionChangeEvent | InventoryDimension,
): InventoryDimension {
if (
value === INVENTORY_DIMENSION.ITEM ||
value === INVENTORY_DIMENSION.WAREHOUSE
) {
return value;
}
return value?.target?.value ?? INVENTORY_DIMENSION.WAREHOUSE;
}
/** 搜索表单 */
export function useGridFormSchema(
onDimensionChange: DimensionChangeHandler,
): VbenFormSchema[] {
return [
{
fieldName: 'type',
label: '统计维度',
component: 'RadioGroup',
defaultValue: INVENTORY_DIMENSION.WAREHOUSE,
componentProps: {
buttonStyle: 'solid',
optionType: 'button',
options: dimensionOptions,
onChange: (value: DimensionChangeEvent | InventoryDimension) => {
void onDimensionChange(getDimensionChangeValue(value));
},
},
},
{
fieldName: 'warehouseId',
label: '仓库',
component: 'ApiSelect',
componentProps: {
allowClear: true,
api: getWarehouseSimpleList,
labelField: 'name',
placeholder: '请选择仓库',
showSearch: true,
valueField: 'id',
},
},
{
fieldName: 'itemName',
label: '商品名称',
component: 'Input',
componentProps: {
allowClear: true,
placeholder: '请输入商品名称',
},
},
{
fieldName: 'itemCode',
label: '商品编号',
component: 'Input',
componentProps: {
allowClear: true,
placeholder: '请输入商品编号',
},
},
{
fieldName: 'skuName',
label: '规格名称',
component: 'Input',
componentProps: {
allowClear: true,
placeholder: '请输入规格名称',
},
},
{
fieldName: 'skuCode',
label: '规格编号',
component: 'Input',
componentProps: {
allowClear: true,
placeholder: '请输入规格编号',
},
},
];
}
const warehouseDimensionColumns: VxeTableGridOptions['columns'] = [
{
field: 'warehouseId',
title: '仓库',
minWidth: 160,
slots: { default: 'warehouseName' },
},
{
field: 'warehouseItemId',
title: '商品信息',
minWidth: 240,
slots: { default: 'itemInfo' },
},
{
field: 'skuId',
title: '规格信息',
minWidth: 220,
slots: { default: 'skuInfo' },
},
{
field: 'quantity',
title: '库存',
align: 'right',
minWidth: 130,
slots: { default: 'quantity' },
},
];
const itemDimensionColumns: VxeTableGridOptions['columns'] = [
{
field: 'warehouseItemId',
title: '商品信息',
minWidth: 240,
slots: { default: 'itemInfo' },
},
{
field: 'skuId',
title: '规格信息',
minWidth: 220,
slots: { default: 'skuInfo' },
},
{
field: 'warehouseId',
title: '仓库',
minWidth: 160,
slots: { default: 'warehouseName' },
},
{
field: 'quantity',
title: '库存',
align: 'right',
minWidth: 130,
slots: { default: 'quantity' },
},
];
/** 库存统计列表字段 */
export function useGridColumns(
dimension: InventoryDimension = INVENTORY_DIMENSION.WAREHOUSE,
): VxeTableGridOptions['columns'] {
return dimension === INVENTORY_DIMENSION.ITEM
? itemDimensionColumns
: warehouseDimensionColumns;
}
/** 库存选择弹窗搜索表单 */
export function useInventorySelectGridFormSchema(): VbenFormSchema[] {
return [
{
fieldName: 'itemName',
label: '商品名称',
component: 'Input',
componentProps: {
allowClear: true,
placeholder: '请输入商品名称',
},
},
{
fieldName: 'itemCode',
label: '商品编号',
component: 'Input',
componentProps: {
allowClear: true,
placeholder: '请输入商品编号',
},
},
{
fieldName: 'skuName',
label: '规格名称',
component: 'Input',
componentProps: {
allowClear: true,
placeholder: '请输入规格名称',
},
},
{
fieldName: 'skuCode',
label: '规格编号',
component: 'Input',
componentProps: {
allowClear: true,
placeholder: '请输入规格编号',
},
},
];
}
/** 库存选择弹窗列表字段 */
export function useInventorySelectGridColumns(): VxeTableGridOptions['columns'] {
return [
{ type: 'checkbox', width: 50 },
{
field: 'itemInfo',
title: '商品信息',
minWidth: 220,
slots: { default: 'itemInfo' },
},
{
field: 'skuInfo',
title: '规格信息',
minWidth: 220,
slots: { default: 'skuInfo' },
},
{
field: 'warehouseName',
title: '仓库',
minWidth: 180,
},
{
field: 'availableQuantity',
title: '可用库存',
align: 'right',
width: 130,
slots: { default: 'availableQuantity' },
},
];
}

View File

@ -0,0 +1,193 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { WmsInventoryApi } from '#/api/wms/inventory';
import { ref } from 'vue';
import { DocAlert, Page } from '@vben/common-ui';
import { Checkbox } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getInventoryPage } from '#/api/wms/inventory';
import { formatQuantity } from '#/views/wms/utils/format';
import {
INVENTORY_DIMENSION,
type InventoryDimension,
useGridColumns,
useGridFormSchema,
} from './data';
interface InventoryRow extends WmsInventoryApi.Inventory {
skuWarehouseId?: string;
warehouseItemId?: string;
}
defineOptions({ name: 'WmsInventory' });
const currentRows = ref<InventoryRow[]>([]);
const currentDimension = ref<InventoryDimension>(INVENTORY_DIMENSION.WAREHOUSE);
const filterZero = ref(false);
/** 补充合并单元格需要的复合 key */
function buildRows(items: WmsInventoryApi.Inventory[]) {
return items.map((item) => ({
...item,
skuWarehouseId: `${item.skuId || 0}-${item.warehouseId || 0}`,
warehouseItemId: `${item.warehouseId || 0}-${item.itemId || 0}`,
}));
}
/** 切换统计维度时同步表单、表头和查询参数 */
async function handleDimensionChange(dimension: InventoryDimension) {
// spanMethod
currentDimension.value = dimension;
//
await gridApi.grid.reloadColumn(useGridColumns(dimension) || []);
// VXE proxy type
await gridApi.formApi.setFieldValue('type', dimension);
const formValues = await gridApi.formApi.getValues();
gridApi.formApi.setLatestSubmissionValues(formValues);
// 使
await gridApi.reload(formValues);
}
/** 切换零库存过滤时按当前搜索条件重新查询 */
function handleFilterZeroChange() {
gridApi.query();
}
/** 按列字段读取行值,兼容 VXE 的 field/property */
function getRowPropertyValue(row: InventoryRow | undefined, property: string) {
return row?.[property as keyof InventoryRow];
}
/** 根据统计维度返回需要合并单元格的字段 */
function getRowSpanProperties() {
if (currentDimension.value === INVENTORY_DIMENSION.ITEM) {
return ['warehouseItemId', 'skuId', 'skuWarehouseId'];
}
return ['warehouseId', 'warehouseItemId'];
}
/** 合并库存统计的维度单元格 */
function handleSpanMethod({
column,
rowIndex,
}: {
column: { field?: string; property?: string };
rowIndex: number;
}) {
const property = column.field || column.property;
if (!property || !getRowSpanProperties().includes(property)) {
return { colspan: 1, rowspan: 1 };
}
const row = currentRows.value[rowIndex];
if (
rowIndex > 0 &&
getRowPropertyValue(currentRows.value[rowIndex - 1], property) ===
getRowPropertyValue(row, property)
) {
return { colspan: 0, rowspan: 0 };
}
let rowspan = 1;
for (let index = rowIndex + 1; index < currentRows.value.length; index += 1) {
if (
getRowPropertyValue(currentRows.value[index], property) !==
getRowPropertyValue(row, property)
) {
break;
}
rowspan += 1;
}
return { colspan: 1, rowspan };
}
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
schema: useGridFormSchema(handleDimensionChange),
},
gridOptions: {
columns: useGridColumns(),
height: 'auto',
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
// reload(formValues) type
const nextDimension = (formValues.type ||
currentDimension.value ||
INVENTORY_DIMENSION.WAREHOUSE) as InventoryDimension;
currentDimension.value = nextDimension;
await gridApi.grid.reloadColumn(useGridColumns(nextDimension) || []);
// type
const queryParams = { ...formValues };
Reflect.deleteProperty(queryParams, 'type');
const data = await getInventoryPage({
pageNo: page.currentPage,
pageSize: page.pageSize,
...queryParams,
type: nextDimension,
onlyPositiveQuantity: filterZero.value ? true : undefined,
});
// spanMethod rowspan
currentRows.value = buildRows(data.list || []);
return {
...data,
list: currentRows.value,
};
},
},
},
rowConfig: {
keyField: 'id',
isHover: true,
},
spanMethod: handleSpanMethod,
toolbarConfig: {
refresh: true,
search: true,
},
} as VxeTableGridOptions<InventoryRow>,
});
</script>
<template>
<Page auto-content-height>
<template #doc>
<DocAlert
title="【库存】库存记录、流水、统计"
url="https://doc.iocoder.cn/wms/inventory/"
/>
</template>
<Grid table-title="">
<template #toolbar-tools>
<Checkbox v-model:checked="filterZero" @change="handleFilterZeroChange">
过滤掉库存为 0 的商品
</Checkbox>
</template>
<template #warehouseName="{ row }">
{{ row.warehouseName || '-' }}
</template>
<template #itemInfo="{ row }">
<div>{{ row.itemName || '-' }}</div>
<div v-if="row.itemCode" class="text-xs text-gray-500">
商品编号{{ row.itemCode }}
</div>
</template>
<template #skuInfo="{ row }">
<div>{{ row.skuName || '-' }}</div>
<div v-if="row.skuCode" class="text-xs text-gray-500">
规格编号{{ row.skuCode }}
</div>
</template>
<template #quantity="{ row }">
{{ formatQuantity(row.quantity) }}
</template>
</Grid>
</Page>
</template>

View File

@ -0,0 +1,247 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import { getWarehouseSimpleList } from '#/api/wms/md/warehouse';
export const INVENTORY_DIMENSION = {
ITEM: 'item',
WAREHOUSE: 'warehouse',
} as const;
export type InventoryDimension =
(typeof INVENTORY_DIMENSION)[keyof typeof INVENTORY_DIMENSION];
const dimensionOptions = [
{ label: '仓库', value: INVENTORY_DIMENSION.WAREHOUSE },
{ label: '商品', value: INVENTORY_DIMENSION.ITEM },
];
interface DimensionChangeEvent {
target?: {
value?: InventoryDimension;
};
}
type DimensionChangeHandler = (
dimension: InventoryDimension,
) => Promise<void> | void;
/** 统一解析 ele 原始值和兼容事件对象 */
function getDimensionChangeValue(
value?: DimensionChangeEvent | InventoryDimension,
): InventoryDimension {
if (
value === INVENTORY_DIMENSION.ITEM ||
value === INVENTORY_DIMENSION.WAREHOUSE
) {
return value;
}
return value?.target?.value ?? INVENTORY_DIMENSION.WAREHOUSE;
}
/** 搜索表单 */
export function useGridFormSchema(
onDimensionChange: DimensionChangeHandler,
): VbenFormSchema[] {
return [
{
fieldName: 'type',
label: '统计维度',
component: 'RadioGroup',
defaultValue: INVENTORY_DIMENSION.WAREHOUSE,
componentProps: {
options: dimensionOptions,
onChange: (value: DimensionChangeEvent | InventoryDimension) => {
void onDimensionChange(getDimensionChangeValue(value));
},
},
},
{
fieldName: 'warehouseId',
label: '仓库',
component: 'ApiSelect',
componentProps: {
api: getWarehouseSimpleList,
clearable: true,
filterable: true,
labelField: 'name',
placeholder: '请选择仓库',
valueField: 'id',
},
},
{
fieldName: 'itemName',
label: '商品名称',
component: 'Input',
componentProps: {
clearable: true,
placeholder: '请输入商品名称',
},
},
{
fieldName: 'itemCode',
label: '商品编号',
component: 'Input',
componentProps: {
clearable: true,
placeholder: '请输入商品编号',
},
},
{
fieldName: 'skuName',
label: '规格名称',
component: 'Input',
componentProps: {
clearable: true,
placeholder: '请输入规格名称',
},
},
{
fieldName: 'skuCode',
label: '规格编号',
component: 'Input',
componentProps: {
clearable: true,
placeholder: '请输入规格编号',
},
},
];
}
const warehouseDimensionColumns: VxeTableGridOptions['columns'] = [
{
field: 'warehouseId',
title: '仓库',
minWidth: 160,
slots: { default: 'warehouseName' },
},
{
field: 'warehouseItemId',
title: '商品信息',
minWidth: 240,
slots: { default: 'itemInfo' },
},
{
field: 'skuId',
title: '规格信息',
minWidth: 220,
slots: { default: 'skuInfo' },
},
{
field: 'quantity',
title: '库存',
align: 'right',
minWidth: 130,
slots: { default: 'quantity' },
},
];
const itemDimensionColumns: VxeTableGridOptions['columns'] = [
{
field: 'warehouseItemId',
title: '商品信息',
minWidth: 240,
slots: { default: 'itemInfo' },
},
{
field: 'skuId',
title: '规格信息',
minWidth: 220,
slots: { default: 'skuInfo' },
},
{
field: 'warehouseId',
title: '仓库',
minWidth: 160,
slots: { default: 'warehouseName' },
},
{
field: 'quantity',
title: '库存',
align: 'right',
minWidth: 130,
slots: { default: 'quantity' },
},
];
/** 库存统计列表字段 */
export function useGridColumns(
dimension: InventoryDimension = INVENTORY_DIMENSION.WAREHOUSE,
): VxeTableGridOptions['columns'] {
return dimension === INVENTORY_DIMENSION.ITEM
? itemDimensionColumns
: warehouseDimensionColumns;
}
/** 库存选择弹窗搜索表单 */
export function useInventorySelectGridFormSchema(): VbenFormSchema[] {
return [
{
fieldName: 'itemName',
label: '商品名称',
component: 'Input',
componentProps: {
clearable: true,
placeholder: '请输入商品名称',
},
},
{
fieldName: 'itemCode',
label: '商品编号',
component: 'Input',
componentProps: {
clearable: true,
placeholder: '请输入商品编号',
},
},
{
fieldName: 'skuName',
label: '规格名称',
component: 'Input',
componentProps: {
clearable: true,
placeholder: '请输入规格名称',
},
},
{
fieldName: 'skuCode',
label: '规格编号',
component: 'Input',
componentProps: {
clearable: true,
placeholder: '请输入规格编号',
},
},
];
}
/** 库存选择弹窗列表字段 */
export function useInventorySelectGridColumns(): VxeTableGridOptions['columns'] {
return [
{ type: 'checkbox', width: 50 },
{
field: 'itemInfo',
title: '商品信息',
minWidth: 220,
slots: { default: 'itemInfo' },
},
{
field: 'skuInfo',
title: '规格信息',
minWidth: 220,
slots: { default: 'skuInfo' },
},
{
field: 'warehouseName',
title: '仓库',
minWidth: 180,
},
{
field: 'availableQuantity',
title: '可用库存',
align: 'right',
width: 130,
slots: { default: 'availableQuantity' },
},
];
}

View File

@ -0,0 +1,193 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { WmsInventoryApi } from '#/api/wms/inventory';
import { ref } from 'vue';
import { DocAlert, Page } from '@vben/common-ui';
import { ElCheckbox } from 'element-plus';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getInventoryPage } from '#/api/wms/inventory';
import { formatQuantity } from '#/views/wms/utils/format';
import {
INVENTORY_DIMENSION,
type InventoryDimension,
useGridColumns,
useGridFormSchema,
} from './data';
interface InventoryRow extends WmsInventoryApi.Inventory {
skuWarehouseId?: string;
warehouseItemId?: string;
}
defineOptions({ name: 'WmsInventory' });
const currentRows = ref<InventoryRow[]>([]);
const currentDimension = ref<InventoryDimension>(INVENTORY_DIMENSION.WAREHOUSE);
const filterZero = ref(false);
/** 补充合并单元格需要的复合 key */
function buildRows(items: WmsInventoryApi.Inventory[]) {
return items.map((item) => ({
...item,
skuWarehouseId: `${item.skuId || 0}-${item.warehouseId || 0}`,
warehouseItemId: `${item.warehouseId || 0}-${item.itemId || 0}`,
}));
}
/** 切换统计维度时同步表单、表头和查询参数 */
async function handleDimensionChange(dimension: InventoryDimension) {
// spanMethod
currentDimension.value = dimension;
//
await gridApi.grid.reloadColumn(useGridColumns(dimension) || []);
// VXE proxy type
await gridApi.formApi.setFieldValue('type', dimension);
const formValues = await gridApi.formApi.getValues();
gridApi.formApi.setLatestSubmissionValues(formValues);
// 使
await gridApi.reload(formValues);
}
/** 切换零库存过滤时按当前搜索条件重新查询 */
function handleFilterZeroChange() {
gridApi.query();
}
/** 按列字段读取行值,兼容 VXE 的 field/property */
function getRowPropertyValue(row: InventoryRow | undefined, property: string) {
return row?.[property as keyof InventoryRow];
}
/** 根据统计维度返回需要合并单元格的字段 */
function getRowSpanProperties() {
if (currentDimension.value === INVENTORY_DIMENSION.ITEM) {
return ['warehouseItemId', 'skuId', 'skuWarehouseId'];
}
return ['warehouseId', 'warehouseItemId'];
}
/** 合并库存统计的维度单元格 */
function handleSpanMethod({
column,
rowIndex,
}: {
column: { field?: string; property?: string };
rowIndex: number;
}) {
const property = column.field || column.property;
if (!property || !getRowSpanProperties().includes(property)) {
return { colspan: 1, rowspan: 1 };
}
const row = currentRows.value[rowIndex];
if (
rowIndex > 0 &&
getRowPropertyValue(currentRows.value[rowIndex - 1], property) ===
getRowPropertyValue(row, property)
) {
return { colspan: 0, rowspan: 0 };
}
let rowspan = 1;
for (let index = rowIndex + 1; index < currentRows.value.length; index += 1) {
if (
getRowPropertyValue(currentRows.value[index], property) !==
getRowPropertyValue(row, property)
) {
break;
}
rowspan += 1;
}
return { colspan: 1, rowspan };
}
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
schema: useGridFormSchema(handleDimensionChange),
},
gridOptions: {
columns: useGridColumns(),
height: 'auto',
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
// reload(formValues) type
const nextDimension = (formValues.type ||
currentDimension.value ||
INVENTORY_DIMENSION.WAREHOUSE) as InventoryDimension;
currentDimension.value = nextDimension;
await gridApi.grid.reloadColumn(useGridColumns(nextDimension) || []);
// type
const queryParams = { ...formValues };
Reflect.deleteProperty(queryParams, 'type');
const data = await getInventoryPage({
pageNo: page.currentPage,
pageSize: page.pageSize,
...queryParams,
type: nextDimension,
onlyPositiveQuantity: filterZero.value ? true : undefined,
});
// spanMethod rowspan
currentRows.value = buildRows(data.list || []);
return {
...data,
list: currentRows.value,
};
},
},
},
rowConfig: {
keyField: 'id',
isHover: true,
},
spanMethod: handleSpanMethod,
toolbarConfig: {
refresh: true,
search: true,
},
} as VxeTableGridOptions<InventoryRow>,
});
</script>
<template>
<Page auto-content-height>
<template #doc>
<DocAlert
title="【库存】库存记录、流水、统计"
url="https://doc.iocoder.cn/wms/inventory/"
/>
</template>
<Grid table-title="">
<template #toolbar-tools>
<ElCheckbox v-model="filterZero" @change="handleFilterZeroChange">
过滤掉库存为 0 的商品
</ElCheckbox>
</template>
<template #warehouseName="{ row }">
{{ row.warehouseName || '-' }}
</template>
<template #itemInfo="{ row }">
<div>{{ row.itemName || '-' }}</div>
<div v-if="row.itemCode" class="text-xs text-gray-500">
商品编号{{ row.itemCode }}
</div>
</template>
<template #skuInfo="{ row }">
<div>{{ row.skuName || '-' }}</div>
<div v-if="row.skuCode" class="text-xs text-gray-500">
规格编号{{ row.skuCode }}
</div>
</template>
<template #quantity="{ row }">
{{ formatQuantity(row.quantity) }}
</template>
</Grid>
</Page>
</template>