feat:子卷质检不合格

This commit is contained in:
2026-08-18 18:44:13 +08:00
parent ed8b688cf8
commit 3a74a3f598
18 changed files with 1879 additions and 1 deletions

View File

@@ -0,0 +1,42 @@
# 子卷下线送待检区任务实现计划
> **面向 AI 代理的工作者:** 在当前功能分支按步骤实现并逐项验证。
**目标:** 完成子卷下线点与待检区点位的并发安全配对和锁定,并由定时任务触发。
**架构:** Mapper XML 提供稳定排序查询和条件更新;业务 Service 在事务内完成点位抢锁、配对及无目的点时的起点释放Job 仅调用业务入口。任务创建暂保留明确的 TODO。
**技术栈:** Java 17、Spring Boot、MyBatis、MySQL、XXL-JOB。
---
### 任务 1实现点位查询和锁操作
**文件:**
- 修改:`nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/dal/mysql/packagepoint/PackagePointMapper.java`
- 修改:`nl-module-lms/nl-module-lms-server/src/main/resources/mapper/packagepoint/PackagePointMapper.xml`
- [ ] 声明可用点位查询、条件加锁、解锁 Mapper 方法。
- [ ] 在 XML 中按点位类型、状态、锁状态和删除标记查询,并稳定排序。
- [ ] 条件加锁仅允许 `lock_status = '0'` 更新为 `'1'`
### 任务 2实现业务配对并接入 Job
**文件:**
- 修改:`nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/service/packagepoint/PackageBoxBusinessService.java`
- 修改:`nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/service/packagepoint/PackageBoxBusinessServiceImpl.java`
- 修改:`nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/job/box/BoxAutoJob.java`
- [ ] 在 Service 接口增加自动创建子卷送检任务入口。
- [ ] 在事务中查询起点和目的点,逐一条件抢锁并配对。
- [ ] 目的点不足时释放未配对起点,配对成功后调用任务模块创建运输任务。
- [ ] Job 调用 Service 业务入口。
### 任务 3同步表结构并验证
**文件:**
- 修改:`sql/mysql/lms_package_point.sql`
- [ ] 在建表脚本补充非空且默认未锁定的 `lock_status` 字段。
- [ ] 编译 `nl-module-lms-server` 及其依赖模块,要求 Maven 退出码为 0。
- [ ] 检查 Git 差异,确认没有覆盖用户已有修改。

View File

@@ -0,0 +1,43 @@
# 子卷下线补充空架子实现计划
**目标:** 完成 `autoSubRollDownCallEmptyJob` 的空架 AGV 任务自动创建及点位状态回调。
**架构:** 复用现有点位查询和条件锁 Mapper业务 Service 在事务内按优先级配对并创建任务;独立 AGV 回调类维护起点、目的点状态Job 仅负责调度。
**技术栈:** Java 17、Spring Boot、MyBatis、XXL-JOB、任务模块 API。
---
### 任务 1增加空架任务类型和业务入口
**文件:**
- 修改:`nl-module-lms/nl-module-lms-api/src/main/java/cn/code/nl/module/lms/enums/PackageTaskTypeEnum.java`
- 修改:`nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/service/packagepoint/PackageBoxBusinessService.java`
- 修改:`nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/job/box/BoxAutoJob.java`
- [x] 新增子卷下线补空架 AGV 任务类型。
- [x] 新增业务方法并由定时器记录日志后调用。
### 任务 2实现点位分配和任务创建
**文件:**
- 修改:`nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/service/packagepoint/PackageBoxBusinessServiceImpl.java`
- [x] 查询下线空位、装箱空载具、待检空载具和待检空位。
- [x] 每个下线空位优先配装箱空载具,其次配待检空载具。
- [x] 将剩余装箱空载具配到待检空位。
- [x] 条件锁定两端点位并创建空架 AGV 任务。
### 任务 3实现空架任务回调
**文件:**
- 创建:`nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/tasks/box/SubRollCallEmptyAgvTask.java`
- [x] 完成时将目的点更新为空载具并解锁。
- [x] 取消时解锁两端点位。
- [x] 取货时将起点更新为空位、清空库存信息并解锁。
### 任务 4验证
- [x] 编译 LMS 服务及其依赖模块,确认 Maven 退出码为 0。
- [x] 检查 Git 差异,确认只修改本功能相关文件且未覆盖用户改动。

View File

@@ -0,0 +1,53 @@
# 查询装箱区点位信息实现计划
> **面向 AI 代理的工作者:** 必需子技能:使用 superpowers:subagent-driven-development推荐或 superpowers:executing-plans 逐任务实现此计划。步骤使用复选框(`- [ ]`)语法来跟踪进度。
**目标:** PDA 端仅传入点位编码,返回装箱区点位的点位编码、子卷号和载具号。
**架构:** 保持现有 `ManaQualityCheckReqVO` 作为请求对象,由 `PackagePdaService` 复用装箱区点位服务完成查询和不存在校验。Controller 使用 `PackagePointRespVO` 作为响应类型,并手动映射需求指定的三个字段。
**技术栈:** Java 17、Spring Boot、Jakarta Validation、MyBatis-Plus
---
### 任务 1补齐 PDA 点位查询接口
**文件:**
- 修改:`nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/controller/pda/vo/ManaQualityCheckReqVO.java`
- 修改:`nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/service/packagepoint/PackagePdaService.java`
- 修改:`nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/service/packagepoint/PackagePdaServiceImpl.java`
- 修改:`nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/controller/pda/PackagePdaController.java`
- [x] **步骤 1校验点位编码**
`ManaQualityCheckReqVO.pointCode` 上添加 `@NotEmpty(message = "点位编码不能为空")`
- [x] **步骤 2声明并实现查询服务**
`PackagePdaService` 声明:
```java
PackagePointDO queryPointInfo(String pointCode);
```
实现类通过 `PackagePointService.getPackagePointByCode(pointCode, false)` 查询;结果为空时抛出 `PACKAGE_POINT_NOT_EXISTS`
- [x] **步骤 3完成 Controller 响应映射**
将接口返回类型改为 `CommonResult<PackagePointRespVO>`,并只设置:
```java
response.setPointCode(packagePoint.getPointCode());
response.setContainerName(packagePoint.getContainerName());
response.setVehicleCode(packagePoint.getVehicleCode());
```
- [x] **步骤 4编译验证**
运行:
```powershell
mvn -pl nl-module-lms/nl-module-lms-server -am -DskipTests compile
```
预期:构建成功,无 Java 编译错误。

View File

@@ -0,0 +1,103 @@
# 装箱区信息看板实现计划
> **面向 AI 代理的工作者:** 必需子技能:使用 superpowers:subagent-driven-development推荐或 superpowers:executing-plans 逐任务实现此计划。步骤使用复选框(`- [ ]`)语法来跟踪进度。
**目标:** 创建一个包含 66 个点位、CAD 式平面布局和完整 Demo 交互的独立装箱区信息看板 HTML。
**架构:** 使用一个 HTML 文件内嵌样式、确定性模拟数据和交互脚本。数据生成、点位渲染、统计计算、地图变换与用户交互使用独立函数组织,不依赖 API、CDN 或构建工具。
**技术栈:** HTML5、CSS3、原生 JavaScript、浏览器 localStorage 与 Clipboard API。
---
## 文件结构
- 创建:`packingAreaDashboard.html`,承载页面结构、视觉样式、模拟数据、布局渲染和交互。
- 创建:`docs/superpowers/specs/2026-08-14-packing-area-dashboard-design.md`,记录已确认设计。
- 创建:`docs/superpowers/plans/2026-08-14-packing-area-dashboard.md`,记录实现和验证步骤。
### 任务 1建立页面骨架与大屏视觉
**文件:**
- 创建:`packingAreaDashboard.html`
- [ ] **步骤 1建立 1920 × 1080 舞台**
创建 `#stage`、顶部标题、KPI、工具条、地图和侧栏结构并使用 `scaleStage()` 根据窗口尺寸等比缩放。
- [ ] **步骤 2实现深色科技风样式**
使用径向渐变、网格背景、青蓝描边、玻璃面板和细微辉光,避免外部字体与图片依赖。
### 任务 2生成 66 个点位与模拟数据
**文件:**
- 修改:`packingAreaDashboard.html`
- [ ] **步骤 1定义点位编码生成函数**
`rangeCodes(prefix, start, end)` 必须生成补零后的编码;北区 6 个、中区 42 个、南区 18 个。
- [ ] **步骤 2定义确定性模拟数据**
`createMockPoint(code, index, zone)` 根据索引生成稳定的库存状态、卷号、箱号、进入时间和停留时长,刷新页面后数据仍保持一致。
- [ ] **步骤 3渲染点位卡片**
`renderSlot(point, className)` 输出编码、状态、卷号、箱号和停留时长,并附带搜索、选中和右键菜单所需数据属性。
### 任务 3绘制北中南平面布局和 AGV 通道
**文件:**
- 修改:`packingAreaDashboard.html`
- [ ] **步骤 1渲染北区**
使用 6 列网格呈现 `ZJXX_01``ZJXX_06`,在区域上下绘制横向 AGV 通道。
- [ ] **步骤 2渲染中区**
使用 7 行 × 6 列网格呈现 `DJQ_01``DJQ_42`,在两列点位之间插入纵向白色通道,并在区域周边绘制闭合通道。
- [ ] **步骤 3渲染南区**
两行分别呈现 8 个 `ZXQ` 点位,在右侧呈现 `ZXDJW_01``ZXDJW_02`,使用不同区域标签区分装箱区 1 和装箱区 2。
### 任务 4实现统计与交互
**文件:**
- 修改:`packingAreaDashboard.html`
- [ ] **步骤 1计算 KPI 和队列**
`updateDashboard()` 根据点位状态和超时阈值刷新总数、占用、空闲、超时、占用率及装箱队列。
- [ ] **步骤 2实现卷号搜索定位**
`locateRoll()` 匹配完整或部分卷号,选中第一个结果,将地图平移到点位附近并显示结果提示。
- [ ] **步骤 3实现地图操作**
`fitMap()``applyTransform()`、滚轮事件和拖动事件共同支持适配、缩放、平移与双击复位。
- [ ] **步骤 4实现点位详情与右键复制**
点位单击后调用 `selectPoint()` 更新详情;右键菜单根据字段是否存在启用复制按钮,并使用 Clipboard API 或文本选择回退方案复制。
### 任务 5执行交付验证
**文件:**
- 验证:`packingAreaDashboard.html`
- [ ] **步骤 1执行静态结构检查**
确认文件包含 66 个点位的生成规则、无 `fetch()` 和外部资源引用,并检查脚本语法。
- [ ] **步骤 2执行浏览器视觉检查**
以 1920 × 1080 视口打开本地文件确认三大区域、KPI、侧栏和点位内容没有遮挡或主体溢出。
- [ ] **步骤 3执行交互检查**
验证卷号搜索、点位选择、缩放、复位和右键菜单,确认浏览器控制台无错误。

View File

@@ -0,0 +1,58 @@
# 木箱类型捆扎选项默认值实现计划
> **面向 AI 代理的工作者:** 必需子技能:使用 superpowers:subagent-driven-development推荐或 superpowers:executing-plans 逐任务实现此计划。步骤使用复选框(`- [ ]`)语法来跟踪进度。
**目标:** 新增木箱类型时,“是否一次捆扎”和“是否二次捆扎”默认选中为 `true`,编辑时保持接口原值。
**架构:** 复用现有 Vben 表单 schema 的默认值能力,在两个布尔字段的 schema 顶层设置默认值。编辑弹窗既有的 `setValues` 会继续使用详情数据覆盖默认值。
**技术栈:** Vue 3、TypeScript、Vben Form、Ant Design Vue
---
## 文件结构
- 修改:`nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/lms/boxtype/data.ts`:定义新增/编辑表单字段及默认值。
### 任务 1设置捆扎选项默认值
**文件:**
- 修改:`nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/lms/boxtype/data.ts`
- [x] **步骤 1为一次捆扎字段增加默认值**
`needLashOne``componentProps` 后增加 schema 顶层属性:
```ts
defaultValue: true,
```
- [x] **步骤 2为二次捆扎字段增加默认值**
`needLashTwo``componentProps` 后增加相同的 schema 顶层属性:
```ts
defaultValue: true,
```
- [x] **步骤 3静态验证改动**
运行:
```powershell
Select-String -Path nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/lms/boxtype/data.ts -Pattern "fieldName: 'needLashOne'|fieldName: 'needLashTwo'|defaultValue: true" -Context 0,10
```
预期:新增/编辑表单中的两个字段各包含一个 schema 顶层 `defaultValue: true`,搜索表单不包含默认值。
- [x] **步骤 4检查编辑回填不受影响**
运行:
```powershell
Select-String -Path nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/lms/boxtype/modules/form.vue -Pattern "formApi.setValues\(formData.value\)"
```
预期:仍能找到编辑详情回填语句,接口原值会覆盖 schema 默认值。
按项目约定,用户未授权测试,因此不新增或运行测试。

View File

@@ -0,0 +1,186 @@
# 木箱类型最大子卷数实现计划
> **面向 AI 代理的工作者:** 必需子技能:使用 superpowers:subagent-driven-development推荐或 superpowers:executing-plans 逐任务实现此计划。步骤使用复选框(`- [ ]`)语法来跟踪进度。
**目标:** 为木箱类型补齐 `maxNum` 的后端持久化与校验、前端新增编辑输入和列表展示能力。
**架构:** 沿用木箱类型现有的 MyBatis Plus、Bean 转换和 Vben 表单/表格链路,在数据对象、请求响应 VO、前端 API 类型和两个表格入口中增加同名字段。前端负责默认值及输入体验,后端以 Bean Validation 保证值为不小于 1 的必填整数。
**技术栈:** Java 17、Spring Boot、Jakarta Validation、MyBatis Plus、Vue 3、TypeScript、Vben Admin、Ant Design Vue `InputNumber`、VXE Table。
---
## 文件结构
- 修改:`nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/dal/dataobject/boxtype/BoxTypeDO.java` —— 持久化 `max_num`
- 修改:`nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/controller/admin/boxtype/vo/BoxTypeSaveReqVO.java` —— 接收并校验新增、编辑请求。
- 修改:`nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/controller/admin/boxtype/vo/BoxTypeRespVO.java` —— 详情、分页和导出返回字段。
- 修改:`nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/api/lms/boxtype/index.ts` —— 声明前端 API 字段类型。
- 修改:`nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/lms/boxtype/data.ts` —— 新增编辑表单和管理列表配置。
- 修改:`nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/lms/boxtype/components/BoxTypeSelectModal.vue` —— 复用选择弹窗展示字段。
### 任务 1补齐后端最大子卷数字段链路
**文件:**
- 修改:`nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/dal/dataobject/boxtype/BoxTypeDO.java:50`
- 修改:`nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/controller/admin/boxtype/vo/BoxTypeSaveReqVO.java:29`
- 修改:`nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/controller/admin/boxtype/vo/BoxTypeRespVO.java:38`
- [ ] **步骤 1在数据对象中声明持久化字段**
在尺寸字段之后增加:
```java
/**
* 最大子卷数
*/
private Integer maxNum;
```
MyBatis Plus 根据项目现有驼峰映射将 `maxNum` 映射到 `max_num`,不修改 Mapper XML。
- [ ] **步骤 2在保存请求中增加必填正整数校验**
在尺寸字段之后增加:
```java
@Schema(description = "最大子卷数", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "最大子卷数不能为空")
@Min(value = 1, message = "最大子卷数不能小于1")
private Integer maxNum;
```
`Integer` 保证 JSON 反序列化后的类型为整数,`@NotNull``@Min(1)` 同时覆盖空值、0 和负数。
- [ ] **步骤 3在响应及导出模型中返回字段**
在尺寸字段之后增加:
```java
@Schema(description = "最大子卷数", requiredMode = Schema.RequiredMode.REQUIRED)
@ExcelProperty("最大子卷数")
private Integer maxNum;
```
- [ ] **步骤 4执行后端静态差异检查**
运行:
```powershell
git -c safe.directory=D:/Code/Work/nl/huachuang diff --check -- nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/dal/dataobject/boxtype/BoxTypeDO.java nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/controller/admin/boxtype/vo/BoxTypeSaveReqVO.java nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/controller/admin/boxtype/vo/BoxTypeRespVO.java
```
预期:命令无输出,退出码为 `0`
### 任务 2增加前端输入和展示
**文件:**
- 修改:`nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/api/lms/boxtype/index.ts:14`
- 修改:`nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/lms/boxtype/data.ts:158`
- 修改:`nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/lms/boxtype/components/BoxTypeSelectModal.vue:36`
- [ ] **步骤 1扩展木箱类型 API 类型**
在尺寸字段后增加必填数字字段:
```ts
maxNum: number; // 最大子卷数
```
- [ ] **步骤 2在新增编辑表单中增加整数输入框**
在尺寸字段组之后增加:
```ts
{
fieldName: 'maxNum',
label: '最大子卷数',
rules: 'required',
component: 'InputNumber',
defaultValue: 1,
componentProps: {
min: 1,
placeholder: '请输入最大子卷数',
precision: 0,
step: 1,
},
},
```
新增表单使用 `defaultValue: 1`;编辑表单继续由现有详情回填覆盖默认值。`precision: 0` 限制小数,`min: 1` 禁止 0 和负数。
- [ ] **步骤 3在管理列表中展示最大子卷数**
在尺寸列之后增加:
```ts
{
field: 'maxNum',
title: '最大子卷数',
minWidth: 120,
},
```
- [ ] **步骤 4在木箱类型选择弹窗中展示最大子卷数**
`baseColumns` 的尺寸列之后增加:
```ts
{ field: 'maxNum', title: '最大子卷数', minWidth: 120 },
```
- [ ] **步骤 5执行前端 ESLint**
`nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben` 目录运行:
```powershell
node node_modules/eslint/bin/eslint.js apps/web-antdv-next/src/api/lms/boxtype/index.ts apps/web-antdv-next/src/views/lms/boxtype/data.ts apps/web-antdv-next/src/views/lms/boxtype/components/BoxTypeSelectModal.vue
```
预期:无 ESLint 错误,退出码为 `0`
### 任务 3验证完整字段链路
**文件:**
- 验证:任务 1 和任务 2 中列出的全部文件
- [ ] **步骤 1搜索确认字段覆盖范围**
运行:
```powershell
rg -n "maxNum|最大子卷数" nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/api/lms/boxtype nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/lms/boxtype
```
预期DO、SaveReqVO、RespVO、API 类型、表单、管理列表和选择弹窗均有匹配项。
- [ ] **步骤 2编译 LMS 后端模块及其依赖**
在仓库根目录运行:
```powershell
mvn -pl nl-module-lms/nl-module-lms-server -am -DskipTests compile
```
预期:输出 `BUILD SUCCESS`
- [ ] **步骤 3检查最终差异格式**
运行:
```powershell
git -c safe.directory=D:/Code/Work/nl/huachuang diff --check -- nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/dal/dataobject/boxtype/BoxTypeDO.java nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/controller/admin/boxtype/vo/BoxTypeSaveReqVO.java nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/controller/admin/boxtype/vo/BoxTypeRespVO.java nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/api/lms/boxtype/index.ts nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/lms/boxtype/data.ts nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/lms/boxtype/components/BoxTypeSelectModal.vue
```
预期:命令无输出,退出码为 `0`
- [ ] **步骤 4审阅差异确保不包含数据库脚本或无关重构**
运行:
```powershell
git -c safe.directory=D:/Code/Work/nl/huachuang diff -- nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/dal/dataobject/boxtype/BoxTypeDO.java nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/controller/admin/boxtype/vo/BoxTypeSaveReqVO.java nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/controller/admin/boxtype/vo/BoxTypeRespVO.java nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/api/lms/boxtype/index.ts nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/lms/boxtype/data.ts nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/lms/boxtype/components/BoxTypeSelectModal.vue
```
预期:只包含已有用户改动及本计划的 `maxNum` 字段修改;不新增数据库迁移文件,不改动无关业务逻辑。

View File

@@ -0,0 +1,204 @@
# 木箱类型选择弹窗实现计划
> **面向 AI 代理的工作者:** 必需子技能:使用 superpowers:subagent-driven-development推荐或 superpowers:executing-plans 逐任务实现此计划。步骤使用复选框(`- [ ]`)语法来跟踪进度。
**目标:** 新增一个可复用的木箱类型选择弹窗,通过 `multiple` 参数支持单选和多选,并通过 `select` 事件返回选中记录。
**架构:** 组件复用现有 `getBoxTypePage` 分页接口和 Vben VxeGrid/Modal 组合。弹窗打开时根据 `multiple` 动态设置 radio 或 checkbox 选择列,关闭时清理选择状态。
**技术栈:** Vue 3、TypeScript、Vben Modal、Vben VxeGrid、Ant Design Vue
---
## 文件结构
- 创建:`nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/lms/boxtype/components/BoxTypeSelectModal.vue`:封装木箱类型分页查询、单选/多选和选中结果回传。
### 任务 1创建木箱类型选择弹窗
**文件:**
- 创建:`nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/lms/boxtype/components/BoxTypeSelectModal.vue`
- [x] **步骤 1创建组件目录和文件**
组件完整实现:
```vue
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { LmsBoxTypeApi } from '#/api/lms/boxtype';
import { ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { DICT_TYPE } from '@vben/constants';
import { message } from 'antdv-next';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getBoxTypePage } from '#/api/lms/boxtype';
const emit = defineEmits<{
select: [rows: LmsBoxTypeApi.BoxType[]];
}>();
const selectedRows = ref<LmsBoxTypeApi.BoxType[]>([]);
const selectModalGridHeight = 600;
function handleRadioChange({ row }: { row?: LmsBoxTypeApi.BoxType }) {
selectedRows.value = row ? [row] : [];
}
function handleCheckboxChange({
records,
}: {
records: LmsBoxTypeApi.BoxType[];
}) {
selectedRows.value = records;
}
const baseColumns = [
{ field: 'boxType', title: '木箱类型', minWidth: 140 },
{ field: 'boxName', title: '木箱描述', minWidth: 180 },
{ field: 'boxLength', title: '木箱长度', minWidth: 100 },
{ field: 'boxWidth', title: '木箱宽度', minWidth: 100 },
{ field: 'boxHigh', title: '木箱高度', minWidth: 100 },
{
field: 'needLashOne',
title: '是否一次捆扎',
minWidth: 120,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING },
},
},
{
field: 'needLashTwo',
title: '是否二次捆扎',
minWidth: 120,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING },
},
},
];
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
schema: [
{
fieldName: 'boxType',
label: '木箱类型',
component: 'Input',
componentProps: {
allowClear: true,
placeholder: '请输入木箱类型',
},
},
{
fieldName: 'boxName',
label: '木箱描述',
component: 'Input',
componentProps: {
allowClear: true,
placeholder: '请输入木箱描述',
},
},
],
},
gridOptions: {
columns: [{ type: 'radio', width: 40 }, ...baseColumns],
height: selectModalGridHeight,
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
return await getBoxTypePage({
pageNo: page.currentPage,
pageSize: page.pageSize,
...formValues,
});
},
},
},
rowConfig: {
keyField: 'id',
isHover: true,
},
toolbarConfig: {
refresh: true,
search: true,
},
} as VxeTableGridOptions<LmsBoxTypeApi.BoxType>,
gridEvents: {
radioChange: handleRadioChange,
checkboxAll: handleCheckboxChange,
checkboxChange: handleCheckboxChange,
},
});
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
if (selectedRows.value.length === 0) {
message.warning('请至少选择一条木箱类型');
return;
}
emit('select', selectedRows.value);
await modalApi.close();
},
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
selectedRows.value = [];
return;
}
const data = modalApi.getData<{ multiple?: boolean }>();
const multiple = data?.multiple ?? false;
gridApi.setGridOptions({
columns: [
{ type: multiple ? 'checkbox' : 'radio', width: 40 },
...baseColumns,
],
});
selectedRows.value = [];
},
});
</script>
<template>
<Modal title="选择木箱类型" class="w-[1100px]">
<Grid />
</Modal>
</template>
```
- [x] **步骤 2运行 ESLint**
`nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben` 目录运行:
```powershell
node node_modules/eslint/bin/eslint.js apps/web-antdv-next/src/views/lms/boxtype/components/BoxTypeSelectModal.vue
```
预期:退出码为 `0`,无 ESLint 错误。
- [x] **步骤 3检查未选择提示和动态选择列**
运行:
```powershell
Select-String -Path apps/web-antdv-next/src/views/lms/boxtype/components/BoxTypeSelectModal.vue -Pattern "multiple \? 'checkbox' : 'radio'|请至少选择一条木箱类型|emit\('select'"
```
预期:三个模式均能匹配。
- [x] **步骤 4检查 Git 差异**
运行:
```powershell
git diff --check -- apps/web-antdv-next/src/views/lms/boxtype/components/BoxTypeSelectModal.vue
```
预期:退出码为 `0`
按项目约定,本任务不新增或运行测试。

View File

@@ -0,0 +1,736 @@
# 子卷包装关系批量新增实现计划
> **面向 AI 代理的工作者:** 必需子技能:使用 superpowers:subagent-driven-development推荐或 superpowers:executing-plans 逐任务实现此计划。步骤使用复选框(`- [ ]`)语法来跟踪进度。
**目标:** 为子卷包装关系提供可在卡片/表格间自由切换的批量新增弹窗,并由后端按“木箱码 + 木箱类型”分组校验容量、隔离保存和逐行返回失败原因。
**架构:** 前端批量弹窗以单一 `rows` 数组驱动两种视图,通过稳定 `clientKey` 映射后端错误;木箱类型复用现有选择弹窗,状态复用 `sub_package_status` 字典。后端新增独立批量 VO 和接口,由主 Service 完成逐行校验、分组和容量判断,再调用独立 `REQUIRES_NEW` 保存服务逐组写入,保证失败组不影响合法组。
**技术栈:** Java 17、Spring Boot、Jakarta Validation、MyBatis-Plus、JUnit 5、Mockito、Vue 3、TypeScript、Vben Modal、Antdv Next、Vitest。
---
## 文件结构
### 后端新增
- `nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/controller/admin/subpackagerelation/vo/SubPackageRelationBatchCreateReqVO.java`:批量请求容器。
- `nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/controller/admin/subpackagerelation/vo/SubPackageRelationBatchCreateItemReqVO.java`:单条批量录入数据。
- `nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/controller/admin/subpackagerelation/vo/SubPackageRelationBatchCreateRespVO.java`:成功/失败数量和失败行结构。
- `nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/service/subpackagerelation/SubPackageRelationBatchSaveService.java`:每个业务分组的独立事务写入器。
- `nl-module-lms/nl-module-lms-server/src/test/java/cn/code/nl/module/lms/service/subpackagerelation/SubPackageRelationServiceImplTest.java`:分组、容量、部分成功和失败映射单元测试。
- `nl-module-lms/nl-module-lms-server/src/test/java/cn/code/nl/module/lms/service/subpackagerelation/SubPackageRelationBatchSaveServiceTest.java`:分组批量写入委托测试。
### 后端修改
- `nl-module-lms/nl-module-lms-server/pom.xml`:补充 Mockito Inline 测试依赖。
- `nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/controller/admin/subpackagerelation/SubPackageRelationController.java`:暴露 `POST /batch-create`
- `nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/service/subpackagerelation/SubPackageRelationService.java`:声明批量创建方法。
- `nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/service/subpackagerelation/SubPackageRelationServiceImpl.java`:逐行校验、类型查询、分组、容量校验、逐组保存和响应组装。
- `nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/dal/mysql/subpackagerelation/SubPackageRelationMapper.java`:统计相同木箱码和木箱类型已有记录。
- `nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/dal/mysql/boxtype/BoxTypeMapper.java`:批量查询木箱类型。
### 前端新增
- `nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/lms/subpackagerelation/modules/batch-form-model.ts`:批量行创建、复制、校验和失败结果归并的纯函数。
- `nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/lms/subpackagerelation/modules/batch-form-model.test.ts`:纯函数单元测试。
- `nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/lms/subpackagerelation/modules/batch-form.vue`:批量新增弹窗、双视图和木箱类型选择组件接入。
### 前端修改
- `nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/packages/constants/src/dict-enum.ts`:增加 `SUB_PACKAGE_STATUS: 'sub_package_status'`
- `nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/api/lms/subpackagerelation/index.ts`:增加批量请求/响应类型和 API 方法。
- `nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/lms/subpackagerelation/index.vue`:增加批量弹窗和工具栏入口。
## 任务 1定义后端批量契约和查询能力
**文件:**
- 创建:`nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/controller/admin/subpackagerelation/vo/SubPackageRelationBatchCreateReqVO.java`
- 创建:`nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/controller/admin/subpackagerelation/vo/SubPackageRelationBatchCreateItemReqVO.java`
- 创建:`nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/controller/admin/subpackagerelation/vo/SubPackageRelationBatchCreateRespVO.java`
- 修改:`nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/dal/mysql/subpackagerelation/SubPackageRelationMapper.java`
- 修改:`nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/dal/mysql/boxtype/BoxTypeMapper.java`
- [ ] **步骤 1创建独立批量请求实体**
请求容器只对列表本身做 Jakarta Validation行字段由 Service 手动校验,以便返回逐行错误而不是整次请求直接返回 400
```java
@Data
@Schema(description = "管理后台 - 子卷包装关系批量新增 Request VO")
public class SubPackageRelationBatchCreateReqVO {
@NotEmpty(message = "批量新增数据不能为空")
@Schema(description = "批量新增数据", requiredMode = Schema.RequiredMode.REQUIRED)
private List<SubPackageRelationBatchCreateItemReqVO> items;
}
```
单条实体字段固定为:
```java
@Data
@Schema(description = "管理后台 - 子卷包装关系批量新增单条数据")
public class SubPackageRelationBatchCreateItemReqVO {
private String clientKey;
private String packageBoxSn;
private String boxType;
private String boxLength;
private String boxWidth;
private String boxHigh;
private String qualityGuaranPeriod;
private String dateOfFgInbound;
private String containerName;
private String status;
}
```
- [ ] **步骤 2创建结构化批量响应实体**
```java
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "管理后台 - 子卷包装关系批量新增 Response VO")
public class SubPackageRelationBatchCreateRespVO {
private Integer successCount;
private Integer failureCount;
private List<Failure> failures;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class Failure {
private String clientKey;
private Integer rowIndex;
private String errorCode;
private String message;
}
}
```
- [ ] **步骤 3增加 Mapper 查询方法**
`BoxTypeMapper` 增加:
```java
default List<BoxTypeDO> selectListByBoxTypes(Collection<String> boxTypes) {
return selectList(BoxTypeDO::getBoxType, boxTypes);
}
```
`SubPackageRelationMapper` 增加:
```java
default Long selectCountByPackageBoxSnAndBoxType(String packageBoxSn, String boxType) {
return selectCount(new LambdaQueryWrapper<SubPackageRelationDO>()
.eq(SubPackageRelationDO::getPackageBoxSn, packageBoxSn)
.eq(SubPackageRelationDO::getBoxType, boxType));
}
```
- [ ] **步骤 4编译后端契约**
运行:
```powershell
mvn -pl nl-module-lms/nl-module-lms-server -am -DskipTests compile
```
预期:`BUILD SUCCESS`,新 VO 和 Mapper 默认方法编译通过。
- [ ] **步骤 5提交后端契约**
```powershell
git add nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/controller/admin/subpackagerelation/vo nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/dal/mysql/boxtype/BoxTypeMapper.java nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/dal/mysql/subpackagerelation/SubPackageRelationMapper.java
git commit -m "feat: 定义子卷包装关系批量新增契约"
```
## 任务 2测试并实现分组事务写入器
**文件:**
- 修改:`nl-module-lms/nl-module-lms-server/pom.xml`
- 创建:`nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/service/subpackagerelation/SubPackageRelationBatchSaveService.java`
- 创建:`nl-module-lms/nl-module-lms-server/src/test/java/cn/code/nl/module/lms/service/subpackagerelation/SubPackageRelationBatchSaveServiceTest.java`
- [ ] **步骤 1增加 Mockito Inline 测试依赖**
`pom.xml` 的测试依赖区增加:
```xml
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-inline</artifactId>
<scope>test</scope>
</dependency>
```
- [ ] **步骤 2编写失败测试**
```java
@ExtendWith(MockitoExtension.class)
class SubPackageRelationBatchSaveServiceTest {
@Mock
private SubPackageRelationMapper subPackageRelationMapper;
@InjectMocks
private SubPackageRelationBatchSaveService service;
@Test
void shouldInsertWholeGroup() {
List<SubPackageRelationDO> group = List.of(
SubPackageRelationDO.builder().packageBoxSn("BX001").boxType("A").build(),
SubPackageRelationDO.builder().packageBoxSn("BX001").boxType("A").build());
service.saveGroup(group);
verify(subPackageRelationMapper).insertBatch(group);
}
}
```
- [ ] **步骤 3运行测试确认失败**
运行:
```powershell
mvn -pl nl-module-lms/nl-module-lms-server -Dtest=SubPackageRelationBatchSaveServiceTest test
```
预期FAIL提示 `SubPackageRelationBatchSaveService` 不存在。
- [ ] **步骤 4实现最小独立事务写入器**
```java
@Service
public class SubPackageRelationBatchSaveService {
@Resource
private SubPackageRelationMapper subPackageRelationMapper;
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
public void saveGroup(List<SubPackageRelationDO> group) {
subPackageRelationMapper.insertBatch(group);
}
}
```
- [ ] **步骤 5运行测试确认通过**
运行同一步骤 3。预期测试 PASS。
- [ ] **步骤 6提交事务写入器**
```powershell
git add nl-module-lms/nl-module-lms-server/pom.xml nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/service/subpackagerelation/SubPackageRelationBatchSaveService.java nl-module-lms/nl-module-lms-server/src/test/java/cn/code/nl/module/lms/service/subpackagerelation/SubPackageRelationBatchSaveServiceTest.java
git commit -m "feat: 增加包装关系分组事务写入器"
```
## 任务 3测试并实现批量业务编排
**文件:**
- 创建:`nl-module-lms/nl-module-lms-server/src/test/java/cn/code/nl/module/lms/service/subpackagerelation/SubPackageRelationServiceImplTest.java`
- 修改:`nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/service/subpackagerelation/SubPackageRelationService.java`
- 修改:`nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/service/subpackagerelation/SubPackageRelationServiceImpl.java`
- [ ] **步骤 1编写容量刚好满足和超限整组失败测试**
使用 `MockitoExtension` mock `SubPackageRelationMapper``BoxTypeMapper``SubPackageRelationBatchSaveService`。构造两组数据:
```java
private SubPackageRelationBatchCreateItemReqVO item(
String clientKey, String packageBoxSn, String boxType, String containerName) {
SubPackageRelationBatchCreateItemReqVO item = new SubPackageRelationBatchCreateItemReqVO();
item.setClientKey(clientKey);
item.setPackageBoxSn(packageBoxSn);
item.setBoxType(boxType);
item.setBoxLength("1200");
item.setBoxWidth("1000");
item.setBoxHigh("900");
item.setQualityGuaranPeriod("180");
item.setDateOfFgInbound("2026-08-17");
item.setContainerName(containerName);
item.setStatus("0");
return item;
}
```
核心断言:
```java
when(boxTypeMapper.selectListByBoxTypes(Set.of("A")))
.thenReturn(List.of(BoxTypeDO.builder().boxType("A").maxNum(3).build()));
when(subPackageRelationMapper.selectCountByPackageBoxSnAndBoxType("BX001", "A"))
.thenReturn(1L);
SubPackageRelationBatchCreateRespVO result = service.batchCreateSubPackageRelation(req(
item("k1", "BX001", "A", "R1"),
item("k2", "BX001", "A", "R2"))));
assertEquals(2, result.getSuccessCount());
assertEquals(0, result.getFailureCount());
verify(batchSaveService).saveGroup(argThat(group -> group.size() == 2));
```
将已有数量改为 `2L` 后断言:成功数为 0、失败数为 2、`errorCode``BOX_CAPACITY_EXCEEDED`、两个 `clientKey` 都返回,且 `batchSaveService` 未被调用。
- [ ] **步骤 2编写部分成功测试**
构造 `BX001/A` 超限、`BX002/A` 合法两个分组,断言:
```java
assertEquals(1, result.getSuccessCount());
assertEquals(2, result.getFailureCount());
assertEquals(Set.of("k1", "k2"), result.getFailures().stream()
.map(SubPackageRelationBatchCreateRespVO.Failure::getClientKey)
.collect(Collectors.toSet()));
verify(batchSaveService, times(1)).saveGroup(argThat(group ->
group.size() == 1 && "BX002".equals(group.get(0).getPackageBoxSn())));
```
- [ ] **步骤 3编写木箱类型不存在、必填失败和保存异常测试**
分别断言:
- 类型不存在返回 `BOX_TYPE_NOT_FOUND`,对应分组不保存。
-`packageBoxSn` 返回 `REQUIRED_FIELD_MISSING``rowIndex` 为请求中的一基行号。
- `batchSaveService.saveGroup` 对一个合法分组抛出异常时,该组返回 `GROUP_SAVE_FAILED`,后续合法组仍调用保存器并计入成功数。
- [ ] **步骤 4运行测试确认失败**
运行:
```powershell
mvn -pl nl-module-lms/nl-module-lms-server -Dtest=SubPackageRelationServiceImplTest test
```
预期FAIL提示批量方法和依赖尚不存在。
- [ ] **步骤 5声明 Service 方法并注入依赖**
`SubPackageRelationService` 增加:
```java
SubPackageRelationBatchCreateRespVO batchCreateSubPackageRelation(
SubPackageRelationBatchCreateReqVO reqVO);
```
`SubPackageRelationServiceImpl` 注入 `BoxTypeMapper``SubPackageRelationBatchSaveService`
- [ ] **步骤 6实现逐行校验、分组与容量判断**
实现顺序必须是:
1. 遍历请求项,按原始下标生成一基 `rowIndex`
2. 校验 `clientKey``packageBoxSn``boxType``qualityGuaranPeriod``dateOfFgInbound``containerName``status` 非空;失败项加入 `failures`,不参与分组。
3. 批量查询去重后的木箱类型并建立 `Map<String, BoxTypeDO>`
4. 按不可变分组键 `packageBoxSn + "\u0000" + boxType` 对合法项分组,同时保留每项的 `clientKey``rowIndex`
5. 类型不存在时整组追加 `BOX_TYPE_NOT_FOUND`
6. 查询相同木箱码和类型的数据库已有数量;当 `existing + group.size() > maxNum` 时整组追加 `BOX_CAPACITY_EXCEEDED`
7. 将合法项手动映射为 `SubPackageRelationDO`,只设置批量请求允许的业务字段。
不得使用 `BeanUtils.copyProperties`;使用显式 setter 或 builder 映射字段。
- [ ] **步骤 7实现逐组隔离保存和响应汇总**
逐个合法分组调用 `batchSaveService.saveGroup(group)`。成功时累加该组条数;捕获运行时异常时,为该组所有请求项追加 `GROUP_SAVE_FAILED`,然后继续处理下一组。
最终返回:
```java
return SubPackageRelationBatchCreateRespVO.builder()
.successCount(successCount)
.failureCount(failures.size())
.failures(failures)
.build();
```
- [ ] **步骤 8运行 Service 测试确认通过**
运行同一步骤 4。预期全部测试 PASS。
- [ ] **步骤 9提交批量业务逻辑**
```powershell
git add nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/service/subpackagerelation nl-module-lms/nl-module-lms-server/src/test/java/cn/code/nl/module/lms/service/subpackagerelation/SubPackageRelationServiceImplTest.java
git commit -m "feat: 实现包装关系分组批量保存"
```
## 任务 4暴露后端批量创建接口
**文件:**
- 修改:`nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/controller/admin/subpackagerelation/SubPackageRelationController.java`
- [ ] **步骤 1增加 Controller 方法**
```java
@PostMapping("/batch-create")
@Operation(summary = "批量创建子卷包装关系")
@PreAuthorize("@ss.hasPermission('lms:sub-package-relation:create')")
public CommonResult<SubPackageRelationBatchCreateRespVO> batchCreateSubPackageRelation(
@Valid @RequestBody SubPackageRelationBatchCreateReqVO reqVO) {
return success(subPackageRelationService.batchCreateSubPackageRelation(reqVO));
}
```
- [ ] **步骤 2运行 LMS Service 测试和编译**
```powershell
mvn -pl nl-module-lms/nl-module-lms-server -Dtest=SubPackageRelationServiceImplTest,SubPackageRelationBatchSaveServiceTest test
mvn -pl nl-module-lms/nl-module-lms-server -am -DskipTests compile
```
预期:测试全部 PASS编译 `BUILD SUCCESS`
- [ ] **步骤 3提交接口**
```powershell
git add nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/controller/admin/subpackagerelation/SubPackageRelationController.java
git commit -m "feat: 暴露包装关系批量新增接口"
```
## 任务 5定义前端字典、类型和 API
**文件:**
- 修改:`nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/packages/constants/src/dict-enum.ts`
- 修改:`nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/api/lms/subpackagerelation/index.ts`
- [ ] **步骤 1增加 LMS 状态字典常量**
`LMS_DICT` 中增加:
```ts
SUB_PACKAGE_STATUS: 'sub_package_status', // 子卷包装关系状态
```
- [ ] **步骤 2增加批量请求和响应类型**
```ts
export interface BatchCreateItem {
clientKey: string;
packageBoxSn: string;
boxType: string;
boxLength?: string;
boxWidth?: string;
boxHigh?: string;
qualityGuaranPeriod: string;
dateOfFgInbound: string;
containerName: string;
status: string;
}
export interface BatchCreateFailure {
clientKey: string;
rowIndex: number;
errorCode: string;
message: string;
}
export interface BatchCreateResult {
successCount: number;
failureCount: number;
failures: BatchCreateFailure[];
}
```
- [ ] **步骤 3增加批量 API 方法**
```ts
export function batchCreateSubPackageRelation(
items: LmsSubPackageRelationApi.BatchCreateItem[],
) {
return requestClient.post<LmsSubPackageRelationApi.BatchCreateResult>(
'/lms/sub-package-relation/batch-create',
{ items },
);
}
```
- [ ] **步骤 4运行目标应用类型检查并记录基线**
```powershell
pnpm -F @vben/web-antdv-next typecheck
```
预期PASS若现有工程基线报错保存完整输出后续验证只允许新增文件不增加错误。
- [ ] **步骤 5提交前端契约**
```powershell
git add nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/packages/constants/src/dict-enum.ts nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/api/lms/subpackagerelation/index.ts
git commit -m "feat: 增加包装关系批量新增前端契约"
```
## 任务 6测试并实现批量行模型
**文件:**
- 创建:`nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/lms/subpackagerelation/modules/batch-form-model.ts`
- 创建:`nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/lms/subpackagerelation/modules/batch-form-model.test.ts`
- [ ] **步骤 1编写失败的纯函数测试**
测试必须覆盖:
```ts
describe('batch form model', () => {
it('creates a blank row with status 0 and a stable key', () => {
const row = createBatchRow();
expect(row.status).toBe('0');
expect(row.clientKey).toBeTruthy();
});
it('copies reusable values and clears unique identifiers', () => {
const copied = copyBatchRow(sourceRow);
expect(copied.boxType).toBe(sourceRow.boxType);
expect(copied.boxLength).toBe(sourceRow.boxLength);
expect(copied.packageBoxSn).toBe('');
expect(copied.containerName).toBe('');
expect(copied.clientKey).not.toBe(sourceRow.clientKey);
});
it('returns field errors for every incomplete row', () => {
const errors = validateBatchRows([createBatchRow()]);
expect(errors.get(0)).toEqual(expect.objectContaining({
packageBoxSn: expect.any(String),
boxType: expect.any(String),
containerName: expect.any(String),
}));
});
it('keeps only backend failure rows and maps messages by clientKey', () => {
const result = applyBatchResult(rows, {
successCount: 1,
failureCount: 1,
failures: [{ clientKey: rows[1]!.clientKey, rowIndex: 2, errorCode: 'BOX_CAPACITY_EXCEEDED', message: '容量超限' }],
});
expect(result.rows).toHaveLength(1);
expect(result.rows[0]!.clientKey).toBe(rows[1]!.clientKey);
expect(result.rowErrors.get(rows[1]!.clientKey)).toBe('容量超限');
});
});
```
- [ ] **步骤 2运行测试确认失败**
在前端仓库根目录运行:
```powershell
pnpm exec vitest run apps/web-antdv-next/src/views/lms/subpackagerelation/modules/batch-form-model.test.ts
```
预期FAIL提示模型模块或导出函数不存在。
- [ ] **步骤 3实现最小批量行模型**
导出以下稳定类型和函数:
```ts
export interface BatchFormRow extends LmsSubPackageRelationApi.BatchCreateItem {}
export type BatchFieldErrors = Partial<Record<keyof BatchFormRow, string>>;
export function createBatchRow(): BatchFormRow;
export function copyBatchRow(source: BatchFormRow): BatchFormRow;
export function validateBatchRows(rows: BatchFormRow[]): Map<number, BatchFieldErrors>;
export function applyBatchResult(
rows: BatchFormRow[],
result: LmsSubPackageRelationApi.BatchCreateResult,
): { rowErrors: Map<string, string>; rows: BatchFormRow[] };
```
`clientKey` 使用 `crypto.randomUUID()`;如果运行环境不可用,则使用时间戳与自增序号组合的模块内 fallback。必填错误信息使用明确中文字段名。
- [ ] **步骤 4运行测试确认通过**
运行同一步骤 2。预期测试 PASS。
- [ ] **步骤 5提交批量行模型**
```powershell
git add nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/lms/subpackagerelation/modules/batch-form-model.ts nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/lms/subpackagerelation/modules/batch-form-model.test.ts
git commit -m "feat: 增加包装关系批量录入模型"
```
## 任务 7实现卡片/表格自由切换弹窗
**文件:**
- 创建:`nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/lms/subpackagerelation/modules/batch-form.vue`
- [ ] **步骤 1建立弹窗状态和木箱类型选择接入**
组件状态固定为:
```ts
const emit = defineEmits<{ success: [] }>();
const rows = ref<BatchFormRow[]>([createBatchRow()]);
const viewMode = ref<'card' | 'table'>('table');
const activeClientKey = ref<string>();
const fieldErrors = ref(new Map<number, BatchFieldErrors>());
const rowErrors = ref(new Map<string, string>());
const [BoxTypeSelectModal, boxTypeSelectModalApi] = useVbenModal({
connectedComponent: BoxTypeSelectModalComponent,
});
```
点击任一行木箱类型时设置 `activeClientKey` 并以 `{ multiple: false }` 打开选择弹窗;`select` 回调按 `clientKey` 精确回填 `boxType``boxLength``boxWidth``boxHigh`
- [ ] **步骤 2实现行操作和双视图共享数据**
新增、复制、删除、清空都只修改 `rows`。删除最后一条或清空时恢复 `[createBatchRow()]`。视图切换只修改 `viewMode`
卡片视图按记录渲染两栏表单;表格视图使用原生 `table` 或 Antdv Next `Table` 的横向可编辑布局。两种视图必须包含相同字段和相同 `v-model` 数据源,尺寸字段只读。
- [ ] **步骤 3接入状态字典和日期控件**
```ts
const statusOptions = getDictOptions(DICT_TYPE.SUB_PACKAGE_STATUS);
```
状态使用 `Select`,新记录默认 `'0'`。入库日期使用日期控件并在请求前格式化为 `YYYY-MM-DD`;不得把 Dayjs 对象传给后端。
- [ ] **步骤 4实现提交和部分成功处理**
确认按钮逻辑:
```ts
const validation = validateBatchRows(rows.value);
if (validation.size > 0) {
fieldErrors.value = validation;
message.warning('请检查必填项');
return;
}
const result = await batchCreateSubPackageRelation(rows.value);
const applied = applyBatchResult(rows.value, result);
if (result.successCount > 0) emit('success');
if (result.failureCount === 0) {
await modalApi.close();
message.success(`成功新增 ${result.successCount} 条子卷包装关系`);
return;
}
rows.value = applied.rows;
rowErrors.value = applied.rowErrors;
message.warning(`成功 ${result.successCount} 条,失败 ${result.failureCount} 条`);
```
`finally` 中解锁弹窗。关闭弹窗时重置所有状态,防止下次打开残留数据和错误。
- [ ] **步骤 5执行组件静态检查**
```powershell
pnpm exec eslint apps/web-antdv-next/src/views/lms/subpackagerelation/modules/batch-form.vue apps/web-antdv-next/src/views/lms/subpackagerelation/modules/batch-form-model.ts
pnpm -F @vben/web-antdv-next typecheck
```
预期:无新增 ESLint 或 TypeScript 错误。
- [ ] **步骤 6提交批量弹窗**
```powershell
git add nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/lms/subpackagerelation/modules/batch-form.vue
git commit -m "feat: 实现包装关系批量新增弹窗"
```
## 任务 8接入列表入口并完成联调验证
**文件:**
- 修改:`nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/lms/subpackagerelation/index.vue`
- [ ] **步骤 1接入批量弹窗**
```ts
import BatchForm from './modules/batch-form.vue';
const [BatchFormModal, batchFormModalApi] = useVbenModal({
connectedComponent: BatchForm,
destroyOnClose: true,
});
function handleBatchCreate() {
batchFormModalApi.open();
}
```
模板中增加:
```vue
<BatchFormModal @success="handleRefresh" />
```
- [ ] **步骤 2增加工具栏按钮**
在普通新增按钮后增加:
```ts
{
label: '批量新增',
type: 'primary',
icon: ACTION_ICON.ADD,
auth: ['lms:sub-package-relation:create'],
onClick: handleBatchCreate,
},
```
- [ ] **步骤 3运行后端定向测试和编译**
```powershell
mvn -pl nl-module-lms/nl-module-lms-server -Dtest=SubPackageRelationServiceImplTest,SubPackageRelationBatchSaveServiceTest test
mvn -pl nl-module-lms/nl-module-lms-server -am -DskipTests compile
```
预期:测试 PASS编译 `BUILD SUCCESS`
- [ ] **步骤 4运行前端测试和检查**
```powershell
pnpm exec vitest run apps/web-antdv-next/src/views/lms/subpackagerelation/modules/batch-form-model.test.ts
pnpm exec eslint apps/web-antdv-next/src/views/lms/subpackagerelation/index.vue apps/web-antdv-next/src/views/lms/subpackagerelation/modules/batch-form.vue apps/web-antdv-next/src/views/lms/subpackagerelation/modules/batch-form-model.ts
pnpm -F @vben/web-antdv-next typecheck
```
预期:测试 PASS目标文件静态检查通过类型检查无新增错误。
- [ ] **步骤 5手工联调验收**
按以下顺序检查:
1. 列表页能打开批量弹窗。
2. 新增两行,在表格输入后切到卡片,数据保持一致。
3. 选择木箱类型,确认回填目标行和尺寸。
4. 确认状态选项来自 `sub_package_status`,新增行默认 `0`
5. 提交一个合法组,弹窗关闭且列表刷新。
6. 提交一个超限组和一个合法组,合法组保存,超限组保留并逐行显示容量错误。
7. 修改失败行后再次提交,成功后弹窗关闭。
- [ ] **步骤 6提交列表接入**
```powershell
git add nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/lms/subpackagerelation/index.vue
git commit -m "feat: 接入包装关系批量新增入口"
```
## 任务 9最终一致性检查
**文件:**
- 检查本计划列出的全部新增和修改文件。
- [ ] **步骤 1检查工作区变更范围**
```powershell
git status --short
git diff --check
git diff --stat
```
预期:没有空白错误;不修改用户已有的配置、原型和其他计划文件。
- [ ] **步骤 2核对规格覆盖**
逐项确认:独立接口、独立 VO、原型字段范围、木箱类型弹窗、尺寸回填、状态字典默认值、双视图共享数据、容量统计包含数据库已有数量、超限整组失败、合法组独立保存、逐行错误展示全部已实现。
- [ ] **步骤 3记录验证证据**
交付说明必须列出实际运行的每条命令、退出码和关键结果;如果检查被现有依赖或工具链阻塞,明确给出原始错误摘要,不得声称对应检查通过。

View File

@@ -0,0 +1,133 @@
# 子卷包装关系批量新增 PC 原型实现计划
> **面向 AI 代理的工作者:** 必需子技能:使用 superpowers:subagent-driven-development推荐或 superpowers:executing-plans 逐任务实现此计划。步骤使用复选框(`- [ ]`)语法来跟踪进度。
**目标:** 创建一个符合现有 Vben Admin + Ant Design 风格、可在 PC 上完整演示批量新增子卷包装关系流程的单文件 HTML 原型。
**架构:** 使用一个自包含 HTML 文件承载页面结构、CSS 设计令牌与原生 JavaScript 状态管理。以行数组作为唯一数据源,集中实现渲染、木箱尺寸联动、复制/删除、校验和本地模拟提交,避免远程依赖与构建步骤。
**技术栈:** HTML5、CSS3、原生 JavaScript、Playwright 或可用浏览器工具进行交互验证。
---
## 文件结构
- 创建:`docs/prototype_design/design/2026-08-17-sub-package-relation-batch-design.html`——完整页面、弹窗、样式、模拟数据和交互逻辑。
- 修改:`docs/superpowers/plans/2026-08-17-sub-package-relation-batch.md`——执行后勾选任务状态。
### 任务 1建立页面骨架与视觉体系
**文件:**
- 创建:`docs/prototype_design/design/2026-08-17-sub-package-relation-batch-design.html`
- [x] **步骤 1定义可验证的静态结构约束**
确认 HTML 必须包含 `#batchModal``#editableBody``#addRowBtn``#clearBtn``#cancelBtn``#submitBtn`,并包含 11 个业务字段对应的输入控件生成逻辑。
- [x] **步骤 2创建完整后台页面骨架**
实现侧边栏、顶栏、面包屑、查询区、子卷包装关系列表和遮罩层,让弹窗处于真实页面上下文中。
- [x] **步骤 3实现 Ant Design 风格令牌**
`:root` 中定义主色 `#1677ff`、边框 `#d9d9d9`、页面底色 `#f5f5f5`、正文色 `#1f2329`、圆角和阴影;实现按钮、输入框、表格、提示条、弹窗及键盘聚焦状态。
- [x] **步骤 4验证静态结构**
运行:
```powershell
rg -n 'batchModal|editableBody|addRowBtn|clearBtn|cancelBtn|submitBtn' docs/prototype_design/design/2026-08-17-sub-package-relation-batch-design.html
```
预期:六个关键 ID 均至少出现一次。
### 任务 2实现批量录入核心交互
**文件:**
- 修改:`docs/prototype_design/design/2026-08-17-sub-package-relation-batch-design.html`
- [x] **步骤 1定义行数据与木箱类型**
定义 `rows` 数组、递增行键和以下演示字典:
```js
const boxTypes = {
'BX-A': { length: 1200, width: 1000, high: 900 },
'BX-B': { length: 1100, width: 900, high: 850 },
'BX-C': { length: 800, width: 600, high: 600 },
};
```
- [x] **步骤 2实现表格渲染与字段绑定**
每行渲染 `package_box_sn``box_weight``quality_guaran_period``sale_order_name``customer_name``date_of_fg_inbound``container_name``box_type``box_length``box_width``box_high`,其中尺寸字段只读。
- [x] **步骤 3实现木箱类型联动**
监听 `box_type` 变化,将选中类型对应的长、宽、高写入当前行;清空类型时同步清空三个尺寸并重新渲染统计。
- [x] **步骤 4实现行级操作**
实现新增、复制和删除。复制时保留公共数据并清空 `package_box_sn``container_name`;删除到最后一行时用新空行替换。
- [x] **步骤 5实现批量清空**
点击“清空全部”弹出确认层,确认后恢复 3 个空行,取消后保持原数据。
### 任务 3实现校验、提交与关闭流程
**文件:**
- 修改:`docs/prototype_design/design/2026-08-17-sub-package-relation-batch-design.html`
- [x] **步骤 1实现逐行必填校验**
要求七个可编辑业务字段和 `box_type` 均有值,`box_weight` 必须大于 0错误记录为 `{ rowKey, field, message }` 并映射到对应单元格。
- [x] **步骤 2实现重复值校验**
使用两个 `Map` 检查 `package_box_sn``container_name`,第二次出现及其首次出现位置都标红,并显示“与第 N 行重复”。
- [x] **步骤 3实现错误摘要与定位**
提交失败时显示顶部红色摘要,更新错误数量,并聚焦首个无效控件;输入修正后清除当前字段错误。
- [x] **步骤 4实现本地模拟提交**
校验通过后显示加载状态,短暂延迟后关闭弹窗、向背景列表追加记录,并显示“成功新增 N 条子卷包装关系”。
- [x] **步骤 5实现关闭保护与重新打开**
有内容时点击取消、关闭或遮罩显示放弃确认;空表直接关闭。背景页“批量新增”按钮重新打开弹窗并恢复 3 行。
### 任务 4浏览器验证与视觉修正
**文件:**
- 修改:`docs/prototype_design/design/2026-08-17-sub-package-relation-batch-design.html`
- 修改:`docs/superpowers/plans/2026-08-17-sub-package-relation-batch.md`
- [x] **步骤 1以目标视口打开原型**
使用 1440 × 900 视口加载本地 HTML确认弹窗未超出视口表格只在内容区内部滚动底部按钮始终可见。
- [x] **步骤 2走通核心流程**
依次操作新增行、选择 BX-B、确认尺寸为 1100 × 900 × 850、复制行、删除行、清空取消、空表提交校验、重复值校验和有效提交。
- [x] **步骤 3检查运行错误**
确认控制台无 JavaScript 错误、无远程资源失败、所有按钮可点击、Tab 键焦点可见。
- [x] **步骤 4修正视觉问题**
重点检查表头分组、冻结列、横向滚动、错误提示、背景遮罩、弹窗间距和 12 至 14px 的信息密度,修复裁切或重叠。
- [x] **步骤 5完成静态检查**
运行:
```powershell
rg -n 'package_box_sn|box_weight|quality_guaran_period|sale_order_name|customer_name|date_of_fg_inbound|container_name|box_type|box_length|box_width|box_high' docs/prototype_design/design/2026-08-17-sub-package-relation-batch-design.html
```
预期11 个字段名全部存在HTML 无远程 `http://``https://` 依赖。