| app/api/task.js | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| app/config.js | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| app/pagesTask/detail.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| ruoyi-admin/src/main/java/com/ruoyi/web/controller/task/SysTaskController.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| ruoyi-system/src/main/java/com/ruoyi/system/domain/SysTaskAssignee.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| ruoyi-system/src/main/java/com/ruoyi/system/service/ISysTaskService.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysTaskServiceImpl.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| ruoyi-system/src/main/resources/mapper/system/SysTaskAssigneeMapper.xml | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| sql/add_assignee_ready_status.sql | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 |
app/api/task.js
@@ -149,3 +149,11 @@ method: 'get' }) } // 标记执行人就绪 export function setAssigneeReady(taskId) { return request({ url: '/task/' + taskId + '/assignee/ready', method: 'post' }) } app/config.js
@@ -59,5 +59,11 @@ url: "/pages/mine/user-agreement/index" } ] }, // 功能开关 features: { // 是否显示执行人“就绪”按钮 showAssigneeReadyButton: true } } app/pagesTask/detail.vue
@@ -59,6 +59,15 @@ > {{ getUserTypeLabel(assignee.userType) }} </view> <view class="ready-badge" :class="{ 'ready': isAssigneeReady(assignee), 'unready': !isAssigneeReady(assignee) }" > {{ isAssigneeReady(assignee) ? '已就绪' : '未就绪' }} </view> </view> </view> </view> @@ -362,18 +371,27 @@ > 修改 </button> <button class="action-btn primary" @click="handleTaskAction('depart')" > 出发 </button> <button class="action-btn cancel" @click="handleTaskAction('cancel')" > 取消 </button> <template v-if="isCurrentUserAssignee()"> <button v-if="showAssigneeReadyFeature() && isMultipleAssignees() && !isCurrentUserReady()" class="action-btn primary" @click="handleReadyAction()" > 就绪 </button> <button class="action-btn primary" @click="handleDepartAction()" > 出发 </button> <button class="action-btn cancel" @click="handleTaskAction('cancel')" > 取消 </button> </template> </template> <!-- 出发中状态: 显示编辑、已到达、强制结束 --> @@ -384,18 +402,20 @@ > 修改 </button> <button class="action-btn primary" @click="handleTaskAction('arrive')" > 已到达 </button> <button class="action-btn cancel" @click="handleTaskAction('forceCancel')" > 强制结束 </button> <template v-if="isCurrentUserAssignee()"> <button class="action-btn primary" @click="handleTaskAction('arrive')" > 已到达 </button> <button class="action-btn cancel" @click="handleTaskAction('forceCancel')" > 强制结束 </button> </template> </template> <!-- 已到达状态: 显示编辑、已返程 --> @@ -406,12 +426,14 @@ > 修改 </button> <button class="action-btn primary" @click="handleTaskAction('return')" > 已返程 </button> <template v-if="isCurrentUserAssignee()"> <button class="action-btn primary" @click="handleTaskAction('return')" > 已返程 </button> </template> </template> <!-- 返程中状态: 显示编辑、已完成 --> @@ -422,12 +444,14 @@ > 修改 </button> <button class="action-btn primary" @click="handleTaskAction('complete')" > 已完成 </button> <template v-if="isCurrentUserAssignee()"> <button class="action-btn primary" @click="handleTaskAction('complete')" > 已完成 </button> </template> </template> <!-- 已完成/已取消: 不显示按钮,但如果是转运任务则显示结算按钮 --> @@ -445,12 +469,13 @@ </template> <script> import { getTask, changeTaskStatus } from '@/api/task' import { getTask, changeTaskStatus, setAssigneeReady } from '@/api/task' import { checkVehicleActiveTasks } from '@/api/task' import { getPaymentInfo } from '@/api/payment' import { formatDateTime } from '@/utils/common' import { validateTaskForDepart, validateTaskForSettlement, getTaskVehicleId, checkTaskCanDepart } from '@/utils/taskValidator' import AttachmentUpload from './components/AttachmentUpload.vue' import config from '@/config' export default { components: { @@ -771,8 +796,7 @@ handleTaskAction(action) { switch (action) { case 'depart': // 出发 -> 检查车辆是否有其他正在进行中的任务 this.checkVehicleAndDepart(); this.ensureReadyThenDepart(); break; case 'cancel': @@ -1235,6 +1259,145 @@ // 附件删除成功回调 onAttachmentDeleted(attachmentId) { console.log('附件删除成功:', attachmentId) }, // 是否显示“就绪”功能(配置开关) showAssigneeReadyFeature() { return !!(config && config.features && config.features.showAssigneeReadyButton) }, // 当前用户是否为该执行人 isAssigneeSelf(assignee) { const userId = this.$store && this.$store.state && this.$store.state.user && this.$store.state.user.userId return assignee && (assignee.userId === userId || assignee.oaUserId === userId) }, // 执行人点击“就绪” markAssigneeReady(assignee) { if (!assignee || !this.taskDetail) { this.$modal.showToast('执行人或任务信息不存在') return } const userId = assignee.userId || assignee.oaUserId if (!userId) { this.$modal.showToast('无法识别执行人ID') return } this.$modal.showLoading && this.$modal.showLoading('提交中...') setAssigneeReady(this.taskId).then(() => { this.$modal.hideLoading && this.$modal.hideLoading() this.$modal.showToast('已就绪') // 刷新任务详情 this.loadTaskDetail() }).catch(err => { this.$modal.hideLoading && this.$modal.hideLoading() console.error('标记就绪失败:', err) this.$modal.showToast('标记就绪失败') }) }, // 是否当前用户是任务执行人 isCurrentUserAssignee() { const userId = this.$store && this.$store.state && this.$store.state.user && this.$store.state.user.userId; console.log("当前用户ID:", userId) const list = (this.taskDetail && Array.isArray(this.taskDetail.assignees)) ? this.taskDetail.assignees : [] return list.some(a => a && (a.userId === userId || a.oaUserId === userId)) }, // 是否多人执行 isMultipleAssignees() { const list = (this.taskDetail && Array.isArray(this.taskDetail.assignees)) ? this.taskDetail.assignees : [] return list.length > 1 }, // 执行人是否已就绪 isAssigneeReady(assignee) { if (!assignee) return false return assignee.isReady === '1' || assignee.ready === true || assignee.readyStatus === 'READY' || assignee.readyFlag === 'Y' }, // 所有执行人是否已就绪 areAllAssigneesReady() { const list = (this.taskDetail && Array.isArray(this.taskDetail.assignees)) ? this.taskDetail.assignees : [] if (list.length === 0) return false return list.every(a => this.isAssigneeReady(a)) }, // 获取当前用户对应的执行人记录 getCurrentUserAssignee() { const userId = this.$store && this.$store.state && this.$store.state.user && this.$store.state.user.userId console.log('userId', userId) const list = (this.taskDetail && Array.isArray(this.taskDetail.assignees)) ? this.taskDetail.assignees : [] return list.find(a => a && (a.userId === userId || a.oaUserId === userId)) || null }, // 操作区就绪按钮(多人任务) markCurrentAssigneeReady() { const me = this.getCurrentUserAssignee() if (!me) { this.$modal.showToast('仅任务执行人可操作') return } this.markAssigneeReady(me) }, // 当前用户是否已就绪 isCurrentUserReady() { const me = this.getCurrentUserAssignee() return me ? this.isAssigneeReady(me) : false }, // 处理就绪按钮点击 async handleReadyAction() { const me = this.getCurrentUserAssignee() if (!me) { this.$modal.showToast('仅任务执行人可操作') return } try { await setAssigneeReady(this.taskId) this.$modal.showToast('已就绪') // 刷新任务详情 await this.loadTaskDetail() } catch (err) { console.error('标记就绪失败:', err) this.$modal.showToast('标记就绪失败') } }, // 处理出发按钮点击 async handleDepartAction() { if (!this.taskDetail) return const list = (this.taskDetail && Array.isArray(this.taskDetail.assignees)) ? this.taskDetail.assignees : [] // 如果开启了就绪功能且是多人任务,需要检查所有人是否就绪 if (this.showAssigneeReadyFeature() && list.length > 1) { if (!this.areAllAssigneesReady()) { this.$modal.showToast('其他人未就绪,所有人就绪后才能出发') return } } // 单人任务或未开启就绪功能:自动标记就绪 if (this.showAssigneeReadyFeature() && list.length === 1) { const me = this.getCurrentUserAssignee() if (me && !this.isAssigneeReady(me)) { try { await setAssigneeReady(this.taskId) } catch (e) { console.error('自动就绪失败:', e) } } } // 执行出发流程 this.checkVehicleAndDepart() }, // 出发前保证就绪(保留向后兼容) async ensureReadyThenDepart() { this.handleDepartAction() } } } @@ -1383,6 +1546,31 @@ background-color: #AF52DE; } } .assignee-ready-btn { margin-left: 12rpx; padding: 8rpx 16rpx; font-size: 24rpx; border-radius: 6rpx; background-color: #34C759; color: #fff; border: none; } .ready-badge { display: inline-block; margin-left: 12rpx; padding: 4rpx 12rpx; font-size: 22rpx; border-radius: 6rpx; &.ready { background-color: #e6ffed; color: #34C759; } &.unready { background-color: #f0f0f0; color: #999; } } } } } ruoyi-admin/src/main/java/com/ruoyi/web/controller/task/SysTaskController.java
@@ -353,6 +353,24 @@ } /** * 执行人点击就绪(APP端) */ @PostMapping("/{taskId}/assignee/ready") public AjaxResult setAssigneeReady(@PathVariable Long taskId) { Long userId = getUserId(); return sysTaskService.setAssigneeReady(taskId, userId); } /** * 执行人取消就绪(APP端) */ @PostMapping("/{taskId}/assignee/cancel-ready") public AjaxResult cancelAssigneeReady(@PathVariable Long taskId) { Long userId = getUserId(); return sysTaskService.cancelAssigneeReady(taskId, userId); } /** * 分配任务请求对象 */ public static class AssignTaskRequest { ruoyi-system/src/main/java/com/ruoyi/system/domain/SysTaskAssignee.java
@@ -39,6 +39,15 @@ @Excel(name = "是否为主要执行人", readConverterExp = "0=否,1=是") private String isPrimary; /** 是否已就绪:0-未就绪,1-已就绪 */ @Excel(name = "是否已就绪", readConverterExp = "0=未就绪,1=已就绪") private String isReady; /** 就绪时间 */ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") @Excel(name = "就绪时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss") private Date readyTime; /** 排序顺序(用于确定领队) */ @Excel(name = "排序顺序") private Integer sortOrder; @@ -98,4 +107,20 @@ public void setSortOrder(Integer sortOrder) { this.sortOrder = sortOrder; } public String getIsReady() { return isReady; } public void setIsReady(String isReady) { this.isReady = isReady; } public Date getReadyTime() { return readyTime; } public void setReadyTime(Date readyTime) { this.readyTime = readyTime; } } ruoyi-system/src/main/java/com/ruoyi/system/service/ISysTaskService.java
@@ -284,4 +284,22 @@ */ public com.ruoyi.common.core.domain.AjaxResult checkTaskCanDepart(Long taskId); /** * 执行人点击就绪 * * @param taskId 任务ID * @param userId 用户ID * @return 结果 */ public com.ruoyi.common.core.domain.AjaxResult setAssigneeReady(Long taskId, Long userId); /** * 取消执行人就绪 * * @param taskId 任务ID * @param userId 用户ID * @return 结果 */ public com.ruoyi.common.core.domain.AjaxResult cancelAssigneeReady(Long taskId, Long userId); } ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysTaskServiceImpl.java
@@ -36,6 +36,7 @@ import com.ruoyi.system.service.ISysTaskEmergencyService; import com.ruoyi.system.service.ITaskAttachmentSyncService; import com.ruoyi.system.service.IMapService; import com.ruoyi.system.service.ISysConfigService; import com.ruoyi.system.event.TaskCreatedEvent; import com.ruoyi.system.event.TaskAssignedEvent; import com.ruoyi.system.event.TaskStatusChangedEvent; @@ -99,6 +100,9 @@ @Autowired(required = false) private IMapService mapService; @Autowired private ISysConfigService configService; /** * 查询任务管理 @@ -2344,6 +2348,22 @@ } } // 3. 检查执行人是否全部就绪(受配置开关控制) String readyCheckEnabled = configService.selectConfigByKey("task.assignee.ready.check.enabled"); if ("true".equalsIgnoreCase(readyCheckEnabled)) { assignees = task.getAssignees(); if (assignees != null && !assignees.isEmpty()) { boolean allReady = assignees.stream() .allMatch(a -> "1".equals(a.getIsReady())); if (!allReady) { Map<String, Object> conflict = new HashMap<>(); conflict.put("type", "assigneeReady"); conflict.put("message", "存在未就绪的执行人,请等待所有执行人点击就绪后再出车"); conflicts.add(conflict); } } } // 返回结果 Map<String, Object> result = new HashMap<>(); result.put("valid", conflicts.isEmpty()); @@ -2351,5 +2371,77 @@ return com.ruoyi.common.core.domain.AjaxResult.success(result); } /** * 执行人点击就绪 * * @param taskId 任务ID * @param userId 用户ID * @return 结果 */ @Override @Transactional public com.ruoyi.common.core.domain.AjaxResult setAssigneeReady(Long taskId, Long userId) { // 1. 查询执行人关联信息 List<SysTaskAssignee> assignees = sysTaskAssigneeMapper.selectSysTaskAssigneeByTaskId(taskId); SysTaskAssignee targetAssignee = assignees.stream() .filter(a -> a.getUserId().equals(userId)) .findFirst() .orElse(null); if (targetAssignee == null) { return com.ruoyi.common.core.domain.AjaxResult.error("您不是该任务的执行人"); } // 2. 更新就绪状态 targetAssignee.setIsReady("1"); targetAssignee.setReadyTime(new Date()); targetAssignee.setUpdateBy(SecurityUtils.getUsername()); targetAssignee.setUpdateTime(new Date()); sysTaskAssigneeMapper.updateSysTaskAssignee(targetAssignee); // 3. 检查是否所有执行人都已就绪 boolean allReady = assignees.stream() .allMatch(a -> a.getUserId().equals(userId) || "1".equals(a.getIsReady())); Map<String, Object> result = new HashMap<>(); result.put("allReady", allReady); result.put("message", "就绪成功"); return com.ruoyi.common.core.domain.AjaxResult.success(result); } /** * 取消执行人就绪 * * @param taskId 任务ID * @param userId 用户ID * @return 结果 */ @Override @Transactional public com.ruoyi.common.core.domain.AjaxResult cancelAssigneeReady(Long taskId, Long userId) { // 查询执行人关联信息 List<SysTaskAssignee> assignees = sysTaskAssigneeMapper.selectSysTaskAssigneeByTaskId(taskId); SysTaskAssignee targetAssignee = assignees.stream() .filter(a -> a.getUserId().equals(userId)) .findFirst() .orElse(null); if (targetAssignee == null) { return com.ruoyi.common.core.domain.AjaxResult.error("您不是该任务的执行人"); } // 更新就绪状态 targetAssignee.setIsReady("0"); targetAssignee.setReadyTime(null); targetAssignee.setUpdateBy(SecurityUtils.getUsername()); targetAssignee.setUpdateTime(new Date()); sysTaskAssigneeMapper.updateSysTaskAssignee(targetAssignee); return com.ruoyi.common.core.domain.AjaxResult.success("已取消就绪"); } } ruoyi-system/src/main/resources/mapper/system/SysTaskAssigneeMapper.xml
@@ -11,6 +11,8 @@ <result property="userName" column="user_name" /> <result property="userType" column="user_type" /> <result property="isPrimary" column="is_primary" /> <result property="isReady" column="is_ready" /> <result property="readyTime" column="ready_time" /> <result property="sortOrder" column="sort_order" /> <result property="createTime" column="create_time" /> <result property="createBy" column="create_by" /> @@ -19,7 +21,7 @@ </resultMap> <sql id="selectSysTaskAssigneeVo"> select id, task_id, user_id, user_name, user_type, is_primary, sort_order, create_time, create_by, update_time, update_by select id, task_id, user_id, user_name, user_type, is_primary, is_ready, ready_time, sort_order, create_time, create_by, update_time, update_by from sys_task_assignee </sql> @@ -78,6 +80,8 @@ <if test="userName != null and userName != ''">user_name = #{userName},</if> <if test="userType != null and userType != ''">user_type = #{userType},</if> <if test="isPrimary != null">is_primary = #{isPrimary},</if> <if test="isReady != null">is_ready = #{isReady},</if> <if test="readyTime != null">ready_time = #{readyTime},</if> <if test="sortOrder != null">sort_order = #{sortOrder},</if> <if test="updateTime != null">update_time = #{updateTime},</if> <if test="updateBy != null and updateBy != ''">update_by = #{updateBy},</if> sql/add_assignee_ready_status.sql
New file @@ -0,0 +1,11 @@ -- 为执行人表添加就绪状态字段 ALTER TABLE sys_task_assignee ADD COLUMN is_ready CHAR(1) DEFAULT '0' COMMENT '是否已就绪(0未就绪 1已就绪)' AFTER is_primary, ADD COLUMN ready_time DATETIME COMMENT '就绪时间' AFTER is_ready; -- 添加系统配置:是否启用执行人就绪检查 INSERT INTO sys_config (config_name, config_key, config_value, config_type, remark, create_by, create_time, update_by, update_time) VALUES ('执行人就绪检查开关', 'task.assignee.ready.check.enabled', 'false', 'Y', '是否启用执行人就绪检查功能(true启用,false关闭)', 'admin', NOW(), 'admin', NOW()); -- 为已存在的执行人数据设置默认值(可选) UPDATE sys_task_assignee SET is_ready = '0' WHERE is_ready IS NULL;