feat(wms):完成 inventory index 的迁移
parent
0280df114f
commit
3d0917f1a9
|
|
@ -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' },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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' },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
Loading…
Reference in New Issue