diff --git a/nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/dal/mysql/subpackagerelation/SubPackageRelationMapper.java b/nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/dal/mysql/subpackagerelation/SubPackageRelationMapper.java index 6f8b5b69..0f95c609 100644 --- a/nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/dal/mysql/subpackagerelation/SubPackageRelationMapper.java +++ b/nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/dal/mysql/subpackagerelation/SubPackageRelationMapper.java @@ -41,4 +41,18 @@ public interface SubPackageRelationMapper extends BaseMapperX selectListByContainerNames(Collection containerNames) { + return selectList(SubPackageRelationDO::getContainerName, containerNames); + } + + /** + * 根据木箱唯一码集合查询包装关系 + */ + default List selectListByPackageBoxSns(Collection packageBoxSns) { + return selectList(SubPackageRelationDO::getPackageBoxSn, packageBoxSns); + } + } diff --git a/nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/service/subpackagerelation/SubPackageRelationServiceImpl.java b/nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/service/subpackagerelation/SubPackageRelationServiceImpl.java index 49b79ebc..5daa928a 100644 --- a/nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/service/subpackagerelation/SubPackageRelationServiceImpl.java +++ b/nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/service/subpackagerelation/SubPackageRelationServiceImpl.java @@ -36,8 +36,12 @@ public class SubPackageRelationServiceImpl implements SubPackageRelationService private static final String REQUIRED_FIELD_MISSING = "REQUIRED_FIELD_MISSING"; /** 前端行标识重复错误码 */ private static final String DUPLICATE_CLIENT_KEY = "DUPLICATE_CLIENT_KEY"; + /** 子卷号重复错误码 */ + private static final String DUPLICATE_CONTAINER_NAME = "DUPLICATE_CONTAINER_NAME"; /** 木箱类型不存在错误码 */ private static final String BOX_TYPE_NOT_FOUND = "BOX_TYPE_NOT_FOUND"; + /** 木箱已绑定其他类型错误码 */ + private static final String BOX_TYPE_MISMATCH = "BOX_TYPE_MISMATCH"; /** 木箱容量超限错误码 */ private static final String BOX_CAPACITY_EXCEEDED = "BOX_CAPACITY_EXCEEDED"; /** 分组保存失败错误码 */ @@ -63,10 +67,14 @@ public class SubPackageRelationServiceImpl implements SubPackageRelationService List failures = new ArrayList<>(); List validItems = new ArrayList<>(); Map clientKeyCounts = new HashMap<>(); + Map containerNameCounts = new HashMap<>(); for (SubPackageRelationBatchCreateItemReqVO item : reqVO.getItems()) { if (item != null && StringUtils.hasText(item.getClientKey())) { clientKeyCounts.merge(item.getClientKey(), 1, Integer::sum); } + if (item != null && StringUtils.hasText(item.getContainerName())) { + containerNameCounts.merge(item.getContainerName().trim(), 1, Integer::sum); + } } for (int index = 0; index < reqVO.getItems().size(); index++) { SubPackageRelationBatchCreateItemReqVO item = reqVO.getItems().get(index); @@ -85,6 +93,12 @@ public class SubPackageRelationServiceImpl implements SubPackageRelationService "前端行标识 clientKey 重复")); continue; } + if (StringUtils.hasText(item.getContainerName()) + && containerNameCounts.get(item.getContainerName().trim()) > 1) { + failures.add(buildFailure(item.getClientKey(), rowIndex, DUPLICATE_CONTAINER_NAME, + "子卷号重复:" + item.getContainerName().trim())); + continue; + } List missingFields = new ArrayList<>(); if (!StringUtils.hasText(item.getPackageBoxSn())) { missingFields.add("packageBoxSn"); @@ -119,6 +133,34 @@ public class SubPackageRelationServiceImpl implements SubPackageRelationService .build(); } + Set containerNames = validItems.stream() + .map(context -> context.item().getContainerName().trim()) + .collect(Collectors.toCollection(LinkedHashSet::new)); + Set existingContainerNames = subPackageRelationMapper.selectListByContainerNames(containerNames) + .stream() + .map(SubPackageRelationDO::getContainerName) + .filter(Objects::nonNull) + .map(String::trim) + .collect(Collectors.toSet()); + List nonDuplicateItems = new ArrayList<>(); + for (BatchItemContext context : validItems) { + String containerName = context.item().getContainerName().trim(); + if (existingContainerNames.contains(containerName)) { + failures.add(buildFailure(context.item().getClientKey(), context.rowIndex(), + DUPLICATE_CONTAINER_NAME, "子卷号已存在:" + containerName)); + continue; + } + nonDuplicateItems.add(context); + } + validItems = nonDuplicateItems; + if (CollUtil.isEmpty(validItems)) { + return SubPackageRelationBatchCreateRespVO.builder() + .successCount(0) + .failureCount(failures.size()) + .failures(failures) + .build(); + } + Set boxTypes = validItems.stream() .map(context -> context.item().getBoxType()) .collect(Collectors.toCollection(LinkedHashSet::new)); @@ -127,11 +169,31 @@ public class SubPackageRelationServiceImpl implements SubPackageRelationService Map> groups = validItems.stream() .collect(Collectors.groupingBy(context -> new GroupKey(context.item().getPackageBoxSn(), context.item().getBoxType()), LinkedHashMap::new, Collectors.toList())); + Set packageBoxSns = groups.keySet().stream() + .map(GroupKey::packageBoxSn) + .collect(Collectors.toCollection(LinkedHashSet::new)); + Map> existingBoxTypesByPackageBoxSn = subPackageRelationMapper + .selectListByPackageBoxSns(packageBoxSns).stream() + .filter(relation -> relation.getPackageBoxSn() != null && relation.getBoxType() != null) + .collect(Collectors.groupingBy(SubPackageRelationDO::getPackageBoxSn, + Collectors.mapping(SubPackageRelationDO::getBoxType, + Collectors.toCollection(LinkedHashSet::new)))); int successCount = 0; for (Map.Entry> entry : groups.entrySet()) { GroupKey key = entry.getKey(); List group = entry.getValue(); + Set existingBoxTypes = existingBoxTypesByPackageBoxSn.getOrDefault( + key.packageBoxSn(), Collections.emptySet()); + if (existingBoxTypes.stream().anyMatch(boxType -> !Objects.equals(boxType, key.boxType()))) { + String message = String.format("木箱 %s 已绑定木箱类型 %s,不能使用类型 %s", + key.packageBoxSn(), String.join("、", existingBoxTypes), key.boxType()); + for (BatchItemContext context : group) { + failures.add(buildFailure(context.item().getClientKey(), context.rowIndex(), + BOX_TYPE_MISMATCH, message)); + } + continue; + } BoxTypeDO boxType = boxTypeMap.get(key.boxType()); if (boxType == null) { for (BatchItemContext context : group) { @@ -152,6 +214,7 @@ public class SubPackageRelationServiceImpl implements SubPackageRelationService continue; } + int qualityInBox = Math.toIntExact(existingCount + group.size()); List saveGroup = new ArrayList<>(); for (BatchItemContext context : group) { SubPackageRelationBatchCreateItemReqVO item = context.item(); @@ -161,6 +224,7 @@ public class SubPackageRelationServiceImpl implements SubPackageRelationService relation.setBoxLength(item.getBoxLength()); relation.setBoxWidth(item.getBoxWidth()); relation.setBoxHigh(item.getBoxHigh()); + relation.setQualityInBox(qualityInBox); relation.setQualityGuaranPeriod(item.getQualityGuaranPeriod()); relation.setDateOfFgInbound(item.getDateOfFgInbound()); relation.setContainerName(item.getContainerName()); diff --git a/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/lms/subpackagerelation/data.ts b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/lms/subpackagerelation/data.ts index 09c7728c..ae1bb34a 100644 --- a/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/lms/subpackagerelation/data.ts +++ b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/lms/subpackagerelation/data.ts @@ -505,7 +505,32 @@ export function useGridColumns(): VxeTableGridOptions { it('creates an empty row with default status and a unique client key', () => { const first = createBatchRow(); const second = createBatchRow(); + const today = new Date(); + const month = String(today.getMonth() + 1).padStart(2, '0'); + const day = String(today.getDate()).padStart(2, '0'); + const todayText = `${today.getFullYear()}-${month}-${day}`; expect(first).toEqual({ clientKey: expect.any(String), @@ -20,7 +24,7 @@ describe('batch-form-model', () => { boxWidth: '', boxHigh: '', qualityGuaranPeriod: '', - dateOfFgInbound: '', + dateOfFgInbound: todayText, containerName: '', status: '0', }); @@ -62,6 +66,7 @@ describe('batch-form-model', () => { it('reports all required fields when values are empty or whitespace', () => { const row = createBatchRow(); row.packageBoxSn = ' '; + row.dateOfFgInbound = ' '; row.status = '\t'; expect(validateBatchRows([row]).get(0)).toEqual({ @@ -99,6 +104,26 @@ describe('batch-form-model', () => { expect(validateBatchRows([row])).toEqual(new Map()); }); + it('reports every row whose container name is duplicated', () => { + const first = Object.assign(createBatchRow(), { + packageBoxSn: 'BOX-001', + boxType: 'TYPE-A', + qualityGuaranPeriod: '90', + containerName: 'ROLL-001', + }); + const second = Object.assign(createBatchRow(), { + packageBoxSn: 'BOX-002', + boxType: 'TYPE-A', + qualityGuaranPeriod: '90', + containerName: ' ROLL-001 ', + }); + + const errors = validateBatchRows([first, second]); + + expect(errors.get(0)?.containerName).toBe('子卷号重复,与第 2 行相同'); + expect(errors.get(1)?.containerName).toBe('子卷号重复,与第 1 行相同'); + }); + it('keeps failed rows in original order and maps server messages by client key', () => { const rows = [createBatchRow(), createBatchRow(), createBatchRow()]; const result = applyBatchResult(rows, { diff --git a/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/lms/subpackagerelation/modules/batch-form-model.ts b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/lms/subpackagerelation/modules/batch-form-model.ts index f7cfa603..3a7d2ade 100644 --- a/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/lms/subpackagerelation/modules/batch-form-model.ts +++ b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/lms/subpackagerelation/modules/batch-form-model.ts @@ -18,6 +18,13 @@ function createClientKey() { return `${Date.now()}-${fallbackSequence}`; } +function createToday() { + const today = new Date(); + const month = String(today.getMonth() + 1).padStart(2, '0'); + const day = String(today.getDate()).padStart(2, '0'); + return `${today.getFullYear()}-${month}-${day}`; +} + export function createBatchRow(): BatchFormRow { return { clientKey: createClientKey(), @@ -27,7 +34,7 @@ export function createBatchRow(): BatchFormRow { boxWidth: '', boxHigh: '', qualityGuaranPeriod: '', - dateOfFgInbound: '', + dateOfFgInbound: createToday(), containerName: '', status: '0', }; @@ -52,6 +59,7 @@ export function validateBatchRows( rows: BatchFormRow[], ): Map { const result = new Map(); + const containerNameRows = new Map(); const requiredFields: [keyof BatchFormRow, string][] = [ ['packageBoxSn', '木箱唯一码不能为空'], ['boxType', '木箱类型不能为空'], @@ -71,8 +79,24 @@ export function validateBatchRows( if (Object.keys(errors).length > 0) { result.set(index, errors); } + const containerName = row.containerName?.trim(); + if (containerName) { + const indexes = containerNameRows.get(containerName) ?? []; + indexes.push(index); + containerNameRows.set(containerName, indexes); + } }); + for (const indexes of containerNameRows.values()) { + if (indexes.length < 2) continue; + indexes.forEach((index) => { + const duplicateIndex = indexes.find((item) => item !== index)!; + const errors = result.get(index) ?? {}; + errors.containerName = `子卷号重复,与第 ${duplicateIndex + 1} 行相同`; + result.set(index, errors); + }); + } + return result; } diff --git a/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/lms/subpackagerelation/modules/batch-form.vue b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/lms/subpackagerelation/modules/batch-form.vue index d5337e2a..6f62b1b1 100644 --- a/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/lms/subpackagerelation/modules/batch-form.vue +++ b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/lms/subpackagerelation/modules/batch-form.vue @@ -8,12 +8,13 @@ import { computed, ref } from 'vue'; import { useVbenModal } from '@vben/common-ui'; import { DICT_TYPE } from '@vben/constants'; import { getDictOptions } from '@vben/hooks'; +import { IconifyIcon } from '@vben/icons'; import { Alert, Button, Card, - FormItem, + DatePicker, Input, message, Segmented, @@ -138,7 +139,14 @@ const [Modal, modalApi] = useVbenModal({ fieldErrors.value = errors; rowErrors.value = new Map(); if (errors.size > 0) { - message.warning('请检查必填项'); + const hasDuplicateContainerName = [...errors.values()].some((item) => + item.containerName?.startsWith('子卷号重复'), + ); + message.warning( + hasDuplicateContainerName + ? '子卷号不能重复,请检查后再提交' + : '请检查标红字段', + ); return; } @@ -182,8 +190,14 @@ const [Modal, modalApi] = useVbenModal({
- - + +
- 序号* 木箱唯一码* 木箱类型 + 序号* 木箱唯一码* 子卷号* 木箱类型 木箱长木箱宽木箱高 - * 保质期(天)* 入库日期* 子卷号 + * 保质期(天)* 入库日期 * 状态操作 @@ -222,15 +236,15 @@ const [Modal, modalApi] = useVbenModal({ {{ index + 1 }} {{ fieldError(index, 'packageBoxSn') }} + {{ fieldError(index, 'containerName') }} {{ fieldError(index, 'boxType') }} - {{ fieldError(index, 'qualityGuaranPeriod') }} - {{ fieldError(index, 'dateOfFgInbound') }} - {{ fieldError(index, 'containerName') }} + {{ fieldError(index, 'qualityGuaranPeriod') }} + {{ fieldError(index, 'dateOfFgInbound') }} - - - - - - - - {{ fieldError(index, 'packageBoxSn') }}
+
{{ fieldError(index, 'containerName') }}
+
{{ fieldError(index, 'boxType') }}
+
+
+
+
+ +
+
时效信息
+
+
{{ fieldError(index, 'qualityGuaranPeriod') }}
+
{{ fieldError(index, 'dateOfFgInbound') }}
+
+
@@ -377,9 +404,23 @@ const [Modal, modalApi] = useVbenModal({ } .batch-table :deep(.ant-input), +.batch-table :deep(.ant-input-affix-wrapper), +.batch-table :deep(.ant-picker), .batch-table :deep(.ant-select-selector) { + height: 36px; min-height: 36px; border-radius: 7px; + box-sizing: border-box; +} + +.batch-table :deep(.ant-input-affix-wrapper) { + padding-top: 0; + padding-bottom: 0; +} + +.batch-table :deep(.ant-input-affix-wrapper .ant-input) { + height: auto; + min-height: 0; } .batch-table :deep(.ant-input[readonly]) { @@ -425,6 +466,10 @@ const [Modal, modalApi] = useVbenModal({ color: #ff4d4f; } +.input-unit { + color: hsl(var(--muted-foreground)); +} + .invalid { border-color: #ff4d4f; } @@ -445,17 +490,147 @@ const [Modal, modalApi] = useVbenModal({ } .card-list :deep(.ant-card) { + overflow: hidden; border-radius: 10px; box-shadow: 0 2px 8px rgb(23 32 51 / 4%); } -.card-grid { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - column-gap: 24px; +.card-list :deep(.ant-card-head) { + min-height: 54px; + background: linear-gradient(90deg, #f8faff, #fff); } -.card-grid :deep(.ant-input[readonly]) { +.card-list :deep(.ant-card-body) { + padding: 4px 24px 8px; +} + +.record-title { + display: flex; + align-items: center; + gap: 12px; + color: #172033; + font-weight: 600; +} + +.record-number { + display: inline-flex; + align-items: center; + justify-content: center; + width: 34px; + height: 34px; + color: #fff; + font-size: 15px; + background: #326cff; + border-radius: 10px; +} + +.card-section { + display: grid; + padding: 18px 0 4px; + grid-template-columns: 170px minmax(0, 1fr); + gap: 18px; +} + +.card-section + .card-section { + border-top: 1px dashed #e3e7ef; +} + +.section-title { + display: flex; + align-items: flex-start; + gap: 10px; + padding-top: 8px; + color: #576071; + font-size: 15px; + font-weight: 600; +} + +.section-title i { + width: 8px; + height: 8px; + margin-top: 7px; + background: #84a9ff; + border-radius: 50%; +} + +.section-fields { + display: grid; + gap: 18px; +} + +.identity-fields { + grid-template-columns: 1.35fr 1.35fr 0.9fr; +} + +.specification-fields { + grid-template-columns: 1.35fr repeat(3, 0.9fr); +} + +.lifecycle-fields { + max-width: 75%; + grid-template-columns: repeat(2, minmax(0, 1fr)); +} + +.card-field { + display: flex; + min-width: 0; + flex-direction: column; +} + +.card-field label { + height: 24px; + margin-bottom: 7px; + color: #596275; + line-height: 24px; +} + +.card-field label.required::before { + margin-right: 4px; + color: #ff4d4f; + content: '*'; +} + +.card-field small { + display: block; + height: 20px; + padding-top: 3px; + color: #ff4d4f; + line-height: 17px; +} + +.section-fields :deep(.ant-input), +.section-fields :deep(.ant-input-affix-wrapper), +.section-fields :deep(.ant-picker), +.section-fields :deep(.ant-select-single .ant-select-selector) { + height: 40px; + min-height: 40px; + border-radius: 8px; + box-sizing: border-box; +} + +.section-fields :deep(.ant-input-affix-wrapper) { + padding-top: 0; + padding-bottom: 0; +} + +.section-fields :deep(.ant-input-affix-wrapper .ant-input) { + height: auto; + min-height: 0; +} + +.section-fields :deep(.ant-select-single .ant-select-selection-item), +.section-fields :deep(.ant-select-single .ant-select-selection-placeholder) { + line-height: 38px; +} + +.card-field.has-error :deep(.ant-input), +.card-field.has-error :deep(.ant-input-affix-wrapper), +.card-field.has-error :deep(.ant-picker), +.card-field.has-error :deep(.ant-select-selector) { + border-color: #ff4d4f; +} + +.section-fields :deep(.ant-input[readonly]) { color: #667085; background: #f2f4f7; } @@ -474,7 +649,14 @@ const [Modal, modalApi] = useVbenModal({ text-align: left; } - .card-grid { + .card-section { + grid-template-columns: 1fr; + } + + .identity-fields, + .specification-fields, + .lifecycle-fields { + max-width: none; grid-template-columns: 1fr; } }