From 4a7f54b0edbb65d903d5f1c0160a35f56639203a Mon Sep 17 00:00:00 2001 From: hongawen <83944980@qq.com> Date: Mon, 22 Jun 2026 19:28:39 +0800 Subject: [PATCH] =?UTF-8?q?refactor(projects):=201=20=E5=B7=A5=E4=BD=9C?= =?UTF-8?q?=E5=8F=B0=E7=BC=93=E5=AD=98=E4=BC=98=E5=8C=96=EF=BC=9B2=20?= =?UTF-8?q?=E5=B7=A5=E4=BD=9C=E6=97=A5=E5=BF=97=E6=80=BB=E5=B7=A5=E6=97=B6?= =?UTF-8?q?=E4=B8=8B=E8=BF=9B=E5=BA=A6=E6=98=BE=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../_builtin/login/modules/pwd-login.vue | 3 - .../execution/modules/task-operate-dialog.vue | 154 ++++++++++++------ .../modules/task-worklog-content.vue | 71 +++++++- .../execution/modules/task-workspace.vue | 73 +++++++-- .../modules/workbench-my-execution.vue | 5 +- .../modules/workbench-project-grid.vue | 5 +- .../workbench/modules/workbench-team-load.vue | 5 +- .../modules/workbench-todo-panel.vue | 12 +- 8 files changed, 242 insertions(+), 86 deletions(-) diff --git a/src/views/_builtin/login/modules/pwd-login.vue b/src/views/_builtin/login/modules/pwd-login.vue index a646837..c3c655d 100644 --- a/src/views/_builtin/login/modules/pwd-login.vue +++ b/src/views/_builtin/login/modules/pwd-login.vue @@ -56,9 +56,6 @@ async function handleSubmit() { -
- {{ $t('page.login.pwdLogin.rememberMe') }} -
(), { defaultParentTaskId: null, + canManageAssignee: false, plannedEndShortcuts: () => [ { text: '三天', days: 3 }, { text: '一星期', days: 7 }, @@ -94,6 +97,56 @@ const dialogTitle = computed(() => { const selectableParentTasks = computed(() => props.taskOptions.filter(item => item.id !== props.rowData?.id)); +/** 编辑态 + 任务已开始(statusCode 离开 pending)→ 负责人锁定不可切换 */ +const ownerLocked = computed(() => { + const status = props.rowData?.statusCode; + return props.mode === 'edit' && Boolean(status) && status !== 'pending'; +}); + +/** + * 负责人下拉选项 = 执行协办人池 ∪ 当前任务负责人(兜底)。 + * owner 可能已不在执行协办人池里,用任务自带 ownerNickname 兜底回显,避免锁定态显示成裸 userId。 + */ +const ownerSelectOptions = computed(() => { + const ownerId = props.rowData?.ownerId; + if (props.mode === 'create' || !ownerId || props.userOptions.some(item => item.id === ownerId)) { + return props.userOptions; + } + return [...props.userOptions, { id: ownerId, nickname: props.rowData?.ownerNickname || ownerId }]; +}); + +/** 编辑态无协办人管理权限时只读回显(create 恒可交互) */ +const assigneeSelectDisabled = computed(() => props.mode !== 'create' && !props.canManageAssignee); + +const assigneePlaceholder = computed(() => (assigneeSelectDisabled.value ? '暂无协办人' : '请选择协办人')); + +/** + * 候选项 = 执行协办人池 ∪ 当前任务已有协办人中已不在池里的成员 ∪ 当前任务负责人。 + * 已有协办人用任务自带 nickname 兜底回显,避免协办人被移出执行后在编辑态显示成裸 userId; + * 负责人并入是为了让 owner 在协办人下拉里以「负责人」标记展示(不可移除),用 ownerNickname 兜底。 + */ +const assigneeSelectOptions = computed(() => { + const options = props.userOptions.map(item => ({ id: item.id, nickname: item.nickname })); + const known = new Set(options.map(item => item.id)); + + if (props.mode !== 'create' && props.rowData) { + props.rowData.assignees?.forEach(assignee => { + if (assignee.userId && !known.has(assignee.userId)) { + options.push({ id: assignee.userId, nickname: assignee.nickname || assignee.userId }); + known.add(assignee.userId); + } + }); + + const ownerId = props.rowData.ownerId; + if (ownerId && !known.has(ownerId)) { + options.push({ id: ownerId, nickname: props.rowData.ownerNickname || ownerId }); + known.add(ownerId); + } + } + + return options; +}); + /** 左栏容器 ref:用其高度动态驱动右侧富文本,让两栏视觉等高 */ const leftColRef = ref(); const editorHeight = ref('45vh'); @@ -212,14 +265,11 @@ function normalizeAssigneeIds(ids: string[]) { const autoOwnerAssigneeId = ref(null); /** - * UI 层把 owner 也加进 model.assigneeUserIds,让协办人 select 视觉上显示 owner - * (体验上让用户感知"负责人也在团队里")。提交时由 normalizeAssigneeIds 过滤掉 owner。 + * UI 层把 owner 也并进 model.assigneeUserIds,让协办人 select 视觉上显示 owner(带「负责人」标记、不可移除), + * 让用户感知"负责人也在团队里"。create / edit 同口径;提交时由 normalizeAssigneeIds 过滤掉 owner。 + * 切换 owner 时按 previousOwnerId(上一任已并入的 owner)移除旧值,避免旧负责人残留在协办人里。 */ function syncOwnerAssignee(ownerId: string | null, previousOwnerId: string | null = autoOwnerAssigneeId.value) { - if (props.mode !== 'create') { - return; - } - const current = Array.from(new Set((model.assigneeUserIds ?? []).filter(Boolean))); const withoutPrevious = previousOwnerId ? current.filter(userId => userId !== previousOwnerId) : current; model.assigneeUserIds = ownerId ? Array.from(new Set([...withoutPrevious, ownerId])) : withoutPrevious; @@ -246,17 +296,16 @@ async function handleConfirm() { attachments: [...model.attachments] }; - if (props.mode === 'create') { - payload.assigneeUserIds = normalizeAssigneeIds(model.assigneeUserIds); - } + // create:作为创建入参直接生效;edit:父级据此与原协办人 diff 出加入/失效调用(更新接口本身忽略此字段) + payload.assigneeUserIds = normalizeAssigneeIds(model.assigneeUserIds); emit('submit', payload); } function handleAssigneeChange(value: string[]) { - // UI 层保持 owner 不掉队;提交时再由 normalizeAssigneeIds 过滤 + // UI 层保持 owner 不掉队(owner 选项 disabled,正常无法取消,这里兜底);提交时再由 normalizeAssigneeIds 过滤 const cleaned = Array.from(new Set(value.filter(Boolean))); - if (props.mode === 'create' && model.ownerId && !cleaned.includes(model.ownerId)) { + if (model.ownerId && !cleaned.includes(model.ownerId)) { cleaned.push(model.ownerId); } model.assigneeUserIds = cleaned; @@ -276,7 +325,10 @@ function applyRowDataToModel() { const row = props.rowData; model.parentTaskId = props.mode === 'create' ? (props.defaultParentTaskId ?? null) : row?.parentTaskId || null; applyBasicFieldsFromRow(row); - model.assigneeUserIds = []; + // 编辑态回显任务现有协办人 + 并入负责人(owner 仅做视觉展示,提交时过滤);创建态留空 + const baseAssignees = props.mode === 'create' ? [] : (row?.assignees ?? []).map(item => item.userId); + model.assigneeUserIds = + model.ownerId && !baseAssignees.includes(model.ownerId) ? [...baseAssignees, model.ownerId] : baseAssignees; model.attachments = row?.attachments ? [...row.attachments] : []; } @@ -288,7 +340,8 @@ watch( } applyRowDataToModel(); - autoOwnerAssigneeId.value = null; + // 记录已自动并入协办人列表的 owner,供后续切换负责人时移除上一任 + autoOwnerAssigneeId.value = model.ownerId || null; await nextTick(); // 让附件组件把当前 model 视作 original,必须在 model 填充之后 @@ -300,8 +353,9 @@ watch( watch( () => model.ownerId, - (ownerId, previousOwnerId) => { - syncOwnerAssignee(ownerId || null, previousOwnerId || null); + ownerId => { + // previousOwnerId 取 autoOwnerAssigneeId(上一任已并入的 owner),避免跨任务残留误删协办人 + syncOwnerAssignee(ownerId || null); } ); @@ -365,35 +419,33 @@ defineExpose({ - - - - - - - - - - @@ -325,9 +361,28 @@ watch( } } +.task-worklog-content__hours-detail-meta { + display: inline-flex; + align-items: baseline; + gap: 4px; + flex: 0 0 auto; +} + .task-worklog-content__hours-detail-hours { flex: 0 0 auto; color: var(--el-color-primary); font-weight: 500; } + +.task-worklog-content__hours-detail-sep { + color: var(--el-text-color-placeholder); +} + +.task-worklog-content__hours-detail-progress { + color: var(--el-text-color-secondary); + + &.is-empty { + color: var(--el-text-color-placeholder); + } +} diff --git a/src/views/project/project/execution/modules/task-workspace.vue b/src/views/project/project/execution/modules/task-workspace.vue index b9713aa..1a2db86 100644 --- a/src/views/project/project/execution/modules/task-workspace.vue +++ b/src/views/project/project/execution/modules/task-workspace.vue @@ -116,6 +116,14 @@ const currentAssigneeTask = ref(null); const currentAssignees = ref([]); const assigneesLoading = ref(false); +/** 编辑弹层内联失效协办人时使用的默认原因(无提示,统一审计文案) */ +const TASK_ASSIGNEE_INLINE_INACTIVE_REASON = '编辑任务时调整协办人'; + +/** 编辑弹层是否允许内联管理协办人(与列表「协办人」按钮同权限口径) */ +const canManageCurrentTaskAssignee = computed(() => + currentTask.value ? canManageTaskAssignee(currentTask.value) : false +); + const worklogDialogVisible = ref(false); const worklogDialogTask = ref(null); @@ -466,25 +474,61 @@ async function handleStatusAction(row: Api.Project.ProjectTask, action: TaskStat statusActionVisible.value = true; } +/** + * 编辑态把弹层选中的协办人与任务原协办人做 diff,翻译成加入 / 失效调用。 + * 更新任务接口本身忽略 assigneeUserIds,协办人只能走独立端点;失效用默认原因、无提示。 + * 返回是否全部成功(含无变更)。 + */ +async function applyTaskAssigneeDiff(task: Api.Project.ProjectTask, nextUserIds: string[]) { + const originalAssignees = task.assignees ?? []; + const nextSet = new Set(nextUserIds); + const originalSet = new Set(originalAssignees.map(item => item.userId)); + + const toAdd = nextUserIds.filter(userId => !originalSet.has(userId)); + const toInactivate = originalAssignees.filter(item => !nextSet.has(item.userId)); + + if (!toAdd.length && !toInactivate.length) return true; + + const requests = [ + ...toAdd.map(userId => fetchCreateProjectTaskAssignee(props.projectId, task.executionId, task.id, { userId })), + ...toInactivate.map(assignee => + fetchInactiveProjectTaskAssignee(props.projectId, task.executionId, task.id, assignee.id, { + reason: TASK_ASSIGNEE_INLINE_INACTIVE_REASON + }) + ) + ]; + + const results = await Promise.all(requests); + return results.every(item => !item.error); +} + async function handleOperateSubmit(payload: Api.Project.SaveProjectTaskParams) { - // 新建必须锚定到具体执行;编辑跟随任务自身的 executionId(跨执行视角下 props.execution 为 null) + let assigneeOk = true; + if (operateMode.value === 'create') { + // 新建必须锚定到具体执行 if (!props.execution) return; - } else if (!currentTask.value) { - return; + const result = await fetchCreateProjectTask(props.projectId, props.execution.id, payload); + if (result.error) return; + window.$message?.success('任务创建成功'); + } else { + // 编辑跟随任务自身的 executionId(跨执行视角下 props.execution 为 null) + const task = currentTask.value; + if (!task) return; + + // 协办人不走更新接口,单独 diff;其余字段照常更新 + const { assigneeUserIds = [], ...taskData } = payload; + const result = await fetchUpdateProjectTask(props.projectId, task.executionId, { taskId: task.id, data: taskData }); + if (result.error) return; + + assigneeOk = !canManageTaskAssignee(task) || (await applyTaskAssigneeDiff(task, assigneeUserIds)); + if (assigneeOk) { + window.$message?.success('任务更新成功'); + } else { + window.$message?.warning('任务已更新,部分协办人调整未成功,请在「协办人」中确认'); + } } - const result = - operateMode.value === 'create' - ? await fetchCreateProjectTask(props.projectId, props.execution!.id, payload) - : await fetchUpdateProjectTask(props.projectId, currentTask.value!.executionId, { - taskId: currentTask.value!.id, - data: payload - }); - - if (result.error) return; - - window.$message?.success(operateMode.value === 'create' ? '任务创建成功' : '任务更新成功'); await taskOperateDialogRef.value?.commit(); operateVisible.value = false; await Promise.all([refreshTableData(), loadTaskStatusBoard()]); @@ -918,6 +962,7 @@ defineExpose({ :default-parent-task-id="presetParentTaskId" :user-options="executionAssigneeOptions" :task-options="taskOptions" + :can-manage-assignee="canManageCurrentTaskAssignee" @submit="handleOperateSubmit" /> diff --git a/src/views/workbench/modules/workbench-my-execution.vue b/src/views/workbench/modules/workbench-my-execution.vue index 392cb47..09de87b 100644 --- a/src/views/workbench/modules/workbench-my-execution.vue +++ b/src/views/workbench/modules/workbench-my-execution.vue @@ -1,5 +1,5 @@