fix: 完善库存跨页选择稳定性

This commit is contained in:
zhouz
2026-07-22 15:23:13 +08:00
parent d4a28480f1
commit 6210286518
8 changed files with 98 additions and 25 deletions

View File

@@ -9,6 +9,8 @@ import java.math.BigDecimal;
@Data @Data
public class AvailableInventoryRespVO { public class AvailableInventoryRespVO {
@Schema(description = "组盘记录标识")
private Long groupId;
@Schema(description = "箱号") @Schema(description = "箱号")
private String vehicleCode; private String vehicleCode;
@Schema(description = "批次") @Schema(description = "批次")

View File

@@ -3,6 +3,7 @@
<mapper namespace="cn.code.nl.module.wms.dal.mysql.iostorinvdtl.IostorinvDtlMapper"> <mapper namespace="cn.code.nl.module.wms.dal.mysql.iostorinvdtl.IostorinvDtlMapper">
<sql id="availableInventoryColumns"> <sql id="availableInventoryColumns">
gp.group_id AS group_id,
gp.vehicle_code AS vehicle_code, gp.vehicle_code AS vehicle_code,
gp.pcsn AS pcsn, gp.pcsn AS pcsn,
gp.material_id AS material_id, gp.material_id AS material_id,

View File

@@ -52,6 +52,7 @@ class IostorinvDtlMapperTest {
List<AvailableInventoryRespVO> expanded = service.expandAvailableInventory(expandReqVO); List<AvailableInventoryRespVO> expanded = service.expandAvailableInventory(expandReqVO);
assertEquals(2, expanded.size()); assertEquals(2, expanded.size());
assertEquals(List.of(1L, 2L), expanded.stream().map(AvailableInventoryRespVO::getGroupId).toList());
assertTrue(expanded.stream().allMatch(item -> "BOX-001".equals(item.getVehicleCode()))); assertTrue(expanded.stream().allMatch(item -> "BOX-001".equals(item.getVehicleCode())));
assertTrue(expanded.stream().allMatch(item -> "STOR-001".equals(item.getStorId()))); assertTrue(expanded.stream().allMatch(item -> "STOR-001".equals(item.getStorId())));
assertEquals(2, expanded.stream().map(AvailableInventoryRespVO::getPcsn).distinct().count()); assertEquals(2, expanded.stream().map(AvailableInventoryRespVO::getPcsn).distinct().count());
@@ -75,6 +76,7 @@ class IostorinvDtlMapperTest {
assertEquals(1L, page.getTotal()); assertEquals(1L, page.getTotal());
assertEquals("PCSN-002", page.getList().get(0).getPcsn()); assertEquals("PCSN-002", page.getList().get(0).getPcsn());
assertEquals(2L, page.getList().get(0).getGroupId());
assertEquals("材料一", page.getList().get(0).getMaterialName()); assertEquals("材料一", page.getList().get(0).getMaterialName());
} }

View File

@@ -1,11 +1,11 @@
import type { PageParam, PageResult } from '@vben/request'; import type { PageParam, PageResult } from '@vben/request';
import type { Dayjs } from 'dayjs';
import { requestClient } from '#/api/request'; import { requestClient } from '#/api/request';
export namespace BaseMaterialBaseApi { export namespace BaseMaterialBaseApi {
/** 物料基本信息信息 */ /** 物料基本信息信息 */
export interface MaterialBase { export interface MaterialBase {
materialId: number; // 物料标识
materialCode?: string; // 物料编码 materialCode?: string; // 物料编码
materialName?: string; // 物料名称 materialName?: string; // 物料名称
materialSpec: string; // 规格 materialSpec: string; // 规格

View File

@@ -67,6 +67,7 @@ export namespace WmsIostorInvApi {
/** 可用库存 */ /** 可用库存 */
export interface AvailableInventory { export interface AvailableInventory {
groupId: string; // 组盘记录标识
vehicleCode: string; // 箱号 vehicleCode: string; // 箱号
pcsn?: null | string; // 批次 pcsn?: null | string; // 批次
materialId: string; // 物料标识 materialId: string; // 物料标识

View File

@@ -4,6 +4,7 @@ import {
buildManualDetail, buildManualDetail,
collectVehicleCodes, collectVehicleCodes,
mapExpandedInventory, mapExpandedInventory,
mergeInventorySelections,
} from './detail-builders'; } from './detail-builders';
describe('mapExpandedInventory', () => { describe('mapExpandedInventory', () => {
@@ -11,6 +12,7 @@ describe('mapExpandedInventory', () => {
const rows = mapExpandedInventory([ const rows = mapExpandedInventory([
{ {
availableQty: 12.5, availableQty: 12.5,
groupId: '101',
extCode: 'OUT-1', extCode: 'OUT-1',
extDtlCode: 'DTL-1', extDtlCode: 'DTL-1',
extType: 'SALE', extType: 'SALE',
@@ -25,6 +27,7 @@ describe('mapExpandedInventory', () => {
}, },
{ {
availableQty: 8, availableQty: 8,
groupId: '102',
materialCode: 'M-1', materialCode: 'M-1',
materialId: '1', materialId: '1',
pcsn: 'P-2', pcsn: 'P-2',
@@ -46,6 +49,21 @@ describe('mapExpandedInventory', () => {
}); });
}); });
describe('mergeInventorySelections', () => {
it('合并当前页和跨页保留记录,并按 groupId 去重', () => {
const reserved = [{ groupId: '1', vehicleCode: 'BOX-1' }];
const current = [
{ groupId: '1', vehicleCode: 'BOX-1' },
{ groupId: '2', vehicleCode: 'BOX-2' },
];
expect(mergeInventorySelections(current, reserved)).toEqual([
reserved[0],
current[1],
]);
});
});
describe('buildManualDetail', () => { describe('buildManualDetail', () => {
it('构造手工汇总行并显式清空箱号、子卷号和 SAP 批次', () => { it('构造手工汇总行并显式清空箱号、子卷号和 SAP 批次', () => {
expect( expect(

View File

@@ -1,12 +1,11 @@
import type { BaseMaterialBaseApi } from '#/api/base/materialbase';
import type { WmsIostorInvApi } from '#/api/wms/iostorinv'; import type { WmsIostorInvApi } from '#/api/wms/iostorinv';
/** 物料选择弹窗返回行中,手工明细所需的真实字段 */ /** 物料选择弹窗返回行中,手工明细所需的真实字段 */
export interface MaterialSelection { export type MaterialSelection = Pick<
baseUnitId?: number; BaseMaterialBaseApi.MaterialBase,
materialCode?: string; 'baseUnitId' | 'materialCode' | 'materialId' | 'materialName'
materialId: number | string; >;
materialName?: string;
}
export function mapExpandedInventory( export function mapExpandedInventory(
inventory: WmsIostorInvApi.AvailableInventory[], inventory: WmsIostorInvApi.AvailableInventory[],
@@ -40,6 +39,16 @@ export function collectVehicleCodes(rows: Array<{ vehicleCode?: string }>) {
return vehicleCodes; return vehicleCodes;
} }
/** 合并当前页与 VXE 跨页保留记录,以组盘记录标识去重 */
export function mergeInventorySelections<T extends { groupId: string }>(
current: T[],
reserved: T[],
) {
const selected = new Map<string, T>();
[...reserved, ...current].forEach((row) => selected.set(row.groupId, row));
return [...selected.values()];
}
export function buildManualDetail( export function buildManualDetail(
material: MaterialSelection, material: MaterialSelection,
planQty: number, planQty: number,

View File

@@ -14,16 +14,26 @@ import {
getAvailableInventoryPage, getAvailableInventoryPage,
} from '#/api/wms/iostorinv'; } from '#/api/wms/iostorinv';
import { collectVehicleCodes, mapExpandedInventory } from './detail-builders'; import {
collectVehicleCodes,
mapExpandedInventory,
mergeInventorySelections,
} from './detail-builders';
interface InventoryGridRow extends WmsIostorInvApi.AvailableInventory { type InventoryGridRow = WmsIostorInvApi.AvailableInventory;
selectionKey: string;
}
const emit = defineEmits<{ const emit = defineEmits<{
select: [rows: WmsIostorInvApi.OutboundDisplayDetail[]]; select: [rows: WmsIostorInvApi.OutboundDisplayDetail[]];
}>(); }>();
const storId = ref(''); const storId = ref('');
const modalOpen = ref(false);
let openToken = 0;
async function clearGridState() {
await gridApi.grid.clearCheckboxRow();
await gridApi.grid.clearCheckboxReserve();
await gridApi.grid.loadData([]);
}
const [Grid, gridApi] = useVbenVxeGrid({ const [Grid, gridApi] = useVbenVxeGrid({
formOptions: { formOptions: {
@@ -60,33 +70,44 @@ const [Grid, gridApi] = useVbenVxeGrid({
], ],
height: 'auto', height: 'auto',
keepSource: true, keepSource: true,
checkboxConfig: { highlight: true, reserve: true },
proxyConfig: { proxyConfig: {
ajax: { ajax: {
query: async ({ page }, formValues) => { query: async ({ page }, formValues) => {
const requestToken = openToken;
const requestStorId = storId.value;
if (!modalOpen.value || !requestStorId) return { list: [], total: 0 };
const result = await getAvailableInventoryPage({ const result = await getAvailableInventoryPage({
...formValues, ...formValues,
pageNo: page.currentPage, pageNo: page.currentPage,
pageSize: page.pageSize, pageSize: page.pageSize,
storId: storId.value, storId: requestStorId,
}); });
return { if (
...result, requestToken !== openToken ||
list: result.list.map((row) => ({ !modalOpen.value ||
...row, requestStorId !== storId.value
selectionKey: `${row.vehicleCode}\u0000${row.pcsn ?? ''}\u0000${row.materialId}`, ) {
})), return { list: [], total: 0 };
}; }
return result;
}, },
}, },
}, },
rowConfig: { isHover: true, keyField: 'selectionKey' }, rowConfig: { isHover: true, keyField: 'groupId' },
toolbarConfig: { refresh: true, search: true }, toolbarConfig: { refresh: true, search: true },
} as VxeTableGridOptions<InventoryGridRow>, } as VxeTableGridOptions<InventoryGridRow>,
}); });
const [Modal, modalApi] = useVbenModal({ const [Modal, modalApi] = useVbenModal({
async onConfirm() { async onConfirm() {
const selected = gridApi.grid.getCheckboxRecords() as InventoryGridRow[]; const confirmToken = openToken;
const confirmStorId = storId.value;
if (!modalOpen.value || !confirmStorId) return;
const selected = mergeInventorySelections(
gridApi.grid.getCheckboxRecords() as InventoryGridRow[],
gridApi.grid.getCheckboxReserveRecords() as InventoryGridRow[],
);
if (selected.length === 0) { if (selected.length === 0) {
message.warning('请至少选择一条库存信息'); message.warning('请至少选择一条库存信息');
return; return;
@@ -101,22 +122,41 @@ const [Modal, modalApi] = useVbenModal({
modalApi.lock(); modalApi.lock();
try { try {
const expanded = await expandAvailableInventory({ const expanded = await expandAvailableInventory({
storId: storId.value, storId: confirmStorId,
vehicleCodes, vehicleCodes,
}); });
if (
confirmToken !== openToken ||
!modalOpen.value ||
confirmStorId !== storId.value
) {
return;
}
emit('select', mapExpandedInventory(expanded)); emit('select', mapExpandedInventory(expanded));
await modalApi.close(); await modalApi.close();
} finally { } finally {
modalApi.unlock(); modalApi.unlock();
} }
}, },
async onOpenChange(isOpen) { async onOpenChange(open) {
if (!isOpen) { const token = ++openToken;
if (!open) {
modalOpen.value = false;
storId.value = ''; storId.value = '';
await clearGridState();
return; return;
} }
modalOpen.value = true;
storId.value = '';
await clearGridState();
if (token !== openToken || !modalOpen.value) return;
const data = modalApi.getData<{ storId: string }>(); const data = modalApi.getData<{ storId: string }>();
storId.value = data?.storId ?? ''; if (!data?.storId) {
message.warning('请先选择仓库');
await modalApi.close();
return;
}
storId.value = data.storId;
await gridApi.query(); await gridApi.query();
}, },
}); });