Pārlūkot izejas kodu

来料检验申请 前后端调通

liuwb 3 mēneši atpakaļ
vecāks
revīzija
8a2b03b67a
22 mainītis faili ar 1288 papildinājumiem un 29 dzēšanām
  1. 186 0
      IQC来料检验申请-后端设计.md
  2. 1 0
      pom.xml
  3. 55 0
      yudao-module-qms/pom.xml
  4. 74 0
      yudao-module-qms/src/main/java/cn/iocoder/yudao/module/qms/controller/admin/iqc/IqcApplyController.java
  5. 32 0
      yudao-module-qms/src/main/java/cn/iocoder/yudao/module/qms/controller/admin/iqc/vo/IqcApplyDetailRespVO.java
  6. 44 0
      yudao-module-qms/src/main/java/cn/iocoder/yudao/module/qms/controller/admin/iqc/vo/IqcApplyDetailSaveReqVO.java
  7. 34 0
      yudao-module-qms/src/main/java/cn/iocoder/yudao/module/qms/controller/admin/iqc/vo/IqcApplyPageReqVO.java
  8. 51 0
      yudao-module-qms/src/main/java/cn/iocoder/yudao/module/qms/controller/admin/iqc/vo/IqcApplyRespVO.java
  9. 37 0
      yudao-module-qms/src/main/java/cn/iocoder/yudao/module/qms/controller/admin/iqc/vo/IqcApplySaveReqVO.java
  10. 73 0
      yudao-module-qms/src/main/java/cn/iocoder/yudao/module/qms/dal/mysql/iqc/IqcApplyMapper.java
  11. 50 0
      yudao-module-qms/src/main/java/cn/iocoder/yudao/module/qms/service/iqc/IqcApplyService.java
  12. 142 0
      yudao-module-qms/src/main/java/cn/iocoder/yudao/module/qms/service/iqc/IqcApplyServiceImpl.java
  13. 137 0
      yudao-module-qms/src/main/resources/mapper/iqc/IqcApplyMapper.xml
  14. 6 0
      yudao-server/pom.xml
  15. 71 0
      yudao-ui/yudao-ui-admin-vue3/src/api/qms/iqc/apply/index.ts
  16. 4 5
      yudao-ui/yudao-ui-admin-vue3/src/config/qmsModules.ts
  17. 2 1
      yudao-ui/yudao-ui-admin-vue3/src/permission.ts
  18. 45 1
      yudao-ui/yudao-ui-admin-vue3/src/router/index.ts
  19. 11 0
      yudao-ui/yudao-ui-admin-vue3/src/router/modules/remaining.ts
  20. 26 0
      yudao-ui/yudao-ui-admin-vue3/src/store/modules/qms/application.ts
  21. 108 20
      yudao-ui/yudao-ui-admin-vue3/src/views/qms/ApplicationForm.vue
  22. 99 2
      yudao-ui/yudao-ui-admin-vue3/src/views/qms/ApplicationList.vue

+ 186 - 0
IQC来料检验申请-后端设计.md

@@ -0,0 +1,186 @@
+IQC 来料检验申请列表 - 后端设计(MVP)
+
+目标
+- 聚焦“来料检验申请”列表接口,满足前端列表展示/筛选/分页
+- 单据编号使用 FBILLNO,申请人用 system_users.username,状态返回中文
+
+1) VO 定义(建议先定 VO)
+
+IqcApplyPageReqVO
+- keyword: String(模糊匹配 FBILLNO/FCOMMENT)
+- statusList: List<String>(值:待检验/检验中/检验完成)
+- applicantIds: List<Long>
+- beginTime: LocalDateTime(申请时间起)
+- endTime: LocalDateTime(申请时间止)
+- pageNo: Integer
+- pageSize: Integer
+
+IqcApplyPageRespVO
+- id: String(FBILLNO)
+- applicantId: Long(FAPPLYUSER)
+- applicantName: String(system_users.username)
+- applyTime: LocalDateTime(FAPPLYTIME)
+- status: String(中文:待检验/检验中/检验完成)
+- materialCount: Integer(子表行数)
+- remark: String(FCOMMENT)
+
+2) DO(最小字段集)
+
+qms_qcp_inspecapplyn(主表)
+- id: Long
+- FBILLNO: String
+- FAPPLYUSER: Long
+- FAPPLYTIME: LocalDateTime
+- FCOMMENT: String
+- FCREATETIME: LocalDateTime(排序可用)
+
+qms_qcp_insappnentry(子表)
+- id: Long
+- glid: Long(关联主表 id)
+- FINSPECTSTATUS: String(中文:待检验/检验中/检验完成)
+
+system_users(用户表)
+- id: Long
+- username: String
+
+3) Mapper SQL 草案(分页列表)
+
+要点
+- 状态由子表汇总:
+  - 任一行“检验中” => 检验中
+  - 全部“检验完成” => 检验完成
+  - 其他/无子表 => 待检验
+- 申请人通过 system_users.id 关联 system_users.username
+
+示意 SQL(MyBatis 可按此改写)
+SELECT
+  a.FBILLNO AS id,
+  a.FAPPLYUSER AS applicantId,
+  u.username AS applicantName,
+  a.FAPPLYTIME AS applyTime,
+  a.FCOMMENT AS remark,
+  COUNT(b.id) AS materialCount,
+  CASE
+    WHEN SUM(CASE WHEN b.FINSPECTSTATUS = '检验中' THEN 1 ELSE 0 END) > 0 THEN '检验中'
+    WHEN COUNT(b.id) > 0
+      AND SUM(CASE WHEN b.FINSPECTSTATUS = '检验完成' THEN 1 ELSE 0 END) = COUNT(b.id) THEN '检验完成'
+    ELSE '待检验'
+  END AS status
+FROM qms_qcp_inspecapplyn a
+LEFT JOIN system_users u ON u.id = a.FAPPLYUSER
+LEFT JOIN qms_qcp_insappnentry b ON b.glid = a.id
+WHERE 1 = 1
+  -- keyword:FBILLNO/FCOMMENT 模糊匹配
+  -- applicantIds:a.FAPPLYUSER IN (...)
+  -- 时间:a.FAPPLYTIME BETWEEN beginTime AND endTime
+GROUP BY a.FBILLNO, a.FAPPLYUSER, u.username, a.FAPPLYTIME, a.FCOMMENT
+-- 若要按状态筛选,可在外层包一层并对 status 过滤
+ORDER BY a.FCREATETIME DESC
+
+4) Mapper XML 示例(分页列表)
+
+说明
+- 先在子查询中计算 status,再在外层按 status 过滤,避免 HAVING 复杂度
+- SQL 仅示意,按实际分页插件(MyBatis-Plus / PageHelper)调整
+
+示例(XML 片段)
+```xml
+<select id="selectIqcApplyPage" resultType="cn.iocoder.yudao.module.qms.controller.admin.iqc.vo.IqcApplyPageRespVO">
+  SELECT *
+  FROM (
+    SELECT
+      a.FBILLNO AS id,
+      a.FAPPLYUSER AS applicantId,
+      u.username AS applicantName,
+      a.FAPPLYTIME AS applyTime,
+      a.FCOMMENT AS remark,
+      COUNT(b.id) AS materialCount,
+      CASE
+        WHEN SUM(CASE WHEN b.FINSPECTSTATUS = '检验中' THEN 1 ELSE 0 END) > 0 THEN '检验中'
+        WHEN COUNT(b.id) > 0
+          AND SUM(CASE WHEN b.FINSPECTSTATUS = '检验完成' THEN 1 ELSE 0 END) = COUNT(b.id) THEN '检验完成'
+        ELSE '待检验'
+      END AS status
+    FROM qms_qcp_inspecapplyn a
+    LEFT JOIN system_users u ON u.id = a.FAPPLYUSER
+    LEFT JOIN qms_qcp_insappnentry b ON b.glid = a.id
+    <where>
+      <if test="keyword != null and keyword != ''">
+        AND (a.FBILLNO LIKE CONCAT('%', #{keyword}, '%')
+             OR a.FCOMMENT LIKE CONCAT('%', #{keyword}, '%'))
+      </if>
+      <if test="applicantIds != null and applicantIds.size() > 0">
+        AND a.FAPPLYUSER IN
+        <foreach collection="applicantIds" item="id" open="(" separator="," close=")">
+          #{id}
+        </foreach>
+      </if>
+      <if test="beginTime != null and endTime != null">
+        AND a.FAPPLYTIME BETWEEN #{beginTime} AND #{endTime}
+      </if>
+    </where>
+    GROUP BY a.FBILLNO, a.FAPPLYUSER, u.username, a.FAPPLYTIME, a.FCOMMENT
+  ) t
+  <where>
+    <if test="statusList != null and statusList.size() > 0">
+      AND t.status IN
+      <foreach collection="statusList" item="status" open="(" separator="," close=")">
+        #{status}
+      </foreach>
+    </if>
+  </where>
+  ORDER BY t.applyTime DESC
+</select>
+```
+
+5) Service / Controller 设计(建议)
+
+Controller
+- GET /admin-api/qms/iqc-apply/page
+  - 入参:IqcApplyPageReqVO
+  - 出参:PageResult<IqcApplyPageRespVO>
+
+Service
+- IqcApplyService#getIqcApplyPage(IqcApplyPageReqVO reqVO)
+  - 负责参数校验、调用 Mapper、封装分页结果
+
+6) 接口契约(示例)
+
+请求示例
+```http
+GET /admin-api/qms/iqc-apply/page?keyword=IQC&statusList=待检验&applicantIds=1&pageNo=1&pageSize=20
+```
+
+响应示例
+```json
+{
+  "code": 0,
+  "data": {
+    "list": [
+      {
+        "id": "IQC20240115001",
+        "applicantId": 1,
+        "applicantName": "zhangsan",
+        "applyTime": "2024-01-15 09:30:00",
+        "status": "待检验",
+        "materialCount": 5,
+        "remark": "紧急物料,请优先处理"
+      }
+    ],
+    "total": 1
+  }
+}
+```
+
+7) 前端对接清单(最小改动)
+
+- 列表接口地址:/admin-api/qms/iqc-apply/page
+- 字段映射:
+  - id -> 单据编号(FBILLNO)
+  - applicantName -> 申请人
+  - applyTime -> 申请时间
+  - status -> 状态(中文)
+  - materialCount -> 物料数
+  - remark -> 备注
+- 状态显示:已在 StatusBadge 与筛选项里支持中文
+

+ 1 - 0
pom.xml

@@ -14,6 +14,7 @@
         <module>yudao-server</module>
         <module>yudao-order-server</module>
         <module>yudao-all-server</module>
+        <module>yudao-module-qms</module>
         <!-- 各种 module 拓展 -->
         <module>yudao-module-system</module>
         <module>yudao-module-infra</module>

+ 55 - 0
yudao-module-qms/pom.xml

@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <groupId>cn.iocoder.boot</groupId>
+        <artifactId>yudao</artifactId>
+        <version>${revision}</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>yudao-module-qms</artifactId>
+    <packaging>jar</packaging>
+
+    <name>${project.artifactId}</name>
+    <description>质量管理模块(IQC/IPQC/FQC/SPEC)</description>
+
+    <dependencies>
+        <!-- 系统基础能力:用户、租户等 -->
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-module-system</artifactId>
+            <version>${revision}</version>
+        </dependency>
+
+        <!-- 业务组件 -->
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-spring-boot-starter-biz-tenant</artifactId>
+        </dependency>
+
+        <!-- Web 能力 -->
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-spring-boot-starter-web</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-spring-boot-starter-security</artifactId>
+        </dependency>
+
+        <!-- DB 能力 -->
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-spring-boot-starter-mybatis</artifactId>
+        </dependency>
+
+        <!-- 测试 -->
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-spring-boot-starter-test</artifactId>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+</project>

+ 74 - 0
yudao-module-qms/src/main/java/cn/iocoder/yudao/module/qms/controller/admin/iqc/IqcApplyController.java

@@ -0,0 +1,74 @@
+package cn.iocoder.yudao.module.qms.controller.admin.iqc;
+
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.qms.controller.admin.iqc.vo.IqcApplyPageReqVO;
+import cn.iocoder.yudao.module.qms.controller.admin.iqc.vo.IqcApplyRespVO;
+import cn.iocoder.yudao.module.qms.controller.admin.iqc.vo.IqcApplySaveReqVO;
+import cn.iocoder.yudao.module.qms.service.iqc.IqcApplyService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.annotation.Resource;
+import jakarta.validation.Valid;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+
+@Tag(name = "管理后台 - IQC 来料检验申请")
+@RestController
+@RequestMapping("/qms/iqc-apply")
+@Validated
+public class IqcApplyController {
+
+    @Resource
+    private IqcApplyService iqcApplyService;
+
+    @GetMapping("/page")
+    @Operation(summary = "获取来料检验申请分页")
+    @PreAuthorize("@ss.hasPermission('qms:iqc:apply:list')")
+    public CommonResult<PageResult<IqcApplyRespVO>> getIqcApplyPage(@Valid IqcApplyPageReqVO pageReqVO) {
+        return success(iqcApplyService.getIqcApplyPage(pageReqVO));
+    }
+
+    @PostMapping("/create")
+    @Operation(summary = "创建来料检验申请")
+    @PreAuthorize("@ss.hasPermission('qms:iqc:apply:create')")
+    public CommonResult<String> createIqcApply(@Valid @RequestBody IqcApplySaveReqVO createReqVO) {
+        return success(iqcApplyService.createIqcApply(createReqVO));
+    }
+
+    @PutMapping("/update")
+    @Operation(summary = "更新来料检验申请")
+    @PreAuthorize("@ss.hasPermission('qms:iqc:apply:update')")
+    public CommonResult<Boolean> updateIqcApply(@Valid @RequestBody IqcApplySaveReqVO updateReqVO) {
+        iqcApplyService.updateIqcApply(updateReqVO);
+        return success(true);
+    }
+
+    @DeleteMapping("/delete")
+    @Operation(summary = "删除来料检验申请")
+    @Parameter(name = "id", description = "单据编号", required = true)
+    @PreAuthorize("@ss.hasPermission('qms:iqc:apply:delete')")
+    public CommonResult<Boolean> deleteIqcApply(@RequestParam("id") String id) {
+        iqcApplyService.deleteIqcApply(id);
+        return success(true);
+    }
+
+    @GetMapping("/get")
+    @Operation(summary = "获得来料检验申请")
+    @Parameter(name = "id", description = "单据编号", required = true)
+    @PreAuthorize("@ss.hasPermission('qms:iqc:apply:query')")
+    public CommonResult<IqcApplyRespVO> getIqcApply(@RequestParam("id") String id) {
+        return success(iqcApplyService.getIqcApply(id));
+    }
+}

+ 32 - 0
yudao-module-qms/src/main/java/cn/iocoder/yudao/module/qms/controller/admin/iqc/vo/IqcApplyDetailRespVO.java

@@ -0,0 +1,32 @@
+package cn.iocoder.yudao.module.qms.controller.admin.iqc.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+@Schema(description = "管理后台 - IQC 来料检验申请明细 Response VO")
+@Data
+public class IqcApplyDetailRespVO {
+
+    @Schema(description = "物料编码")
+    private String materialCode;
+
+    @Schema(description = "物料名称")
+    private String materialName;
+
+    @Schema(description = "规格")
+    private String specification;
+
+    @Schema(description = "批次")
+    private String batch;
+
+    @Schema(description = "数量")
+    private BigDecimal quantity;
+
+    @Schema(description = "单位")
+    private String unit;
+
+    @Schema(description = "备注")
+    private String remark;
+}

+ 44 - 0
yudao-module-qms/src/main/java/cn/iocoder/yudao/module/qms/controller/admin/iqc/vo/IqcApplyDetailSaveReqVO.java

@@ -0,0 +1,44 @@
+package cn.iocoder.yudao.module.qms.controller.admin.iqc.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+@Schema(description = "管理后台 - IQC 来料检验申请明细保存 Request VO")
+@Data
+public class IqcApplyDetailSaveReqVO {
+
+    @Schema(description = "明细编号", hidden = true)
+    private Long id;
+
+    @Schema(description = "明细序号", hidden = true)
+    private Integer seq;
+
+    @Schema(description = "物料编码")
+    private String materialCode;
+
+    @Schema(description = "物料名称")
+    private String materialName;
+
+    @Schema(description = "规格")
+    private String specification;
+
+    @Schema(description = "批次")
+    private String batch;
+
+    @Schema(description = "数量")
+    private BigDecimal quantity;
+
+    @Schema(description = "单位")
+    private String unit;
+
+    @Schema(description = "备注")
+    private String remark;
+
+    @Schema(description = "物料编号", hidden = true)
+    private Long materialId;
+
+    @Schema(description = "检验状态", hidden = true)
+    private String inspectStatus;
+}

+ 34 - 0
yudao-module-qms/src/main/java/cn/iocoder/yudao/module/qms/controller/admin/iqc/vo/IqcApplyPageReqVO.java

@@ -0,0 +1,34 @@
+package cn.iocoder.yudao.module.qms.controller.admin.iqc.vo;
+
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+@Schema(description = "管理后台 - IQC 来料检验申请分页 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class IqcApplyPageReqVO extends PageParam {
+
+    @Schema(description = "关键字(单据编号/备注)", example = "IQC202401")
+    private String keyword;
+
+    @Schema(description = "单据状态列表", example = "[\"待检验\",\"检验中\"]")
+    private List<String> statusList;
+
+    @Schema(description = "申请人编号列表", example = "[1, 2]")
+    private List<Long> applicantIds;
+
+    @Schema(description = "申请时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime[] applyTime;
+
+}

+ 51 - 0
yudao-module-qms/src/main/java/cn/iocoder/yudao/module/qms/controller/admin/iqc/vo/IqcApplyRespVO.java

@@ -0,0 +1,51 @@
+package cn.iocoder.yudao.module.qms.controller.admin.iqc.vo;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.TIME_ZONE_DEFAULT;
+
+@Schema(description = "管理后台 - IQC 来料检验申请 Response VO")
+@Data
+public class IqcApplyRespVO {
+
+    @Schema(description = "申请单主键", hidden = true)
+    private Long applyId;
+
+    @Schema(description = "单据编号(FBILLNO)", example = "IQC20240115001")
+    private String id;
+
+    @Schema(description = "申请人编号", example = "1")
+    private Long applicantId;
+
+    @Schema(description = "申请人用户名", example = "zhangsan")
+    private String applicantName;
+
+    @Schema(description = "申请时间")
+    @JsonFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND, timezone = TIME_ZONE_DEFAULT)
+    private LocalDateTime applyTime;
+
+    @Schema(description = "业务类型")
+    private String businessType;
+
+    @Schema(description = "组织/部门")
+    private String department;
+
+    @Schema(description = "单据状态", example = "待检验")
+    private String status;
+
+    @Schema(description = "物料数", example = "5")
+    private Integer materialCount;
+
+    @Schema(description = "备注", example = "紧急物料,请优先处理")
+    private String remark;
+
+    @Schema(description = "明细列表")
+    private List<IqcApplyDetailRespVO> details;
+
+}

+ 37 - 0
yudao-module-qms/src/main/java/cn/iocoder/yudao/module/qms/controller/admin/iqc/vo/IqcApplySaveReqVO.java

@@ -0,0 +1,37 @@
+package cn.iocoder.yudao.module.qms.controller.admin.iqc.vo;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.TIME_ZONE_DEFAULT;
+
+@Schema(description = "管理后台 - IQC 来料检验申请创建/更新 Request VO")
+@Data
+public class IqcApplySaveReqVO {
+
+    @Schema(description = "单据编号(更新时必填)", example = "IQC20240115001")
+    private String id;
+
+    @Schema(description = "申请时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotNull(message = "申请时间不能为空")
+    @JsonFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND, timezone = TIME_ZONE_DEFAULT)
+    private LocalDateTime applyTime;
+
+    @Schema(description = "业务类型")
+    private String businessType;
+
+    @Schema(description = "组织/部门")
+    private String department;
+
+    @Schema(description = "备注")
+    private String remark;
+
+    @Schema(description = "明细列表")
+    private List<IqcApplyDetailSaveReqVO> details;
+}

+ 73 - 0
yudao-module-qms/src/main/java/cn/iocoder/yudao/module/qms/dal/mysql/iqc/IqcApplyMapper.java

@@ -0,0 +1,73 @@
+package cn.iocoder.yudao.module.qms.dal.mysql.iqc;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils;
+import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore;
+import cn.iocoder.yudao.module.qms.controller.admin.iqc.vo.IqcApplyDetailRespVO;
+import cn.iocoder.yudao.module.qms.controller.admin.iqc.vo.IqcApplyPageReqVO;
+import cn.iocoder.yudao.module.qms.controller.admin.iqc.vo.IqcApplyRespVO;
+import cn.iocoder.yudao.module.qms.controller.admin.iqc.vo.IqcApplyDetailSaveReqVO;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+/**
+ * IQC 来料检验申请 Mapper
+ */
+@Mapper
+public interface IqcApplyMapper {
+
+    /**
+     * 分页查询
+     */
+    @TenantIgnore
+    Page<IqcApplyRespVO> selectApplyPage(Page<IqcApplyRespVO> page, @Param("req") IqcApplyPageReqVO req);
+
+    default PageResult<IqcApplyRespVO> selectApplyPage(IqcApplyPageReqVO reqVO) {
+        Page<IqcApplyRespVO> page = MyBatisUtils.buildPage(reqVO);
+        IPage<IqcApplyRespVO> result = selectApplyPage(page, reqVO);
+        return new PageResult<>(result.getRecords(), result.getTotal());
+    }
+
+    @TenantIgnore
+    IqcApplyRespVO selectApplyByBillNo(@Param("billNo") String billNo);
+
+    @TenantIgnore
+    Long selectApplyIdByBillNo(@Param("billNo") String billNo);
+
+    @TenantIgnore
+    List<IqcApplyDetailRespVO> selectApplyDetailsByApplyId(@Param("applyId") Long applyId);
+
+    @TenantIgnore
+    void insertApply(@Param("id") Long id,
+                     @Param("billNo") String billNo,
+                     @Param("status") String status,
+                     @Param("applyUserId") Long applyUserId,
+                     @Param("applyTime") LocalDateTime applyTime,
+                     @Param("billType") String billType,
+                     @Param("remark") String remark,
+                     @Param("creatorId") Long creatorId,
+                     @Param("createTime") LocalDateTime createTime);
+
+    @TenantIgnore
+    int updateApplyByBillNo(@Param("billNo") String billNo,
+                            @Param("applyTime") LocalDateTime applyTime,
+                            @Param("billType") String billType,
+                            @Param("remark") String remark,
+                            @Param("modifierId") Long modifierId,
+                            @Param("modifyTime") LocalDateTime modifyTime);
+
+    @TenantIgnore
+    int deleteApplyById(@Param("applyId") Long applyId);
+
+    @TenantIgnore
+    int deleteApplyDetailsByApplyId(@Param("applyId") Long applyId);
+
+    @TenantIgnore
+    int insertApplyDetails(@Param("applyId") Long applyId,
+                           @Param("details") List<IqcApplyDetailSaveReqVO> details);
+}

+ 50 - 0
yudao-module-qms/src/main/java/cn/iocoder/yudao/module/qms/service/iqc/IqcApplyService.java

@@ -0,0 +1,50 @@
+package cn.iocoder.yudao.module.qms.service.iqc;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.qms.controller.admin.iqc.vo.IqcApplyPageReqVO;
+import cn.iocoder.yudao.module.qms.controller.admin.iqc.vo.IqcApplyRespVO;
+import cn.iocoder.yudao.module.qms.controller.admin.iqc.vo.IqcApplySaveReqVO;
+
+/**
+ * IQC 来料检验申请 Service 接口
+ */
+public interface IqcApplyService {
+
+    /**
+     * 获取来料检验申请分页
+     *
+     * @param pageReqVO 分页参数
+     * @return 分页结果
+     */
+    PageResult<IqcApplyRespVO> getIqcApplyPage(IqcApplyPageReqVO pageReqVO);
+
+    /**
+     * 创建来料检验申请
+     *
+     * @param createReqVO 创建信息
+     * @return 单据编号
+     */
+    String createIqcApply(IqcApplySaveReqVO createReqVO);
+
+    /**
+     * 更新来料检验申请
+     *
+     * @param updateReqVO 更新信息
+     */
+    void updateIqcApply(IqcApplySaveReqVO updateReqVO);
+
+    /**
+     * 删除来料检验申请
+     *
+     * @param id 单据编号
+     */
+    void deleteIqcApply(String id);
+
+    /**
+     * 获得来料检验申请
+     *
+     * @param id 单据编号
+     * @return 申请信息
+     */
+    IqcApplyRespVO getIqcApply(String id);
+}

+ 142 - 0
yudao-module-qms/src/main/java/cn/iocoder/yudao/module/qms/service/iqc/IqcApplyServiceImpl.java

@@ -0,0 +1,142 @@
+package cn.iocoder.yudao.module.qms.service.iqc;
+
+import cn.hutool.core.util.IdUtil;
+import cn.hutool.core.util.RandomUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.qms.controller.admin.iqc.vo.IqcApplyDetailSaveReqVO;
+import cn.iocoder.yudao.module.qms.controller.admin.iqc.vo.IqcApplyDetailRespVO;
+import cn.iocoder.yudao.module.qms.controller.admin.iqc.vo.IqcApplyPageReqVO;
+import cn.iocoder.yudao.module.qms.controller.admin.iqc.vo.IqcApplyRespVO;
+import cn.iocoder.yudao.module.qms.controller.admin.iqc.vo.IqcApplySaveReqVO;
+import cn.iocoder.yudao.module.qms.dal.mysql.iqc.IqcApplyMapper;
+import jakarta.annotation.Resource;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.Collections;
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
+
+/**
+ * IQC 来料检验申请 Service 实现
+ */
+@Service
+public class IqcApplyServiceImpl implements IqcApplyService {
+
+    private static final String DEFAULT_STATUS = "待检验";
+    private static final DateTimeFormatter BILL_NO_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
+
+    @Resource
+    private IqcApplyMapper iqcApplyMapper;
+
+    @Override
+    public PageResult<IqcApplyRespVO> getIqcApplyPage(IqcApplyPageReqVO pageReqVO) {
+        return iqcApplyMapper.selectApplyPage(pageReqVO);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public String createIqcApply(IqcApplySaveReqVO createReqVO) {
+        Long applyId = IdUtil.getSnowflakeNextId();
+        String billNo = generateBillNo();
+        Long userId = getLoginUserId();
+        LocalDateTime now = LocalDateTime.now();
+        iqcApplyMapper.insertApply(applyId, billNo, DEFAULT_STATUS, userId,
+                createReqVO.getApplyTime(), createReqVO.getBusinessType(), createReqVO.getRemark(), userId, now);
+        List<IqcApplyDetailSaveReqVO> details = normalizeDetails(createReqVO.getDetails());
+        if (!details.isEmpty()) {
+            iqcApplyMapper.insertApplyDetails(applyId, details);
+        }
+        return billNo;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void updateIqcApply(IqcApplySaveReqVO updateReqVO) {
+        if (StrUtil.isBlank(updateReqVO.getId())) {
+            return;
+        }
+        Long applyId = iqcApplyMapper.selectApplyIdByBillNo(updateReqVO.getId());
+        if (applyId == null) {
+            return;
+        }
+        Long userId = getLoginUserId();
+        LocalDateTime now = LocalDateTime.now();
+        iqcApplyMapper.updateApplyByBillNo(updateReqVO.getId(), updateReqVO.getApplyTime(),
+                updateReqVO.getBusinessType(), updateReqVO.getRemark(), userId, now);
+        iqcApplyMapper.deleteApplyDetailsByApplyId(applyId);
+        List<IqcApplyDetailSaveReqVO> details = normalizeDetails(updateReqVO.getDetails());
+        if (!details.isEmpty()) {
+            iqcApplyMapper.insertApplyDetails(applyId, details);
+        }
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void deleteIqcApply(String id) {
+        if (StrUtil.isBlank(id)) {
+            return;
+        }
+        Long applyId = iqcApplyMapper.selectApplyIdByBillNo(id);
+        if (applyId == null) {
+            return;
+        }
+        iqcApplyMapper.deleteApplyDetailsByApplyId(applyId);
+        iqcApplyMapper.deleteApplyById(applyId);
+    }
+
+    @Override
+    public IqcApplyRespVO getIqcApply(String id) {
+        if (StrUtil.isBlank(id)) {
+            return null;
+        }
+        IqcApplyRespVO apply = iqcApplyMapper.selectApplyByBillNo(id);
+        if (apply == null) {
+            return null;
+        }
+        Long applyId = apply.getApplyId();
+        if (applyId != null) {
+            List<IqcApplyDetailRespVO> details = iqcApplyMapper.selectApplyDetailsByApplyId(applyId);
+            apply.setDetails(details);
+        } else {
+            apply.setDetails(Collections.emptyList());
+        }
+        return apply;
+    }
+
+    private String generateBillNo() {
+        String timestamp = LocalDateTime.now().format(BILL_NO_FORMATTER);
+        return "IQC" + timestamp + RandomUtil.randomNumbers(3);
+    }
+
+    private List<IqcApplyDetailSaveReqVO> normalizeDetails(List<IqcApplyDetailSaveReqVO> details) {
+        if (details == null || details.isEmpty()) {
+            return Collections.emptyList();
+        }
+        int seq = 1;
+        for (IqcApplyDetailSaveReqVO detail : details) {
+            detail.setId(IdUtil.getSnowflakeNextId());
+            detail.setSeq(seq++);
+            detail.setMaterialId(parseMaterialId(detail.getMaterialCode()));
+            if (StrUtil.isBlank(detail.getInspectStatus())) {
+                detail.setInspectStatus(DEFAULT_STATUS);
+            }
+        }
+        return details;
+    }
+
+    private Long parseMaterialId(String materialCode) {
+        if (StrUtil.isBlank(materialCode)) {
+            return null;
+        }
+        try {
+            return Long.parseLong(materialCode.trim());
+        } catch (NumberFormatException ex) {
+            return null;
+        }
+    }
+}

+ 137 - 0
yudao-module-qms/src/main/resources/mapper/iqc/IqcApplyMapper.xml

@@ -0,0 +1,137 @@
+<?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.iocoder.yudao.module.qms.dal.mysql.iqc.IqcApplyMapper">
+
+    <select id="selectApplyPage" resultType="cn.iocoder.yudao.module.qms.controller.admin.iqc.vo.IqcApplyRespVO">
+        SELECT *
+        FROM (
+            SELECT
+                a.id AS applyId,
+                a.FBILLNO AS id,
+                a.FAPPLYUSER AS applicantId,
+                u.username AS applicantName,
+                a.FAPPLYTIME AS applyTime,
+                a.FBILLTYPE AS businessType,
+                a.FCOMMENT AS remark,
+                COUNT(b.id) AS materialCount,
+                CASE
+                    WHEN SUM(CASE WHEN b.FINSPECTSTATUS = '检验中' THEN 1 ELSE 0 END) > 0 THEN '检验中'
+                    WHEN COUNT(b.id) > 0
+                        AND SUM(CASE WHEN b.FINSPECTSTATUS = '检验完成' THEN 1 ELSE 0 END) = COUNT(b.id) THEN '检验完成'
+                    ELSE '待检验'
+                END AS status
+            FROM qms_qcp_inspecapplyn a
+            LEFT JOIN system_users u ON u.id = a.FAPPLYUSER
+            LEFT JOIN qms_qcp_insappnentry b ON b.glid = a.id
+            <where>
+                <if test="req.keyword != null and req.keyword != ''">
+                    AND (a.FBILLNO LIKE CONCAT('%', #{req.keyword}, '%')
+                    OR a.FCOMMENT LIKE CONCAT('%', #{req.keyword}, '%'))
+                </if>
+                <if test="req.applicantIds != null and req.applicantIds.size > 0">
+                    AND a.FAPPLYUSER IN
+                    <foreach collection="req.applicantIds" item="id" open="(" separator="," close=")">
+                        #{id}
+                    </foreach>
+                </if>
+                <if test="req.applyTime != null and req.applyTime.length == 2">
+                    AND a.FAPPLYTIME BETWEEN #{req.applyTime[0]} AND #{req.applyTime[1]}
+                </if>
+            </where>
+            GROUP BY a.id, a.FBILLNO, a.FAPPLYUSER, u.username, a.FAPPLYTIME, a.FBILLTYPE, a.FCOMMENT
+        ) t
+        <where>
+            <if test="req.statusList != null and req.statusList.size > 0">
+                AND t.status IN
+                <foreach collection="req.statusList" item="status" open="(" separator="," close=")">
+                    #{status}
+                </foreach>
+            </if>
+        </where>
+        ORDER BY t.applyTime DESC
+    </select>
+
+    <select id="selectApplyByBillNo" resultType="cn.iocoder.yudao.module.qms.controller.admin.iqc.vo.IqcApplyRespVO">
+        SELECT
+            a.id AS applyId,
+            a.FBILLNO AS id,
+            a.FAPPLYUSER AS applicantId,
+            u.username AS applicantName,
+            a.FAPPLYTIME AS applyTime,
+            a.FBILLTYPE AS businessType,
+            a.FCOMMENT AS remark,
+            COUNT(b.id) AS materialCount,
+            CASE
+                WHEN SUM(CASE WHEN b.FINSPECTSTATUS = '检验中' THEN 1 ELSE 0 END) > 0 THEN '检验中'
+                WHEN COUNT(b.id) > 0
+                    AND SUM(CASE WHEN b.FINSPECTSTATUS = '检验完成' THEN 1 ELSE 0 END) = COUNT(b.id) THEN '检验完成'
+                ELSE '待检验'
+            END AS status
+        FROM qms_qcp_inspecapplyn a
+        LEFT JOIN system_users u ON u.id = a.FAPPLYUSER
+        LEFT JOIN qms_qcp_insappnentry b ON b.glid = a.id
+        WHERE a.FBILLNO = #{billNo}
+        GROUP BY a.id, a.FBILLNO, a.FAPPLYUSER, u.username, a.FAPPLYTIME, a.FBILLTYPE, a.FCOMMENT
+    </select>
+
+    <select id="selectApplyIdByBillNo" resultType="long">
+        SELECT id
+        FROM qms_qcp_inspecapplyn
+        WHERE FBILLNO = #{billNo}
+    </select>
+
+    <select id="selectApplyDetailsByApplyId" resultType="cn.iocoder.yudao.module.qms.controller.admin.iqc.vo.IqcApplyDetailRespVO">
+        SELECT
+            FSRCORDERNUM AS materialCode,
+            FSRCORDERTYPE AS materialName,
+            NULL AS specification,
+            FLOTNUMBER AS batch,
+            FAPPLYQTY AS quantity,
+            FUNIT AS unit,
+            NULL AS remark
+        FROM qms_qcp_insappnentry
+        WHERE glid = #{applyId}
+        ORDER BY FSEQ ASC
+    </select>
+
+    <insert id="insertApply">
+        INSERT INTO qms_qcp_inspecapplyn
+            (id, FBILLNO, FBILLSTATUS, FCREATORID, FCREATETIME, FMODIFIERID, FMODIFYTIME,
+             FAPPLYUSER, FAPPLYTIME, FBILLTYPE, FCOMMENT)
+        VALUES
+            (#{id}, #{billNo}, #{status}, #{creatorId}, #{createTime}, #{creatorId}, #{createTime},
+             #{applyUserId}, #{applyTime}, #{billType}, #{remark})
+    </insert>
+
+    <update id="updateApplyByBillNo">
+        UPDATE qms_qcp_inspecapplyn
+        SET FAPPLYTIME = #{applyTime},
+            FBILLTYPE = #{billType},
+            FCOMMENT = #{remark},
+            FMODIFIERID = #{modifierId},
+            FMODIFYTIME = #{modifyTime}
+        WHERE FBILLNO = #{billNo}
+    </update>
+
+    <delete id="deleteApplyById">
+        DELETE FROM qms_qcp_inspecapplyn
+        WHERE id = #{applyId}
+    </delete>
+
+    <delete id="deleteApplyDetailsByApplyId">
+        DELETE FROM qms_qcp_insappnentry
+        WHERE glid = #{applyId}
+    </delete>
+
+    <insert id="insertApplyDetails">
+        INSERT INTO qms_qcp_insappnentry
+            (id, glid, FSEQ, FMATERIELID, FSRCORDERNUM, FSRCORDERTYPE, FLOTNUMBER, FUNIT, FAPPLYQTY, FINSPECTSTATUS)
+        VALUES
+        <foreach collection="details" item="detail" separator=",">
+            (#{detail.id}, #{applyId}, #{detail.seq}, #{detail.materialId},
+             #{detail.materialCode}, #{detail.materialName}, #{detail.batch},
+             #{detail.unit}, #{detail.quantity}, #{detail.inspectStatus})
+        </foreach>
+    </insert>
+
+</mapper>

+ 6 - 0
yudao-server/pom.xml

@@ -43,6 +43,12 @@
             <artifactId>yudao-all-server</artifactId>
             <version>${revision}</version>
         </dependency>
+        <!-- 质量管理模块 -->
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-module-qms</artifactId>
+            <version>${revision}</version>
+        </dependency>
 
 
         <!-- 会员中心。默认注释,保证编译速度 -->

+ 71 - 0
yudao-ui/yudao-ui-admin-vue3/src/api/qms/iqc/apply/index.ts

@@ -0,0 +1,71 @@
+import request from '@/config/axios'
+
+export interface IqcApplyPageReqVO extends PageParam {
+  keyword?: string
+  statusList?: string[]
+  applicantIds?: Array<number | string>
+  applyTime?: string[]
+}
+
+export interface IqcApplyRespVO {
+  id: string
+  applyId?: number
+  applicantId: number | string
+  applicantName: string
+  applyTime: string
+  businessType?: string
+  department?: string
+  status: string
+  materialCount: number
+  remark?: string
+  details?: IqcApplyDetailRespVO[]
+}
+
+export const getIqcApplyPage = (params: IqcApplyPageReqVO) => {
+  return request.get<PageResult<IqcApplyRespVO[]>>({ url: '/qms/iqc-apply/page', params })
+}
+
+export interface IqcApplyDetailSaveReqVO {
+  materialCode?: string
+  materialName?: string
+  specification?: string
+  batch?: string
+  quantity?: number
+  unit?: string
+  remark?: string
+}
+
+export interface IqcApplySaveReqVO {
+  id?: string
+  applyTime: string
+  businessType?: string
+  department?: string
+  remark?: string
+  details?: IqcApplyDetailSaveReqVO[]
+}
+
+export interface IqcApplyDetailRespVO {
+  materialCode?: string
+  materialName?: string
+  specification?: string
+  batch?: string
+  quantity?: number
+  unit?: string
+  remark?: string
+}
+
+export const getIqcApply = (id: string) => {
+  return request.get<IqcApplyRespVO>({ url: '/qms/iqc-apply/get', params: { id } })
+}
+
+export const createIqcApply = (data: IqcApplySaveReqVO) => {
+  return request.post<string>({ url: '/qms/iqc-apply/create', data })
+}
+
+export const updateIqcApply = (data: IqcApplySaveReqVO) => {
+  return request.put<boolean>({ url: '/qms/iqc-apply/update', data })
+}
+
+export const deleteIqcApply = (id: string) => {
+  return request.delete<boolean>({ url: '/qms/iqc-apply/delete', params: { id } })
+}

+ 4 - 5
yudao-ui/yudao-ui-admin-vue3/src/config/qmsModules.ts

@@ -22,10 +22,11 @@ export const QMS_MODULES: Record<string, any> = {
         create: '新建申请',
         guide: true
       },
-      keywordFields: ['id', 'remark'],
+      keywordFields: ['id'],
+      keywordPlaceholder: '搜索单据编号',
       columns: [
         { type: 'selection', width: 55 },
-        { key: 'id', label: '单据编号', width: 120, type: 'applicationLink' },
+        { key: 'id', label: '单据编号', width: 200, type: 'applicationLink' },
         { key: 'applicantName', label: '申请人', width: 100 },
         { key: 'applyTime', label: '申请时间', width: 160, type: 'datetime' },
         { key: 'status', label: '单据状态', width: 110, type: 'status' },
@@ -33,9 +34,7 @@ export const QMS_MODULES: Record<string, any> = {
         { key: 'remark', label: '备注', minWidth: 200, type: 'tooltip' }
       ],
       advancedFilters: [
-        { key: 'applicantIds', label: '申请人', type: 'select', multiple: true, optionsKey: 'applicants' },
-        { key: 'statusList', label: '单据状态', type: 'select', multiple: true, optionsKey: 'applicationStatuses' },
-        { key: 'timeRange', label: '申请时间', type: 'datetimerange' }
+        { key: 'statusList', label: '单据状态', type: 'select', multiple: true, optionsKey: 'applicationStatuses' }
       ]
     },
     applicationForm: {

+ 2 - 1
yudao-ui/yudao-ui-admin-vue3/src/permission.ts

@@ -1,4 +1,4 @@
-import router from './router'
+import router, { ensureQmsRoutes } from './router'
 import type { RouteRecordRaw } from 'vue-router'
 import { isRelogin } from '@/config/axios/service'
 import { getAccessToken } from '@/utils/auth'
@@ -58,6 +58,7 @@ const whiteList = [
 
 // 路由加载前
 router.beforeEach(async (to, from, next) => {
+  ensureQmsRoutes()
   start()
   loadStart()
   if (getAccessToken()) {

+ 45 - 1
yudao-ui/yudao-ui-admin-vue3/src/router/index.ts

@@ -2,6 +2,26 @@ import type { App } from 'vue'
 import type { RouteRecordRaw } from 'vue-router'
 import { createRouter, createWebHistory } from 'vue-router'
 import remainingRouter from './modules/remaining'
+import { Layout } from '@/utils/routerHelper'
+
+const collectRouteNames = (routes: RouteRecordRaw[], names = new Set<string>()) => {
+  routes.forEach((route) => {
+    if (route.name) names.add(route.name as string)
+    if (route.children?.length) collectRouteNames(route.children as RouteRecordRaw[], names)
+  })
+  return names
+}
+
+const staticRouteNames = collectRouteNames(remainingRouter as RouteRecordRaw[])
+
+const qmsFallbackRoutes: RouteRecordRaw[] = [
+  { path: 'iqc/apply/list', name: 'IqcApplicationList', component: () => import('@/views/qms/ApplicationList.vue'), meta: { module: 'iqc', title: '来料检验申请', noCache: true, hidden: true, canTo: true, activeMenu: '/qms' } },
+  { path: 'ipqc/apply/list', name: 'IpqcApplicationList', component: () => import('@/views/qms/ApplicationList.vue'), meta: { module: 'ipqc', title: '过程检验申请', noCache: true, hidden: true, canTo: true, activeMenu: '/qms' } },
+  { path: 'fqc/apply/list', name: 'FqcApplicationList', component: () => import('@/views/qms/ApplicationList.vue'), meta: { module: 'fqc', title: '成品检验申请', noCache: true, hidden: true, canTo: true, activeMenu: '/qms' } },
+  { path: 'iqc/task/list', name: 'IqcTaskList', component: () => import('@/views/qms/TaskList.vue'), meta: { module: 'iqc', title: '来料检验任务', noCache: true, hidden: true, canTo: true, activeMenu: '/qms' } },
+  { path: 'ipqc/task/list', name: 'IpqcTaskList', component: () => import('@/views/qms/TaskList.vue'), meta: { module: 'ipqc', title: '过程检验任务', noCache: true, hidden: true, canTo: true, activeMenu: '/qms' } },
+  { path: 'fqc/task/list', name: 'FqcTaskList', component: () => import('@/views/qms/TaskList.vue'), meta: { module: 'fqc', title: '成品检验任务', noCache: true, hidden: true, canTo: true, activeMenu: '/qms' } }
+]
 
 // 创建路由实例
 const router = createRouter({
@@ -15,14 +35,38 @@ export const resetRouter = (): void => {
   const resetWhiteNameList = ['Redirect', 'Login', 'NoFound', 'Home']
   router.getRoutes().forEach((route) => {
     const { name } = route
-    if (name && !resetWhiteNameList.includes(name as string)) {
+    if (name && !resetWhiteNameList.includes(name as string) && !staticRouteNames.has(name as string)) {
       router.hasRoute(name) && router.removeRoute(name)
     }
   })
 }
 
+export const ensureQmsRoutes = () => {
+  let parentName = router.getRoutes().find((route) => route.path === '/qms' && route.name)?.name as string | undefined
+  const ensureParent = () => {
+    if (parentName && router.hasRoute(parentName)) return parentName
+    if (!router.hasRoute('QmsCenter')) {
+      router.addRoute({ path: '/qms', component: Layout, name: 'QmsCenter', meta: { hidden: true } })
+    }
+    parentName = 'QmsCenter'
+    return parentName
+  }
+
+  qmsFallbackRoutes.forEach((route) => {
+    const fullPath = `/qms/${route.path}`
+    if (router.getRoutes().some((r) => r.path === fullPath)) {
+      return
+    }
+    if (route.name && router.hasRoute(route.name as string)) {
+      router.removeRoute(route.name as string)
+    }
+    router.addRoute(ensureParent(), route)
+  })
+}
+
 export const setupRouter = (app: App<Element>) => {
   app.use(router)
+  ensureQmsRoutes()
 }
 
 export default router

+ 11 - 0
yudao-ui/yudao-ui-admin-vue3/src/router/modules/remaining.ts

@@ -759,6 +759,17 @@ const remainingRouter: AppRouteRecordRaw[] = [
     ]
   },
   {
+    // S5 菜单路径兼容,避免点击侧边栏 404
+    path: '/s5',
+    component: Layout,
+    name: 'S5Center',
+    meta: { hidden: true },
+    children: [
+      { path: 'iqc/iqc/apply/list', name: 'S5IqcApplicationList', component: () => import('@/views/qms/ApplicationList.vue'), meta: { module: 'iqc', title: '来料检验申请', noCache: true, hidden: true, canTo: true, activeMenu: '/s5' } },
+      { path: 'iqc/qms/iqc/apply/list', name: 'S5IqcApplicationListCompat', component: () => import('@/views/qms/ApplicationList.vue'), meta: { module: 'iqc', title: '来料检验申请', noCache: true, hidden: true, canTo: true, activeMenu: '/s5' } }
+    ]
+  },
+  {
     path: '/s0',
     component: Layout,
     name: 'S0Center',

+ 26 - 0
yudao-ui/yudao-ui-admin-vue3/src/store/modules/qms/application.ts

@@ -1,5 +1,6 @@
 import { defineStore } from 'pinia'
 import { getQmsModuleConfig, QMS_MODULE_CODES } from '@/config/qmsModules'
+import { getIqcApplyPage, deleteIqcApply } from '@/api/qms/iqc/apply'
 
 const createDefaultFilters = () => ({
   keyword: '', searchMode: 'AND', applicantIds: [], statusList: [], timeRange: [],
@@ -27,6 +28,8 @@ const createSampleData = () => ({
 })
 
 const MODULE_PREFIX: Record<string, string> = { iqc: 'IQC', ipqc: 'IPQC', fqc: 'FQC' }
+const REMOTE_MODULES = new Set(['iqc'])
+const isRemoteModule = (module: string) => REMOTE_MODULES.has(module)
 
 export const useQmsApplicationStore = defineStore('qmsApplication', {
   state: () => ({
@@ -37,7 +40,11 @@ export const useQmsApplicationStore = defineStore('qmsApplication', {
     currentApplication: null as any
   }),
   getters: {
+    isRemoteModule: () => (module: string) => isRemoteModule(module),
     filteredApplications: (state) => (module: string) => {
+      if (isRemoteModule(module)) {
+        return state.applications[module] || []
+      }
       const config = getQmsModuleConfig(module)
       const filters = state.filters[module] || createDefaultFilters()
       const applications = [...(state.applications[module] || [])]
@@ -64,11 +71,30 @@ export const useQmsApplicationStore = defineStore('qmsApplication', {
   },
   actions: {
     async fetchApplications(module: string) {
+      if (isRemoteModule(module)) {
+        const filters = this.filters[module] || createDefaultFilters()
+        const pagination = this.pagination[module] || createDefaultPagination()
+        const params = {
+          pageNo: pagination.currentPage,
+          pageSize: pagination.pageSize,
+          keyword: (filters.keyword || '').trim() || undefined,
+          statusList: filters.statusList?.length ? filters.statusList : undefined
+        }
+        const pageResult = await getIqcApplyPage(params)
+        const pageData: any = (pageResult as any)?.data ?? pageResult
+        this.applications[module] = pageData?.list || []
+        this.pagination[module].total = pageData?.total || 0
+        return pageData?.list
+      }
       return new Promise((resolve) => {
         setTimeout(() => { const list = this.filteredApplications(module); this.pagination[module].total = list.length; resolve(list) }, 300)
       })
     },
     async deleteApplication(module: string, id: string) {
+      if (isRemoteModule(module)) {
+        await deleteIqcApply(id)
+        return true
+      }
       return new Promise((resolve) => {
         setTimeout(() => {
           const list = this.applications[module] || []

+ 108 - 20
yudao-ui/yudao-ui-admin-vue3/src/views/qms/ApplicationForm.vue

@@ -57,9 +57,7 @@
 
         <div class="form-actions">
           <el-button @click="handleBack">返回列表</el-button>
-          <el-button v-if="isEditable" @click="handleSaveDraft" :loading="saving">保存草稿</el-button>
           <el-button v-if="isEditable" type="primary" @click="handleSubmit" :loading="submitting">提交</el-button>
-          <el-button v-if="isEditable" @click="handleReset">重置</el-button>
         </div>
       </el-form>
     </div>
@@ -72,18 +70,21 @@ defineOptions({ name: 'IqcApplicationForm' })
 import { reactive, ref, computed, onMounted } from 'vue'
 import { useRoute, useRouter } from 'vue-router'
 import IqcStatusBadge from '@/components/Qms/StatusBadge.vue'
-import { useQmsApplicationStore } from '@/store/modules/qms/application'
 import { getQmsModuleConfig, DEFAULT_QMS_MODULE } from '@/config/qmsModules'
+import { createIqcApply, getIqcApply, updateIqcApply } from '@/api/qms/iqc/apply'
 
 const route = useRoute()
 const router = useRouter()
-const applicationStore = useQmsApplicationStore()
-
 const moduleCode = computed(() => (route.meta.module as string) || DEFAULT_QMS_MODULE)
-const formConfig = computed(() => getQmsModuleConfig(moduleCode.value).applicationForm)
+const moduleConfig = computed(() => getQmsModuleConfig(moduleCode.value))
+const isIqcModule = computed(() => moduleCode.value === 'iqc')
+const formConfig = computed(() => moduleConfig.value.applicationForm)
+const listPath = computed(() => {
+  const navItem = moduleConfig.value.navItems?.find((item: any) => item.key === 'applications')
+  return navItem?.path || `/qms/${moduleConfig.value.code}/apply/list`
+})
 
 const loading = ref(false)
-const saving = ref(false)
 const submitting = ref(false)
 const applicationData = ref<any>(null)
 
@@ -93,6 +94,7 @@ const isCreate = computed(() => route.meta.mode === 'create')
 const isEdit = computed(() => route.meta.mode === 'edit')
 const isView = computed(() => route.meta.mode === 'view')
 const isEditable = computed(() => isCreate.value || isEdit.value)
+const routeId = computed(() => String(route.params.id || ''))
 
 const pageTitle = computed(() => {
   if (isCreate.value) return `新建${formConfig.value.title}`
@@ -120,7 +122,7 @@ const getFieldComponent = (field: any) => {
 }
 
 const getFieldProps = (field: any) => {
-  if (field.type === 'datetime') return { type: 'datetime', placeholder: '请选择时间', format: 'YYYY-MM-DD HH:mm', 'value-format': 'YYYY-MM-DD HH:mm:ss', style: 'width: 100%' }
+  if (field.type === 'datetime') return { type: 'datetime', placeholder: '请选择时间', format: 'YYYY-MM-DD HH:mm:ss', 'value-format': 'x', style: 'width: 100%' }
   if (field.type === 'textarea') return { type: 'textarea', rows: field.rows || 3, placeholder: `请输入${field.label}` }
   return { placeholder: `请输入${field.label}`, style: 'width: 100%' }
 }
@@ -142,24 +144,110 @@ const getDetailProps = (column: any) => {
 
 const handleAddDetail = () => { formData.details.push(createDetailRow()) }
 const handleDeleteDetail = (index: number) => { formData.details.splice(index, 1); if (!formData.details.length) formData.details.push(createDetailRow()) }
-const handleBack = () => { router.push(`/qms/${moduleCode.value}/apply/list`) }
-const handleReset = () => { initFormData(); formData.details = [createDetailRow()] }
-
-const handleSaveDraft = async () => {
-  saving.value = true
-  try { ElMessage.success('保存成功'); handleBack() }
-  catch { ElMessage.error('保存失败') }
-  finally { saving.value = false }
-}
-
+const handleBack = () => { router.push(listPath.value) }
 const handleSubmit = async () => {
   submitting.value = true
-  try { ElMessage.success('提交成功'); handleBack() }
+  try {
+    await saveApplication()
+    ElMessage.success('提交成功')
+    handleBack()
+  }
   catch { ElMessage.error('提交失败') }
   finally { submitting.value = false }
 }
 
-onMounted(() => { initFormData(); formData.details = [createDetailRow()] })
+const unwrapResult = (result: any) => (result && result.data !== undefined ? result.data : result)
+
+const toTimestamp = (value: any) => {
+  if (value === null || value === undefined || value === '') return undefined
+  if (value instanceof Date) return value.getTime()
+  if (typeof value === 'number') return value
+  const text = String(value).trim()
+  if (/^\d+$/.test(text)) return Number(text)
+  const normalized = text.includes('T') ? text : text.replace(' ', 'T')
+  const parsed = new Date(normalized)
+  if (Number.isNaN(parsed.getTime())) return undefined
+  return parsed.getTime()
+}
+
+const buildPayload = () => ({
+  id: isEdit.value ? routeId.value : undefined,
+  applyTime: toTimestamp(formData.applyTime || applicationData.value?.applyTime),
+  businessType: formData.businessType || undefined,
+  department: formData.department || undefined,
+  remark: formData.remark || undefined,
+  details: (formData.details || []).map((detail: any) => ({
+    materialCode: detail.materialCode || '',
+    materialName: detail.materialName || '',
+    specification: detail.specification || '',
+    batch: detail.batch || '',
+    quantity: detail.quantity ?? 0,
+    unit: detail.unit || '',
+    remark: detail.remark || ''
+  }))
+})
+
+const applyResponseToForm = (data: any) => {
+  initFormData()
+  formData.applicantName = data?.applicantName || ''
+  formData.applyTime = toTimestamp(data?.applyTime)
+  formData.businessType = data?.businessType || ''
+  formData.department = data?.department || ''
+  formData.remark = data?.remark || ''
+  formData.details = (data?.details && data.details.length)
+    ? data.details.map((detail: any) => ({
+      materialCode: detail.materialCode || '',
+      materialName: detail.materialName || '',
+      specification: detail.specification || '',
+      batch: detail.batch || '',
+      quantity: detail.quantity ?? 0,
+      unit: detail.unit || '',
+      remark: detail.remark || ''
+    }))
+    : [createDetailRow()]
+}
+
+const fetchApplication = async () => {
+  if (isCreate.value) {
+    initFormData()
+    formData.details = [createDetailRow()]
+    return
+  }
+  if (!isIqcModule.value) {
+    initFormData()
+    formData.details = [createDetailRow()]
+    return
+  }
+  loading.value = true
+  try {
+    const result = await getIqcApply(routeId.value)
+    const data = unwrapResult(result)
+    applicationData.value = data
+    applyResponseToForm(data)
+  } catch (error) {
+    ElMessage.error('获取申请详情失败')
+  } finally {
+    loading.value = false
+  }
+}
+
+const saveApplication = async () => {
+  if (!isIqcModule.value) return
+  const payload = buildPayload()
+  if (!payload.applyTime) {
+    ElMessage.warning('申请时间不能为空')
+    return
+  }
+  if (isCreate.value) {
+    await createIqcApply(payload)
+  } else {
+    await updateIqcApply(payload)
+  }
+}
+
+onMounted(() => {
+  fetchApplication()
+})
 </script>
 
 <style lang="scss" scoped>

+ 99 - 2
yudao-ui/yudao-ui-admin-vue3/src/views/qms/ApplicationList.vue

@@ -26,7 +26,39 @@
       </div>
     </div>
 
+    <div v-if="isIqcModule" class="filter-bar">
+      <div class="filter-group">
+        <el-input
+          v-model="iqcFilters.keyword"
+          :placeholder="keywordPlaceholder"
+          clearable
+          class="filter-input"
+          @clear="handleIqcSearch"
+        >
+          <template #append>
+            <el-button @click="handleIqcSearch"><Icon icon="ep:search" /></el-button>
+          </template>
+        </el-input>
+        <el-select
+          v-model="iqcFilters.statusList"
+          class="filter-select"
+          multiple
+          clearable
+          placeholder="单据状态"
+          @change="handleIqcSearch"
+        >
+          <el-option
+            v-for="option in filterOptions.applicationStatuses"
+            :key="option.value"
+            :label="option.label"
+            :value="option.value"
+          />
+        </el-select>
+        <el-button plain @click="handleIqcReset"><Icon icon="ep:refresh" class="mr-5px" />重置</el-button>
+      </div>
+    </div>
     <IqcSearchFilter
+      v-else
       :filters="currentFilters"
       :advanced-filters="listConfig.advancedFilters"
       :options="filterOptions"
@@ -142,21 +174,29 @@ const selectedRows = ref<any[]>([])
 
 const moduleCode = computed(() => (route.meta.module as string) || DEFAULT_QMS_MODULE)
 const listConfig = computed(() => getQmsModuleConfig(moduleCode.value).applicationList)
+const isIqcModule = computed(() => moduleCode.value === 'iqc')
 
 const pagination = computed(() => applicationStore.pagination[moduleCode.value])
 const currentFilters = computed(() => applicationStore.filters[moduleCode.value])
 const listColumns = computed(() => listConfig.value.columns || [])
-const keywordPlaceholder = computed(() => `搜索${(listConfig.value.keywordFields || ['关键字段']).join('/')}`)
+const keywordPlaceholder = computed(() => listConfig.value.keywordPlaceholder || `搜索${(listConfig.value.keywordFields || ['关键字段']).join('/')}`)
+const useRemoteData = computed(() => applicationStore.isRemoteModule(moduleCode.value))
 
 const fullList = computed(() => applicationStore.filteredApplications(moduleCode.value) || [])
 
 const tableData = computed(() => {
   const list = fullList.value
+  if (useRemoteData.value) return list
   const { currentPage, pageSize } = pagination.value
   const start = (currentPage - 1) * pageSize
   return list.slice(start, start + pageSize)
 })
 
+const iqcFilters = ref({
+  keyword: '',
+  statusList: [] as string[]
+})
+
 const uniqueOptions = (list: any[], field: string, labelField?: string) => {
   const map = new Map()
   list.forEach((item) => {
@@ -225,7 +265,17 @@ const filterOptions = computed(() => {
 
 const formatDateTime = (value: any) => {
   if (!value) return '-'
-  return String(value).replace('T', ' ').slice(0, 16)
+  const date = typeof value === 'number' || /^\d+$/.test(String(value))
+    ? new Date(Number(value))
+    : new Date(String(value))
+  if (Number.isNaN(date.getTime())) return String(value)
+  const yyyy = String(date.getFullYear())
+  const MM = String(date.getMonth() + 1).padStart(2, '0')
+  const dd = String(date.getDate()).padStart(2, '0')
+  const HH = String(date.getHours()).padStart(2, '0')
+  const mm = String(date.getMinutes()).padStart(2, '0')
+  const ss = String(date.getSeconds()).padStart(2, '0')
+  return `${yyyy}-${MM}-${dd} ${HH}:${mm}:${ss}`
 }
 
 const formatNumber = (value: any) => {
@@ -303,6 +353,18 @@ const handleSearch = (payload: any) => {
   fetchData()
 }
 
+const handleIqcSearch = () => {
+  handleSearch({
+    keyword: iqcFilters.value.keyword,
+    statusList: iqcFilters.value.statusList
+  })
+}
+
+const handleIqcReset = () => {
+  iqcFilters.value = { keyword: '', statusList: [] }
+  handleReset()
+}
+
 const handleReset = () => {
   applicationStore.resetFilters(moduleCode.value)
   applicationStore.updatePagination(moduleCode.value, { currentPage: 1 })
@@ -334,6 +396,18 @@ watch(moduleCode, () => {
   fetchData()
 })
 
+watch(
+  currentFilters,
+  (value) => {
+    if (!isIqcModule.value) return
+    iqcFilters.value = {
+      keyword: value.keyword || '',
+      statusList: value.statusList || []
+    }
+  },
+  { deep: true, immediate: true }
+)
+
 onMounted(() => {
   fetchData()
 })
@@ -376,6 +450,25 @@ onMounted(() => {
   }
 }
 
+.filter-bar {
+  margin-bottom: 16px;
+}
+
+.filter-group {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  flex-wrap: wrap;
+}
+
+.filter-input {
+  width: 200px;
+}
+
+.filter-select {
+  width: 200px;
+}
+
 .pagination-container {
   padding: 16px;
   display: flex;
@@ -404,5 +497,9 @@ onMounted(() => {
     align-items: flex-start;
     gap: 12px;
   }
+
+  .filter-input {
+    width: 100%;
+  }
 }
 </style>