Browse Source

Merge remote-tracking branch 'origin/master'

Pengxy 2 months ago
parent
commit
6878a0ee2f
43 changed files with 2557 additions and 473 deletions
  1. 209 0
      IQC来料检验任务流程开发说明.md
  2. 186 0
      IQC来料检验申请-后端设计.md
  3. 199 0
      Order_Change_流程开发说明.md
  4. 1 0
      pom.xml
  5. 62 0
      yudao-module-qms/pom.xml
  6. 74 0
      yudao-module-qms/src/main/java/cn/iocoder/yudao/module/qms/controller/admin/iqc/IqcApplyController.java
  7. 59 0
      yudao-module-qms/src/main/java/cn/iocoder/yudao/module/qms/controller/admin/iqc/IqcTaskController.java
  8. 35 0
      yudao-module-qms/src/main/java/cn/iocoder/yudao/module/qms/controller/admin/iqc/vo/IqcApplyDetailRespVO.java
  9. 44 0
      yudao-module-qms/src/main/java/cn/iocoder/yudao/module/qms/controller/admin/iqc/vo/IqcApplyDetailSaveReqVO.java
  10. 34 0
      yudao-module-qms/src/main/java/cn/iocoder/yudao/module/qms/controller/admin/iqc/vo/IqcApplyPageReqVO.java
  11. 51 0
      yudao-module-qms/src/main/java/cn/iocoder/yudao/module/qms/controller/admin/iqc/vo/IqcApplyRespVO.java
  12. 37 0
      yudao-module-qms/src/main/java/cn/iocoder/yudao/module/qms/controller/admin/iqc/vo/IqcApplySaveReqVO.java
  13. 54 0
      yudao-module-qms/src/main/java/cn/iocoder/yudao/module/qms/controller/admin/iqc/vo/IqcTaskDetailRespVO.java
  14. 18 0
      yudao-module-qms/src/main/java/cn/iocoder/yudao/module/qms/controller/admin/iqc/vo/IqcTaskPageReqVO.java
  15. 33 0
      yudao-module-qms/src/main/java/cn/iocoder/yudao/module/qms/controller/admin/iqc/vo/IqcTaskProcessInfoVO.java
  16. 51 0
      yudao-module-qms/src/main/java/cn/iocoder/yudao/module/qms/controller/admin/iqc/vo/IqcTaskRespVO.java
  17. 14 0
      yudao-module-qms/src/main/java/cn/iocoder/yudao/module/qms/controller/admin/iqc/vo/IqcTaskStartReqVO.java
  18. 73 0
      yudao-module-qms/src/main/java/cn/iocoder/yudao/module/qms/dal/mysql/iqc/IqcApplyMapper.java
  19. 53 0
      yudao-module-qms/src/main/java/cn/iocoder/yudao/module/qms/dal/mysql/iqc/IqcTaskMapper.java
  20. 50 0
      yudao-module-qms/src/main/java/cn/iocoder/yudao/module/qms/service/iqc/IqcApplyService.java
  21. 142 0
      yudao-module-qms/src/main/java/cn/iocoder/yudao/module/qms/service/iqc/IqcApplyServiceImpl.java
  22. 44 0
      yudao-module-qms/src/main/java/cn/iocoder/yudao/module/qms/service/iqc/IqcTaskService.java
  23. 103 0
      yudao-module-qms/src/main/java/cn/iocoder/yudao/module/qms/service/iqc/IqcTaskServiceImpl.java
  24. 36 0
      yudao-module-qms/src/main/java/cn/iocoder/yudao/module/qms/service/iqc/listener/IqcTaskStatusListener.java
  25. 138 0
      yudao-module-qms/src/main/resources/mapper/iqc/IqcApplyMapper.xml
  26. 87 0
      yudao-module-qms/src/main/resources/mapper/iqc/IqcTaskMapper.xml
  27. 6 0
      yudao-server/pom.xml
  28. 1 1
      yudao-server/src/main/java/cn/iocoder/yudao/server/YudaoServerApplication.java
  29. 73 0
      yudao-ui/yudao-ui-admin-vue3/src/api/qms/iqc/apply/index.ts
  30. 50 0
      yudao-ui/yudao-ui-admin-vue3/src/api/qms/iqc/task/index.ts
  31. 0 112
      yudao-ui/yudao-ui-admin-vue3/src/config/modules.ts
  32. 14 117
      yudao-ui/yudao-ui-admin-vue3/src/config/qmsModules.ts
  33. 2 1
      yudao-ui/yudao-ui-admin-vue3/src/permission.ts
  34. 45 1
      yudao-ui/yudao-ui-admin-vue3/src/router/index.ts
  35. 13 3
      yudao-ui/yudao-ui-admin-vue3/src/router/modules/remaining.ts
  36. 26 0
      yudao-ui/yudao-ui-admin-vue3/src/store/modules/qms/application.ts
  37. 23 0
      yudao-ui/yudao-ui-admin-vue3/src/store/modules/qms/task.ts
  38. 7 1
      yudao-ui/yudao-ui-admin-vue3/src/views/bpm/processInstance/detail/ProcessInstanceOperationButton.vue
  39. 118 23
      yudao-ui/yudao-ui-admin-vue3/src/views/qms/ApplicationForm.vue
  40. 99 2
      yudao-ui/yudao-ui-admin-vue3/src/views/qms/ApplicationList.vue
  41. 0 164
      yudao-ui/yudao-ui-admin-vue3/src/views/qms/TaskForm.vue
  42. 51 48
      yudao-ui/yudao-ui-admin-vue3/src/views/qms/TaskList.vue
  43. 142 0
      yudao-ui/yudao-ui-admin-vue3/src/views/qms/iqc/task/ProcessDetailForm.vue

+ 209 - 0
IQC来料检验任务流程开发说明.md

@@ -0,0 +1,209 @@
+# IQC 来料检验任务流程接入工作流开发说明(参照 oa_leave)
+
+> 目标:参照请假流程(`oa_leave`)的实现方式,将 IQC 来料检验任务接入工作流,实现任务与流程实例关联、流程状态落库、发起/详情/列表页面对齐,以及流程结束结果监听与业务状态回写。
+
+---
+
+## 0. 流程节点(已确认)
+- 来料检验
+- 领导审批
+- SQE 处理
+
+---
+
+## 1. 流程实例关联字段设计
+
+### 1.1 业务表字段(已确认)
+任务数据当前来自子表:`qms_qcp_insappnentry`(主表:`qms_qcp_inspecapplyn`)。
+
+任务子表使用字段:
+- `process_instance_id`:流程实例 ID(用于与工作流关联)
+- `flowstate`:流程状态(未发起 / 审批中 / 审批完成 / 已驳回 / 已取消)
+
+> 字段名称已确认使用 `process_instance_id` / `flowstate`。
+
+### 1.2 关联关系
+`qms_qcp_insappnentry.process_instance_id` 对应流程引擎历史流程实例表:
+
+- 表:`ACT_HI_PROCINST`
+- 字段:`PROC_INST_ID_`
+
+即:
+- `qms_qcp_insappnentry.process_instance_id` ↔ `ACT_HI_PROCINST.PROC_INST_ID_`
+
+---
+
+## 2. 流程状态字段与回调更新策略
+
+### 2.1 状态字段
+建议使用:
+- `flowstate`:流程状态
+- `FINSPECTSTATUS`:检验状态(业务状态,列表展示用)
+
+示例状态:
+- `flowstate`:未发起 / 审批中 / 审批完成 / 已驳回 / 已取消
+- `FINSPECTSTATUS`:待检验 / 检验中 / 检验完成
+
+### 2.2 状态更新方式
+通过 Listener 监听流程回调并更新 `flowstate`。  
+业务状态 `FINSPECTSTATUS` 可在流程节点触发或流程结束时统一更新。
+
+#### 2.2.1 流程提交后:更新为“审批中”
+```sql
+update qms_qcp_insappnentry
+set flowstate = '审批中'
+where id = '{<TaskEntryId>}';
+```
+
+#### 2.2.2 流程通过后:更新为“审批完成”
+```sql
+update qms_qcp_insappnentry
+set flowstate = '审批完成',
+    FINSPECTSTATUS = '检验完成'
+where id = '{<TaskEntryId>}';
+```
+
+#### 2.2.3 流程取消:回退为“待检验”
+```sql
+update qms_qcp_insappnentry
+set flowstate = '已取消',
+    FINSPECTSTATUS = '待检验'
+where id = '{<TaskEntryId>}';
+```
+
+#### 2.2.4 流程驳回:更新为“检验中”
+```sql
+update qms_qcp_insappnentry
+set flowstate = '已驳回',
+    FINSPECTSTATUS = '检验中'
+where id = '{<TaskEntryId>}';
+```
+
+---
+
+## 3. 后端业务逻辑开发(参照 oa_leave)
+
+### 3.1 参考类
+请假流程后端参考:
+- `BpmOALeaveController`
+- `BpmOALeaveServiceImpl`
+
+IQC 任务流程需实现类似结构的 Controller + Service,并按“业务表 + 流程”方式接入。
+
+### 3.2 流程模型编号(PROCESS_KEY)
+请假:`oa_leave`  
+IQC 任务:`qms_iqc_task`
+
+> 该 Key 必须与流程模型实际配置一致。
+
+### 3.3 创建流程实例
+使用 `BpmProcessInstanceApi` 创建流程实例:
+- `#createProcessInstance(...)`
+
+发起 IQC 任务流程时建议完成:
+- 创建流程实例
+- 回写 `qms_qcp_insappnentry.process_instance_id`
+- 更新 `flowstate = '审批中'`
+
+---
+
+## 4. 前端业务逻辑开发(参照 oa_leave)
+
+### 4.1 参考页面
+请假模块前端参考:
+- 发起:`leave/create.vue`
+- 详情:`leave/detail.vue`
+- 列表:`leave/index.vue`
+
+### 4.2 IQC 任务页面对齐建议
+可沿用现有 QMS 任务页面逻辑:
+- 列表:`TaskList.vue`(已存在)
+- 表单:`TaskForm.vue`(已存在)
+
+接入流程后建议实现:
+- 列表 “开始检验 / 继续检验” → 发起流程(create)
+- 详情查看 → 进入流程详情(detail)
+
+---
+
+## 5. 路由配置(参照 remaining.ts 的请假流程配置)
+
+为 IQC 任务流程新增隐藏路由(create/detail),用于:
+- 从任务列表发起流程
+- 在流程流转/审批中查看业务详情
+
+示例(路径可按实际模块调整):
+```ts
+{
+  path: '/qms',
+  component: Layout,
+  name: 'qms',
+  meta: { hidden: true },
+  children: [
+    {
+      path: 'iqc/task/create',
+      component: () => import('@/views/qms/TaskForm.vue'),
+      name: 'IqcTaskCreate',
+      meta: {
+        noCache: true,
+        hidden: true,
+        canTo: true,
+        title: '发起 IQC 任务',
+        activeMenu: '/qms/iqc/task/list'
+      }
+    },
+    {
+      path: 'iqc/task/detail/:id',
+      component: () => import('@/views/qms/TaskForm.vue'),
+      name: 'IqcTaskDetail',
+      meta: {
+        noCache: true,
+        hidden: true,
+        canTo: true,
+        title: '查看 IQC 任务',
+        activeMenu: '/qms/iqc/task/list'
+      }
+    }
+  ]
+}
+```
+
+---
+
+## 6. 流程结束回调监听与业务状态更新(必须)
+
+### 6.1 监听器机制
+审批结束时(通过 / 不通过 / 取消),后端必须监听最终结果并更新业务表状态。
+
+请假流程实现参考:
+- `BpmOALeaveStatusListener`
+(继承 `BpmProcessInstanceStatusEventListener`)
+
+### 6.2 IQC 任务监听器要求
+需新增监听器(示例命名:`BpmIqcTaskStatusListener`),继承:
+- `BpmProcessInstanceStatusEventListener`
+
+监听流程结束结果并更新:
+- `qms_qcp_insappnentry.flowstate`
+- `qms_qcp_insappnentry.FINSPECTSTATUS`(通过=检验完成;取消=待检验;驳回=检验中)
+
+---
+
+## 7. 交付清单(Checklist)
+
+### 7.1 后端
+- [ ] 定义 `PROCESS_KEY = "qms_iqc_task"`
+- [ ] Controller(参照 `BpmOALeaveController`)
+- [ ] ServiceImpl(参照 `BpmOALeaveServiceImpl`)
+- [ ] 调用 `BpmProcessInstanceApi#createProcessInstance(...)` 创建流程实例
+- [ ] 回写 `process_instance_id`
+- [ ] 提交后/结束后 `flowstate` 更新逻辑(建议走 Listener)
+- [ ] 新增流程结束监听器(继承 `BpmProcessInstanceStatusEventListener`)
+
+### 7.2 前端
+- [ ] 任务列表 “开始检验/继续检验” 触发流程发起
+- [ ] create 页面(复用 `TaskForm.vue` 或新建)
+- [ ] detail 页面(复用 `TaskForm.vue` 或新建)
+- [ ] `router/modules/remaining.ts` 增加 IQC 任务 create/detail 路由
+teste
+test

+ 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 与筛选项里支持中文
+

+ 199 - 0
Order_Change_流程开发说明.md

@@ -0,0 +1,199 @@
+# 订单变更流程(Order_Change)接入工作流开发说明(参照 oa_leave)
+
+> 目标:参照请假流程(`oa_leave`)的实现方式,继续开发订单变更流程(`Order_Change`)的前后端,实现业务表与流程实例关联、流程状态落库、发起/详情/列表页面对齐,以及流程结束结果监听与业务状态回写。
+
+---
+
+## 1. 流程实例关联字段设计
+
+### 1.1 业务表字段
+业务表:`crm_seorder`
+
+字段:`process_instance_id`  
+用途:关联流程实例。
+
+### 1.2 关联关系
+`crm_seorder.process_instance_id` 对应流程引擎历史流程实例表:
+
+- 表:`ACT_HI_PROCINST`
+- 字段:`PROC_INST_ID_`
+
+即:
+
+- `crm_seorder.process_instance_id` ↔ `ACT_HI_PROCINST.PROC_INST_ID_`
+
+---
+
+## 2. 流程状态字段与回调更新策略
+
+### 2.1 状态字段
+业务表:`crm_seorder`
+
+字段:`flowstate`  
+用途:记录流程状态(例如:未发起 / 审批中 / 审批完成)。
+
+### 2.2 状态更新方式
+通过 Listener 监听流程引擎回调,并更新 `flowstate`。
+
+#### 2.2.1 流程提交后:更新为“审批中”
+```sql
+update crm_seorder
+set flowstate='审批中'
+where id='{<InstanceId>}'
+```
+
+#### 2.2.2 流程结束后:更新为“审批完成”,并重置订单明细进度为 0
+```sql
+update crm_seorder
+set flowstate='审批完成'
+where id='{<InstanceId>}';
+
+update crm_seorderentry
+set progress=0
+where seorder_id='{<InstanceId>}'
+```
+
+---
+
+## 3. 后端业务逻辑开发(参照 oa_leave)
+
+### 3.1 参考类
+请假流程后端可参考:
+
+- `BpmOALeaveController`
+- `BpmOALeaveServiceImpl`
+
+订单变更流程需实现类似结构的 Controller + Service,并按“业务表 + 流程”方式接入。
+
+### 3.2 流程模型编号(PROCESS_KEY)
+- 请假:`oa_leave`
+- 订单变更:`Order_Change`
+
+订单变更模块需定义静态变量:
+- `PROCESS_KEY = "Order_Change"`
+
+### 3.3 创建流程实例
+使用 `BpmProcessInstanceApi` 提供的创建方法:
+
+- `#createProcessInstance(...)`:用于创建流程实例
+
+订单变更发起时建议完成:
+- 创建流程实例
+- 回写 `crm_seorder.process_instance_id`
+- 同步更新 `flowstate`(建议通过 listener 统一处理,或创建成功后立即设置)
+
+---
+
+## 4. 前端业务逻辑开发(参照 oa_leave,但发起模式保持不变)
+
+### 4.1 参考页面
+请假模块前端可参考:
+
+- 发起界面:`leave/create.vue`
+- 详情界面:`leave/detail.vue`
+- 列表界面:`leave/index.vue`
+
+### 4.2 发起模式要求(订单变更特有约束)
+订单变更流程的发起模式不可修改,仍需:
+
+- 在订单行点击“变更”按钮发起流程  
+- 点击按钮后,可改为打开订单变更流程标签页  
+- 剩余操作形式(创建、查看、表单结构)可与请假流程一致
+
+---
+
+## 5. 路由配置(参照 remaining.ts 的请假流程配置)
+
+### 5.1 请假 create/detail 路由定义示例
+请假业务流程表单在 `router/modules/remaining.ts` 中定义 `create.vue` 和 `detail.vue` 路由,示例:
+
+```ts
+{
+  path: '/bpm',
+  component: Layout,
+  name: 'bpm',
+  meta: {
+    hidden: true
+  },
+  children: [
+    {
+      path: 'oa/leave/create',
+      component: () => import('@/views/bpm/oa/leave/create.vue'),
+      name: 'OALeaveCreate',
+      meta: {
+        noCache: true,
+        hidden: true,
+        canTo: true,
+        title: '发起 OA 请假',
+        activeMenu: '/bpm/oa/leave'
+      }
+    },
+    {
+      path: 'oa/leave/detail',
+      component: () => import('@/views/bpm/oa/leave/detail.vue'),
+      name: 'OALeaveDetail',
+      meta: {
+        noCache: true,
+        hidden: true,
+        canTo: true,
+        title: '查看 OA 请假',
+        activeMenu: '/bpm/oa/leave'
+      }
+    }
+  ]
+}
+```
+
+### 5.2 订单变更路由要求
+为订单变更流程新增类似的隐藏路由(create/detail),用于:
+
+- 从订单行按钮跳转打开流程发起页面(create)
+- 在流程流转/审批中查看业务详情(detail)
+
+---
+
+## 6. 流程结束回调监听与业务状态更新(必须)
+
+### 6.1 监听器机制
+审批结束时(通过 / 不通过 / 取消),后端必须监听最终结果,并更新业务表状态。
+
+请假流程实现参考:
+
+- `BpmOALeaveStatusListener`
+
+它继承框架封装的监听器抽象类:
+
+- `BpmProcessInstanceStatusEventListener`
+
+该监听器特点:
+- 监听流程实例最终结束状态
+- 流程实例结束时回调通知最终结果(通过/不通过/取消)
+
+### 6.2 订单变更监听器要求
+订单变更业务接入工作流并需要监听审批结果时,必须:
+
+- 继承 `BpmProcessInstanceStatusEventListener`
+- 实现订单变更监听器(示例命名:`BpmOrderChangeStatusListener`)
+- 在流程结束回调里更新:
+  - `crm_seorder.flowstate`
+  - `crm_seorderentry.progress`(如需求:流程结束后置 0)
+
+---
+
+## 7. 交付清单(Checklist)
+
+### 7.1 后端
+- [ ] 定义 `PROCESS_KEY = "Order_Change"`
+- [ ] Controller(参照 `BpmOALeaveController`)
+- [ ] ServiceImpl(参照 `BpmOALeaveServiceImpl`)
+- [ ] 调用 `BpmProcessInstanceApi#createProcessInstance(...)` 创建流程实例
+- [ ] 回写 `crm_seorder.process_instance_id`
+- [ ] 提交后/结束后 `flowstate` 更新逻辑(建议走 Listener)
+- [ ] 新增流程结束监听器(继承 `BpmProcessInstanceStatusEventListener`)
+
+### 7.2 前端
+- [ ] 订单行“变更”按钮发起流程(保持发起模式不变)
+- [ ] 新增 create 页面(参照 `leave/create.vue`)
+- [ ] 新增 detail 页面(参照 `leave/detail.vue`)
+- [ ](可选)新增列表页面(参照 `leave/index.vue`)
+- [ ] 在 `router/modules/remaining.ts` 增加订单变更 create/detail 路由

+ 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>

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

@@ -0,0 +1,62 @@
+<?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>
+
+        <!-- BPM 能力 -->
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-module-bpm</artifactId>
+            <version>${revision}</version>
+        </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));
+    }
+}

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

@@ -0,0 +1,59 @@
+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.framework.common.exception.enums.GlobalErrorCodeConstants;
+import cn.iocoder.yudao.module.qms.controller.admin.iqc.vo.IqcTaskDetailRespVO;
+import cn.iocoder.yudao.module.qms.controller.admin.iqc.vo.IqcTaskPageReqVO;
+import cn.iocoder.yudao.module.qms.controller.admin.iqc.vo.IqcTaskRespVO;
+import cn.iocoder.yudao.module.qms.controller.admin.iqc.vo.IqcTaskStartReqVO;
+import cn.iocoder.yudao.module.qms.service.iqc.IqcTaskService;
+import io.swagger.v3.oas.annotations.Operation;
+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.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+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-task")
+@Validated
+public class IqcTaskController {
+
+    @Resource
+    private IqcTaskService iqcTaskService;
+
+    @GetMapping("/page")
+    @Operation(summary = "获取来料检验任务分页")
+    @PreAuthorize("@ss.hasPermission('qms:iqc:task:list')")
+    public CommonResult<PageResult<IqcTaskRespVO>> getIqcTaskPage(@Valid IqcTaskPageReqVO pageReqVO) {
+        return success(iqcTaskService.getIqcTaskPage(pageReqVO));
+    }
+
+    @GetMapping("/get")
+    @Operation(summary = "获取来料检验任务详情")
+    @PreAuthorize("@ss.hasPermission('qms:iqc:task:list')")
+    public CommonResult<IqcTaskDetailRespVO> getIqcTask(@RequestParam("id") Long id) {
+        return success(iqcTaskService.getIqcTaskDetail(id));
+    }
+
+    @PostMapping("/start")
+    @Operation(summary = "发起来料检验任务流程")
+    @PreAuthorize("@ss.hasPermission('qms:iqc:task:start')")
+    public CommonResult<String> startIqcTaskProcess(@Valid @RequestBody IqcTaskStartReqVO reqVO) {
+        String processInstanceId = iqcTaskService.startIqcTaskProcess(reqVO.getId());
+        if (processInstanceId == null) {
+            return CommonResult.error(GlobalErrorCodeConstants.NOT_FOUND.getCode(), "任务不存在");
+        }
+        return success(processInstanceId);
+    }
+}

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

@@ -0,0 +1,35 @@
+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 String inspectStatus;
+
+    @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;
+}

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

@@ -0,0 +1,54 @@
+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;
+import java.time.LocalDateTime;
+
+@Schema(description = "管理后台 - IQC 来料检验任务详情 Response VO")
+@Data
+public class IqcTaskDetailRespVO {
+
+    @Schema(description = "任务编号", example = "1024")
+    private Long id;
+
+    @Schema(description = "申请单编号", example = "IQC20240115001")
+    private String applicationId;
+
+    @Schema(description = "申请时间")
+    private LocalDateTime applyTime;
+
+    @Schema(description = "申请人编号")
+    private Long applicantId;
+
+    @Schema(description = "申请人")
+    private String applicantName;
+
+    @Schema(description = "流程实例编号")
+    private String processInstanceId;
+
+    @Schema(description = "流程状态")
+    private String flowState;
+
+    @Schema(description = "检验状态")
+    private String inspectStatus;
+
+    @Schema(description = "来源单号")
+    private String sourceOrderNum;
+
+    @Schema(description = "来源单类型")
+    private String sourceOrderType;
+
+    @Schema(description = "批次")
+    private String batch;
+
+    @Schema(description = "数量")
+    private BigDecimal quantity;
+
+    @Schema(description = "单位")
+    private String unit;
+
+    @Schema(description = "备注")
+    private String remark;
+}

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

@@ -0,0 +1,18 @@
+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;
+
+@Schema(description = "管理后台 - IQC 来料检验任务分页 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class IqcTaskPageReqVO extends PageParam {
+
+    @Schema(description = "任务状态", example = "pending")
+    private String status;
+
+}

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

@@ -0,0 +1,33 @@
+package cn.iocoder.yudao.module.qms.controller.admin.iqc.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+@Schema(description = "管理后台 - IQC 来料检验任务流程信息 VO")
+@Data
+public class IqcTaskProcessInfoVO {
+
+    @Schema(description = "任务编号")
+    private Long id;
+
+    @Schema(description = "申请单号")
+    private String applicationId;
+
+    @Schema(description = "申请人编号")
+    private Long applicantId;
+
+    @Schema(description = "来源单号")
+    private String sourceOrderNum;
+
+    @Schema(description = "批次")
+    private String batch;
+
+    @Schema(description = "流程实例编号")
+    private String processInstanceId;
+
+    @Schema(description = "流程状态")
+    private String flowState;
+
+    @Schema(description = "检验状态")
+    private String inspectStatus;
+}

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

@@ -0,0 +1,51 @@
+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;
+import java.time.LocalDateTime;
+
+@Schema(description = "管理后台 - IQC 来料检验任务 Response VO")
+@Data
+public class IqcTaskRespVO {
+
+    @Schema(description = "任务编号", example = "1024")
+    private Long id;
+
+    @Schema(description = "申请单编号", example = "IQC20240115001")
+    private String applicationId;
+
+    @Schema(description = "任务状态", example = "pending")
+    private String status;
+
+    @Schema(description = "来源单号")
+    private String sourceOrderNum;
+
+    @Schema(description = "来源单类型")
+    private String sourceOrderType;
+
+    @Schema(description = "批次")
+    private String batch;
+
+    @Schema(description = "数量")
+    private BigDecimal quantity;
+
+    @Schema(description = "单位")
+    private String unit;
+
+    @Schema(description = "申请时间")
+    private LocalDateTime applyTime;
+
+    @Schema(description = "申请人编号")
+    private Long applicantId;
+
+    @Schema(description = "申请人")
+    private String applicantName;
+
+    @Schema(description = "流程实例编号")
+    private String processInstanceId;
+
+    @Schema(description = "备注")
+    private String remark;
+}

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

@@ -0,0 +1,14 @@
+package cn.iocoder.yudao.module.qms.controller.admin.iqc.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+@Schema(description = "管理后台 - IQC 来料检验任务发起流程 Request VO")
+@Data
+public class IqcTaskStartReqVO {
+
+    @Schema(description = "任务编号", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotNull(message = "任务编号不能为空")
+    private Long id;
+}

+ 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);
+}

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

@@ -0,0 +1,53 @@
+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.IqcTaskDetailRespVO;
+import cn.iocoder.yudao.module.qms.controller.admin.iqc.vo.IqcTaskProcessInfoVO;
+import cn.iocoder.yudao.module.qms.controller.admin.iqc.vo.IqcTaskPageReqVO;
+import cn.iocoder.yudao.module.qms.controller.admin.iqc.vo.IqcTaskRespVO;
+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;
+
+/**
+ * IQC 来料检验任务 Mapper
+ */
+@Mapper
+public interface IqcTaskMapper {
+
+    /**
+     * 分页查询
+     */
+    @TenantIgnore
+    Page<IqcTaskRespVO> selectTaskPage(Page<IqcTaskRespVO> page, @Param("req") IqcTaskPageReqVO req);
+
+    /**
+     * 查询流程相关信息
+     */
+    @TenantIgnore
+    IqcTaskProcessInfoVO selectTaskProcessInfo(@Param("id") Long id);
+
+    /**
+     * 查询任务详情
+     */
+    @TenantIgnore
+    IqcTaskDetailRespVO selectTaskDetail(@Param("id") Long id);
+
+    /**
+     * 更新流程相关字段
+     */
+    @TenantIgnore
+    void updateTaskProcessInfo(@Param("id") Long id,
+                               @Param("processInstanceId") String processInstanceId,
+                               @Param("flowState") String flowState,
+                               @Param("inspectStatus") String inspectStatus);
+
+    default PageResult<IqcTaskRespVO> selectTaskPage(IqcTaskPageReqVO reqVO) {
+        Page<IqcTaskRespVO> page = MyBatisUtils.buildPage(reqVO);
+        IPage<IqcTaskRespVO> result = selectTaskPage(page, reqVO);
+        return new PageResult<>(result.getRecords(), result.getTotal());
+    }
+}

+ 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;
+        }
+    }
+}

+ 44 - 0
yudao-module-qms/src/main/java/cn/iocoder/yudao/module/qms/service/iqc/IqcTaskService.java

@@ -0,0 +1,44 @@
+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.IqcTaskDetailRespVO;
+import cn.iocoder.yudao.module.qms.controller.admin.iqc.vo.IqcTaskPageReqVO;
+import cn.iocoder.yudao.module.qms.controller.admin.iqc.vo.IqcTaskRespVO;
+
+/**
+ * IQC 来料检验任务 Service 接口
+ */
+public interface IqcTaskService {
+
+    /**
+     * 获取来料检验任务分页
+     *
+     * @param pageReqVO 分页参数
+     * @return 分页结果
+     */
+    PageResult<IqcTaskRespVO> getIqcTaskPage(IqcTaskPageReqVO pageReqVO);
+
+    /**
+     * 获取来料检验任务详情
+     *
+     * @param taskId 任务编号
+     * @return 任务详情
+     */
+    IqcTaskDetailRespVO getIqcTaskDetail(Long taskId);
+
+    /**
+     * 发起来料检验任务流程
+     *
+     * @param taskId 任务编号
+     * @return 流程实例编号
+     */
+    String startIqcTaskProcess(Long taskId);
+
+    /**
+     * 更新任务流程结果
+     *
+     * @param taskId 任务编号
+     * @param processStatus 流程实例状态
+     */
+    void updateIqcTaskProcessStatus(Long taskId, Integer processStatus);
+}

+ 103 - 0
yudao-module-qms/src/main/java/cn/iocoder/yudao/module/qms/service/iqc/IqcTaskServiceImpl.java

@@ -0,0 +1,103 @@
+package cn.iocoder.yudao.module.qms.service.iqc;
+
+import cn.hutool.core.util.StrUtil;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.bpm.api.task.BpmProcessInstanceApi;
+import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO;
+import cn.iocoder.yudao.module.bpm.enums.task.BpmProcessInstanceStatusEnum;
+import cn.iocoder.yudao.module.qms.controller.admin.iqc.vo.IqcTaskDetailRespVO;
+import cn.iocoder.yudao.module.qms.controller.admin.iqc.vo.IqcTaskPageReqVO;
+import cn.iocoder.yudao.module.qms.controller.admin.iqc.vo.IqcTaskProcessInfoVO;
+import cn.iocoder.yudao.module.qms.controller.admin.iqc.vo.IqcTaskRespVO;
+import cn.iocoder.yudao.module.qms.dal.mysql.iqc.IqcTaskMapper;
+import jakarta.annotation.Resource;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
+
+/**
+ * IQC 来料检验任务 Service 实现
+ */
+@Service
+public class IqcTaskServiceImpl implements IqcTaskService {
+
+    public static final String PROCESS_KEY = "qms_iqc_task";
+
+    private static final String FLOW_STATE_RUNNING = "审批中";
+    private static final String FLOW_STATE_APPROVED = "审批完成";
+    private static final String FLOW_STATE_REJECTED = "已驳回";
+    private static final String FLOW_STATE_CANCELED = "已取消";
+
+    private static final String INSPECT_STATUS_PENDING = "待检验";
+    private static final String INSPECT_STATUS_PROCESSING = "检验中";
+    private static final String INSPECT_STATUS_COMPLETED = "检验完成";
+
+    @Resource
+    private IqcTaskMapper iqcTaskMapper;
+
+    @Resource
+    private BpmProcessInstanceApi processInstanceApi;
+
+    @Override
+    public PageResult<IqcTaskRespVO> getIqcTaskPage(IqcTaskPageReqVO pageReqVO) {
+        return iqcTaskMapper.selectTaskPage(pageReqVO);
+    }
+
+    @Override
+    public IqcTaskDetailRespVO getIqcTaskDetail(Long taskId) {
+        if (taskId == null) {
+            return null;
+        }
+        return iqcTaskMapper.selectTaskDetail(taskId);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public String startIqcTaskProcess(Long taskId) {
+        IqcTaskProcessInfoVO taskInfo = iqcTaskMapper.selectTaskProcessInfo(taskId);
+        if (taskInfo == null) {
+            return null;
+        }
+        if (StrUtil.isNotBlank(taskInfo.getProcessInstanceId())) {
+            return taskInfo.getProcessInstanceId();
+        }
+
+        Map<String, Object> variables = new HashMap<>();
+        variables.put("taskId", taskInfo.getId());
+        variables.put("applicationId", taskInfo.getApplicationId());
+        variables.put("applicantId", taskInfo.getApplicantId());
+        variables.put("sourceOrderNum", taskInfo.getSourceOrderNum());
+        variables.put("batch", taskInfo.getBatch());
+
+        Long userId = getLoginUserId();
+        String processInstanceId = processInstanceApi.createProcessInstance(userId,
+                new BpmProcessInstanceCreateReqDTO().setProcessDefinitionKey(PROCESS_KEY)
+                        .setBusinessKey(String.valueOf(taskInfo.getId()))
+                        .setVariables(variables));
+
+        iqcTaskMapper.updateTaskProcessInfo(taskId, processInstanceId, FLOW_STATE_RUNNING, INSPECT_STATUS_PROCESSING);
+        return processInstanceId;
+    }
+
+    @Override
+    public void updateIqcTaskProcessStatus(Long taskId, Integer processStatus) {
+        if (taskId == null || processStatus == null) {
+            return;
+        }
+        if (BpmProcessInstanceStatusEnum.APPROVE.getStatus().equals(processStatus)) {
+            iqcTaskMapper.updateTaskProcessInfo(taskId, null, FLOW_STATE_APPROVED, INSPECT_STATUS_COMPLETED);
+            return;
+        }
+        if (BpmProcessInstanceStatusEnum.CANCEL.getStatus().equals(processStatus)) {
+            iqcTaskMapper.updateTaskProcessInfo(taskId, null, FLOW_STATE_CANCELED, INSPECT_STATUS_PENDING);
+            return;
+        }
+        if (BpmProcessInstanceStatusEnum.REJECT.getStatus().equals(processStatus)) {
+            iqcTaskMapper.updateTaskProcessInfo(taskId, null, FLOW_STATE_REJECTED, INSPECT_STATUS_PROCESSING);
+        }
+    }
+}

+ 36 - 0
yudao-module-qms/src/main/java/cn/iocoder/yudao/module/qms/service/iqc/listener/IqcTaskStatusListener.java

@@ -0,0 +1,36 @@
+package cn.iocoder.yudao.module.qms.service.iqc.listener;
+
+import cn.iocoder.yudao.module.bpm.api.event.BpmProcessInstanceStatusEvent;
+import cn.iocoder.yudao.module.bpm.api.event.BpmProcessInstanceStatusEventListener;
+import cn.iocoder.yudao.module.qms.service.iqc.IqcTaskService;
+import cn.iocoder.yudao.module.qms.service.iqc.IqcTaskServiceImpl;
+import jakarta.annotation.Resource;
+import org.springframework.stereotype.Component;
+
+/**
+ * IQC 来料检验任务流程结果监听器
+ */
+@Component
+public class IqcTaskStatusListener extends BpmProcessInstanceStatusEventListener {
+
+    @Resource
+    private IqcTaskService iqcTaskService;
+
+    @Override
+    protected String getProcessDefinitionKey() {
+        return IqcTaskServiceImpl.PROCESS_KEY;
+    }
+
+    @Override
+    protected void onEvent(BpmProcessInstanceStatusEvent event) {
+        if (event.getBusinessKey() == null) {
+            return;
+        }
+        try {
+            Long taskId = Long.parseLong(event.getBusinessKey());
+            iqcTaskService.updateIqcTaskProcessStatus(taskId, event.getStatus());
+        } catch (NumberFormatException ignore) {
+            // 业务主键不是任务编号时忽略
+        }
+    }
+}

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

@@ -0,0 +1,138 @@
+<?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,
+            COALESCE(FINSPECTSTATUS, '待检验') AS inspectStatus,
+            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>

+ 87 - 0
yudao-module-qms/src/main/resources/mapper/iqc/IqcTaskMapper.xml

@@ -0,0 +1,87 @@
+<?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.IqcTaskMapper">
+
+    <select id="selectTaskPage" resultType="cn.iocoder.yudao.module.qms.controller.admin.iqc.vo.IqcTaskRespVO">
+        SELECT
+            b.id AS id,
+            a.FBILLNO AS applicationId,
+            a.FAPPLYTIME AS applyTime,
+            a.FAPPLYUSER AS applicantId,
+            u.username AS applicantName,
+            a.FCOMMENT AS remark,
+            b.process_instance_id AS processInstanceId,
+            b.FSRCORDERNUM AS sourceOrderNum,
+            b.FSRCORDERTYPE AS sourceOrderType,
+            b.FLOTNUMBER AS batch,
+            b.FAPPLYQTY AS quantity,
+            b.FUNIT AS unit,
+            CASE
+                WHEN COALESCE(b.FINSPECTSTATUS, a.FBILLSTATUS) IN ('processing', '检验中') THEN 'processing'
+                WHEN COALESCE(b.FINSPECTSTATUS, a.FBILLSTATUS) IN ('completed', '检验完成') THEN 'completed'
+                ELSE 'pending'
+            END AS status
+        FROM qms_qcp_insappnentry b
+        LEFT JOIN qms_qcp_inspecapplyn a ON a.id = b.glid
+        LEFT JOIN system_users u ON u.id = a.FAPPLYUSER
+        <where>
+            <if test="req.status != null and req.status != ''">
+                AND
+                CASE
+                    WHEN COALESCE(b.FINSPECTSTATUS, a.FBILLSTATUS) IN ('processing', '检验中') THEN 'processing'
+                    WHEN COALESCE(b.FINSPECTSTATUS, a.FBILLSTATUS) IN ('completed', '检验完成') THEN 'completed'
+                    ELSE 'pending'
+                END = #{req.status}
+            </if>
+        </where>
+        ORDER BY a.FAPPLYTIME DESC, b.id DESC
+    </select>
+
+    <select id="selectTaskProcessInfo" resultType="cn.iocoder.yudao.module.qms.controller.admin.iqc.vo.IqcTaskProcessInfoVO">
+        SELECT
+            b.id AS id,
+            a.FBILLNO AS applicationId,
+            a.FAPPLYUSER AS applicantId,
+            b.FSRCORDERNUM AS sourceOrderNum,
+            b.FLOTNUMBER AS batch,
+            b.process_instance_id AS processInstanceId,
+            b.flowstate AS flowState,
+            b.FINSPECTSTATUS AS inspectStatus
+        FROM qms_qcp_insappnentry b
+        LEFT JOIN qms_qcp_inspecapplyn a ON a.id = b.glid
+        WHERE b.id = #{id}
+    </select>
+
+    <select id="selectTaskDetail" resultType="cn.iocoder.yudao.module.qms.controller.admin.iqc.vo.IqcTaskDetailRespVO">
+        SELECT
+            b.id AS id,
+            a.FBILLNO AS applicationId,
+            a.FAPPLYTIME AS applyTime,
+            a.FAPPLYUSER AS applicantId,
+            u.username AS applicantName,
+            a.FCOMMENT AS remark,
+            b.process_instance_id AS processInstanceId,
+            b.flowstate AS flowState,
+            b.FINSPECTSTATUS AS inspectStatus,
+            b.FSRCORDERNUM AS sourceOrderNum,
+            b.FSRCORDERTYPE AS sourceOrderType,
+            b.FLOTNUMBER AS batch,
+            b.FAPPLYQTY AS quantity,
+            b.FUNIT AS unit
+        FROM qms_qcp_insappnentry b
+        LEFT JOIN qms_qcp_inspecapplyn a ON a.id = b.glid
+        LEFT JOIN system_users u ON u.id = a.FAPPLYUSER
+        WHERE b.id = #{id}
+    </select>
+
+    <update id="updateTaskProcessInfo">
+        UPDATE qms_qcp_insappnentry
+        <set>
+            <if test="processInstanceId != null">process_instance_id = #{processInstanceId},</if>
+            <if test="flowState != null">flowstate = #{flowState},</if>
+            <if test="inspectStatus != null">FINSPECTSTATUS = #{inspectStatus},</if>
+        </set>
+        WHERE id = #{id}
+    </update>
+
+</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>
 
 
         <!-- 会员中心。默认注释,保证编译速度 -->

+ 1 - 1
yudao-server/src/main/java/cn/iocoder/yudao/server/YudaoServerApplication.java

@@ -1,4 +1,4 @@
-package cn.iocoder.yudao.server;
+  package cn.iocoder.yudao.server;
 
 import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;

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

@@ -0,0 +1,73 @@
+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
+  inspectStatus?: 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
+  inspectStatus?: 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 } })
+}

+ 50 - 0
yudao-ui/yudao-ui-admin-vue3/src/api/qms/iqc/task/index.ts

@@ -0,0 +1,50 @@
+import request from '@/config/axios'
+
+export interface IqcTaskPageReqVO extends PageParam {
+  status?: string
+}
+
+export interface IqcTaskRespVO {
+  id: number
+  applicationId: string
+  status: string
+  sourceOrderNum?: string
+  sourceOrderType?: string
+  batch?: string
+  quantity?: number
+  unit?: string
+  applyTime?: string | number
+  applicantId?: number | string
+  applicantName?: string
+  processInstanceId?: string
+  remark?: string
+}
+
+export interface IqcTaskDetailRespVO {
+  id: number | string
+  applicationId?: string
+  applyTime?: string | number
+  applicantId?: number | string
+  applicantName?: string
+  processInstanceId?: string
+  flowState?: string
+  inspectStatus?: string
+  sourceOrderNum?: string
+  sourceOrderType?: string
+  batch?: string
+  quantity?: number
+  unit?: string
+  remark?: string
+}
+
+export const getIqcTaskPage = (params: IqcTaskPageReqVO) => {
+  return request.get<PageResult<IqcTaskRespVO[]>>({ url: '/qms/iqc-task/page', params })
+}
+
+export const getIqcTaskDetail = (id: number | string) => {
+  return request.get<IqcTaskDetailRespVO>({ url: `/qms/iqc-task/get?id=${id}` })
+}
+
+export const startIqcTaskProcess = (id: number | string) => {
+  return request.post<string>({ url: '/qms/iqc-task/start', data: { id } })
+}

+ 0 - 112
yudao-ui/yudao-ui-admin-vue3/src/config/modules.ts

@@ -1,112 +0,0 @@
-/**
- * 模块配置
- */
-
-export const DEFAULT_MODULE = 'iqc'
-
-// 模块配置
-const moduleConfigs: Record<string, any> = {
-  iqc: {
-    name: 'IQC检验',
-    applicationList: {
-      title: '检验申请列表',
-      columns: [
-        { type: 'selection', width: 55 },
-        { key: 'applicationNo', label: '申请单号', type: 'applicationLink', width: 180 },
-        { key: 'status', label: '状态', type: 'status', width: 100 },
-        { key: 'applicantName', label: '申请人', width: 120 },
-        { key: 'createTime', label: '申请时间', type: 'datetime', width: 160 },
-        { key: 'materialCode', label: '物料编码', width: 150 },
-        { key: 'materialName', label: '物料名称', minWidth: 200 },
-        { key: 'quantity', label: '数量', type: 'number', width: 100 },
-        { key: 'remark', label: '备注', type: 'tooltip', minWidth: 150 }
-      ],
-      keywordFields: ['申请单号', '物料编码', '物料名称'],
-      actionButtons: {
-        create: '新建申请',
-        import: true,
-        export: true,
-        guide: true
-      }
-    },
-    taskList: {
-      title: '检验任务列表',
-      columns: [
-        { type: 'selection', width: 55 },
-        { key: 'taskNo', label: '任务单号', width: 180 },
-        { key: 'status', label: '状态', type: 'status', width: 100 },
-        { key: 'inspectorName', label: '检验员', width: 120 },
-        { key: 'createTime', label: '创建时间', type: 'datetime', width: 160 },
-        { key: 'materialCode', label: '物料编码', width: 150 },
-        { key: 'materialName', label: '物料名称', minWidth: 200 }
-      ],
-      keywordFields: ['任务单号', '物料编码']
-    },
-    favorites: {
-      title: '我的收藏',
-      emptyText: '暂无收藏内容'
-    },
-    help: {
-      title: '帮助中心',
-      contacts: [
-        { name: '技术支持', phone: '400-xxx-xxxx' }
-      ],
-      faqs: [
-        { question: '如何创建检验申请?', answer: '点击"新建申请"按钮,填写相关信息后提交即可。' },
-        { question: '如何查看检验结果?', answer: '在任务列表中找到对应任务,点击查看详情。' }
-      ]
-    },
-    applicationForm: {
-      title: '检验申请',
-      baseFields: [
-        { key: 'applicantName', label: '申请人', type: 'input', span: 8, required: true },
-        { key: 'applyTime', label: '申请时间', type: 'datetime', span: 8 },
-        { key: 'materialCode', label: '物料编码', type: 'input', span: 8, required: true },
-        { key: 'materialName', label: '物料名称', type: 'input', span: 8, required: true },
-        { key: 'quantity', label: '数量', type: 'number', span: 8, required: true },
-        { key: 'remark', label: '备注', type: 'textarea', span: 24 }
-      ],
-      detail: {
-        title: '检验明细',
-        columns: [
-          { key: 'itemNo', label: '序号', width: 80 },
-          { key: 'checkItem', label: '检验项目', width: 150 },
-          { key: 'standard', label: '标准值', width: 120 },
-          { key: 'result', label: '检验结果', width: 120 },
-          { key: 'conclusion', label: '结论', width: 100 }
-        ],
-        defaultRow: { itemNo: '', checkItem: '', standard: '', result: '', conclusion: '' }
-      }
-    },
-    taskForm: {
-      title: '检验任务',
-      baseFields: [
-        { key: 'taskNo', label: '任务单号', type: 'input', span: 8 },
-        { key: 'inspectorName', label: '检验员', type: 'input', span: 8 },
-        { key: 'createTime', label: '创建时间', type: 'datetime', span: 8 }
-      ],
-      detail: {
-        title: '任务明细',
-        columns: [],
-        defaultRow: {}
-      }
-    }
-  }
-}
-
-/**
- * 获取模块配置
- */
-export const getModuleConfig = (moduleCode: string) => {
-  return moduleConfigs[moduleCode] || moduleConfigs[DEFAULT_MODULE]
-}
-
-/**
- * 获取所有模块
- */
-export const getAllModules = () => {
-  return Object.keys(moduleConfigs).map(code => ({
-    code,
-    name: moduleConfigs[code].name
-  }))
-}

+ 14 - 117
yudao-ui/yudao-ui-admin-vue3/src/config/qmsModules.ts

@@ -22,20 +22,18 @@ 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' },
         { key: 'materialCount', label: '物料数', width: 100, type: 'number' },
         { 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: {
@@ -54,6 +52,7 @@ export const QMS_MODULES: Record<string, any> = {
           { key: 'materialName', label: '名称', type: 'input', width: 160, required: true },
           { key: 'specification', label: '规格', type: 'input', width: 160 },
           { key: 'batch', label: '批次', type: 'input', width: 140 },
+          { key: 'inspectStatus', label: '状态', type: 'select', width: 120, options: ['待检验', '检验中', '检验完成'] },
           { key: 'quantity', label: '数量', type: 'number', width: 120, min: 0 },
           { key: 'unit', label: '单位', type: 'input', width: 100 },
           { key: 'remark', label: '备注', type: 'input', minWidth: 200 }
@@ -63,6 +62,7 @@ export const QMS_MODULES: Record<string, any> = {
           materialName: '',
           specification: '',
           batch: '',
+          inspectStatus: '待检验',
           quantity: 1,
           unit: 'PCS',
           remark: ''
@@ -78,65 +78,21 @@ export const QMS_MODULES: Record<string, any> = {
         { key: 'processing', label: '检验中' },
         { key: 'completed', label: '已完成' }
       ],
-      keywordFields: ['applicationId', 'materialName'],
+      keywordFields: ['applicationId', 'sourceOrderNum', 'batch'],
       columns: [
         { type: 'selection', width: 55 },
         { key: 'status', label: '状态', width: 90, type: 'status' },
-        { key: 'id', label: '任务编号', width: 130 },
-        { key: 'applicationId', label: '单据编号', width: 140, type: 'taskLink' },
-        { key: 'materialInfo', label: '物料信息', minWidth: 220, type: 'textLines', lines: ['materialCode', 'materialName', 'specification'] },
-        { key: 'supplierInfo', label: '供应商', width: 180, type: 'textLines', lines: ['supplierName', 'supplierCode'] },
+        { key: 'id', label: '任务编号', width: 140 },
+        { key: 'applicationId', label: '单据编号', width: 160, type: 'taskLink' },
+        { key: 'sourceOrderNum', label: '来源单号', width: 160 },
         { key: 'batch', label: '批次', width: 130, type: 'batch' },
-        { key: 'location', label: '仓位', width: 110 },
-        { key: 'responsibleName', label: '负责人', width: 100 },
-        { key: 'priority', label: '优先级', width: 100, type: 'priority' },
-        { key: 'startTime', label: '开始时间', width: 170, type: 'datetime' },
-        { key: 'completeTime', label: '完成时间', width: 170, type: 'datetime' },
+        { key: 'quantity', label: '数量', width: 100 },
+        { key: 'unit', label: '单位', width: 80 },
+        { key: 'applyTime', label: '申请时间', width: 170, type: 'datetime' },
         { key: 'remark', label: '备注', minWidth: 200, type: 'tooltip' }
       ],
-      advancedFilters: [
-        { key: 'statusList', label: '状态', type: 'select', multiple: true, optionsKey: 'taskStatuses' },
-        { key: 'supplierIds', label: '供应商', type: 'select', multiple: true, optionsKey: 'suppliers' },
-        { key: 'materialCodes', label: '物料编码', type: 'select', multiple: true, optionsKey: 'materials' },
-        { key: 'batchList', label: '批次', type: 'select', multiple: true, optionsKey: 'batches' },
-        { key: 'responsibleIds', label: '负责人', type: 'select', multiple: true, optionsKey: 'responsibles' },
-        { key: 'priorities', label: '优先级', type: 'select', multiple: true, options: [
-          { label: '高', value: 'high' },
-          { label: '中', value: 'medium' },
-          { label: '低', value: 'low' }
-        ] },
-        { key: 'timeRange', label: '时间范围', type: 'datetimerange' }
-      ]
+      advancedFilters: []
     },
-    taskForm: {
-      title: '来料检验单',
-      infoItems: [
-        { key: 'supplierName', label: '供应商' },
-        { key: 'materialName', label: '物料' },
-        { key: 'batch', label: '批次' },
-        { key: 'arrivalTime', label: '到货时间', type: 'datetime' },
-        { key: 'inspectionSpecVersion', label: '检规版本' }
-      ],
-      projectColumns: [
-        { key: 'itemName', label: '项目', width: 200 },
-        { key: 'standard', label: '标准', width: 180 },
-        { key: 'upperLimit', label: '上限', width: 120 },
-        { key: 'lowerLimit', label: '下限', width: 120 },
-        { key: 'unit', label: '单位', width: 100 },
-        { key: 'result', label: '结果', width: 140, editable: true },
-        { key: 'decision', label: '判定', width: 120, editable: true, type: 'select', options: ['合格', '不合格'] },
-        { key: 'remark', label: '备注', minWidth: 200, editable: true }
-      ],
-      statisticsFields: [
-        { key: 'sampleSize', label: '样本数' },
-        { key: 'passCount', label: '合格数' },
-        { key: 'failCount', label: '不合格数' },
-        { key: 'result', label: '判定结果' }
-      ],
-      exceptionFields: {
-        typeOptions: ['尺寸偏差', '外观缺陷', '功能异常']
-      }
-    }
   },
   ipqc: {
     code: 'ipqc',
@@ -242,34 +198,6 @@ export const QMS_MODULES: Record<string, any> = {
         { key: 'timeRange', label: '时间范围', type: 'datetimerange' }
       ]
     },
-    taskForm: {
-      title: '过程检验单',
-      infoItems: [
-        { key: 'workOrder', label: '生产订单' },
-        { key: 'process', label: '工序/工步', formatter: (row: any) => `${row.process || ''}/${row.step || ''}` },
-        { key: 'line', label: '线体/工位/设备', formatter: (row: any) => `${row.line || ''} / ${row.station || ''} / ${row.equipment || ''}` },
-        { key: 'shift', label: '班次' },
-        { key: 'inspectionSpecVersion', label: '检规版本' }
-      ],
-      projectColumns: [
-        { key: 'category', label: '类别', width: 140 },
-        { key: 'itemName', label: '项目', width: 200 },
-        { key: 'standard', label: '标准', width: 200 },
-        { key: 'upperLimit', label: '上限', width: 120 },
-        { key: 'lowerLimit', label: '下限', width: 120 },
-        { key: 'unit', label: '单位', width: 100 },
-        { key: 'result', label: '结果', width: 140, editable: true },
-        { key: 'decision', label: '判定', width: 120, editable: true, type: 'select', options: ['合格', '不合格'] },
-        { key: 'remark', label: '备注', minWidth: 200, editable: true }
-      ],
-      statisticsFields: [
-        { key: 'sampleSize', label: '样本数' },
-        { key: 'passCount', label: '合格数' },
-        { key: 'failCount', label: '不合格数' },
-        { key: 'result', label: '判定结果' }
-      ],
-      exceptionFields: { typeOptions: ['制程异常', '设备异常', '人员操作'] }
-    }
   },
   fqc: {
     code: 'fqc',
@@ -374,37 +302,6 @@ export const QMS_MODULES: Record<string, any> = {
         { key: 'timeRange', label: '时间范围', type: 'datetimerange' }
       ]
     },
-    taskForm: {
-      title: '成品检验单',
-      infoItems: [
-        { key: 'productName', label: '成品名称' },
-        { key: 'productModel', label: '型号' },
-        { key: 'customer', label: '客户' },
-        { key: 'salesOrder', label: '销售订单/出货单' },
-        { key: 'packageBatch', label: '包装批次/箱号' },
-        { key: 'inspectionSpecVersion', label: '检规版本' }
-      ],
-      projectColumns: [
-        { key: 'category', label: '类别', width: 140 },
-        { key: 'itemName', label: '项目', width: 200 },
-        { key: 'standard', label: '标准', width: 200 },
-        { key: 'upperLimit', label: '上限', width: 120 },
-        { key: 'lowerLimit', label: '下限', width: 120 },
-        { key: 'unit', label: '单位', width: 100 },
-        { key: 'result', label: '结果', width: 140, editable: true },
-        { key: 'decision', label: '判定', width: 120, editable: true, type: 'select', options: ['合格', '不合格', '特采'] },
-        { key: 'remark', label: '备注', minWidth: 200, editable: true }
-      ],
-      statisticsFields: [
-        { key: 'sampleSize', label: '样本数' },
-        { key: 'passCount', label: '合格数' },
-        { key: 'failCount', label: '不合格数' },
-        { key: 'specialAcceptCount', label: '特采数' },
-        { key: 'result', label: '判定结果' }
-      ],
-      exceptionFields: { typeOptions: ['外观异常', '功能异常', '包装损坏', '标签错误'] },
-      releaseActions: ['放行', '返工', '隔离', '特采']
-    }
   }
 }
 

+ 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

+ 13 - 3
yudao-ui/yudao-ui-admin-vue3/src/router/modules/remaining.ts

@@ -730,7 +730,6 @@ const remainingRouter: AppRouteRecordRaw[] = [
       { path: 'iqc/apply/edit/:id', name: 'IqcApplicationEdit', component: () => import('@/views/qms/ApplicationForm.vue'), meta: { module: 'iqc', title: '编辑申请', noCache: true, hidden: true, canTo: true, activeMenu: '/qms', mode: 'edit' } },
       { path: 'iqc/apply/view/:id', name: 'IqcApplicationView', component: () => import('@/views/qms/ApplicationForm.vue'), meta: { module: 'iqc', title: '查看申请', noCache: true, hidden: true, canTo: true, activeMenu: '/qms', mode: 'view' } },
       { 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: 'iqc/task/:id', name: 'IqcTaskDetail', component: () => import('@/views/qms/TaskForm.vue'), meta: { module: 'iqc', title: '检验任务', noCache: true, hidden: true, canTo: true, activeMenu: '/qms' } },
       { path: 'iqc/favorites', name: 'IqcFavorites', component: () => import('@/views/qms/Favorites.vue'), meta: { module: 'iqc', title: '我的关注', noCache: true, hidden: true, canTo: true, activeMenu: '/qms' } },
       { path: 'iqc/help', name: 'IqcHelp', component: () => import('@/views/qms/Help.vue'), meta: { module: 'iqc', title: '帮助', noCache: true, hidden: true, canTo: true, activeMenu: '/qms' } },
       // IPQC 过程检验
@@ -739,7 +738,6 @@ const remainingRouter: AppRouteRecordRaw[] = [
       { path: 'ipqc/apply/edit/:id', name: 'IpqcApplicationEdit', component: () => import('@/views/qms/ApplicationForm.vue'), meta: { module: 'ipqc', title: '编辑申请', noCache: true, hidden: true, canTo: true, activeMenu: '/qms', mode: 'edit' } },
       { path: 'ipqc/apply/view/:id', name: 'IpqcApplicationView', component: () => import('@/views/qms/ApplicationForm.vue'), meta: { module: 'ipqc', title: '查看申请', noCache: true, hidden: true, canTo: true, activeMenu: '/qms', mode: 'view' } },
       { 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: 'ipqc/task/:id', name: 'IpqcTaskDetail', component: () => import('@/views/qms/TaskForm.vue'), meta: { module: 'ipqc', title: '检验任务', noCache: true, hidden: true, canTo: true, activeMenu: '/qms' } },
       { path: 'ipqc/favorites', name: 'IpqcFavorites', component: () => import('@/views/qms/Favorites.vue'), meta: { module: 'ipqc', title: '我的关注', noCache: true, hidden: true, canTo: true, activeMenu: '/qms' } },
       { path: 'ipqc/help', name: 'IpqcHelp', component: () => import('@/views/qms/Help.vue'), meta: { module: 'ipqc', title: '帮助', noCache: true, hidden: true, canTo: true, activeMenu: '/qms' } },
       // FQC 成品检验
@@ -748,7 +746,6 @@ const remainingRouter: AppRouteRecordRaw[] = [
       { path: 'fqc/apply/edit/:id', name: 'FqcApplicationEdit', component: () => import('@/views/qms/ApplicationForm.vue'), meta: { module: 'fqc', title: '编辑申请', noCache: true, hidden: true, canTo: true, activeMenu: '/qms', mode: 'edit' } },
       { path: 'fqc/apply/view/:id', name: 'FqcApplicationView', component: () => import('@/views/qms/ApplicationForm.vue'), meta: { module: 'fqc', title: '查看申请', noCache: true, hidden: true, canTo: true, activeMenu: '/qms', mode: 'view' } },
       { path: 'fqc/task/list', name: 'FqcTaskList', component: () => import('@/views/qms/TaskList.vue'), meta: { module: 'fqc', title: '成品检验任务', noCache: true, hidden: true, canTo: true, activeMenu: '/qms' } },
-      { path: 'fqc/task/:id', name: 'FqcTaskDetail', component: () => import('@/views/qms/TaskForm.vue'), meta: { module: 'fqc', title: '检验任务', noCache: true, hidden: true, canTo: true, activeMenu: '/qms' } },
       { path: 'fqc/favorites', name: 'FqcFavorites', component: () => import('@/views/qms/Favorites.vue'), meta: { module: 'fqc', title: '我的关注', noCache: true, hidden: true, canTo: true, activeMenu: '/qms' } },
       { path: 'fqc/help', name: 'FqcHelp', component: () => import('@/views/qms/Help.vue'), meta: { module: 'fqc', title: '帮助', noCache: true, hidden: true, canTo: true, activeMenu: '/qms' } },
       // SPEC 检规管理
@@ -759,6 +756,19 @@ 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: 'iqc/iqc/task/list', name: 'S5IqcTaskList', component: () => import('@/views/qms/TaskList.vue'), meta: { module: 'iqc', title: '来料检验任务', noCache: true, hidden: true, canTo: true, activeMenu: '/s5' } },
+      { path: 'iqc/qms/iqc/task/list', name: 'S5IqcTaskListCompat', component: () => import('@/views/qms/TaskList.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] || []

+ 23 - 0
yudao-ui/yudao-ui-admin-vue3/src/store/modules/qms/task.ts

@@ -1,5 +1,6 @@
 import { defineStore } from 'pinia'
 import { QMS_MODULE_CODES } from '@/config/qmsModules'
+import { getIqcTaskPage, startIqcTaskProcess } from '@/api/qms/iqc/task'
 
 const createDefaultFilters = () => ({
   keyword: '', searchMode: 'AND', statusList: [], supplierIds: [], materialCodes: [], batchList: [],
@@ -26,6 +27,9 @@ const createTaskSamples = () => ({
   ]
 })
 
+const REMOTE_MODULES = new Set(['iqc'])
+const isRemoteModule = (module: string) => REMOTE_MODULES.has(module)
+
 export const useQmsTaskStore = defineStore('qmsTask', {
   state: () => ({
     tasks: createTaskSamples() as Record<string, any[]>,
@@ -36,6 +40,7 @@ export const useQmsTaskStore = defineStore('qmsTask', {
     pagination: QMS_MODULE_CODES.reduce((acc, module) => { acc[module] = createDefaultPagination(); return acc }, {} as Record<string, any>)
   }),
   getters: {
+    isRemoteModule: () => (module: string) => isRemoteModule(module),
     activeTab: (state) => (module: string) => state.activeTabs[module] || 'pending',
     filteredTasks: (state) => (module: string) => {
       const filters = state.filters[module] || createDefaultFilters()
@@ -64,12 +69,30 @@ export const useQmsTaskStore = defineStore('qmsTask', {
   },
   actions: {
     async fetchTasks(module: string) {
+      if (isRemoteModule(module)) {
+        const pagination = this.pagination[module] || createDefaultPagination()
+        const status = this.activeTabs[module]
+        const params = {
+          pageNo: pagination.currentPage,
+          pageSize: pagination.pageSize,
+          status: status && status !== 'all' ? status : undefined
+        }
+        const pageResult = await getIqcTaskPage(params)
+        const pageData: any = (pageResult as any)?.data ?? pageResult
+        this.tasks[module] = pageData?.list || []
+        this.pagination[module].total = pageData?.total || 0
+        return pageData?.list
+      }
       return new Promise((resolve) => {
         setTimeout(() => { const list = this.filteredTasks(module); this.pagination[module].total = list.length; resolve(list) }, 300)
       })
     },
     setActiveTab(module: string, tab: string) { this.activeTabs[module] = tab },
     async startInspection(module: string, taskId: string) {
+      if (isRemoteModule(module)) {
+        const result: any = await startIqcTaskProcess(taskId)
+        return result?.data ?? result
+      }
       return new Promise((resolve, reject) => {
         setTimeout(() => {
           const tasks = this.tasks[module] || []

+ 7 - 1
yudao-ui/yudao-ui-admin-vue3/src/views/bpm/processInstance/detail/ProcessInstanceOperationButton.vue

@@ -25,7 +25,7 @@
           :rules="approveReasonRule"
           label-width="100px"
         >
-          <el-card v-if="runningTask?.formId > 0" class="mb-15px !-mt-10px">
+          <el-card v-if="runningTask?.formId > 0 && !shouldSkipNormalForm" class="mb-15px !-mt-10px">
             <template #header>
               <span class="el-icon-picture-outline"> 填写表单【{{ runningTask?.formName }}】 </span>
             </template>
@@ -1071,6 +1071,9 @@ const getButtonDisplayName = (btnType: OperationButtonType) => {
   return displayName
 }
 
+const PROCESS_SKIP_NORMAL_FORM_KEYS = new Set(['qms_iqc_task'])
+const shouldSkipNormalForm = computed(() => PROCESS_SKIP_NORMAL_FORM_KEYS.has(props.processDefinition?.key))
+
 const loadTodoTask = (task: any) => {
   approveForm.value = {}
   runningTask.value = task
@@ -1089,6 +1092,9 @@ const loadTodoTask = (task: any) => {
 
 /** 校验流程表单 */
 const validateNormalForm = async () => {
+  if (shouldSkipNormalForm.value) {
+    return true
+  }
   if (props.processDefinition?.formType === BpmModelFormType.NORMAL) {
     let valid = true
     try {

+ 118 - 23
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%' }
 }
@@ -135,31 +137,124 @@ const getDetailComponent = (column: any) => {
 }
 
 const getDetailProps = (column: any) => {
-  if (column.type === 'number') return { min: column.min ?? 0, controlsPosition: 'right', style: 'width: 100%' }
-  if (column.type === 'select') return { placeholder: '请选择', style: 'width: 100%' }
-  return { placeholder: '请输入', style: 'width: 100%' }
+  const baseProps = { disabled: !!column.disabled, style: 'width: 100%' }
+  if (column.type === 'number') {
+    return { ...baseProps, min: column.min ?? 0, controlsPosition: 'right' }
+  }
+  if (column.type === 'select') {
+    return { ...baseProps, placeholder: '请选择' }
+  }
+  return { ...baseProps, placeholder: '请输入' }
 }
 
 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 || '',
+    inspectStatus: detail.inspectStatus || '待检验',
+    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 || '',
+      inspectStatus: detail.inspectStatus || '待检验',
+      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>

+ 0 - 164
yudao-ui/yudao-ui-admin-vue3/src/views/qms/TaskForm.vue

@@ -1,164 +0,0 @@
-<template>
-  <div class="page-container" v-loading="loading">
-    <div class="page-header">
-      <h1>{{ formConfig.title }} - {{ taskData?.id || '--' }}</h1>
-      <IqcStatusBadge :status="taskData?.status || 'pending'" />
-    </div>
-
-    <div class="info-card">
-      <el-row :gutter="20">
-        <el-col :span="6">
-          <div class="info-item"><span class="info-label">负责人:</span><span class="info-value">{{ taskData?.responsibleName || '-' }}</span></div>
-        </el-col>
-        <el-col :span="6">
-          <div class="info-item"><span class="info-label">优先级:</span><el-tag size="small" :color="getPriorityColor(taskData?.priority)">{{ getPriorityName(taskData?.priority) }}</el-tag></div>
-        </el-col>
-        <el-col :span="6">
-          <div class="info-item"><span class="info-label">最近更新:</span><span class="info-value">{{ formatDateTime(taskData?.startTime) }}</span></div>
-        </el-col>
-        <el-col :span="6">
-          <div class="info-item"><span class="info-label">检规版本:</span><span class="info-value">{{ taskData?.inspectionSpecVersion || '-' }}</span></div>
-        </el-col>
-      </el-row>
-    </div>
-
-    <div class="steps-container">
-      <el-steps :active="currentStep" finish-status="success">
-        <el-step title="准备" /><el-step title="检验" /><el-step title="完成" />
-      </el-steps>
-    </div>
-
-    <div class="form-container">
-      <div class="form-section">
-        <div class="section-title">基础信息</div>
-        <el-row :gutter="20">
-          <el-col v-for="item in basicInfoItems" :key="item.label" :span="8">
-            <div class="info-item"><span class="info-label">{{ item.label }}:</span><span class="info-value">{{ item.value }}</span></div>
-          </el-col>
-        </el-row>
-      </div>
-
-      <div class="form-section">
-        <div class="section-title">检验项目<div class="section-actions" v-if="isEditable"><el-button size="small" type="primary" @click="handleAddProject"><Icon icon="ep:plus" class="mr-5px" />新增项目</el-button></div></div>
-        <el-table :data="detail.projects" border>
-          <el-table-column v-for="column in formConfig.projectColumns" :key="column.key" :prop="column.key" :label="column.label" :width="column.width" :min-width="column.minWidth">
-            <template #default="{ row }">
-              <template v-if="isEditable && column.editable !== false">
-                <el-input v-if="!column.type || column.type === 'input'" v-model="row[column.key]" placeholder="请输入" />
-                <el-select v-else-if="column.type === 'select'" v-model="row[column.key]" placeholder="请选择">
-                  <el-option v-for="option in column.options || []" :key="option" :label="option" :value="option" />
-                </el-select>
-                <el-input v-else v-model="row[column.key]" placeholder="请输入" />
-              </template>
-              <template v-else>{{ row[column.key] ?? '-' }}</template>
-            </template>
-          </el-table-column>
-          <el-table-column v-if="isEditable" label="操作" width="90">
-            <template #default="{ $index }"><el-button type="danger" link size="small" @click="handleDeleteProject($index)">删除</el-button></template>
-          </el-table-column>
-        </el-table>
-      </div>
-
-      <div class="form-actions">
-        <el-button @click="handleBack">返回列表</el-button>
-        <el-button v-if="isEditable" @click="handleSave" :loading="saving">保存</el-button>
-        <el-button v-if="isEditable" type="primary" @click="handleSubmit" :loading="submitting">提交审核</el-button>
-        <el-button @click="handlePrint">打印</el-button>
-      </div>
-    </div>
-  </div>
-</template>
-
-<script setup lang="ts">
-defineOptions({ name: 'IqcTaskForm' })
-
-import { reactive, ref, computed, onMounted } from 'vue'
-import { useRoute, useRouter } from 'vue-router'
-import IqcStatusBadge from '@/components/Qms/StatusBadge.vue'
-import { useQmsTaskStore } from '@/store/modules/qms/task'
-import { useQmsUserStore } from '@/store/modules/qms/user'
-import { getQmsModuleConfig, DEFAULT_QMS_MODULE } from '@/config/qmsModules'
-
-const route = useRoute()
-const router = useRouter()
-const taskStore = useQmsTaskStore()
-const userStore = useQmsUserStore()
-
-const moduleCode = computed(() => (route.meta.module as string) || DEFAULT_QMS_MODULE)
-const formConfig = computed(() => getQmsModuleConfig(moduleCode.value).taskForm)
-
-const loading = ref(false)
-const saving = ref(false)
-const submitting = ref(false)
-const taskData = ref<any>(null)
-const detail = reactive({ projects: [] as any[], statistics: {} as any, exceptions: [] as any[] })
-
-const PRIORITY_MAP: Record<string, string> = { high: '高', medium: '中', low: '低' }
-const PRIORITY_COLOR: Record<string, string> = { high: '#F56C6C', medium: '#E6A23C', low: '#67C23A' }
-const getPriorityName = (priority?: string) => PRIORITY_MAP[priority || ''] || '未知'
-const getPriorityColor = (priority?: string) => PRIORITY_COLOR[priority || ''] || '#909399'
-
-const currentStep = computed(() => {
-  switch (taskData.value?.status) { case 'pending': return 0; case 'processing': return 1; case 'completed': return 2; default: return 0 }
-})
-
-const isEditable = computed(() => {
-  if (!taskData.value) return false
-  const status = taskData.value.status
-  const isResponsible = userStore.isResponsible(taskData.value.responsibleId)
-  return isResponsible && (status === 'pending' || status === 'processing')
-})
-
-const formatDateTime = (value?: string) => { if (!value) return '-'; return String(value).replace('T', ' ').slice(0, 16) }
-
-const basicInfoItems = computed(() => {
-  const infoItems = formConfig.value.infoItems || []
-  const basicInfo = { ...(taskData.value?.detail?.basicInfo || {}), ...(taskData.value || {}) }
-  return infoItems.map((item: any) => {
-    const raw = item.formatter ? item.formatter(basicInfo) : (basicInfo[item.key] ?? taskData.value?.[item.key])
-    return { label: item.label, value: raw || '-' }
-  })
-})
-
-const createDefaultProject = () => {
-  const project: any = {}
-  ;(formConfig.value.projectColumns || []).forEach((column: any) => { project[column.key] = column.default ?? '' })
-  return project
-}
-
-const loadTaskDetail = async () => {
-  loading.value = true
-  try {
-    const task = await taskStore.fetchTaskDetail(moduleCode.value, route.params.id as string)
-    if (!task) { ElMessage.error('检验任务不存在'); handleBack(); return }
-    taskData.value = task
-    detail.projects = task.detail?.projects ? task.detail.projects.map((item: any) => ({ ...item })) : []
-    detail.statistics = { ...(task.detail?.statistics || {}) }
-    detail.exceptions = task.detail?.exceptions ? task.detail.exceptions.map((item: any) => ({ ...item })) : []
-  } catch { ElMessage.error('加载检验单失败') }
-  finally { loading.value = false }
-}
-
-const handleAddProject = () => { detail.projects.push(createDefaultProject()) }
-const handleDeleteProject = (index: number) => { detail.projects.splice(index, 1) }
-const handleBack = () => { router.push(`/qms/${moduleCode.value}/task/list`) }
-const handleSave = async () => { saving.value = true; setTimeout(() => { ElMessage.success('保存成功'); saving.value = false }, 300) }
-const handleSubmit = async () => { submitting.value = true; setTimeout(() => { ElMessage.success('提交成功'); handleBack(); submitting.value = false }, 300) }
-const handlePrint = () => { ElMessage.info('打印功能开发中...') }
-
-onMounted(() => { loadTaskDetail() })
-</script>
-
-<style lang="scss" scoped>
-.page-container { padding: 20px; min-height: calc(100vh - 72px); }
-.page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; h1 { font-size: 20px; font-weight: 600; color: #303133; margin: 0; } }
-.info-card { background: #fff; border-radius: 8px; padding: 16px 20px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.06); margin-bottom: 20px; }
-.info-item { display: flex; align-items: center; gap: 4px; .info-label { color: #909399; font-size: 13px; } .info-value { color: #303133; font-size: 14px; } }
-.steps-container { background: #fff; border-radius: 8px; padding: 24px 20px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.06); margin-bottom: 20px; }
-.form-container { background: #fff; border-radius: 8px; padding: 24px 20px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.06); display: flex; flex-direction: column; gap: 24px; }
-.form-section { display: flex; flex-direction: column; gap: 16px; }
-.section-title { display: flex; align-items: center; justify-content: space-between; font-size: 16px; font-weight: 600; color: #303133; }
-.section-actions { display: flex; gap: 8px; }
-.form-actions { display: flex; gap: 12px; justify-content: flex-end; }
-@media (max-width: 768px) { .page-container { padding: 12px; } .form-actions { flex-wrap: wrap; justify-content: flex-start; } }
-</style>

+ 51 - 48
yudao-ui/yudao-ui-admin-vue3/src/views/qms/TaskList.vue

@@ -3,20 +3,10 @@
     <div class="page-header">
       <h1>{{ taskConfig.title }}</h1>
       <div class="header-actions">
-        <el-button @click="handleExport"><Icon icon="ep:download" class="mr-5px" />导出</el-button>
         <el-button plain @click="handleShowGuide"><Icon icon="ep:question-filled" class="mr-5px" />使用指引</el-button>
       </div>
     </div>
 
-    <IqcSearchFilter
-      :filters="currentFilters"
-      :advanced-filters="taskConfig.advancedFilters"
-      :options="filterOptions"
-      :keyword-placeholder="keywordPlaceholder"
-      @search="handleSearch"
-      @reset="handleReset"
-    />
-
     <el-tabs v-model="activeTab" @tab-change="handleTabChange" class="task-tabs">
       <el-tab-pane v-for="tab in taskConfig.tabs" :key="tab.key" :label="tab.label" :name="tab.key">
         <el-table v-loading="loading" :data="tableData" stripe @selection-change="handleSelectionChange">
@@ -46,7 +36,6 @@
                 <IqcPermissionButton v-if="row.status === 'pending'" :responsible-id="row.responsibleId" type="primary" size="small" @click="handleStartInspection(row)">开始检验</IqcPermissionButton>
                 <IqcPermissionButton v-if="row.status === 'processing'" :responsible-id="row.responsibleId" type="primary" size="small" @click="handleContinueInspection(row)">继续检验</IqcPermissionButton>
                 <el-button type="primary" link size="small" @click="handleView(row)">查看</el-button>
-                <el-button v-if="row.status === 'completed'" type="primary" link size="small" @click="handlePrint(row)">打印</el-button>
               </div>
             </template>
           </el-table-column>
@@ -64,7 +53,6 @@ defineOptions({ name: 'IqcTaskList' })
 
 import { computed, ref, watch, onMounted } from 'vue'
 import { useRoute, useRouter } from 'vue-router'
-import IqcSearchFilter from '@/components/Qms/SearchFilter.vue'
 import IqcStatusBadge from '@/components/Qms/StatusBadge.vue'
 import IqcPermissionButton from '@/components/Qms/PermissionButton.vue'
 import { useQmsTaskStore } from '@/store/modules/qms/task'
@@ -79,6 +67,7 @@ const selectedRows = ref<any[]>([])
 
 const moduleCode = computed(() => (route.meta.module as string) || DEFAULT_QMS_MODULE)
 const taskConfig = computed(() => getQmsModuleConfig(moduleCode.value).taskList)
+const useRemoteData = computed(() => taskStore.isRemoteModule(moduleCode.value))
 
 const activeTab = computed({
   get: () => taskStore.activeTab(moduleCode.value),
@@ -86,68 +75,82 @@ const activeTab = computed({
 })
 
 const pagination = computed(() => taskStore.pagination[moduleCode.value])
-const currentFilters = computed(() => taskStore.filters[moduleCode.value])
 const taskColumns = computed(() => taskConfig.value.columns || [])
-const keywordPlaceholder = computed(() => `搜索${(taskConfig.value.keywordFields || ['关键字段']).join('/')}`)
 
 const filteredList = computed(() => taskStore.tasksByStatus(moduleCode.value, activeTab.value))
 const tableData = computed(() => {
   const list = filteredList.value || []
+  if (useRemoteData.value) return list
   const { currentPage, pageSize } = pagination.value
   const start = (currentPage - 1) * pageSize
   return list.slice(start, start + pageSize)
 })
-const totalCount = computed(() => (filteredList.value || []).length)
+const totalCount = computed(() => (useRemoteData.value ? pagination.value.total : (filteredList.value || []).length))
 
 const PRIORITY_MAP: Record<string, string> = { high: '高', medium: '中', low: '低' }
 const PRIORITY_COLOR: Record<string, string> = { high: '#F56C6C', medium: '#E6A23C', low: '#67C23A' }
 const getPriorityName = (priority: string) => PRIORITY_MAP[priority] || '未知'
 const getPriorityColor = (priority: string) => PRIORITY_COLOR[priority] || '#909399'
 
-const filterOptions = computed(() => {
-  const list = taskStore.tasks[moduleCode.value] || []
-  const uniqueOptions = (list: any[], field: string, labelField?: string) => {
-    const map = new Map()
-    list.forEach((item) => { const value = item[field]; if (!value) return; const label = labelField ? item[labelField] : value; if (!map.has(value)) map.set(value, { label, value }) })
-    return Array.from(map.values())
-  }
-  return {
-    taskStatuses: [{ label: '待检验', value: 'pending' }, { label: '检验中', value: 'processing' }, { label: '已完成', value: 'completed' }],
-    suppliers: uniqueOptions(list, 'supplierCode', 'supplierName'),
-    materials: uniqueOptions(list, 'materialCode', 'materialName'),
-    batches: uniqueOptions(list, 'batch'),
-    responsibles: uniqueOptions(list, 'responsibleId', 'responsibleName'),
-    workOrders: uniqueOptions(list, 'workOrder'),
-    customers: uniqueOptions(list, 'customer'),
-    productModels: uniqueOptions(list, 'productModel'),
-    packageBatches: uniqueOptions(list, 'packageBatch')
-  }
-})
-
-const formatDateTime = (value: any) => { if (!value) return '-'; return String(value).replace('T', ' ').slice(0, 16) }
+const formatDateTime = (value: any) => {
+  if (!value) return '-'
+  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 getColumnProps = (column: any) => {
   if (column.type === 'selection') return { type: 'selection', width: column.width || 55, fixed: column.fixed || false }
   return { prop: column.key, label: column.label, width: column.width, minWidth: column.minWidth, fixed: column.fixed, showOverflowTooltip: column.type === 'tooltip' }
 }
 
 const handleSelectionChange = (selection: any[]) => { selectedRows.value = selection }
-const handleSearch = (payload: any) => { taskStore.updateFilters(moduleCode.value, payload); taskStore.updatePagination(moduleCode.value, { currentPage: 1 }); fetchData() }
-const handleReset = () => { taskStore.resetFilters(moduleCode.value); taskStore.updatePagination(moduleCode.value, { currentPage: 1 }); fetchData() }
-const handleSizeChange = (size: number) => { taskStore.updatePagination(moduleCode.value, { pageSize: size, currentPage: 1 }) }
-const handleCurrentChange = (page: number) => { taskStore.updatePagination(moduleCode.value, { currentPage: page }) }
-const handleTabChange = () => { taskStore.updatePagination(moduleCode.value, { currentPage: 1 }) }
-
-const handleStartInspection = async (row: any) => { try { await taskStore.startInspection(moduleCode.value, row.id); ElMessage.success('开始检验成功'); fetchData() } catch { ElMessage.error('开始检验失败') } }
-const handleContinueInspection = (row: any) => { router.push(`/qms/${moduleCode.value}/task/${row.id}`) }
-const handleView = (row: any) => { router.push(`/qms/${moduleCode.value}/task/${row.id}`) }
-const handlePrint = () => { ElMessage.info('打印功能开发中...') }
-const handleExport = () => { ElMessage.info('导出功能开发中...') }
+const handleSizeChange = (size: number) => { taskStore.updatePagination(moduleCode.value, { pageSize: size, currentPage: 1 }); fetchData() }
+const handleCurrentChange = (page: number) => { taskStore.updatePagination(moduleCode.value, { currentPage: page }); fetchData() }
+const handleTabChange = () => { taskStore.updatePagination(moduleCode.value, { currentPage: 1 }); fetchData() }
+
+const goToProcessDetail = (row: any, processInstanceId?: string) => {
+  const targetId = processInstanceId || row?.processInstanceId
+  if (targetId) {
+    router.push({ name: 'BpmProcessInstanceDetail', query: { id: targetId } })
+    return
+  }
+  ElMessage.warning('流程未发起,请先点击开始检验')
+}
+const handleStartInspection = async (row: any) => {
+  try {
+    const processInstanceId = await taskStore.startInspection(moduleCode.value, row.id)
+    ElMessage.success('开始检验成功')
+    goToProcessDetail(row, processInstanceId)
+  } catch {
+    ElMessage.error('开始检验失败')
+  }
+}
+const handleContinueInspection = async (row: any) => {
+  if (row?.processInstanceId) {
+    goToProcessDetail(row)
+    return
+  }
+  try {
+    const processInstanceId = await taskStore.startInspection(moduleCode.value, row.id)
+    goToProcessDetail(row, processInstanceId)
+  } catch {
+    ElMessage.error('继续检验失败')
+  }
+}
+const handleView = (row: any) => { goToProcessDetail(row) }
 const handleShowGuide = () => { ElMessage.info('使用指引功能开发中...') }
 
 const fetchData = async () => { loading.value = true; try { await taskStore.fetchTasks(moduleCode.value) } catch { ElMessage.error('获取任务列表失败') } finally { loading.value = false } }
 
 watch(moduleCode, () => { fetchData() })
-watch(() => activeTab.value, () => { taskStore.updatePagination(moduleCode.value, { currentPage: 1 }) })
 onMounted(() => { fetchData() })
 </script>
 

+ 142 - 0
yudao-ui/yudao-ui-admin-vue3/src/views/qms/iqc/task/ProcessDetailForm.vue

@@ -0,0 +1,142 @@
+<template>
+  <div class="iqc-process-form" v-loading="loading">
+    <template v-if="detail">
+      <div class="form-section">
+        <div class="section-title">流程信息</div>
+        <el-descriptions :column="3" border>
+          <el-descriptions-item label="流程实例">
+            {{ detail.processInstanceId || '-' }}
+          </el-descriptions-item>
+          <el-descriptions-item label="流程状态">
+            <el-tag v-if="detail.flowState" type="info">{{ detail.flowState }}</el-tag>
+            <span v-else>-</span>
+          </el-descriptions-item>
+          <el-descriptions-item label="检验状态">
+            <el-tag v-if="detail.inspectStatus" type="success">{{ detail.inspectStatus }}</el-tag>
+            <span v-else>-</span>
+          </el-descriptions-item>
+        </el-descriptions>
+      </div>
+
+      <div class="form-section">
+        <div class="section-title">申请信息</div>
+        <el-descriptions :column="3" border>
+          <el-descriptions-item label="任务编号">
+            {{ detail.id ?? '-' }}
+          </el-descriptions-item>
+          <el-descriptions-item label="单据编号">
+            {{ detail.applicationId || '-' }}
+          </el-descriptions-item>
+          <el-descriptions-item label="申请时间">
+            {{ formatDateTime(detail.applyTime) }}
+          </el-descriptions-item>
+          <el-descriptions-item label="申请人">
+            {{ detail.applicantName || '-' }}
+          </el-descriptions-item>
+          <el-descriptions-item label="来源单号">
+            {{ detail.sourceOrderNum || '-' }}
+          </el-descriptions-item>
+          <el-descriptions-item label="来源类型">
+            {{ detail.sourceOrderType || '-' }}
+          </el-descriptions-item>
+        </el-descriptions>
+      </div>
+
+      <div class="form-section">
+        <div class="section-title">检验明细</div>
+        <el-table :data="detailRows" border>
+          <el-table-column prop="batch" label="批次" width="160" />
+          <el-table-column prop="quantity" label="数量" width="120" />
+          <el-table-column prop="unit" label="单位" width="100" />
+          <el-table-column prop="sourceOrderNum" label="来源单号" min-width="200" />
+          <el-table-column prop="sourceOrderType" label="来源类型" width="140" />
+        </el-table>
+      </div>
+
+      <div class="form-section">
+        <div class="section-title">备注</div>
+        <el-input
+          :model-value="remarkValue"
+          type="textarea"
+          :rows="3"
+          readonly
+          placeholder="-"
+        />
+      </div>
+    </template>
+    <el-empty v-else description="暂无任务详情" />
+  </div>
+</template>
+
+<script setup lang="ts">
+defineOptions({ name: 'IqcTaskProcessDetailForm' })
+
+import { computed, ref, watch } from 'vue'
+import { getIqcTaskDetail, IqcTaskDetailRespVO } from '@/api/qms/iqc/task'
+
+const props = defineProps<{ id: string }>()
+
+const loading = ref(false)
+const detail = ref<IqcTaskDetailRespVO | null>(null)
+
+const remarkValue = computed(() => detail.value?.remark || '')
+const detailRows = computed(() => {
+  if (!detail.value) return []
+  return [{
+    batch: detail.value.batch || '-',
+    quantity: detail.value.quantity ?? '-',
+    unit: detail.value.unit || '-',
+    sourceOrderNum: detail.value.sourceOrderNum || '-',
+    sourceOrderType: detail.value.sourceOrderType || '-'
+  }]
+})
+
+const formatDateTime = (value: any) => {
+  if (!value) return '-'
+  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 fetchDetail = async () => {
+  if (!props.id) {
+    detail.value = null
+    return
+  }
+  loading.value = true
+  try {
+    const result = await getIqcTaskDetail(props.id)
+    detail.value = (result as any)?.data ?? result
+  } catch {
+    detail.value = null
+    ElMessage.error('获取任务详情失败')
+  } finally {
+    loading.value = false
+  }
+}
+
+watch(() => props.id, () => { fetchDetail() }, { immediate: true })
+</script>
+
+<style scoped lang="scss">
+.iqc-process-form {
+  padding: 4px 0;
+}
+.form-section {
+  margin-bottom: 16px;
+  .section-title {
+    font-size: 14px;
+    font-weight: 600;
+    color: var(--el-text-color-primary);
+    margin-bottom: 10px;
+  }
+}
+</style>