1326 lines
44 KiB
Markdown
1326 lines
44 KiB
Markdown
# 出库单新增页面 实现计划
|
||
|
||
> **面向 AI 代理的工作者:** 必需子技能:使用 superpowers:subagent-driven-development(推荐)或 superpowers:executing-plans 逐任务实现此计划。步骤使用复选框(`- [ ]`)语法来跟踪进度。
|
||
|
||
**目标:** 将出入库单主表的新增弹窗重构为出库单新增页面,包含主表表单 + 出库明细 table,支持从库存选择带入和手动新增汇总两种方式。
|
||
|
||
**架构:** 前端大弹窗页面(form.vue 重构),主表用 `useVbenForm` 水平布局,明细用 vxe-table 内嵌,新增库存选择弹窗和手动汇总弹窗两个子组件。后端新增 `createWithDetails` 接口,事务写入主表 + 明细,库存查询 SQL 写在 Mapper XML 中。通过 Feign 调用 base 模块的 CodeGenApi 生成单据号。
|
||
|
||
**技术栈:** Java 17, Spring Boot 3, MyBatis-Plus, OpenFeign, Vue 3, Ant Design Vue Next, Vxe Table
|
||
|
||
---
|
||
|
||
### 文件结构
|
||
|
||
```
|
||
# 后端 — 新增/修改
|
||
nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/
|
||
├── framework/rpc/config/RpcConfiguration.java [新增] Feign 客户端配置
|
||
├── controller/admin/iostorinv/vo/
|
||
│ ├── IostorInvWithDetailsSaveReqVO.java [新增] 带明细的创建请求 VO
|
||
│ └── AvailableInventoryVO.java [新增] 可用库存查询结果 VO
|
||
├── dal/mysql/iostorinvdtl/IostorinvDtlMapper.java [修改] 新增 selectAvailableInventory 方法
|
||
├── resources/mapper/iostorinvdtl/IostorinvDtlMapper.xml [修改] 新增库存查询 SQL
|
||
├── service/iostorinv/IostorInvService.java [修改] 新增 createWithDetails 方法
|
||
├── service/iostorinv/IostorInvServiceImpl.java [修改] 实现 createWithDetails
|
||
├── service/iostorinvdtl/IostorinvDtlService.java [修改] 新增 batchInsert 方法
|
||
├── service/iostorinvdtl/IostorinvDtlServiceImpl.java [修改] 实现 batchInsert
|
||
└── controller/admin/iostorinv/IostorInvController.java [修改] 新增 createWithDetails 端点
|
||
|
||
# 前端 — 新增/修改
|
||
nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/
|
||
├── api/wms/iostorinv/index.ts [修改] 新增类型和 API
|
||
├── views/wms/iostorinv/modules/
|
||
│ ├── form.vue [重构] 大弹窗 + 主表 + 明细
|
||
│ ├── inventory-select.vue [新增] 库存选择弹窗
|
||
│ └── manual-detail.vue [新增] 手动新增汇总弹窗
|
||
└── packages/constants/src/dict-enum.ts [修改] 新增 out_bill_type 字典类型
|
||
```
|
||
|
||
---
|
||
|
||
### 任务 1:后端 — Feign 客户端配置
|
||
|
||
**文件:**
|
||
- 创建:`nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/framework/rpc/config/RpcConfiguration.java`
|
||
|
||
- [ ] **步骤 1:创建 RpcConfiguration,注册 CodeGenApi Feign 客户端**
|
||
|
||
```java
|
||
package cn.code.nl.module.wms.framework.rpc.config;
|
||
|
||
import cn.code.nl.module.base.api.codegen.CodeGenApi;
|
||
import org.springframework.cloud.openfeign.EnableFeignClients;
|
||
import org.springframework.context.annotation.Configuration;
|
||
|
||
/**
|
||
* WMS 模块 RPC 配置
|
||
*
|
||
* @author zhouz
|
||
*/
|
||
@Configuration(value = "wmsRpcConfiguration", proxyBeanMethods = false)
|
||
@EnableFeignClients(clients = {CodeGenApi.class})
|
||
public class RpcConfiguration {
|
||
}
|
||
```
|
||
|
||
> **注意:** 需要确认 `nl-module-wms-server` 的 pom.xml 已依赖 `nl-module-base-api`,否则 Feign 接口编译不到。如缺失,添加依赖:
|
||
> ```xml
|
||
> <dependency>
|
||
> <groupId>cn.code.nl</groupId>
|
||
> <artifactId>nl-module-base-api</artifactId>
|
||
> <version>${project.version}</version>
|
||
> </dependency>
|
||
> ```
|
||
|
||
- [ ] **步骤 2:Commit**
|
||
|
||
```bash
|
||
git add nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/framework/rpc/config/RpcConfiguration.java
|
||
git commit -m "feat: 添加 WMS 模块 Feign 客户端配置,注册 CodeGenApi"
|
||
```
|
||
|
||
---
|
||
|
||
### 任务 2:后端 — 新增请求 VO 和库存查询 VO
|
||
|
||
**文件:**
|
||
- 创建:`nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/iostorinv/vo/IostorInvWithDetailsSaveReqVO.java`
|
||
- 创建:`nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/iostorinv/vo/AvailableInventoryVO.java`
|
||
|
||
- [ ] **步骤 1:创建带明细的创建请求 VO**
|
||
|
||
```java
|
||
package cn.code.nl.module.wms.controller.admin.iostorinv.vo;
|
||
|
||
import io.swagger.v3.oas.annotations.media.Schema;
|
||
import jakarta.validation.Valid;
|
||
import jakarta.validation.constraints.NotEmpty;
|
||
import jakarta.validation.constraints.NotNull;
|
||
import lombok.Data;
|
||
|
||
import java.math.BigDecimal;
|
||
import java.time.LocalDateTime;
|
||
import java.util.List;
|
||
|
||
/**
|
||
* 管理后台 - 出库单新增(含明细)Request VO
|
||
*
|
||
* @author zhouz
|
||
*/
|
||
@Schema(description = "管理后台 - 出库单新增(含明细)Request VO")
|
||
@Data
|
||
public class IostorInvWithDetailsSaveReqVO {
|
||
|
||
@Schema(description = "单据类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "生产领料出库")
|
||
@NotEmpty(message = "单据类型不能为空")
|
||
private String billType;
|
||
|
||
@Schema(description = "仓库标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "12635")
|
||
@NotEmpty(message = "仓库标识不能为空")
|
||
private String storId;
|
||
|
||
@Schema(description = "仓库编码")
|
||
private String storCode;
|
||
|
||
@Schema(description = "仓库名称", example = "原料仓库")
|
||
private String storName;
|
||
|
||
@Schema(description = "业务日期", requiredMode = Schema.RequiredMode.REQUIRED)
|
||
@NotNull(message = "业务日期不能为空")
|
||
private LocalDateTime bizDate;
|
||
|
||
@Schema(description = "备注")
|
||
private String remark;
|
||
|
||
@Schema(description = "出库明细列表", requiredMode = Schema.RequiredMode.REQUIRED)
|
||
@NotEmpty(message = "请至少添加一条出库明细")
|
||
@Valid
|
||
private List<DetailVO> details;
|
||
|
||
/**
|
||
* 出库明细 VO
|
||
*/
|
||
@Data
|
||
public static class DetailVO {
|
||
|
||
@Schema(description = "物料编码", requiredMode = Schema.RequiredMode.REQUIRED)
|
||
@NotEmpty(message = "物料编码不能为空")
|
||
private String materialCode;
|
||
|
||
@Schema(description = "物料标识")
|
||
private String materialId;
|
||
|
||
@Schema(description = "批次/子卷号")
|
||
private String pcsn;
|
||
|
||
@Schema(description = "出库重量", requiredMode = Schema.RequiredMode.REQUIRED)
|
||
@NotNull(message = "出库重量不能为空")
|
||
private BigDecimal planQty;
|
||
|
||
@Schema(description = "计量单位标识")
|
||
private String qtyUnitId;
|
||
|
||
@Schema(description = "计量单位名称")
|
||
private String qtyUnitName;
|
||
|
||
@Schema(description = "备注")
|
||
private String remark;
|
||
|
||
@Schema(description = "来源单据号")
|
||
private String sourceBillCode;
|
||
|
||
@Schema(description = "来源单据类型")
|
||
private String sourceBillType;
|
||
|
||
@Schema(description = "来源单据明细标识")
|
||
private String sourceBilldtlId;
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **步骤 2:创建可用库存查询结果 VO**
|
||
|
||
```java
|
||
package cn.code.nl.module.wms.controller.admin.iostorinv.vo;
|
||
|
||
import io.swagger.v3.oas.annotations.media.Schema;
|
||
import lombok.Data;
|
||
|
||
import java.math.BigDecimal;
|
||
|
||
/**
|
||
* 管理后台 - 可用库存查询结果 VO
|
||
*
|
||
* @author zhouz
|
||
*/
|
||
@Schema(description = "管理后台 - 可用库存查询结果 VO")
|
||
@Data
|
||
public class AvailableInventoryVO {
|
||
|
||
@Schema(description = "载具编码(箱号)")
|
||
private String vehicleCode;
|
||
|
||
@Schema(description = "批次/子卷号")
|
||
private String pcsn;
|
||
|
||
@Schema(description = "物料编码")
|
||
private String materialCode;
|
||
|
||
@Schema(description = "物料标识")
|
||
private String materialId;
|
||
|
||
@Schema(description = "组盘数量")
|
||
private BigDecimal qty;
|
||
|
||
@Schema(description = "冻结数量")
|
||
private BigDecimal frozenQty;
|
||
|
||
@Schema(description = "可用数量")
|
||
private BigDecimal availableQty;
|
||
|
||
@Schema(description = "计量单位标识")
|
||
private String qtyUnitId;
|
||
|
||
@Schema(description = "计量单位名称")
|
||
private String qtyUnitName;
|
||
|
||
@Schema(description = "来源单据号")
|
||
private String extCode;
|
||
|
||
@Schema(description = "来源单据类型")
|
||
private String extType;
|
||
|
||
@Schema(description = "来源单据明细号")
|
||
private String extDtlCode;
|
||
|
||
@Schema(description = "仓库标识")
|
||
private String storId;
|
||
|
||
@Schema(description = "仓库编码")
|
||
private String storCode;
|
||
|
||
@Schema(description = "仓库名称")
|
||
private String storName;
|
||
}
|
||
```
|
||
|
||
- [ ] **步骤 3:Commit**
|
||
|
||
```bash
|
||
git add nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/iostorinv/vo/IostorInvWithDetailsSaveReqVO.java nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/iostorinv/vo/AvailableInventoryVO.java
|
||
git commit -m "feat: 新增出库单带明细创建请求 VO 和可用库存查询 VO"
|
||
```
|
||
|
||
---
|
||
|
||
### 任务 3:后端 — Mapper XML 新增库存查询 SQL
|
||
|
||
**文件:**
|
||
- 修改:`nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/dal/mysql/iostorinvdtl/IostorinvDtlMapper.java`
|
||
- 修改:`nl-module-wms/nl-module-wms-server/src/main/resources/mapper/iostorinvdtl/IostorinvDtlMapper.xml`
|
||
|
||
- [ ] **步骤 1:在 IostorinvDtlMapper 接口中新增方法**
|
||
|
||
在文件末尾(`}` 之前)添加:
|
||
|
||
```java
|
||
/**
|
||
* 查询可用库存(wms_group_plate 关联 wms_structattr)
|
||
*
|
||
* @param storId 仓库标识(必传)
|
||
* @param materialCode 物料编码(可选)
|
||
* @param pcsn 批次(可选)
|
||
* @param vehicleCode 载具编码(可选)
|
||
* @return 可用库存列表
|
||
*/
|
||
List<AvailableInventoryVO> selectAvailableInventory(
|
||
@Param("storId") String storId,
|
||
@Param("materialCode") String materialCode,
|
||
@Param("pcsn") String pcsn,
|
||
@Param("vehicleCode") String vehicleCode);
|
||
```
|
||
|
||
同时新增 import:
|
||
|
||
```java
|
||
import cn.code.nl.module.wms.controller.admin.iostorinv.vo.AvailableInventoryVO;
|
||
import org.apache.ibatis.annotations.Param;
|
||
```
|
||
|
||
- [ ] **步骤 2:在 IostorinvDtlMapper.xml 中新增 SQL**
|
||
|
||
替换空的 XML 文件内容为:
|
||
|
||
```xml
|
||
<?xml version="1.0" encoding="UTF-8"?>
|
||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||
<mapper namespace="cn.code.nl.module.wms.dal.mysql.iostorinvdtl.IostorinvDtlMapper">
|
||
|
||
<!--
|
||
一般情况下,尽可能使用 Mapper 进行 CRUD 增删改查即可。
|
||
无法满足的场景,例如说多表关联查询,才使用 XML 编写 SQL。
|
||
-->
|
||
|
||
<select id="selectAvailableInventory"
|
||
resultType="cn.code.nl.module.wms.controller.admin.iostorinv.vo.AvailableInventoryVO">
|
||
SELECT
|
||
gp.vehicle_code AS vehicleCode,
|
||
gp.pcsn AS pcsn,
|
||
gp.material_code AS materialCode,
|
||
gp.material_id AS materialId,
|
||
gp.qty AS qty,
|
||
gp.frozen_qty AS frozenQty,
|
||
(gp.qty - gp.frozen_qty) AS availableQty,
|
||
gp.qty_unit_id AS qtyUnitId,
|
||
gp.qty_unit_name AS qtyUnitName,
|
||
gp.ext_code AS extCode,
|
||
gp.ext_type AS extType,
|
||
gp.ext_dtl_code AS extDtlCode,
|
||
sa.stor_id AS storId,
|
||
sa.stor_code AS storCode,
|
||
sa.stor_name AS storName
|
||
FROM wms_group_plate gp
|
||
INNER JOIN wms_structattr sa
|
||
ON gp.vehicle_code = sa.storagevehicle_code
|
||
WHERE sa.stor_id = #{storId}
|
||
AND gp.status = '可用'
|
||
AND (gp.qty - gp.frozen_qty) > 0
|
||
<if test="materialCode != null and materialCode != ''">
|
||
AND gp.material_code = #{materialCode}
|
||
</if>
|
||
<if test="pcsn != null and pcsn != ''">
|
||
AND gp.pcsn = #{pcsn}
|
||
</if>
|
||
<if test="vehicleCode != null and vehicleCode != ''">
|
||
AND gp.vehicle_code = #{vehicleCode}
|
||
</if>
|
||
</select>
|
||
|
||
</mapper>
|
||
```
|
||
|
||
- [ ] **步骤 3:Commit**
|
||
|
||
```bash
|
||
git add nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/dal/mysql/iostorinvdtl/IostorinvDtlMapper.java nl-module-wms/nl-module-wms-server/src/main/resources/mapper/iostorinvdtl/IostorinvDtlMapper.xml
|
||
git commit -m "feat: 新增可用库存查询 SQL(group_plate 关联 structattr)"
|
||
```
|
||
|
||
---
|
||
|
||
### 任务 4:后端 — Service 层新增 createWithDetails 和 batchInsert
|
||
|
||
**文件:**
|
||
- 修改:`nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/service/iostorinv/IostorInvService.java`
|
||
- 修改:`nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/service/iostorinv/IostorInvServiceImpl.java`
|
||
- 修改:`nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/service/iostorinvdtl/IostorinvDtlService.java`
|
||
- 修改:`nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/service/iostorinvdtl/IostorinvDtlServiceImpl.java`
|
||
|
||
- [ ] **步骤 1:在 IostorInvService 接口中新增方法声明**
|
||
|
||
在文件末尾的 `}` 之前添加:
|
||
|
||
```java
|
||
/**
|
||
* 创建出库单(含明细)
|
||
*
|
||
* @param reqVO 创建请求(含明细列表)
|
||
* @return 出入单标识
|
||
*/
|
||
String createIostorInvWithDetails(@Valid IostorInvWithDetailsSaveReqVO reqVO);
|
||
```
|
||
|
||
新增 import:
|
||
|
||
```java
|
||
import cn.code.nl.module.wms.controller.admin.iostorinv.vo.IostorInvWithDetailsSaveReqVO;
|
||
```
|
||
|
||
- [ ] **步骤 2:在 IostorInvServiceImpl 中实现 createIostorInvWithDetails**
|
||
|
||
添加 `@Resource` 注入:
|
||
|
||
```java
|
||
@Resource
|
||
private cn.code.nl.module.base.api.codegen.CodeGenApi codeGenApi;
|
||
|
||
@Resource
|
||
private cn.code.nl.module.wms.dal.mysql.iostorinvdtl.IostorinvDtlMapper iostorinvDtlMapper;
|
||
```
|
||
|
||
新增方法实现(在文件末尾 `}` 之前):
|
||
|
||
```java
|
||
@Override
|
||
@Transactional(rollbackFor = Exception.class)
|
||
public String createIostorInvWithDetails(IostorInvWithDetailsSaveReqDTO reqVO) {
|
||
// 1. 调用 base 模块生成单据号
|
||
CodeGenerateReqDTO codeReq = new CodeGenerateReqDTO();
|
||
codeReq.setRuleCode("IO_CODE");
|
||
CommonResult<String> codeResult = codeGenApi.generate(codeReq);
|
||
if (codeResult == null || codeResult.getData() == null) {
|
||
throw exception(IOSTOR_INV_BILL_CODE_GENERATE_FAIL);
|
||
}
|
||
String billCode = codeResult.getData();
|
||
|
||
// 2. 构建主表数据
|
||
IostorInvDO master = new IostorInvDO();
|
||
master.setBillCode(billCode);
|
||
master.setIoType("OUT");
|
||
master.setBillType(reqVO.getBillType());
|
||
master.setBizDate(reqVO.getBizDate());
|
||
master.setStorId(reqVO.getStorId());
|
||
master.setStorCode(reqVO.getStorCode());
|
||
master.setStorName(reqVO.getStorName());
|
||
master.setBillStatus("生成");
|
||
master.setRemark(reqVO.getRemark());
|
||
master.setDetailCount(reqVO.getDetails().size());
|
||
|
||
// 3. 计算总重量
|
||
BigDecimal totalWeight = reqVO.getDetails().stream()
|
||
.map(IostorInvWithDetailsSaveReqVO.DetailVO::getPlanQty)
|
||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||
master.setTotalWeight(totalWeight);
|
||
|
||
// 4. 插入主表
|
||
iostorInvMapper.insert(master);
|
||
|
||
// 5. 批量插入明细
|
||
String iostorinvId = master.getIostorinvId();
|
||
int seqNo = 1;
|
||
for (IostorInvWithDetailsSaveReqVO.DetailVO detail : reqVO.getDetails()) {
|
||
IostorinvDtlDO dtl = new IostorinvDtlDO();
|
||
dtl.setIostorinvId(iostorinvId);
|
||
dtl.setSeqNo(seqNo++);
|
||
dtl.setMaterialCode(detail.getMaterialCode());
|
||
dtl.setMaterialId(detail.getMaterialId());
|
||
dtl.setPcsn(detail.getPcsn());
|
||
dtl.setPlanQty(detail.getPlanQty());
|
||
dtl.setUnassignQty(detail.getPlanQty()); // 初始未分配 = 计划数量
|
||
dtl.setAssignQty(BigDecimal.ZERO);
|
||
dtl.setQtyUnitId(detail.getQtyUnitId());
|
||
dtl.setQtyUnitName(detail.getQtyUnitName());
|
||
dtl.setRemark(detail.getRemark());
|
||
dtl.setSourceBillCode(detail.getSourceBillCode());
|
||
dtl.setSourceBillType(detail.getSourceBillType());
|
||
dtl.setSourceBilldtlId(detail.getSourceBilldtlId());
|
||
iostorinvDtlMapper.insert(dtl);
|
||
}
|
||
|
||
return iostorinvId;
|
||
}
|
||
```
|
||
|
||
新增 import:
|
||
|
||
```java
|
||
import cn.code.nl.module.base.api.codegen.CodeGenApi;
|
||
import cn.code.nl.module.base.api.codegen.dto.CodeGenerateReqDTO;
|
||
import cn.code.nl.module.framework.common.pojo.CommonResult;
|
||
import cn.code.nl.module.wms.controller.admin.iostorinv.vo.IostorInvWithDetailsSaveReqVO;
|
||
import cn.code.nl.module.wms.dal.dataobject.iostorinvdtl.IostorinvDtlDO;
|
||
import cn.code.nl.module.wms.dal.mysql.iostorinvdtl.IostorinvDtlMapper;
|
||
import java.math.BigDecimal;
|
||
```
|
||
|
||
- [ ] **步骤 3:在 ErrorCodeConstants 中新增错误码**
|
||
|
||
修改 `nl-module-wms/nl-module-wms-api/src/main/java/cn/code/nl/module/wms/enums/ErrorCodeConstants.java`,在文件末尾添加:
|
||
|
||
```java
|
||
// ========== 出入库单 1-030-001-000 ==========
|
||
ErrorCode IOSTOR_INV_BILL_CODE_GENERATE_FAIL = new ErrorCode(1_030_001_000, "单据号生成失败,请稍后重试");
|
||
```
|
||
|
||
> **注意:** 确认错误码 `1_030_001_000` 不与已有错误码冲突。如冲突则顺延。
|
||
|
||
- [ ] **步骤 4:IostorinvDtlService 和 Impl 暂不改动**
|
||
|
||
明细的批量插入直接在 `IostorInvServiceImpl` 中通过 `iostorinvDtlMapper.insert()` 完成,无需改动 DtlService 接口。跳过此改动。
|
||
|
||
- [ ] **步骤 5:Commit**
|
||
|
||
```bash
|
||
git add nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/service/iostorinv/IostorInvService.java nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/service/iostorinv/IostorInvServiceImpl.java nl-module-wms/nl-module-wms-api/src/main/java/cn/code/nl/module/wms/enums/ErrorCodeConstants.java
|
||
git commit -m "feat: 实现出库单带明细创建 Service(含 CodeGen 单据号生成)"
|
||
```
|
||
|
||
---
|
||
|
||
### 任务 5:后端 — Controller 新增 createWithDetails 端点
|
||
|
||
**文件:**
|
||
- 修改:`nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/iostorinv/IostorInvController.java`
|
||
|
||
- [ ] **步骤 1:新增 createWithDetails 方法和可用库存查询方法**
|
||
|
||
在类末尾的 `}` 之前添加:
|
||
|
||
```java
|
||
@PostMapping("/create-with-details")
|
||
@Operation(summary = "创建出库单(含明细)")
|
||
@PreAuthorize("@ss.hasPermission('wms:iostor-inv:create')")
|
||
public CommonResult<String> createIostorInvWithDetails(
|
||
@Valid @RequestBody IostorInvWithDetailsSaveReqVO reqVO) {
|
||
return success(iostorInvService.createIostorInvWithDetails(reqVO));
|
||
}
|
||
```
|
||
|
||
新增 import:
|
||
|
||
```java
|
||
import cn.code.nl.module.wms.controller.admin.iostorinv.vo.IostorInvWithDetailsSaveReqVO;
|
||
```
|
||
|
||
- [ ] **步骤 2:Commit**
|
||
|
||
```bash
|
||
git add nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/iostorinv/IostorInvController.java
|
||
git commit -m "feat: 新增出库单带明细创建 API 端点 POST /create-with-details"
|
||
```
|
||
|
||
---
|
||
|
||
### 任务 6:前端 — API 层扩展
|
||
|
||
**文件:**
|
||
- 修改:`nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/api/wms/iostorinv/index.ts`
|
||
|
||
- [ ] **步骤 1:新增请求类型和 API 方法**
|
||
|
||
在文件末尾的 `export function exportIostorInv` 之前添加类型定义,并在最后添加 API 方法:
|
||
|
||
```typescript
|
||
// --- 出库单带明细创建 ---
|
||
|
||
/** 出库明细项 */
|
||
export interface OutboundDetail {
|
||
materialCode: string;
|
||
materialId?: string;
|
||
pcsn?: string;
|
||
planQty: number;
|
||
qtyUnitId?: string;
|
||
qtyUnitName?: string;
|
||
remark?: string;
|
||
sourceBillCode?: string;
|
||
sourceBillType?: string;
|
||
sourceBilldtlId?: string;
|
||
}
|
||
|
||
/** 出库单创建请求(含明细) */
|
||
export interface OutboundCreateReq {
|
||
billType: string;
|
||
storId: string;
|
||
storCode?: string;
|
||
storName?: string;
|
||
bizDate: string;
|
||
remark?: string;
|
||
details: OutboundDetail[];
|
||
}
|
||
|
||
/** 可用库存项 */
|
||
export interface AvailableInventory {
|
||
vehicleCode: string;
|
||
pcsn: string;
|
||
materialCode: string;
|
||
materialId: string;
|
||
qty: number;
|
||
frozenQty: number;
|
||
availableQty: number;
|
||
qtyUnitId: string;
|
||
qtyUnitName: string;
|
||
extCode: string;
|
||
extType: string;
|
||
extDtlCode: string;
|
||
storId: string;
|
||
storCode: string;
|
||
storName: string;
|
||
}
|
||
|
||
/** 可用库存查询参数 */
|
||
export interface AvailableInventoryQuery {
|
||
storId: string;
|
||
materialCode?: string;
|
||
pcsn?: string;
|
||
vehicleCode?: string;
|
||
}
|
||
```
|
||
|
||
在文件末尾(`export function exportIostorInv` 之后)添加:
|
||
|
||
```typescript
|
||
/** 创建出库单(含明细) */
|
||
export function createIostorInvWithDetails(data: OutboundCreateReq) {
|
||
return requestClient.post<string>('/wms/iostor-inv/create-with-details', data);
|
||
}
|
||
|
||
/** 查询可用库存 */
|
||
export function getAvailableInventory(params: AvailableInventoryQuery) {
|
||
return requestClient.get<AvailableInventory[]>(
|
||
'/wms/iostorinvdtl/available-inventory',
|
||
{ params },
|
||
);
|
||
}
|
||
```
|
||
|
||
- [ ] **步骤 2:Commit**
|
||
|
||
```bash
|
||
git add nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/api/wms/iostorinv/index.ts
|
||
git commit -m "feat: 前端 API 层新增出库单带明细创建和可用库存查询"
|
||
```
|
||
|
||
---
|
||
|
||
### 任务 7:后端 — 可用库存查询 Controller(补充)
|
||
|
||
> 前面任务 3 只在 Mapper 加了 SQL,但前端需要一个 HTTP 接口调用。需要在 Controller 或现有 Controller 中暴露。
|
||
|
||
**文件:**
|
||
- 修改:`nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/iostorinvdtl/IostorinvDtlController.java`
|
||
|
||
- [ ] **步骤 1:在 IostorinvDtlController 中新增可用库存查询端点**
|
||
|
||
在类末尾 `}` 之前添加:
|
||
|
||
```java
|
||
@GetMapping("/available-inventory")
|
||
@Operation(summary = "查询可用库存(组盘关联仓位)")
|
||
@PreAuthorize("@ss.hasPermission('wms:iostor-inv:query')")
|
||
public CommonResult<List<AvailableInventoryVO>> getAvailableInventory(
|
||
@RequestParam("storId") String storId,
|
||
@RequestParam(value = "materialCode", required = false) String materialCode,
|
||
@RequestParam(value = "pcsn", required = false) String pcsn,
|
||
@RequestParam(value = "vehicleCode", required = false) String vehicleCode) {
|
||
List<AvailableInventoryVO> list = iostorinvDtlMapper.selectAvailableInventory(
|
||
storId, materialCode, pcsn, vehicleCode);
|
||
return success(list);
|
||
}
|
||
```
|
||
|
||
注入 Mapper:
|
||
|
||
```java
|
||
@Resource
|
||
private IostorinvDtlMapper iostorinvDtlMapper;
|
||
```
|
||
|
||
新增 import:
|
||
|
||
```java
|
||
import cn.code.nl.module.wms.controller.admin.iostorinv.vo.AvailableInventoryVO;
|
||
import cn.code.nl.module.wms.dal.mysql.iostorinvdtl.IostorinvDtlMapper;
|
||
import jakarta.annotation.Resource;
|
||
```
|
||
|
||
- [ ] **步骤 2:Commit**
|
||
|
||
```bash
|
||
git add nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/iostorinvdtl/IostorinvDtlController.java
|
||
git commit -m "feat: 新增可用库存查询端点 GET /available-inventory"
|
||
```
|
||
|
||
---
|
||
|
||
### 任务 8:前端 — 重构 form.vue(主表表单 + 明细 Table)
|
||
|
||
**文件:**
|
||
- 修改:`nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/form.vue`
|
||
|
||
- [ ] **步骤 1:重写 form.vue — 脚本部分**
|
||
|
||
```vue
|
||
<script lang="ts" setup>
|
||
import type { WmsBsrealStorAttrApi } from '#/api/wms/bsrealstorattr';
|
||
|
||
import { computed, ref, watch } from 'vue';
|
||
|
||
import { useVbenModal } from '@vben/common-ui';
|
||
import { DICT_TYPE } from '@vben/constants';
|
||
import { getDictOptions } from '@vben/hooks';
|
||
|
||
import { message } from 'antdv-next';
|
||
|
||
import { useVbenForm } from '#/adapter/form';
|
||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||
import {
|
||
type OutboundCreateReq,
|
||
type OutboundDetail,
|
||
createIostorInvWithDetails,
|
||
} from '#/api/wms/iostorinv';
|
||
import { getBsrealStorAttrSimpleList } from '#/api/wms/bsrealstorattr';
|
||
import { getSimpleDictDataList } from '#/api/system/dict/data';
|
||
import { $t } from '#/locales';
|
||
|
||
import InventorySelect from './inventory-select.vue';
|
||
import ManualDetail from './manual-detail.vue';
|
||
|
||
const emit = defineEmits(['success']);
|
||
|
||
// 仓库选项
|
||
const storOptions = ref<{ label: string; value: string; storCode: string; storName: string }[]>([]);
|
||
async function loadStorOptions() {
|
||
const list = await getBsrealStorAttrSimpleList();
|
||
storOptions.value = (list || []).map((item) => ({
|
||
label: item.storName,
|
||
value: item.storId,
|
||
storCode: item.storCode,
|
||
storName: item.storName,
|
||
}));
|
||
}
|
||
|
||
// 业务类型选项
|
||
const billTypeOptions = ref<{ label: string; value: string }[]>([]);
|
||
async function loadBillTypeOptions() {
|
||
const list = await getSimpleDictDataList();
|
||
billTypeOptions.value = (list || [])
|
||
.filter((d) => d.dictType === 'out_bill_type')
|
||
.map((d) => ({ label: d.label, value: d.value }));
|
||
}
|
||
|
||
// 主表表单
|
||
const [Form, formApi] = useVbenForm({
|
||
commonConfig: {
|
||
componentProps: { class: 'w-full' },
|
||
formItemClass: 'col-span-2',
|
||
labelWidth: 80,
|
||
},
|
||
layout: 'horizontal',
|
||
schema: [
|
||
{
|
||
fieldName: 'storId',
|
||
label: '仓库',
|
||
rules: 'required',
|
||
component: 'Select',
|
||
componentProps: {
|
||
options: storOptions,
|
||
placeholder: '请选择仓库',
|
||
fieldNames: { label: 'label', value: 'value' },
|
||
},
|
||
},
|
||
{
|
||
fieldName: 'billType',
|
||
label: '业务类型',
|
||
rules: 'required',
|
||
component: 'Select',
|
||
componentProps: {
|
||
options: billTypeOptions,
|
||
placeholder: '请选择业务类型',
|
||
},
|
||
},
|
||
{
|
||
fieldName: 'billCode',
|
||
label: '单据号',
|
||
component: 'Input',
|
||
componentProps: { disabled: true, placeholder: '保存时自动生成' },
|
||
},
|
||
{
|
||
fieldName: 'bizDate',
|
||
label: '业务日期',
|
||
rules: 'required',
|
||
defaultValue: new Date(),
|
||
component: 'DatePicker',
|
||
componentProps: { showTime: true, format: 'YYYY-MM-DD HH:mm:ss', valueFormat: 'YYYY-MM-DD HH:mm:ss' },
|
||
},
|
||
{
|
||
fieldName: 'billStatus',
|
||
label: '单据状态',
|
||
component: 'Input',
|
||
componentProps: { disabled: true },
|
||
defaultValue: '生成',
|
||
},
|
||
{
|
||
fieldName: 'detailCount',
|
||
label: '明细数',
|
||
component: 'Input',
|
||
componentProps: { disabled: true },
|
||
defaultValue: 0,
|
||
},
|
||
{
|
||
fieldName: 'totalWeight',
|
||
label: '总重量',
|
||
component: 'Input',
|
||
componentProps: { disabled: true },
|
||
defaultValue: '0',
|
||
},
|
||
{
|
||
fieldName: 'remark',
|
||
label: '备注',
|
||
component: 'Input',
|
||
componentProps: { placeholder: '请输入备注' },
|
||
},
|
||
],
|
||
showDefaultActions: false,
|
||
});
|
||
|
||
// 明细数据
|
||
const detailList = ref<(OutboundDetail & { vehicleCode?: string; materialName?: string })[]>([]);
|
||
|
||
// 明细表格
|
||
const gridOptions = computed(() => ({
|
||
columns: [
|
||
{ type: 'seq', title: '序号', width: 50 },
|
||
{ field: 'vehicleCode', title: '箱号', width: 100 },
|
||
{ field: 'materialCode', title: '物料编码', width: 120 },
|
||
{ field: 'materialName', title: '物料名称', width: 150 },
|
||
{ field: 'pcsn', title: '子卷号', width: 100 },
|
||
{ field: 'planQty', title: '出库重量', width: 100 },
|
||
{ field: 'qtyUnitName', title: '单位', width: 60 },
|
||
{ field: 'remark', title: '备注', width: 120 },
|
||
{ title: '操作', width: 80, slots: { default: 'actions' } },
|
||
],
|
||
data: detailList.value,
|
||
height: 'auto',
|
||
rowConfig: { keyField: 'materialCode', isHover: true },
|
||
}));
|
||
|
||
// 联动:仓库切换 → 清空明细
|
||
watch(() => formApi.getValues().storId, () => {
|
||
detailList.value = [];
|
||
updateSummary();
|
||
});
|
||
|
||
// 更新主表汇总字段
|
||
function updateSummary() {
|
||
const totalWeight = detailList.value
|
||
.reduce((sum, d) => sum + (Number(d.planQty) || 0), 0)
|
||
.toFixed(3);
|
||
formApi.setValues({
|
||
detailCount: detailList.value.length,
|
||
totalWeight: String(totalWeight),
|
||
});
|
||
}
|
||
|
||
// 删除明细行
|
||
function handleDeleteDetail(index: number) {
|
||
detailList.value.splice(index, 1);
|
||
updateSummary();
|
||
}
|
||
|
||
// 库存选择弹窗
|
||
const [InventorySelectModal, inventorySelectModalApi] = useVbenModal({
|
||
connectedComponent: InventorySelect,
|
||
destroyOnClose: true,
|
||
});
|
||
function handleSelectFromInventory() {
|
||
const storId = formApi.getValues().storId;
|
||
if (!storId) {
|
||
message.warning('请先选择仓库');
|
||
return;
|
||
}
|
||
inventorySelectModalApi.setData({ storId }).open();
|
||
}
|
||
function onInventorySelected(rows: any[]) {
|
||
detailList.value.push(
|
||
...rows.map((r) => ({
|
||
vehicleCode: r.vehicleCode,
|
||
materialCode: r.materialCode,
|
||
materialId: r.materialId,
|
||
materialName: r.materialName,
|
||
pcsn: r.pcsn,
|
||
planQty: r.planQty ?? r.availableQty,
|
||
qtyUnitId: r.qtyUnitId,
|
||
qtyUnitName: r.qtyUnitName,
|
||
sourceBillCode: r.extCode,
|
||
sourceBillType: r.extType,
|
||
sourceBilldtlId: r.extDtlCode,
|
||
})),
|
||
);
|
||
updateSummary();
|
||
}
|
||
|
||
// 手动新增弹窗
|
||
const [ManualDetailModal, manualDetailModalApi] = useVbenModal({
|
||
connectedComponent: ManualDetail,
|
||
destroyOnClose: true,
|
||
});
|
||
function handleManualAdd() {
|
||
manualDetailModalApi.setData({}).open();
|
||
}
|
||
function onManualAdded(detail: OutboundDetail & { vehicleCode?: string; materialName?: string }) {
|
||
detailList.value.push(detail);
|
||
updateSummary();
|
||
}
|
||
|
||
// 提交
|
||
const [Modal, modalApi] = useVbenModal({
|
||
async onConfirm() {
|
||
const masterValues = await formApi.getValues();
|
||
if (!masterValues.storId || !masterValues.billType) {
|
||
message.warning('请填写仓库和业务类型');
|
||
return;
|
||
}
|
||
if (detailList.value.length === 0) {
|
||
message.warning('请至少添加一条出库明细');
|
||
return;
|
||
}
|
||
modalApi.lock();
|
||
try {
|
||
const stor = storOptions.value.find((s) => s.value === masterValues.storId);
|
||
const req: OutboundCreateReq = {
|
||
billType: masterValues.billType,
|
||
storId: masterValues.storId,
|
||
storCode: stor?.storCode,
|
||
storName: stor?.storName,
|
||
bizDate: masterValues.bizDate,
|
||
remark: masterValues.remark,
|
||
details: detailList.value.map((d) => ({
|
||
materialCode: d.materialCode,
|
||
materialId: d.materialId,
|
||
pcsn: d.pcsn,
|
||
planQty: Number(d.planQty),
|
||
qtyUnitId: d.qtyUnitId,
|
||
qtyUnitName: d.qtyUnitName,
|
||
remark: d.remark,
|
||
sourceBillCode: d.sourceBillCode,
|
||
sourceBillType: d.sourceBillType,
|
||
sourceBilldtlId: d.sourceBilldtlId,
|
||
})),
|
||
};
|
||
await createIostorInvWithDetails(req);
|
||
await modalApi.close();
|
||
emit('success');
|
||
message.success($t('ui.actionMessage.operationSuccess'));
|
||
} catch (e: any) {
|
||
message.error(e?.message || '保存失败');
|
||
} finally {
|
||
modalApi.unlock();
|
||
}
|
||
},
|
||
async onOpenChange(isOpen: boolean) {
|
||
if (isOpen) {
|
||
await loadStorOptions();
|
||
await loadBillTypeOptions();
|
||
}
|
||
if (!isOpen) {
|
||
detailList.value = [];
|
||
}
|
||
},
|
||
});
|
||
</script>
|
||
```
|
||
|
||
- [ ] **步骤 2:重写 form.vue — 模板部分**
|
||
|
||
```vue
|
||
<template>
|
||
<Modal title="出库新增" class="w-[1200px]">
|
||
<div class="mx-4">
|
||
<h4 class="mb-3 text-base font-medium">基本信息</h4>
|
||
<Form />
|
||
|
||
<div class="mt-6">
|
||
<div class="mb-3 flex items-center justify-between">
|
||
<h4 class="text-base font-medium">出库明细</h4>
|
||
<div class="flex gap-2">
|
||
<a-button type="primary" @click="handleSelectFromInventory">
|
||
从库存选择
|
||
</a-button>
|
||
<a-button @click="handleManualAdd">
|
||
手动新增汇总
|
||
</a-button>
|
||
</div>
|
||
</div>
|
||
|
||
<VxeTable :grid-options="gridOptions">
|
||
<template #actions="{ rowIndex }">
|
||
<a-button type="link" danger @click="handleDeleteDetail(rowIndex)">
|
||
删除
|
||
</a-button>
|
||
</template>
|
||
</VxeTable>
|
||
</div>
|
||
</div>
|
||
|
||
<InventorySelectModal @select="onInventorySelected" />
|
||
<ManualDetailModal @confirm="onManualAdded" />
|
||
</Modal>
|
||
</template>
|
||
```
|
||
|
||
> **注意:** 模板中使用了 `VxeTable` 组件,需要确认该组件在项目中的实际名称和用法。参考 `index.vue` 中 `useVbenVxeGrid` 的使用方式。如 `VxeTable` 不可用,改用 `vxe-grid` 直接绑定。
|
||
|
||
- [ ] **步骤 3:Commit**
|
||
|
||
```bash
|
||
git add nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/form.vue
|
||
git commit -m "feat: 重构出库单新增表单为带明细的单页弹窗"
|
||
```
|
||
|
||
---
|
||
|
||
### 任务 9:前端 — 新增库存选择弹窗组件
|
||
|
||
**文件:**
|
||
- 创建:`nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/inventory-select.vue`
|
||
|
||
- [ ] **步骤 1:创建 inventory-select.vue**
|
||
|
||
```vue
|
||
<script lang="ts" setup>
|
||
import type { AvailableInventory } from '#/api/wms/iostorinv';
|
||
|
||
import { ref } from 'vue';
|
||
|
||
import { useVbenModal } from '@vben/common-ui';
|
||
|
||
import { message } from 'antdv-next';
|
||
|
||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||
import { getAvailableInventory } from '#/api/wms/iostorinv';
|
||
import { getMaterialBaseList } from '#/api/base/materialbase';
|
||
|
||
const emit = defineEmits(['select']);
|
||
|
||
const storId = ref('');
|
||
|
||
// 搜索表单
|
||
const formValues = ref({
|
||
materialCode: '',
|
||
pcsn: '',
|
||
vehicleCode: '',
|
||
});
|
||
|
||
// 表格
|
||
const [Grid, gridApi] = useVbenVxeGrid({
|
||
formOptions: {
|
||
schema: [
|
||
{
|
||
fieldName: 'materialCode',
|
||
label: '物料编码',
|
||
component: 'Input',
|
||
componentProps: { allowClear: true, placeholder: '请输入物料编码' },
|
||
},
|
||
{
|
||
fieldName: 'pcsn',
|
||
label: '批次',
|
||
component: 'Input',
|
||
componentProps: { allowClear: true, placeholder: '请输入批次' },
|
||
},
|
||
{
|
||
fieldName: 'vehicleCode',
|
||
label: '载具编码',
|
||
component: 'Input',
|
||
componentProps: { allowClear: true, placeholder: '请输入载具编码' },
|
||
},
|
||
],
|
||
},
|
||
gridOptions: {
|
||
columns: [
|
||
{ type: 'checkbox', width: 40 },
|
||
{ field: 'vehicleCode', title: '箱号', width: 100 },
|
||
{ field: 'materialCode', title: '物料编码', width: 120 },
|
||
{ field: 'pcsn', title: '子卷号', width: 100 },
|
||
{ field: 'availableQty', title: '可用数量', width: 100 },
|
||
{ field: 'qtyUnitName', title: '单位', width: 60 },
|
||
],
|
||
height: 400,
|
||
keepSource: true,
|
||
proxyConfig: {
|
||
ajax: {
|
||
query: async ({ page, form }, formVals) => {
|
||
if (!storId.value) return { rows: [], total: 0 };
|
||
const list = await getAvailableInventory({
|
||
storId: storId.value,
|
||
materialCode: formVals.materialCode || undefined,
|
||
pcsn: formVals.pcsn || undefined,
|
||
vehicleCode: formVals.vehicleCode || undefined,
|
||
});
|
||
// 批量查询物料名称
|
||
const materialCodes = [...new Set((list || []).map((i) => i.materialCode))];
|
||
let nameMap: Record<string, string> = {};
|
||
if (materialCodes.length > 0) {
|
||
try {
|
||
const materials = await getMaterialBaseList({ materialCodes });
|
||
(materials || []).forEach((m: any) => {
|
||
nameMap[m.materialCode] = m.materialName;
|
||
});
|
||
} catch { /* ignore */ }
|
||
}
|
||
const rows = (list || []).map((item) => ({
|
||
...item,
|
||
materialName: nameMap[item.materialCode] || '',
|
||
planQty: item.availableQty, // 默认出库重量 = 可用数量
|
||
}));
|
||
return { rows, total: rows.length };
|
||
},
|
||
},
|
||
},
|
||
rowConfig: { keyField: 'vehicleCode', isHover: true },
|
||
checkboxConfig: {
|
||
checkMethod: ({ row, checkedRecords }) => {
|
||
// 同 vehicle_code 联动勾选
|
||
if (!row) return false;
|
||
const sameVehicle = gridApi
|
||
.getGridData()
|
||
.filter((r: any) => r.vehicleCode === row.vehicleCode);
|
||
if (checkedRecords.some((r: any) => r.vehicleCode === row.vehicleCode)) {
|
||
return true;
|
||
}
|
||
return false;
|
||
},
|
||
},
|
||
} as any,
|
||
gridEvents: {
|
||
checkboxChange: ({ row, checked }: { row: any; checked: boolean }) => {
|
||
// 勾选/取消时,同 vehicle_code 的行一起变化
|
||
const allRows = gridApi.getGridData();
|
||
const sameVehicleRows = allRows.filter((r: any) => r.vehicleCode === row.vehicleCode);
|
||
if (checked) {
|
||
sameVehicleRows.forEach((r: any) => gridApi.setCheckboxRow(r, true));
|
||
} else {
|
||
sameVehicleRows.forEach((r: any) => gridApi.setCheckboxRow(r, false));
|
||
}
|
||
},
|
||
},
|
||
});
|
||
|
||
const [Modal, modalApi] = useVbenModal({
|
||
async onConfirm() {
|
||
const checked = gridApi.getCheckboxRecords();
|
||
if (checked.length === 0) {
|
||
message.warning('请至少选择一条库存记录');
|
||
return;
|
||
}
|
||
emit('select', checked);
|
||
modalApi.close();
|
||
},
|
||
async onOpenChange(isOpen: boolean) {
|
||
if (isOpen) {
|
||
const data = modalApi.getData<{ storId: string }>();
|
||
storId.value = data?.storId || '';
|
||
gridApi.query();
|
||
}
|
||
},
|
||
});
|
||
</script>
|
||
|
||
<template>
|
||
<Modal title="选择库存" class="w-[1000px]">
|
||
<Grid />
|
||
</Modal>
|
||
</template>
|
||
```
|
||
|
||
> **注意:** `getMaterialBaseList` 接口参数需要确认。如接口仅支持 `MaterialBaseListReqDTO` 条件查询而非批量按编码查询,可能需要使用循环调用 `getMaterialBaseByCode` 或调整查询方式。此处为简化说明假设支持批量查询。
|
||
|
||
- [ ] **步骤 2:Commit**
|
||
|
||
```bash
|
||
git add nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/inventory-select.vue
|
||
git commit -m "feat: 新增库存选择弹窗组件(支持同箱号联动勾选)"
|
||
```
|
||
|
||
---
|
||
|
||
### 任务 10:前端 — 新增手动新增汇总弹窗组件
|
||
|
||
**文件:**
|
||
- 创建:`nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/manual-detail.vue`
|
||
|
||
- [ ] **步骤 1:创建 manual-detail.vue**
|
||
|
||
```vue
|
||
<script lang="ts" setup>
|
||
import { ref } from 'vue';
|
||
|
||
import { useVbenModal } from '@vben/common-ui';
|
||
|
||
import { message } from 'antdv-next';
|
||
|
||
const emit = defineEmits(['confirm']);
|
||
|
||
const formData = ref({
|
||
materialCode: '',
|
||
materialId: '',
|
||
materialName: '',
|
||
planQty: undefined as number | undefined,
|
||
qtyUnitId: '',
|
||
qtyUnitName: '',
|
||
pcsn: '',
|
||
remark: '',
|
||
});
|
||
|
||
function onMaterialSelected(rows: any[]) {
|
||
if (rows.length > 0) {
|
||
const m = rows[0];
|
||
formData.value.materialCode = m.materialCode;
|
||
formData.value.materialId = m.materialId;
|
||
formData.value.materialName = m.materialName;
|
||
formData.value.qtyUnitId = m.baseUnitId;
|
||
formData.value.qtyUnitName = ''; // 单位名称需另行查询或通过字典转换
|
||
}
|
||
}
|
||
|
||
const [Modal, modalApi] = useVbenModal({
|
||
async onConfirm() {
|
||
if (!formData.value.materialCode) {
|
||
message.warning('请选择物料');
|
||
return;
|
||
}
|
||
if (!formData.value.planQty || formData.value.planQty <= 0) {
|
||
message.warning('请输入出库重量');
|
||
return;
|
||
}
|
||
emit('confirm', { ...formData.value });
|
||
modalApi.close();
|
||
},
|
||
async onOpenChange(isOpen: boolean) {
|
||
if (!isOpen) {
|
||
formData.value = {
|
||
materialCode: '', materialId: '', materialName: '',
|
||
planQty: undefined, qtyUnitId: '', qtyUnitName: '',
|
||
pcsn: '', remark: '',
|
||
};
|
||
}
|
||
},
|
||
});
|
||
</script>
|
||
|
||
<template>
|
||
<Modal title="手动新增汇总" class="w-[500px]">
|
||
<div class="flex flex-col gap-4">
|
||
<div class="flex items-center gap-2">
|
||
<span class="w-20 text-right">物料:</span>
|
||
<a-input
|
||
:value="formData.materialName || formData.materialCode"
|
||
placeholder="点击选择物料"
|
||
readonly
|
||
class="flex-1"
|
||
/>
|
||
<MaterialSelectModal @select="onMaterialSelected" />
|
||
<!--
|
||
注意:MaterialSelectModal 的使用方式参见
|
||
src/views/base/materialbase/components/MaterialSelectModal.vue
|
||
需要通过 useVbenModal 集成
|
||
-->
|
||
</div>
|
||
<div class="flex items-center gap-2">
|
||
<span class="w-20 text-right">出库重量:</span>
|
||
<a-input-number
|
||
v-model:value="formData.planQty"
|
||
placeholder="请输入出库重量"
|
||
:min="0"
|
||
class="flex-1"
|
||
/>
|
||
<span v-if="formData.qtyUnitName" class="text-gray-500">{{ formData.qtyUnitName }}</span>
|
||
</div>
|
||
<div class="flex items-center gap-2">
|
||
<span class="w-20 text-right">批次:</span>
|
||
<a-input
|
||
v-model:value="formData.pcsn"
|
||
placeholder="请输入批次(可选)"
|
||
class="flex-1"
|
||
/>
|
||
</div>
|
||
<div class="flex items-center gap-2">
|
||
<span class="w-20 text-right">备注:</span>
|
||
<a-input
|
||
v-model:value="formData.remark"
|
||
placeholder="请输入备注(可选)"
|
||
class="flex-1"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</Modal>
|
||
</template>
|
||
```
|
||
|
||
> **注意:** MaterialSelectModal 在此处的集成方式需要参考原组件的 `useVbenModal` 用法。当前简化示例——实际集成时应在 script 中用 `useVbenModal({ connectedComponent: MaterialSelectModal })` 打开物料选择弹窗。
|
||
|
||
- [ ] **步骤 2:Commit**
|
||
|
||
```bash
|
||
git add nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/manual-detail.vue
|
||
git commit -m "feat: 新增手动新增汇总弹窗组件"
|
||
```
|
||
|
||
---
|
||
|
||
### 任务 11:前端 — 字典常量补充 & 最终联调验证
|
||
|
||
**文件:**
|
||
- 修改:`nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/packages/constants/src/dict-enum.ts`(可选,取决于实际使用方式)
|
||
- 不强制修改,因为已在 form.vue 中通过 `getSimpleDictDataList()` 动态过滤 `out_bill_type`
|
||
|
||
- [ ] **步骤 1:验证 checklist**
|
||
|
||
1. 确认 `nl-module-wms-server` 的 pom.xml 包含 `nl-module-base-api` 依赖
|
||
2. 确认 `IostorinvDtlController` 文件存在,且注入 `IostorinvDtlMapper` 无编译错误
|
||
3. 启动后端服务,调用 `POST /wms/iostor-inv/create-with-details` 接口验证
|
||
4. 调用 `GET /wms/iostorinvdtl/available-inventory?storId=xxx` 验证库存查询
|
||
5. 前端页面打开"出库新增"弹窗,验证:
|
||
- 仓库下拉加载正确
|
||
- 业务类型下拉加载正确(out_bill_type 字典)
|
||
- 单据号显示占位提示
|
||
- "从库存选择"弹窗:只显示对应仓库的库存,同箱号联动勾选
|
||
- "手动新增汇总"弹窗:选物料、填重量
|
||
- 明细增删后,明细数和总重量自动更新
|
||
- 提交保存成功
|
||
|
||
---
|
||
|
||
## 自检
|
||
|
||
**1. 规格覆盖度:**
|
||
- [x] 主表字段映射 → 任务 8(form.vue schema)+ 任务 4(ServiceImpl 字段赋值)
|
||
- [x] 明细字段映射 → 任务 8(detailList 结构)+ 任务 4(dtl.setXxx)
|
||
- [x] 方式 A 从库存选择 → 任务 9(inventory-select.vue)+ 任务 3(Mapper SQL)+ 任务 7(Controller)
|
||
- [x] 方式 B 手动新增 → 任务 10(manual-detail.vue)
|
||
- [x] 单据号 CodeGenApi → 任务 1(RpcConfig)+ 任务 4(ServiceImpl 调用)
|
||
- [x] 一对多联动 → 任务 9(gridEvents.checkboxChange)
|
||
- [x] planQty/unassignQty/assignQty → 任务 4(ServiceImpl)
|
||
- [x] 箱号不存 → 任务 8(vehicleCode 仅 detailList 前端字段)
|
||
- [x] 库存查询 Mapper XML → 任务 3
|
||
- [x] 错误处理 → 任务 4(异常抛出)+ 任务 8(前端校验)
|
||
- [x] 不考虑的内容(入库/编辑/分配/回传)→ 已明确范围,无遗漏
|
||
|
||
**2. 占位符扫描:** 无待定/TODO/占位符
|
||
|
||
**3. 类型一致性:**
|
||
- `OutboundCreateReq` / `OutboundDetail` → 前端 API 层(任务 6)和 form.vue(任务 8)一致
|
||
- `IostorInvWithDetailsSaveReqVO` → 后端 Controller(任务 5)和 Service(任务 4)一致
|
||
- `AvailableInventoryVO` → Mapper(任务 3)、Controller(任务 7)、前端 API(任务 6)一致
|
||
- `AvailableInventory` → 前端 API(任务 6)和 inventory-select(任务 9)一致
|