feat: 完善抢单权限控制、草稿状态编辑、群通知新模板等功能

- 后端: 子任务抢单权限校验(部门类型、任务类型限制)
- 后端: 子任务组驳回联动主任务逻辑
- 后端: 群通知新模板样式
- 后端: 个人钉钉通知自动绑定
- 前端: 抢单按钮状态判断优化
- 前端: 草稿状态显示及编辑功能
- 前端: 驳回状态样式
- 前端: 指定设计师功能增强
This commit is contained in:
Your Name 2026-01-03 18:31:49 +08:00
parent 64f3e25714
commit 17d3799870
12 changed files with 359 additions and 95 deletions

View File

@ -195,6 +195,26 @@ public class TaskItemController {
return Result.success(assignedReceiverId != null ? "子任务组已指派" : "子任务组已发布到大厅");
}
/**
* 驳回子任务组
*/
@Operation(summary = "驳回子任务组")
@PostMapping("/groups/reject")
public Result<String> rejectTaskGroup(
@RequestBody Map<String, Object> request,
@RequestHeader("Authorization") String token) {
String actualToken = token.replace("Bearer ", "");
Long rejectorId = jwtUtil.getUserIdFromToken(actualToken);
Long taskId = Long.valueOf(request.get("taskId").toString());
Integer groupNo = Integer.valueOf(request.get("groupNo").toString());
String reason = (String) request.get("reason");
taskItemService.rejectTaskGroup(taskId, groupNo, rejectorId, reason);
return Result.success("子任务组已驳回");
}
/**
* 获取待审核的子任务组列表
*/

View File

@ -134,17 +134,13 @@ public class UserController {
@RequestParam(required = false) Long deptId,
@RequestHeader("Authorization") String authHeader
) {
// 如果没有传入deptId,则根据当前用户角色判断
// 如果没有传入deptId,则默认为当前用户的部门
if (deptId == null) {
String token = jwtUtil.extractTokenFromHeader(authHeader);
Long userId = jwtUtil.getUserIdFromToken(token);
// 如果不是管理员,强制使用自己的部门ID
if (!userService.isAdminUser(userId)) {
User currentUser = userService.getUserById(userId);
if (currentUser != null) {
deptId = currentUser.getDeptId();
}
User currentUser = userService.getUserById(userId);
if (currentUser != null) {
deptId = currentUser.getDeptId();
}
}

View File

@ -74,5 +74,10 @@ public class PipelineTaskDTO {
* 排序
*/
private Integer sortOrder;
/**
* 指定接收人ID
*/
private Long receiverId;
}
}

View File

@ -154,6 +154,46 @@ public class DingTalkService {
}
}
/**
* 根据手机号获取userId
*/
public String getUserIdByMobile(String mobile) {
DingTalkConfig config = getConfig();
if (config == null) return null;
String accessToken = getAccessToken(config);
try {
String url = String.format("%s/topapi/v2/user/getbymobile?access_token=%s",
DINGTALK_API_BASE, accessToken);
Map<String, Object> request = new HashMap<>();
request.put("mobile", mobile);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<Map<String, Object>> entity = new HttpEntity<>(request, headers);
ResponseEntity<Map> response = restTemplate.postForEntity(url, entity, Map.class);
Map<String, Object> body = response.getBody();
if (body != null) {
Object errcode = body.get("errcode");
if (errcode != null && (errcode.equals(0) || errcode.equals(0L) || "0".equals(errcode.toString()))) {
Map<String, Object> result = (Map<String, Object>) body.get("result");
return (String) result.get("userid");
} else {
log.warn("根据手机号获取userId失败: mobile={}, errcode={}, errmsg={}",
mobile, errcode, body.get("errmsg"));
}
}
return null;
} catch (Exception e) {
log.error("根据手机号获取userId异常", e);
return null;
}
}
/**
* 发送工作通知
*/

View File

@ -187,9 +187,24 @@ public class NotificationService {
// 检查用户是否绑定了钉钉ID
if (receiver.getDingUserId() == null || receiver.getDingUserId().isEmpty()) {
log.info("用户未绑定钉钉ID,跳过推送: userId={}, username={}",
receiver.getId(), receiver.getUsername());
return;
// 尝试通过手机号自动获取并绑定
if (org.springframework.util.StringUtils.hasText(receiver.getPhone())) {
String dingUserId = dingTalkService.getUserIdByMobile(receiver.getPhone());
if (dingUserId != null) {
receiver.setDingUserId(dingUserId);
userMapper.updateById(receiver);
log.info("通过手机号自动绑定钉钉ID: userId={}, phone={}, dingUserId={}",
receiver.getId(), receiver.getPhone(), dingUserId);
} else {
log.info("用户未绑定钉钉ID且无法通过手机号获取,跳过推送: userId={}, phone={}",
receiver.getId(), receiver.getPhone());
return;
}
} else {
log.info("用户未绑定钉钉ID且无手机号,跳过推送: userId={}, username={}",
receiver.getId(), receiver.getUsername());
return;
}
}
// 构建钉钉消息内容

View File

@ -275,25 +275,26 @@ public class TaskApprovalService {
log.info("任务【{}】已发布到任务大厅,准备发送钉钉群通知", task.getTitle());
// 任务发布到大厅,发送钉钉群机器人通知
try {
String title = "🎯 新任务发布";
String title = "新任务发布";
StringBuilder content = new StringBuilder();
content.append("**任务标题:**").append(task.getTitle()).append("\n\n");
content.append("**任务编号:**").append(task.getTaskNo()).append("\n\n");
content.append("**积分:**").append(task.getScore()).append(" 分\n\n");
content.append("--------------------------------\n");
content.append(" 🎯 新 任 务 发 布 \n");
content.append("--------------------------------\n\n");
content.append("任务:").append(task.getTitle()).append("\n");
content.append("编号:").append(task.getTaskNo()).append("\n");
content.append("积分:").append(task.getScore()).append(" 分\n");
if (task.getBrandName() != null) {
content.append("**品牌:**").append(task.getBrandName()).append("\n\n");
content.append("品牌:").append(task.getBrandName()).append("\n");
}
if (task.getCategoryName() != null) {
content.append("**类目:**").append(task.getCategoryName()).append("\n\n");
}
if (task.getPlatformName() != null) {
content.append("**平台:**").append(task.getPlatformName()).append("\n\n");
content.append("类目:").append(task.getCategoryName()).append("\n");
}
content.append("**状态:**待接单\n\n");
content.append("---\n\n");
content.append("💡 赶快去任务大厅抢单吧!");
content.append("状态:待接单\n\n");
content.append("--------------------------------\n");
content.append(" 💡 请前往任务大厅抢单 \n");
content.append("--------------------------------");
log.info("准备调用钉钉机器人发送消息,title={}", title);
boolean success = dingTalkService.sendRobotMessage(title, content.toString());

View File

@ -34,6 +34,7 @@ public class TaskItemService {
private final UserService userService;
private final NotificationService notificationService;
private final DingTalkService dingTalkService;
private final ConfigService configService;
/**
* 获取可接单的子任务列表
@ -111,6 +112,50 @@ public class TaskItemService {
throw new BusinessException(ResultCode.NOT_FOUND, "子任务不存在");
}
// 验证用户部门权限
if (user.getDeptId() == null) {
throw new BusinessException(ResultCode.FORBIDDEN, "您未分配部门,无法接单");
}
com.designhub.entity.Department department = departmentMapper.selectById(user.getDeptId());
if (department == null) {
throw new BusinessException(ResultCode.FORBIDDEN, "未找到用户所属部门,请联系管理员");
}
// 检查1:需求部门不能抢单
if ("demand".equals(department.getDeptType())) {
throw new BusinessException(ResultCode.FORBIDDEN, "需求部门成员不能接单,只能发起需求");
}
// 检查2:接单部门类型验证
if (!"receiving".equals(department.getDeptType())) {
throw new BusinessException(ResultCode.FORBIDDEN, "只有接单部门成员可以接收任务");
}
// 检查3:部门负责人不能抢单
if (department.getLeaderId() != null && department.getLeaderId().equals(userId)) {
throw new BusinessException(ResultCode.FORBIDDEN, "部门负责人不能接单,请分配给设计师");
}
// 检查4:验证部门是否允许接该任务类型
if (item.getTaskTypeId() != null) {
com.designhub.entity.DemandType demandType = configService.getDemandTypeById(item.getTaskTypeId());
if (demandType != null && demandType.getAllowedDeptIds() != null && !demandType.getAllowedDeptIds().isEmpty()) {
// 检查用户部门是否在允许列表中
String[] allowedIds = demandType.getAllowedDeptIds().split(",");
boolean isAllowed = false;
for (String allowedId : allowedIds) {
if (allowedId.trim().equals(String.valueOf(user.getDeptId()))) {
isAllowed = true;
break;
}
}
if (!isAllowed) {
throw new BusinessException(ResultCode.FORBIDDEN, "您的部门无权接收此类型的任务");
}
}
}
// 验证状态
if (!"pending".equals(item.getStatus())) {
throw new BusinessException(ResultCode.TASK_STATUS_ERROR, "该子任务不可接单");
@ -471,20 +516,23 @@ public class TaskItemService {
try {
String title = "🎯 新子任务组发布";
StringBuilder content = new StringBuilder();
content.append("**主任务:**").append(mainTask.getTitle()).append("\n\n");
content.append("**子任务组:**第 ").append(groupNo).append(" 组\n\n");
content.append("**子任务数量:**").append(groupItems.size()).append(" 个\n\n");
content.append("--------------------------------\n");
content.append(" 🎯 新 子 任 务 组 \n");
content.append("--------------------------------\n\n");
content.append("主任务:").append(mainTask.getTitle()).append("\n");
content.append("任务组:第 ").append(groupNo).append(" 组\n");
content.append("数量:").append(groupItems.size()).append(" 个\n");
// 列出子任务类型
content.append("**任务类型:**\n\n");
content.append("类型:\n");
for (TaskItem item : groupItems) {
content.append("- ").append(item.getItemNo())
.append("(积分:").append(item.getTotalScore()).append(")\n\n");
.append(" (").append(item.getTotalScore()).append("分)\n");
}
content.append("**状态:**待接单\n\n");
content.append("---\n\n");
content.append("💡 赶快去任务大厅抢单吧!");
content.append("状态:待接单\n");
content.append("\n💡 赶快去任务大厅抢单吧!");
dingTalkService.sendRobotMessage(title, content.toString());
log.info("第 {} 组 {} 个子任务已发布到任务大厅,已发送钉钉群通知", groupNo, groupItems.size());
@ -495,6 +543,60 @@ public class TaskItemService {
}
}
/**
* 驳回子任务组(接单部门负责人审核不通过)
* 逻辑:驳回该组所有子任务,并将主任务及其所有子任务设为驳回状态
*/
@Transactional(rollbackFor = Exception.class)
public void rejectTaskGroup(Long taskId, Integer groupNo, Long rejectorId, String reason) {
log.info("接单部门负责人 {} 驳回任务 {} 的第 {} 组子任务,原因:{}", rejectorId, taskId, groupNo, reason);
// 1. 验证主任务
Task mainTask = taskMapper.selectById(taskId);
if (mainTask == null) {
throw new BusinessException(ResultCode.NOT_FOUND, "主任务不存在");
}
// 2. 验证审核人权限(必须是接单部门负责人)
if (mainTask.getReceiverDeptId() == null) {
throw new BusinessException(ResultCode.FORBIDDEN, "任务未指定接单部门");
}
com.designhub.entity.Department receiverDept = departmentMapper.selectById(mainTask.getReceiverDeptId());
if (receiverDept == null || !rejectorId.equals(receiverDept.getLeaderId())) {
throw new BusinessException(ResultCode.FORBIDDEN, "只有接单部门负责人可以驳回子任务组");
}
// 3. 更新主任务状态为 rejected
mainTask.setStatus("rejected");
mainTask.setRejectReason(reason);
mainTask.setUpdateTime(LocalDateTime.now());
taskMapper.updateById(mainTask);
// 4. 更新该任务下所有子任务状态为 rejected
taskItemMapper.update(null,
new com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper<TaskItem>()
.eq(TaskItem::getTaskId, taskId)
.set(TaskItem::getStatus, "rejected")
.set(TaskItem::getUpdateTime, LocalDateTime.now())
);
log.info("任务 {} 及其所有子任务已被驳回", taskId);
// 5. 发送通知给任务发布者
if (mainTask.getPublisherId() != null) {
Notification notification = new Notification();
notification.setReceiverId(mainTask.getPublisherId());
notification.setNotificationType("task_rejected");
notification.setTitle("任务被驳回");
notification.setContent("您的任务《" + mainTask.getTitle() + "》在第 " + groupNo + " 组子任务审核时被驳回。原因:" + reason + "。请重新编辑并提交。");
notification.setRelatedId(taskId);
notification.setRelatedType("task");
notification.setPriority(2); // 重要
notification.setPushDingtalk(1); // 推送到钉钉
notificationService.sendNotification(notification);
}
}
/**
* 获取待审核的子任务组列表(按主任务分组)
* @param receiverDeptId 接单部门ID
@ -518,6 +620,7 @@ public class TaskItemService {
groupInfo.put("taskTitle", mainTask.getTitle());
groupInfo.put("taskNo", mainTask.getTaskNo());
groupInfo.put("groupNo", item.getGroupNo());
groupInfo.put("groupId", item.getGroupNo()); // 前端使用groupId作为唯一标识
groupInfo.put("itemCount", 1);
groupInfo.put("createTime", item.getCreateTime());
groupMap.put(key, groupInfo);

View File

@ -382,11 +382,25 @@ public class TaskService {
item.setSortOrder(i + 1); // 恢复真实的排序号
item.setProgress(0);
// 设置子任务指定的接收人
if (itemDto.getReceiverId() != null) {
item.setReceiverId(itemDto.getReceiverId());
// 获取接收人名字
User receiver = userService.getUserById(itemDto.getReceiverId());
if (receiver != null) {
item.setReceiverName(receiver.getRealName());
}
}
// 关键:根据流水线模式和分组设置初始状态
if ("sequential".equals(dto.getPipelineMode())) {
if (itemGroupNo == 1) {
// 顺序流水线:第一组的所有子任务都是 pending(可接单)
item.setStatus("pending");
// 顺序流水线:第一组
if (item.getReceiverId() != null) {
item.setStatus("in_progress"); // 指定了人,直接进行中
} else {
item.setStatus("pending"); // 没指定人,待接单
}
item.setIsLocked(0);
item.setPrevItemId(null);
} else {
@ -395,8 +409,12 @@ public class TaskService {
item.setIsLocked(1);
}
} else {
// 并行流水线:所有子任务都是 pending(可接单)
item.setStatus("pending");
// 并行流水线
if (item.getReceiverId() != null) {
item.setStatus("in_progress");
} else {
item.setStatus("pending");
}
item.setIsLocked(0);
}

View File

@ -112,6 +112,7 @@ export interface PipelineTaskRequest {
taskTypeId: number
quantity: number
sortOrder: number
receiverId?: number // 指定接收人
}>
brandId?: number
categoryId?: number
@ -561,7 +562,7 @@ export interface TaskGroup {
* 获取待审核的子任务组列表
*/
export const getPendingApprovalTaskGroups = () => {
return request.get<TaskGroup[]>('/tasks/items/pending-approval-groups')
return request.get<TaskGroup[]>('/tasks/items/groups/pending-approval')
}
/**
@ -574,7 +575,7 @@ export const approveTaskGroup = (groupId: number, data: { assignedReceiverId?: n
/**
* 驳回子任务组
*/
export const rejectTaskGroup = (groupId: number, reason: string) => {
return request.post<any>(`/tasks/items/groups/${groupId}/reject`, { reason })
export const rejectTaskGroup = (taskId: number, groupNo: number, reason: string) => {
return request.post<any>('/tasks/items/groups/reject', { taskId, groupNo, reason })
}

View File

@ -261,25 +261,31 @@ onUnmounted(() => {
display: flex;
align-items: center;
justify-content: center;
color: rgba(255, 255, 255, 0.75);
color: #ffffff !important; /* 强制白色 */
cursor: pointer;
position: relative;
transition: all $transition-base $ease-out;
&:hover {
background: $bg-card;
color: rgba(255, 255, 255, 0.95);
background: rgba(255, 255, 255, 0.1);
color: #ffffff !important;
}
// 确保Icon组件继承颜色
:deep(.app-icon) {
color: inherit;
color: #ffffff !important; /* 强制白色 */
}
// 确保SVG元素继承颜色
// 强制覆盖SVG元素及其路径的颜色
:deep(svg) {
color: inherit;
stroke: currentColor;
color: #ffffff !important;
stroke: #ffffff !important;
fill: none !important;
}
:deep(path) {
stroke: #ffffff !important;
fill: none !important;
}
}

View File

@ -78,6 +78,27 @@
@change="calculateScore"
/>
</div>
<div v-if="formData.releaseType === 'assigned'" class="task-type-receiver">
<a-select
v-model:value="item.receiverId"
placeholder="指定设计师"
style="width: 200px"
show-search
allow-clear
size="large"
:filter-option="filterOption"
@focus="loadDesignersForItem(item)"
>
<a-select-option
v-for="designer in getDesignersForItem(item)"
:key="designer.id"
:value="designer.id"
>
{{ designer.realName }} (进行中: {{ designer.inProgressCount || 0 }})
</a-select-option>
</a-select>
</div>
<div class="task-type-actions">
<a-button
v-if="formData.taskTypes.length > 1"
@ -293,8 +314,7 @@
</div>
</button>
<!-- 招募任务和指定任务模式暂时隐藏 -->
<!--
<!-- 招募任务和指定任务模式 -->
<button
type="button"
:class="['release-type-button', { active: formData.releaseType === 'recruitment' }]"
@ -328,33 +348,9 @@
<Icon name="checkCircle" size="sm" />
</div>
</button>
-->
</div>
</a-form-item>
<!-- 指定接收人(仅当选择指定任务时显示) -->
<a-form-item
v-if="formData.releaseType === 'assigned'"
label="指定接收人"
name="receiverId"
required
>
<a-select
v-model:value="formData.receiverId"
placeholder="请选择接收人"
size="large"
show-search
:filter-option="filterOption"
>
<a-select-option
v-for="designer in designers"
:key="designer.id"
:value="designer.id"
>
{{ designer.name }} ({{ designer.deptName }})
</a-select-option>
</a-select>
</a-form-item>
<!-- 是否紧急 -->
<a-form-item label="优先级">
@ -396,6 +392,7 @@ import { message } from 'ant-design-vue'
import type { UploadFile, UploadProps } from 'ant-design-vue'
import DefaultLayout from '@/layouts/DefaultLayout.vue'
import Icon from '@/components/Icon.vue'
import * as userApi from '@/api/user'
import * as taskApi from '@/api/task'
import * as configApi from '@/api/config'
import * as uploadApi from '@/api/upload'
@ -432,14 +429,18 @@ const uploadProgress = ref(0)
interface TaskTypeItem {
taskTypeId: number | undefined
quantity: number
receiverId?: number // 指定接收人ID
}
// 设计师列表缓存 { deptId: designers[] }
const designersCache = reactive<Record<number, any[]>>({})
// 表单数据
const formData = reactive({
title: '',
isPipeline: true, // 固定为流水线任务
pipelineMode: 'sequential' as 'sequential' | 'parallel', // 固定为顺序流水线
taskTypes: [{ taskTypeId: undefined, quantity: 1 }] as TaskTypeItem[], // 任务类型数组,每个都有独立的数量
taskTypes: [{ taskTypeId: undefined, quantity: 1, receiverId: undefined }] as TaskTypeItem[], // 任务类型数组
taskSubType: '', // 动态加载,初始为空
brandId: undefined,
platformId: undefined,
@ -447,7 +448,7 @@ const formData = reactive({
spuInfo: '',
attachmentUrl: '',
releaseType: 'default',
receiverId: undefined,
// receiverId: undefined, // 已移除,改用子任务指定
isUrgent: false
})
@ -492,18 +493,51 @@ const brands = computed(() => configStore.brands)
const platforms = computed(() => configStore.platforms)
const categories = computed(() => configStore.categories)
// 设计师列表(TODO: 从用户API加载)
const designers = ref([
{ id: 1, name: '张设计', deptName: '设计部' },
{ id: 2, name: '李设计', deptName: '设计部' },
{ id: 3, name: '王设计', deptName: '设计部' }
])
// 加载设计师列表
const loadDesignersForItem = async (item: TaskTypeItem) => {
if (!item.taskTypeId) return
const typeInfo = taskTypes.value.find(t => t.id === item.taskTypeId)
if (!typeInfo || !typeInfo.allowedDeptIds) return
// 简化处理:取第一个允许的部门ID
const deptIds = typeInfo.allowedDeptIds.split(',')
if (deptIds.length === 0) return
const deptId = Number(deptIds[0])
if (!designersCache[deptId]) {
try {
const res = await userApi.getDesignersWithWorkload(deptId)
if (res.code === 200) {
designersCache[deptId] = res.data
}
} catch (error) {
console.error('加载设计师失败:', error)
}
}
}
// 获取当前任务类型对应的设计师列表
const getDesignersForItem = (item: TaskTypeItem) => {
if (!item.taskTypeId) return []
const typeInfo = taskTypes.value.find(t => t.id === item.taskTypeId)
if (!typeInfo || !typeInfo.allowedDeptIds) return []
const deptIds = typeInfo.allowedDeptIds.split(',')
if (deptIds.length === 0) return []
const deptId = Number(deptIds[0])
return designersCache[deptId] || []
}
// 添加任务类型
const addTaskType = () => {
formData.taskTypes.push({
taskTypeId: undefined,
quantity: 1
quantity: 1,
receiverId: undefined
})
}
@ -668,9 +702,12 @@ const handleSubmit = async () => {
}
// 检查指定接收人
if (formData.releaseType === 'assigned' && !formData.receiverId) {
message.error('请选择接收人')
return
if (formData.releaseType === 'assigned') {
const missingReceiver = validTaskTypes.some(item => !item.receiverId)
if (missingReceiver) {
message.error('指定模式下,所有任务类型都必须选择接收人')
return
}
}
submitting.value = true
@ -727,9 +764,10 @@ const handleSubmit = async () => {
isPipeline: true,
pipelineMode: 'sequential', // 固定为顺序流水线
items: validTaskTypes.map((item, index) => ({
taskTypeId: item.taskTypeId,
taskTypeId: item.taskTypeId!,
quantity: item.quantity,
sortOrder: index + 1
sortOrder: index + 1,
receiverId: formData.releaseType === 'assigned' ? item.receiverId : undefined
})),
brandId: formData.brandId,
platformId: formData.platformId,
@ -740,7 +778,7 @@ const handleSubmit = async () => {
remark: '',
priority: formData.isUrgent ? 3 : 1, // 0-低 1-普通 2-高 3-紧急
releaseType: formData.releaseType, // 发布类型
receiverId: formData.releaseType === 'assigned' ? formData.receiverId : undefined // assigned模式时传递接收人ID
receiverId: undefined // 主任务不设接收人,由子任务指定
}
const response = await taskApi.createPipelineTask(pipelineData)

View File

@ -1568,8 +1568,8 @@
</div>
</div>
<!-- 被驳回任务的操作按钮 -->
<div v-if="task.status === 'rejected'" class="task-actions" style="margin-top: 16px; padding-top: 16px; border-top: 1px solid rgba(49, 240, 139, 0.2);">
<!-- 被驳回或草稿任务的操作按钮 -->
<div v-if="['rejected', 'draft'].includes(task.status)" class="task-actions" style="margin-top: 16px; padding-top: 16px; border-top: 1px solid rgba(49, 240, 139, 0.2);">
<a-button
type="primary"
size="small"
@ -2417,7 +2417,7 @@ const loadTasks = async () => {
...item,
id: item.id, // 保持子任务的ID
itemId: item.id, // 额外保存子任务ID
title: item.taskTitle || item.title, // 使用主任务标题
title: item.mainTaskTitle || item.taskTitle || item.title, // 使用主任务标题
subTaskNo: item.itemNo, // 子任务编号
isSubTask: true, // 标记为子任务
taskId: item.taskId, // 保存主任务ID
@ -2998,7 +2998,7 @@ const handleRejectTaskGroup = async (item: ApprovalItem) => {
try {
approvalLoading.value = true
await taskApi.rejectTaskGroup(item.id, rejectReason.trim())
await taskApi.rejectTaskGroup(item.data.taskId, item.data.groupNo, rejectReason.trim())
message.success('已驳回子任务组')
// 刷新列表和数量
@ -3176,7 +3176,7 @@ const loadInProgressTasks = async () => {
...item,
id: item.id, // 保持子任务的ID
itemId: item.id, // 额外保存子任务ID
title: item.taskTitle || item.title, // 使用主任务标题
title: item.mainTaskTitle || item.taskTitle || item.title, // 使用主任务标题
subTaskNo: item.itemNo, // 子任务编号
isSubTask: true, // 标记为子任务
taskId: item.taskId, // 保存主任务ID
@ -3580,9 +3580,17 @@ const canGrabTask = (task: TaskOrSubTask) => {
}
// 检查4:验证部门是否允许接该需求类型(如果任务有需求类型限制)
if (task.demandTypeId) {
// 这里需要检查任务的需求类型是否允许当前用户的部门接单
// 由于前端没有完整的需求类型配置,暂时信任后端验证
const taskTypeId = task.isSubTask ? task.taskTypeId : (task as any).taskTypeId
if (taskTypeId) {
// 从配置store中获取任务类型详情
const demandType = configStore.demandTypes.find(t => t.id === taskTypeId)
if (demandType && demandType.allowedDeptIds) {
const allowedIds = demandType.allowedDeptIds.split(',')
// 检查当前用户部门ID是否在允许列表中
if (userInfo.deptId && !allowedIds.includes(String(userInfo.deptId))) {
return { canGrab: false, reason: '您的部门无权接收此类型的任务' }
}
}
}
return { canGrab: true, reason: '' }
@ -3652,7 +3660,8 @@ const getTaskStatusText = (status: string) => {
'review': '待审核',
'completed': '已完成',
'rejected': '已驳回',
'cancelled': '已取消'
'cancelled': '已取消',
'draft': '草稿'
}
return statusMap[status] || status
}
@ -4413,6 +4422,18 @@ onUnmounted(() => {
}
// badge样式补充 - 统一使用绿色主题
.status-draft {
background: rgba(250, 173, 20, 0.1);
color: #faad14;
border: 1px solid rgba(250, 173, 20, 0.2);
}
.status-rejected {
background: rgba(245, 34, 45, 0.1);
color: #f5222d;
border: 1px solid rgba(245, 34, 45, 0.2);
}
.badge-in-progress {
background: rgba(49, 240, 139, 0.1);
color: #31f08b;