feat: 添加任务节点查询功能 - 支持通过任务编号或标题查询任务流程状态和当前节点

This commit is contained in:
Your Name 2026-01-24 10:35:04 +08:00
parent e3cb59539f
commit c575d6fedb
8 changed files with 1615 additions and 1 deletions

View File

@ -618,5 +618,16 @@ public class TaskController {
taskApprovalService.completeTaskDetail(id, userId, taskItems);
return Result.success();
}
// ==================== 任务流程查询接口 ====================
@Operation(summary = "查询任务流程状态", description = "根据任务编号或关键字查询任务当前所在流程节点")
@GetMapping("/flow-status")
public Result<com.designhub.vo.TaskFlowStatusVO> getTaskFlowStatus(
@Parameter(description = "任务编号") @RequestParam(required = false) String taskNo,
@Parameter(description = "任务标题关键字") @RequestParam(required = false) String keyword) {
log.info("查询任务流程状态: taskNo={}, keyword={}", taskNo, keyword);
return Result.success(taskService.getTaskFlowStatus(taskNo, keyword));
}
}

View File

@ -1082,6 +1082,19 @@ public class TaskApprovalService {
}
}
/**
* 获取任务的审批日志列表
* @param taskId 任务ID
* @return 审批日志列表
*/
public List<TaskApprovalLog> getApprovalLogs(Long taskId) {
return approvalLogMapper.selectList(
new LambdaQueryWrapper<TaskApprovalLog>()
.eq(TaskApprovalLog::getTaskId, taskId)
.orderByAsc(TaskApprovalLog::getCreateTime)
);
}
/**
* 记录审核日志
*/

View File

@ -4466,5 +4466,483 @@ public class TaskService {
taskMapper.updateById(task);
log.info("任务 {} 子任务创建完成,共 {} 个子任务组,总积分 {}", taskId, groupedItems.size(), totalScore);
}
// ==================== 任务流程状态查询 ====================
/**
* 获取任务流程状态(用于任务节点查询功能)
* @param taskNo 任务编号
* @param keyword 任务标题关键字
* @return 任务流程状态信息
*/
public com.designhub.vo.TaskFlowStatusVO getTaskFlowStatus(String taskNo, String keyword) {
// 1. 查询任务
Task task = null;
if (taskNo != null && !taskNo.trim().isEmpty()) {
task = taskMapper.selectOne(
new LambdaQueryWrapper<Task>()
.eq(Task::getTaskNo, taskNo.trim())
.eq(Task::getDeleted, 0)
);
} else if (keyword != null && !keyword.trim().isEmpty()) {
task = taskMapper.selectOne(
new LambdaQueryWrapper<Task>()
.like(Task::getTitle, keyword.trim())
.eq(Task::getDeleted, 0)
.orderByDesc(Task::getId)
.last("LIMIT 1")
);
}
if (task == null) {
throw new BusinessException(ResultCode.NOT_FOUND, "任务不存在");
}
// 2. 构建返回结果
com.designhub.vo.TaskFlowStatusVO vo = new com.designhub.vo.TaskFlowStatusVO();
vo.setTask(buildTaskBasicInfo(task));
vo.setCurrentNode(buildCurrentNodeInfo(task));
vo.setFlowTimeline(buildFlowTimeline(task));
vo.setRelatedUsers(buildRelatedUsers(task));
return vo;
}
/**
* 构建任务基本信息
*/
private com.designhub.vo.TaskFlowStatusVO.TaskBasicInfo buildTaskBasicInfo(Task task) {
com.designhub.vo.TaskFlowStatusVO.TaskBasicInfo info = new com.designhub.vo.TaskFlowStatusVO.TaskBasicInfo();
info.setId(task.getId());
info.setTaskNo(task.getTaskNo());
info.setTitle(task.getTitle());
info.setScore(task.getScore());
info.setIsPrimaryTask(task.getIsPrimaryTask() != null && task.getIsPrimaryTask() == 1);
info.setIsPipeline(task.getIsPipeline() != null && task.getIsPipeline() == 1);
info.setStatus(task.getStatus());
info.setStatusName(getStatusName(task.getStatus()));
info.setCreateTime(task.getCreateTime());
return info;
}
/**
* 构建当前节点信息
*/
private com.designhub.vo.TaskFlowStatusVO.CurrentNodeInfo buildCurrentNodeInfo(Task task) {
com.designhub.vo.TaskFlowStatusVO.CurrentNodeInfo info = new com.designhub.vo.TaskFlowStatusVO.CurrentNodeInfo();
String approvalStatus = task.getApprovalStatus();
String status = task.getStatus();
boolean isPrimaryTask = task.getIsPrimaryTask() != null && task.getIsPrimaryTask() == 1;
info.setStatus(approvalStatus != null ? approvalStatus : status);
info.setStatusName(getApprovalStatusName(approvalStatus, status, isPrimaryTask));
info.setDescription(getApprovalStatusDescription(approvalStatus, status, isPrimaryTask));
// 设置等待操作人
if (task.getCurrentApproverId() != null) {
com.designhub.vo.TaskFlowStatusVO.WaitingUser waitingUser = new com.designhub.vo.TaskFlowStatusVO.WaitingUser();
waitingUser.setUserId(task.getCurrentApproverId());
User approver = userMapper.selectById(task.getCurrentApproverId());
if (approver != null) {
waitingUser.setUserName(approver.getRealName());
waitingUser.setDeptName(approver.getDeptName());
}
String role = task.getCurrentApproverRole();
waitingUser.setRole(role);
waitingUser.setRoleName(getApproverRoleName(role));
info.setWaitingFor(waitingUser);
} else if ("pending".equals(status) && task.getReceiverId() == null) {
// 待接单状态
com.designhub.vo.TaskFlowStatusVO.WaitingUser waitingUser = new com.designhub.vo.TaskFlowStatusVO.WaitingUser();
waitingUser.setRoleName("等待设计师接单");
info.setWaitingFor(waitingUser);
} else if ("in_progress".equals(status) && task.getReceiverId() != null) {
// 进行中状态
com.designhub.vo.TaskFlowStatusVO.WaitingUser waitingUser = new com.designhub.vo.TaskFlowStatusVO.WaitingUser();
waitingUser.setUserId(task.getReceiverId());
User receiver = userMapper.selectById(task.getReceiverId());
if (receiver != null) {
waitingUser.setUserName(receiver.getRealName());
waitingUser.setDeptName(receiver.getDeptName());
}
waitingUser.setRoleName("设计师执行中");
info.setWaitingFor(waitingUser);
}
return info;
}
/**
* 构建流程时间线
*/
private java.util.List<com.designhub.vo.TaskFlowStatusVO.FlowTimelineNode> buildFlowTimeline(Task task) {
java.util.List<com.designhub.vo.TaskFlowStatusVO.FlowTimelineNode> timeline = new java.util.ArrayList<>();
boolean isPrimaryTask = task.getIsPrimaryTask() != null && task.getIsPrimaryTask() == 1;
String approvalStatus = task.getApprovalStatus();
String status = task.getStatus();
// 查询审批日志
java.util.List<TaskApprovalLog> logs = taskApprovalService.getApprovalLogs(task.getId());
java.util.Map<String, TaskApprovalLog> logMap = new java.util.HashMap<>();
for (TaskApprovalLog log : logs) {
logMap.put(log.getAction(), log);
}
if (isPrimaryTask) {
// 一级需求任务流程
// 1. 一级需求发起
com.designhub.vo.TaskFlowStatusVO.FlowTimelineNode node1 = new com.designhub.vo.TaskFlowStatusVO.FlowTimelineNode();
node1.setNodeName("一级需求发起");
node1.setStatus("completed");
node1.setTime(task.getCreateTime());
if (task.getPrimaryPublisherId() != null) {
User publisher = userMapper.selectById(task.getPrimaryPublisherId());
if (publisher != null) {
node1.setOperatorName(publisher.getRealName());
node1.setOperatorDept(publisher.getDeptName());
}
}
timeline.add(node1);
// 2. 一级需求审批
com.designhub.vo.TaskFlowStatusVO.FlowTimelineNode node2 = new com.designhub.vo.TaskFlowStatusVO.FlowTimelineNode();
node2.setNodeName("一级需求审批");
TaskApprovalLog primaryLog = logMap.get("approve_primary");
if (primaryLog != null) {
node2.setStatus("completed");
node2.setOperatorName(primaryLog.getApproverName());
node2.setTime(primaryLog.getCreateTime());
node2.setComment(primaryLog.getComment());
// 获取审批人部门
User approver = userMapper.selectById(primaryLog.getApproverId());
if (approver != null) {
node2.setOperatorDept(approver.getDeptName());
}
} else if ("pending_primary_approval".equals(approvalStatus)) {
node2.setStatus("pending");
if (task.getCurrentApproverId() != null) {
User approver = userMapper.selectById(task.getCurrentApproverId());
if (approver != null) {
node2.setOperatorName(approver.getRealName());
node2.setOperatorDept(approver.getDeptName());
}
}
} else {
node2.setStatus("waiting");
}
timeline.add(node2);
// 3. 明细完善
com.designhub.vo.TaskFlowStatusVO.FlowTimelineNode node3 = new com.designhub.vo.TaskFlowStatusVO.FlowTimelineNode();
node3.setNodeName("明细完善");
TaskApprovalLog detailLog = logMap.get("complete_detail");
if (detailLog != null) {
node3.setStatus("completed");
node3.setOperatorName(detailLog.getApproverName());
node3.setTime(detailLog.getCreateTime());
node3.setComment(detailLog.getComment());
User completer = userMapper.selectById(detailLog.getApproverId());
if (completer != null) {
node3.setOperatorDept(completer.getDeptName());
}
} else if ("pending_detail_completion".equals(approvalStatus)) {
node3.setStatus("pending");
if (task.getDetailCompleterId() != null) {
User completer = userMapper.selectById(task.getDetailCompleterId());
if (completer != null) {
node3.setOperatorName(completer.getRealName());
node3.setOperatorDept(completer.getDeptName());
}
}
} else if (primaryLog != null) {
node3.setStatus("waiting");
} else {
node3.setStatus("waiting");
}
timeline.add(node3);
// 4. 接单部门审批(如果是流水线任务)
boolean isPipeline = task.getIsPipeline() != null && task.getIsPipeline() == 1;
if (isPipeline || "pending_receiving_approval".equals(approvalStatus) ||
logMap.containsKey("approve_receiving") || logMap.containsKey("reject_receiving")) {
com.designhub.vo.TaskFlowStatusVO.FlowTimelineNode node4 = new com.designhub.vo.TaskFlowStatusVO.FlowTimelineNode();
node4.setNodeName("接单部门审批");
TaskApprovalLog receivingLog = logMap.get("approve_receiving");
if (receivingLog != null) {
node4.setStatus("completed");
node4.setOperatorName(receivingLog.getApproverName());
node4.setTime(receivingLog.getCreateTime());
node4.setComment(receivingLog.getComment());
User approver = userMapper.selectById(receivingLog.getApproverId());
if (approver != null) {
node4.setOperatorDept(approver.getDeptName());
}
} else if ("pending_receiving_approval".equals(approvalStatus)) {
node4.setStatus("pending");
if (task.getCurrentApproverId() != null) {
User approver = userMapper.selectById(task.getCurrentApproverId());
if (approver != null) {
node4.setOperatorName(approver.getRealName());
node4.setOperatorDept(approver.getDeptName());
}
}
} else {
node4.setStatus("waiting");
}
timeline.add(node4);
}
// 5. 任务发布/执行
com.designhub.vo.TaskFlowStatusVO.FlowTimelineNode node5 = new com.designhub.vo.TaskFlowStatusVO.FlowTimelineNode();
node5.setNodeName("任务执行");
if ("approved".equals(approvalStatus) && ("pending".equals(status) || "in_progress".equals(status) || "completed".equals(status))) {
if ("completed".equals(status)) {
node5.setStatus("completed");
node5.setTime(task.getCompleteTime());
} else if ("in_progress".equals(status)) {
node5.setStatus("pending");
} else {
node5.setStatus("pending");
}
if (task.getReceiverId() != null) {
User receiver = userMapper.selectById(task.getReceiverId());
if (receiver != null) {
node5.setOperatorName(receiver.getRealName());
node5.setOperatorDept(receiver.getDeptName());
}
}
} else {
node5.setStatus("waiting");
}
timeline.add(node5);
} else {
// 普通任务流程
// 1. 任务发起
com.designhub.vo.TaskFlowStatusVO.FlowTimelineNode node1 = new com.designhub.vo.TaskFlowStatusVO.FlowTimelineNode();
node1.setNodeName("任务发起");
node1.setStatus("completed");
node1.setTime(task.getCreateTime());
if (task.getPublisherId() != null) {
User publisher = userMapper.selectById(task.getPublisherId());
if (publisher != null) {
node1.setOperatorName(publisher.getRealName());
node1.setOperatorDept(publisher.getDeptName());
}
}
timeline.add(node1);
// 2. 任务审核
com.designhub.vo.TaskFlowStatusVO.FlowTimelineNode node2 = new com.designhub.vo.TaskFlowStatusVO.FlowTimelineNode();
node2.setNodeName("任务审核");
if ("approved".equals(approvalStatus) || "pending".equals(status) || "in_progress".equals(status) || "completed".equals(status)) {
node2.setStatus("completed");
// 查找审批日志
TaskApprovalLog approveLog = logMap.get("approve");
if (approveLog != null) {
node2.setOperatorName(approveLog.getApproverName());
node2.setTime(approveLog.getCreateTime());
node2.setComment(approveLog.getComment());
User approver = userMapper.selectById(approveLog.getApproverId());
if (approver != null) {
node2.setOperatorDept(approver.getDeptName());
}
}
} else if ("pending_approval".equals(approvalStatus)) {
node2.setStatus("pending");
if (task.getCurrentApproverId() != null) {
User approver = userMapper.selectById(task.getCurrentApproverId());
if (approver != null) {
node2.setOperatorName(approver.getRealName());
node2.setOperatorDept(approver.getDeptName());
}
}
} else {
node2.setStatus("waiting");
}
timeline.add(node2);
// 3. 任务执行
com.designhub.vo.TaskFlowStatusVO.FlowTimelineNode node3 = new com.designhub.vo.TaskFlowStatusVO.FlowTimelineNode();
node3.setNodeName("任务执行");
if ("completed".equals(status)) {
node3.setStatus("completed");
node3.setTime(task.getCompleteTime());
if (task.getReceiverId() != null) {
User receiver = userMapper.selectById(task.getReceiverId());
if (receiver != null) {
node3.setOperatorName(receiver.getRealName());
node3.setOperatorDept(receiver.getDeptName());
}
}
} else if ("in_progress".equals(status)) {
node3.setStatus("pending");
if (task.getReceiverId() != null) {
User receiver = userMapper.selectById(task.getReceiverId());
if (receiver != null) {
node3.setOperatorName(receiver.getRealName());
node3.setOperatorDept(receiver.getDeptName());
}
}
} else if ("pending".equals(status)) {
node3.setStatus("waiting");
} else {
node3.setStatus("waiting");
}
timeline.add(node3);
}
return timeline;
}
/**
* 构建相关人员信息
*/
private com.designhub.vo.TaskFlowStatusVO.RelatedUsers buildRelatedUsers(Task task) {
com.designhub.vo.TaskFlowStatusVO.RelatedUsers users = new com.designhub.vo.TaskFlowStatusVO.RelatedUsers();
// 发起人
Long publisherId = task.getPrimaryPublisherId() != null ? task.getPrimaryPublisherId() : task.getPublisherId();
if (publisherId != null) {
User publisher = userMapper.selectById(publisherId);
if (publisher != null) {
com.designhub.vo.TaskFlowStatusVO.UserInfo info = new com.designhub.vo.TaskFlowStatusVO.UserInfo();
info.setUserId(publisherId);
info.setName(publisher.getRealName());
info.setDept(publisher.getDeptName());
users.setPublisher(info);
}
}
// 明细完善人
if (task.getDetailCompleterId() != null) {
User completer = userMapper.selectById(task.getDetailCompleterId());
if (completer != null) {
com.designhub.vo.TaskFlowStatusVO.UserInfo info = new com.designhub.vo.TaskFlowStatusVO.UserInfo();
info.setUserId(task.getDetailCompleterId());
info.setName(completer.getRealName());
info.setDept(completer.getDeptName());
users.setDetailCompleter(info);
}
}
// 接单人
if (task.getReceiverId() != null) {
User receiver = userMapper.selectById(task.getReceiverId());
if (receiver != null) {
com.designhub.vo.TaskFlowStatusVO.UserInfo info = new com.designhub.vo.TaskFlowStatusVO.UserInfo();
info.setUserId(task.getReceiverId());
info.setName(receiver.getRealName());
info.setDept(receiver.getDeptName());
users.setReceiver(info);
}
}
// 当前审批人
if (task.getCurrentApproverId() != null) {
User approver = userMapper.selectById(task.getCurrentApproverId());
if (approver != null) {
com.designhub.vo.TaskFlowStatusVO.UserInfo info = new com.designhub.vo.TaskFlowStatusVO.UserInfo();
info.setUserId(task.getCurrentApproverId());
info.setName(approver.getRealName());
info.setDept(approver.getDeptName());
users.setCurrentApprover(info);
}
}
return users;
}
/**
* 获取任务执行状态中文名称
*/
private String getStatusName(String status) {
if (status == null) return "未知";
switch (status) {
case "draft": return "草稿";
case "pending_audit": return "待审核";
case "pending": return "待接单";
case "in_progress": return "进行中";
case "completed": return "已完成";
case "cancelled": return "已取消";
default: return status;
}
}
/**
* 获取审批状态中文名称
*/
private String getApprovalStatusName(String approvalStatus, String status, boolean isPrimaryTask) {
if (approvalStatus == null) {
return getStatusName(status);
}
switch (approvalStatus) {
case "draft": return "草稿";
case "pending_primary_approval": return "待一级需求审批";
case "pending_detail_completion": return "待完善明细";
case "pending_demand_approval": return "待需求部门审批";
case "pending_receiving_approval": return "待接单部门审批";
case "pending_approval": return "待审核";
case "approved":
// 已审核通过,显示执行状态
return getStatusName(status);
case "rejected": return "已驳回";
case "rejected_by_demand": return "需求部门驳回";
case "rejected_by_receiving": return "接单部门驳回";
default: return approvalStatus;
}
}
/**
* 获取审批状态描述
*/
private String getApprovalStatusDescription(String approvalStatus, String status, boolean isPrimaryTask) {
if (approvalStatus == null) {
return "";
}
switch (approvalStatus) {
case "draft": return "任务已创建但未提交";
case "pending_primary_approval": return "等待需求部门负责人审核并指定明细完善人";
case "pending_detail_completion": return "等待指定的明细完善人填写任务明细";
case "pending_demand_approval": return "明细完善后等待需求部门负责人审核";
case "pending_receiving_approval": return "等待接单部门负责人审核";
case "pending_approval": return "等待审核通过后发布";
case "approved":
if ("pending".equals(status)) {
return "任务已发布,等待设计师接单";
} else if ("in_progress".equals(status)) {
return "设计师正在执行任务";
} else if ("completed".equals(status)) {
return "任务已完成";
}
return "任务已审核通过";
case "rejected": return "任务被驳回,请修改后重新提交";
case "rejected_by_demand": return "被需求部门负责人驳回";
case "rejected_by_receiving": return "被接单部门负责人驳回";
default: return "";
}
}
/**
* 获取审批人角色中文名称
*/
private String getApproverRoleName(String role) {
if (role == null) return "";
switch (role) {
case "demand_leader": return "需求部门负责人";
case "detail_completer": return "明细完善人";
case "receiving_leader": return "接单部门负责人";
default: return role;
}
}
}

View File

@ -0,0 +1,144 @@
package com.designhub.vo;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
/**
* 任务流程状态VO - 用于任务节点查询功能
*/
@Data
public class TaskFlowStatusVO {
/**
* 任务基本信息
*/
private TaskBasicInfo task;
/**
* 当前节点信息
*/
private CurrentNodeInfo currentNode;
/**
* 流程时间线
*/
private List<FlowTimelineNode> flowTimeline;
/**
* 相关人员信息
*/
private RelatedUsers relatedUsers;
/**
* 任务基本信息
*/
@Data
public static class TaskBasicInfo {
private Long id;
private String taskNo;
private String title;
private BigDecimal score;
private Boolean isPrimaryTask;
private Boolean isPipeline;
private String status;
private String statusName;
private LocalDateTime createTime;
}
/**
* 当前节点信息
*/
@Data
public static class CurrentNodeInfo {
private String status;
private String statusName;
private String description;
private WaitingUser waitingFor;
}
/**
* 等待操作的用户
*/
@Data
public static class WaitingUser {
private Long userId;
private String userName;
private String deptName;
private String role;
private String roleName;
}
/**
* 流程时间线节点
*/
@Data
public static class FlowTimelineNode {
/**
* 节点名称
*/
private String nodeName;
/**
* 节点状态:completed-已完成, pending-进行中, waiting-等待中
*/
private String status;
/**
* 操作人姓名
*/
private String operatorName;
/**
* 操作人部门
*/
private String operatorDept;
/**
* 操作时间
*/
private LocalDateTime time;
/**
* 操作备注
*/
private String comment;
}
/**
* 相关人员信息
*/
@Data
public static class RelatedUsers {
/**
* 发起人(普通任务)或一级需求发起人
*/
private UserInfo publisher;
/**
* 明细完善人(一级需求任务)
*/
private UserInfo detailCompleter;
/**
* 接单人/执行人
*/
private UserInfo receiver;
/**
* 当前审批人
*/
private UserInfo currentApprover;
}
/**
* 用户信息
*/
@Data
public static class UserInfo {
private Long userId;
private String name;
private String dept;
}
}

View File

@ -0,0 +1,499 @@
# 任务节点查询功能开发文档
## 1. 需求背景
用户反馈经常找不到任务当前处于什么节点,不清楚:
- 任务当前状态是什么
- 下一步需要谁操作
- 任务经历了哪些审批流程
因此需要开发一个**任务节点查询功能**,让用户可以通过任务编号快速查询任务的流程状态。
---
## 2. 功能设计
### 2.1 功能入口
在页面顶部导航栏或任务大厅页面增加「任务查询」按钮,点击后弹出查询对话框。
### 2.2 查询方式
- **任务编号查询**:输入完整的任务编号(如 `TASK202601221029343146`)
- **模糊搜索**:支持输入任务标题关键字搜索
### 2.3 展示内容
#### 基本信息卡片
| 字段 | 说明 |
|------|------|
| 任务编号 | TASK202601221029343146 |
| 任务标题 | AD0259-欧意朗-天猫 |
| 任务类型 | 一级需求任务 / 普通任务 |
| 任务积分 | 5.20 分 |
| 创建时间 | 2026-01-22 10:29:34 |
#### 当前节点信息(重点展示)
```
┌─────────────────────────────────────────────┐
│ 📍 当前节点:待接单部门审核 │
│ │
│ 等待操作人:清云(营销策划中心) │
│ 操作说明:接单部门负责人需审核此任务 │
└─────────────────────────────────────────────┘
```
#### 流程时间线(可视化展示)
```
● 一级需求发起 2026-01-22 10:29:34 瑶池(亿家达组)
↓
● 一级需求审批通过 2026-01-22 13:46:56 步惊云(策划三部)
↓
● 明细完善完成 2026-01-23 10:44:24 殷云(策划四部)
↓
○ 接单部门审核 等待中... 清云(营销策划中心) ← 当前
↓
○ 任务发布 - -
```
#### 相关人员信息
| 角色 | 姓名 | 部门 |
|------|------|------|
| 发起人 | 瑶池 | 亿家达组 |
| 明细完善人 | 殷云 | 策划四部 |
| 接单人 | 乐游 | 平面视觉五组 |
| 当前审批人 | 清云 | 营销策划中心 |
---
## 3. 任务状态映射表
### 3.1 普通任务状态
| approval_status | 中文名称 | 说明 |
|-----------------|----------|------|
| draft | 草稿 | 任务已创建但未提交 |
| pending_approval | 待审核 | 等待需求部门负责人审核 |
| approved | 已审核 | 审核通过,任务已发布 |
| rejected | 已驳回 | 审核被驳回 |
### 3.2 一级需求任务状态
| approval_status | 中文名称 | 当前操作人角色 | 说明 |
|-----------------|----------|----------------|------|
| draft | 草稿 | 发起人 | 任务已创建但未提交 |
| pending_primary_approval | 待一级需求审批 | 需求部门负责人 | 等待需求部门负责人审核并指定明细完善人 |
| pending_detail_completion | 待完善明细 | 明细完善人 | 等待指定的明细完善人填写任务明细 |
| pending_demand_approval | 待需求部门审批 | 需求部门负责人 | 明细完善后等待需求部门负责人审核 |
| pending_receiving_approval | 待接单部门审批 | 接单部门负责人 | 等待接单部门负责人审核 |
| approved | 已审核 | - | 所有审批通过,任务已发布 |
| rejected_by_demand | 需求部门驳回 | 明细完善人 | 被需求部门负责人驳回 |
| rejected_by_receiving | 接单部门驳回 | 需求部门负责人 | 被接单部门负责人驳回 |
### 3.3 任务执行状态
| status | 中文名称 | 说明 |
|--------|----------|------|
| draft | 草稿 | 未提交 |
| pending_audit | 待审核 | 审批流程中 |
| pending | 待接单/进行中 | 已发布等待接单或执行中 |
| in_progress | 进行中 | 设计师正在执行 |
| completed | 已完成 | 任务完成 |
| cancelled | 已取消 | 任务被取消 |
---
## 4. 技术实现
### 4.1 后端接口
#### 4.1.1 任务节点查询接口
**接口地址**:`GET /api/task/flow-status`
**请求参数**:
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| taskNo | String | 否 | 任务编号 |
| keyword | String | 否 | 任务标题关键字 |
**响应示例**:
```json
{
"code": 200,
"message": "success",
"data": {
"task": {
"id": 432,
"taskNo": "TASK202601221029343146",
"title": "AD0259-欧意朗-天猫",
"score": 5.20,
"isPrimaryTask": true,
"createTime": "2026-01-22 10:29:34"
},
"currentNode": {
"status": "pending_receiving_approval",
"statusName": "待接单部门审批",
"description": "等待接单部门负责人审核此任务",
"waitingFor": {
"userId": 17,
"userName": "清云",
"deptName": "营销策划中心",
"role": "接单部门负责人"
}
},
"flowTimeline": [
{
"nodeName": "一级需求发起",
"status": "completed",
"operatorName": "瑶池",
"operatorDept": "亿家达组",
"time": "2026-01-22 10:29:34"
},
{
"nodeName": "一级需求审批",
"status": "completed",
"operatorName": "步惊云",
"operatorDept": "策划三部",
"time": "2026-01-22 13:46:56",
"comment": "审核通过,请完善任务明细"
},
{
"nodeName": "明细完善",
"status": "completed",
"operatorName": "殷云",
"operatorDept": "策划四部",
"time": "2026-01-23 10:44:24"
},
{
"nodeName": "接单部门审批",
"status": "pending",
"operatorName": "清云",
"operatorDept": "营销策划中心",
"time": null
},
{
"nodeName": "任务发布",
"status": "waiting",
"operatorName": null,
"operatorDept": null,
"time": null
}
],
"relatedUsers": {
"publisher": { "name": "瑶池", "dept": "亿家达组" },
"detailCompleter": { "name": "殷云", "dept": "策划四部" },
"receiver": { "name": "乐游", "dept": "平面视觉五组" },
"currentApprover": { "name": "清云", "dept": "营销策划中心" }
}
}
}
```
### 4.2 后端实现
#### 4.2.1 Controller 层
```java
// TaskController.java
/**
* 查询任务流程状态
*/
@GetMapping("/flow-status")
public Result<TaskFlowStatusVO> getTaskFlowStatus(
@RequestParam(required = false) String taskNo,
@RequestParam(required = false) String keyword) {
return Result.success(taskService.getTaskFlowStatus(taskNo, keyword));
}
```
#### 4.2.2 Service 层
```java
// TaskService.java
/**
* 获取任务流程状态
*/
public TaskFlowStatusVO getTaskFlowStatus(String taskNo, String keyword) {
// 1. 查询任务
Task task = findTaskByNoOrKeyword(taskNo, keyword);
if (task == null) {
throw new BusinessException("任务不存在");
}
// 2. 构建基本信息
TaskFlowStatusVO vo = new TaskFlowStatusVO();
vo.setTask(buildTaskInfo(task));
// 3. 构建当前节点信息
vo.setCurrentNode(buildCurrentNode(task));
// 4. 构建流程时间线
vo.setFlowTimeline(buildFlowTimeline(task));
// 5. 构建相关人员信息
vo.setRelatedUsers(buildRelatedUsers(task));
return vo;
}
```
#### 4.2.3 VO 类
```java
// TaskFlowStatusVO.java
@Data
public class TaskFlowStatusVO {
private TaskBasicInfo task;
private CurrentNodeInfo currentNode;
private List<FlowTimelineNode> flowTimeline;
private RelatedUsers relatedUsers;
@Data
public static class TaskBasicInfo {
private Long id;
private String taskNo;
private String title;
private BigDecimal score;
private Boolean isPrimaryTask;
private LocalDateTime createTime;
}
@Data
public static class CurrentNodeInfo {
private String status;
private String statusName;
private String description;
private WaitingUser waitingFor;
}
@Data
public static class WaitingUser {
private Long userId;
private String userName;
private String deptName;
private String role;
}
@Data
public static class FlowTimelineNode {
private String nodeName;
private String status; // completed, pending, waiting
private String operatorName;
private String operatorDept;
private LocalDateTime time;
private String comment;
}
@Data
public static class RelatedUsers {
private UserInfo publisher;
private UserInfo detailCompleter;
private UserInfo receiver;
private UserInfo currentApprover;
}
@Data
public static class UserInfo {
private String name;
private String dept;
}
}
```
### 4.3 前端实现
#### 4.3.1 查询对话框组件
```vue
<!-- TaskFlowQueryDialog.vue -->
<template>
<el-dialog title="任务节点查询" v-model="visible" width="700px">
<!-- 搜索框 -->
<div class="search-box">
<el-input
v-model="searchInput"
placeholder="请输入任务编号或任务标题"
@keyup.enter="handleSearch"
clearable>
<template #append>
<el-button @click="handleSearch" :loading="loading">
<el-icon><Search /></el-icon> 查询
</el-button>
</template>
</el-input>
</div>
<!-- 查询结果 -->
<div v-if="result" class="result-container">
<!-- 基本信息 -->
<el-card class="info-card">
<template #header>任务信息</template>
<el-descriptions :column="2" border>
<el-descriptions-item label="任务编号">{{ result.task.taskNo }}</el-descriptions-item>
<el-descriptions-item label="任务标题">{{ result.task.title }}</el-descriptions-item>
<el-descriptions-item label="任务类型">
<el-tag :type="result.task.isPrimaryTask ? 'warning' : ''">
{{ result.task.isPrimaryTask ? '一级需求任务' : '普通任务' }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="任务积分">{{ result.task.score }} 分</el-descriptions-item>
</el-descriptions>
</el-card>
<!-- 当前节点(重点展示) -->
<el-card class="current-node-card">
<div class="current-node">
<div class="node-icon">📍</div>
<div class="node-info">
<div class="node-status">当前节点:{{ result.currentNode.statusName }}</div>
<div class="node-waiting">
等待操作人:{{ result.currentNode.waitingFor?.userName }}
({{ result.currentNode.waitingFor?.deptName }})
</div>
<div class="node-desc">{{ result.currentNode.description }}</div>
</div>
</div>
</el-card>
<!-- 流程时间线 -->
<el-card class="timeline-card">
<template #header>流程进度</template>
<el-timeline>
<el-timeline-item
v-for="node in result.flowTimeline"
:key="node.nodeName"
:type="getTimelineType(node.status)"
:hollow="node.status !== 'completed'">
<div class="timeline-node">
<div class="node-name">{{ node.nodeName }}</div>
<div class="node-operator" v-if="node.operatorName">
{{ node.operatorName }}({{ node.operatorDept }})
</div>
<div class="node-time" v-if="node.time">{{ formatTime(node.time) }}</div>
<div class="node-comment" v-if="node.comment">备注:{{ node.comment }}</div>
</div>
</el-timeline-item>
</el-timeline>
</el-card>
</div>
<!-- 空状态 -->
<el-empty v-else-if="searched && !result" description="未找到相关任务" />
</el-dialog>
</template>
```
#### 4.3.2 样式设计
```scss
.current-node-card {
margin: 16px 0;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
.current-node {
display: flex;
align-items: center;
color: #fff;
.node-icon {
font-size: 48px;
margin-right: 20px;
}
.node-status {
font-size: 20px;
font-weight: bold;
margin-bottom: 8px;
}
.node-waiting {
font-size: 16px;
margin-bottom: 4px;
}
.node-desc {
font-size: 14px;
opacity: 0.9;
}
}
}
.timeline-card {
.timeline-node {
.node-name {
font-weight: bold;
font-size: 15px;
}
.node-operator {
color: #409eff;
margin-top: 4px;
}
.node-time {
color: #909399;
font-size: 12px;
margin-top: 4px;
}
.node-comment {
color: #67c23a;
font-size: 12px;
margin-top: 4px;
}
}
}
```
---
## 5. 功能入口位置
### 5.1 方案A:顶部导航栏
在导航栏右侧增加「🔍 任务查询」按钮,全局可用。
### 5.2 方案B:任务大厅页面
在任务大厅页面顶部筛选区域增加「任务节点查询」按钮。
### 5.3 方案C:快捷搜索框
在页面顶部增加全局搜索框,支持直接输入任务编号快速查询。
**推荐方案**:方案A + 方案C 组合,既有明显入口,又支持快捷查询。
---
## 6. 权限控制
| 用户角色 | 可查询范围 |
|----------|-----------|
| 管理员 | 所有任务 |
| 需求部门成员 | 本部门发起的任务 + 自己参与的任务 |
| 接单部门成员 | 自己接单的任务 + 本部门的任务 |
| 普通用户 | 自己发起或参与的任务 |
---
## 7. 开发工作量估算
| 模块 | 工作内容 | 预估时间 |
|------|----------|----------|
| 后端 | 接口开发、VO类、Service逻辑 | 4小时 |
| 前端 | 对话框组件、时间线、样式 | 4小时 |
| 测试 | 功能测试、边界情况 | 2小时 |
| **合计** | | **10小时** |
---
## 8. 后续扩展
1. **消息通知集成**:在通知消息中增加「查看任务进度」链接
2. **批量查询**:支持一次查询多个任务的状态
3. **导出功能**:支持导出任务流程记录
4. **订阅提醒**:用户可订阅任务状态变更通知

View File

@ -792,3 +792,51 @@ export interface TaskItemDTO {
export const completeTaskDetail = (taskId: number, taskItems: TaskItemDTO[]) => {
return request.post(`/tasks/${taskId}/complete-detail`, taskItems)
}
// ==================== 任务流程查询接口 ====================
// 任务流程状态响应类型
export interface TaskFlowStatusVO {
task: {
id: number
taskNo: string
title: string
score: number
isPrimaryTask: boolean
isPipeline: boolean
status: string
statusName: string
createTime: string
}
currentNode: {
status: string
statusName: string
description: string
waitingFor?: {
userId: number
userName: string
deptName: string
role: string
roleName: string
}
}
flowTimeline: Array<{
nodeName: string
status: 'completed' | 'pending' | 'waiting'
operatorName?: string
operatorDept?: string
time?: string
comment?: string
}>
relatedUsers: {
publisher?: { userId: number; name: string; dept: string }
detailCompleter?: { userId: number; name: string; dept: string }
receiver?: { userId: number; name: string; dept: string }
currentApprover?: { userId: number; name: string; dept: string }
}
}
// 查询任务流程状态
export const getTaskFlowStatus = (params: { taskNo?: string; keyword?: string }) => {
return request.get<TaskFlowStatusVO>('/tasks/flow-status', { params })
}

View File

@ -84,19 +84,35 @@
<!-- 通知铃铛 -->
<NotificationBell v-if="navbarConfigStore.config.actions.notification" />
<!-- 任务查询 -->
<div
class="navbar-icon"
@click="showTaskFlowQuery = true"
title="任务节点查询"
>
<Icon name="search" size="md" />
</div>
</div>
</div>
<!-- 任务流程查询对话框 -->
<TaskFlowQueryDialog v-model="showTaskFlowQuery" />
</nav>
</template>
<script setup lang="ts">
import { computed, onMounted, onUnmounted } from 'vue'
import { computed, onMounted, onUnmounted, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
import { useSiteSettingsStore } from '@/stores/siteSettings'
import { useNavbarConfigStore } from '@/stores/navbarConfig'
import NotificationBell from '@/components/NotificationBell.vue'
import Icon from '@/components/Icon.vue'
import TaskFlowQueryDialog from '@/components/TaskFlowQueryDialog.vue'
// 任务查询对话框显示状态
const showTaskFlowQuery = ref(false)
const route = useRoute()
const router = useRouter()

View File

@ -0,0 +1,405 @@
<template>
<el-dialog
v-model="dialogVisible"
title="任务节点查询"
width="720px"
:close-on-click-modal="false"
@close="handleClose"
>
<!-- 搜索框 -->
<div class="search-box">
<el-input
v-model="searchInput"
placeholder="请输入任务编号或任务标题关键字"
clearable
size="large"
@keyup.enter="handleSearch"
>
<template #prefix>
<el-icon><Search /></el-icon>
</template>
<template #append>
<el-button type="primary" :loading="loading" @click="handleSearch">
查询
</el-button>
</template>
</el-input>
<div class="search-tip">
支持输入完整任务编号(如 TASK202601221029343146)或任务标题关键字搜索
</div>
</div>
<!-- 查询结果 -->
<div v-if="result" class="result-container">
<!-- 基本信息卡片 -->
<el-card class="info-card" shadow="never">
<template #header>
<div class="card-header">
<span>任务信息</span>
<el-tag v-if="result.task.isPrimaryTask" type="warning" size="small">一级需求任务</el-tag>
<el-tag v-else size="small">普通任务</el-tag>
<el-tag v-if="result.task.isPipeline" type="info" size="small" style="margin-left: 8px;">流水线</el-tag>
</div>
</template>
<el-descriptions :column="2" border size="small">
<el-descriptions-item label="任务编号">
<el-text type="primary" copyable>{{ result.task.taskNo }}</el-text>
</el-descriptions-item>
<el-descriptions-item label="任务标题">{{ result.task.title }}</el-descriptions-item>
<el-descriptions-item label="任务积分">
<el-text type="success">{{ result.task.score }} 分</el-text>
</el-descriptions-item>
<el-descriptions-item label="创建时间">{{ formatTime(result.task.createTime) }}</el-descriptions-item>
</el-descriptions>
</el-card>
<!-- 当前节点卡片(重点展示) -->
<div class="current-node-card">
<div class="current-node">
<div class="node-icon">📍</div>
<div class="node-info">
<div class="node-status">当前节点:{{ result.currentNode.statusName }}</div>
<div class="node-waiting" v-if="result.currentNode.waitingFor">
<span v-if="result.currentNode.waitingFor.userName">
等待操作人:<strong>{{ result.currentNode.waitingFor.userName }}</strong>
<span v-if="result.currentNode.waitingFor.deptName">({{ result.currentNode.waitingFor.deptName }})</span>
</span>
<span v-else-if="result.currentNode.waitingFor.roleName">
{{ result.currentNode.waitingFor.roleName }}
</span>
</div>
<div class="node-desc">{{ result.currentNode.description }}</div>
</div>
</div>
</div>
<!-- 流程时间线 -->
<el-card class="timeline-card" shadow="never">
<template #header>
<span>流程进度</span>
</template>
<el-timeline>
<el-timeline-item
v-for="(node, index) in result.flowTimeline"
:key="index"
:type="getTimelineType(node.status)"
:hollow="node.status !== 'completed'"
:timestamp="node.time ? formatTime(node.time) : (node.status === 'pending' ? '进行中...' : '等待中')"
placement="top"
>
<div class="timeline-node">
<div class="node-name">
{{ node.nodeName }}
<el-tag v-if="node.status === 'pending'" type="warning" size="small" style="margin-left: 8px;">当前</el-tag>
</div>
<div class="node-operator" v-if="node.operatorName">
<el-icon><User /></el-icon>
{{ node.operatorName }}
<span v-if="node.operatorDept">({{ node.operatorDept }})</span>
</div>
<div class="node-comment" v-if="node.comment">
<el-icon><ChatDotRound /></el-icon>
{{ node.comment }}
</div>
</div>
</el-timeline-item>
</el-timeline>
</el-card>
<!-- 相关人员 -->
<el-card class="users-card" shadow="never">
<template #header>
<span>相关人员</span>
</template>
<div class="users-grid">
<div class="user-item" v-if="result.relatedUsers.publisher">
<div class="user-role">发起人</div>
<div class="user-name">{{ result.relatedUsers.publisher.name }}</div>
<div class="user-dept">{{ result.relatedUsers.publisher.dept }}</div>
</div>
<div class="user-item" v-if="result.relatedUsers.detailCompleter">
<div class="user-role">明细完善人</div>
<div class="user-name">{{ result.relatedUsers.detailCompleter.name }}</div>
<div class="user-dept">{{ result.relatedUsers.detailCompleter.dept }}</div>
</div>
<div class="user-item" v-if="result.relatedUsers.receiver">
<div class="user-role">接单人</div>
<div class="user-name">{{ result.relatedUsers.receiver.name }}</div>
<div class="user-dept">{{ result.relatedUsers.receiver.dept }}</div>
</div>
<div class="user-item" v-if="result.relatedUsers.currentApprover">
<div class="user-role">当前审批人</div>
<div class="user-name">{{ result.relatedUsers.currentApprover.name }}</div>
<div class="user-dept">{{ result.relatedUsers.currentApprover.dept }}</div>
</div>
</div>
</el-card>
</div>
<!-- 空状态 -->
<el-empty v-else-if="searched && !result && !loading" description="未找到相关任务" />
<!-- 初始状态 -->
<div v-else-if="!searched" class="initial-state">
<el-icon class="search-icon"><Search /></el-icon>
<p>输入任务编号或标题关键字查询任务流程状态</p>
</div>
<template #footer>
<el-button @click="dialogVisible = false">关闭</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
import { Search, User, ChatDotRound } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import { getTaskFlowStatus, type TaskFlowStatusVO } from '@/api/task'
const props = defineProps<{
modelValue: boolean
initialTaskNo?: string
}>()
const emit = defineEmits<{
(e: 'update:modelValue', value: boolean): void
}>()
const dialogVisible = ref(props.modelValue)
const searchInput = ref('')
const loading = ref(false)
const searched = ref(false)
const result = ref<TaskFlowStatusVO | null>(null)
// 同步外部 v-model
watch(() => props.modelValue, (val) => {
dialogVisible.value = val
if (val && props.initialTaskNo) {
searchInput.value = props.initialTaskNo
handleSearch()
}
})
watch(dialogVisible, (val) => {
emit('update:modelValue', val)
})
// 处理搜索
const handleSearch = async () => {
if (!searchInput.value.trim()) {
ElMessage.warning('请输入任务编号或关键字')
return
}
loading.value = true
searched.value = true
result.value = null
try {
const input = searchInput.value.trim()
// 判断是否为任务编号格式
const isTaskNo = /^TASK\d+$/.test(input)
const params = isTaskNo
? { taskNo: input }
: { keyword: input }
const res = await getTaskFlowStatus(params)
if (res.data) {
result.value = res.data
}
} catch (error: any) {
ElMessage.error(error.message || '查询失败')
} finally {
loading.value = false
}
}
// 处理关闭
const handleClose = () => {
searchInput.value = ''
result.value = null
searched.value = false
}
// 获取时间线类型
const getTimelineType = (status: string) => {
switch (status) {
case 'completed':
return 'success'
case 'pending':
return 'warning'
default:
return 'info'
}
}
// 格式化时间
const formatTime = (time: string | undefined) => {
if (!time) return ''
const date = new Date(time)
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')} ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`
}
// 暴露方法给父组件
defineExpose({
search: (taskNo: string) => {
searchInput.value = taskNo
handleSearch()
}
})
</script>
<style scoped lang="scss">
.search-box {
margin-bottom: 20px;
.search-tip {
margin-top: 8px;
font-size: 12px;
color: #909399;
}
}
.result-container {
.info-card {
margin-bottom: 16px;
.card-header {
display: flex;
align-items: center;
gap: 8px;
}
}
.current-node-card {
margin-bottom: 16px;
padding: 20px;
border-radius: 8px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
.current-node {
display: flex;
align-items: flex-start;
color: #fff;
.node-icon {
font-size: 40px;
margin-right: 16px;
line-height: 1;
}
.node-info {
flex: 1;
.node-status {
font-size: 18px;
font-weight: bold;
margin-bottom: 8px;
}
.node-waiting {
font-size: 14px;
margin-bottom: 6px;
opacity: 0.95;
strong {
font-weight: 600;
}
}
.node-desc {
font-size: 13px;
opacity: 0.85;
}
}
}
}
.timeline-card {
margin-bottom: 16px;
.timeline-node {
.node-name {
font-weight: 600;
font-size: 14px;
color: #303133;
display: flex;
align-items: center;
}
.node-operator {
display: flex;
align-items: center;
gap: 4px;
color: #409eff;
font-size: 13px;
margin-top: 6px;
}
.node-comment {
display: flex;
align-items: center;
gap: 4px;
color: #67c23a;
font-size: 12px;
margin-top: 4px;
}
}
}
.users-card {
.users-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
.user-item {
text-align: center;
padding: 12px;
background: #f5f7fa;
border-radius: 8px;
.user-role {
font-size: 12px;
color: #909399;
margin-bottom: 6px;
}
.user-name {
font-size: 14px;
font-weight: 600;
color: #303133;
margin-bottom: 4px;
}
.user-dept {
font-size: 12px;
color: #606266;
}
}
}
}
}
.initial-state {
text-align: center;
padding: 60px 0;
color: #909399;
.search-icon {
font-size: 48px;
margin-bottom: 16px;
}
p {
font-size: 14px;
}
}
// 暗色模式适配
:deep(.el-card) {
--el-card-bg-color: var(--el-bg-color);
}
</style>