fix(我的事项、产品/项目、工作台我的待办): 修复已知的一些问题。

This commit is contained in:
dk
2026-06-30 11:55:43 +08:00
parent 4f357a35a9
commit a650f6e96d
11 changed files with 373 additions and 109 deletions

View File

@@ -26,6 +26,7 @@ import { useRouterPush } from '@/hooks/common/router';
import { useDict } from '@/hooks/business/dict';
import type { WorklogChangedPayload } from '@/views/project/project/execution/shared';
import TaskStatusActionDialog from '@/views/project/project/execution/modules/status-action-dialog.vue';
import TaskInfoReadonly from '@/views/project/project/execution/modules/task-info-readonly.vue';
import TaskWorklogDialog from '@/views/project/project/execution/modules/task-worklog-dialog.vue';
import PersonalItemDetailDialog from '@/views/personal-center/my-item/modules/personal-item-detail-dialog.vue';
import PersonalItemOperateDialog from '@/views/personal-center/my-item/modules/personal-item-operate-dialog.vue';
@@ -57,6 +58,7 @@ import { useWorkbenchRefresh } from '../composables/use-workbench-refresh';
import { useWorkbenchWorklogSignal } from '../composables/use-workbench-worklog-signal';
import WorkbenchModuleCard from './workbench-module-card.vue';
import IconMdiCheckCircleOutline from '~icons/mdi/check-circle-outline';
import IconMdiClipboardCheckOutline from '~icons/mdi/clipboard-check-outline';
import IconMdiClipboardEditOutline from '~icons/mdi/clipboard-edit-outline';
import IconMdiCloseCircleOutline from '~icons/mdi/close-circle-outline';
import IconMdiEyeOutline from '~icons/mdi/eye-outline';
@@ -127,7 +129,7 @@ const mainTabs: Array<{ key: WorkbenchTodoMainTab; label: string }> = [
{ key: 'all', label: '全部' },
{ key: 'task', label: '任务' },
{ key: 'ticket', label: '工单' },
{ key: 'personal', label: '个人事项' },
{ key: 'personal', label: '我的事项' },
{ key: 'approval', label: '待审批' }
];
@@ -169,7 +171,7 @@ const myTaskItems = ref<WorkbenchTodoItem[]>([]);
// 保留任务原始行,供操作图标按 availableActions 渲染并取 projectId / executionId 调状态变更接口
const myTaskRows = ref<Api.Project.MyTaskItem[]>([]);
const personalTodoItems = ref<WorkbenchTodoItem[]>([]);
// 保留个人事项原始行,供操作图标(详情/填报/完成)按 availableActions 渲染并回传给弹层
// 保留我的事项原始行,供操作图标(详情/填报/完成)按 availableActions 渲染并回传给弹层
const personalItemRows = ref<Api.PersonalItem.PersonalItem[]>([]);
const overtimeApprovalItems = ref<WorkbenchTodoItem[]>([]);
const overtimeApprovalRows = ref<Api.OvertimeApplication.OvertimeApplication[]>([]);
@@ -188,14 +190,14 @@ const mergedItems = computed(() => [
...workReportApprovalItems.value
]);
// 个人事项操作弹层:详情用 view 模式、新增用 add 模式,共用同一实例
// 我的事项操作弹层:详情用 view 模式、新增用 add 模式,共用同一实例
const personalOperateVisible = ref(false);
const personalOperateType = ref<'add' | 'view'>('add');
const personalOperateRow = ref<Api.PersonalItem.PersonalItem | null>(null);
// 个人事项填报:复用详情弹层的 worklog tab
// 我的事项填报:复用详情弹层的 worklog tab
const personalDetailVisible = ref(false);
const personalDetailRow = ref<Api.PersonalItem.PersonalItem | null>(null);
// 个人事项完成:复用状态动作弹层收集原因 + 二次确认
// 我的事项完成:复用状态动作弹层收集原因 + 二次确认
const personalStatusVisible = ref(false);
const personalStatusRow = ref<Api.PersonalItem.PersonalItem | null>(null);
const personalStatusAction = ref<Api.PersonalItem.PersonalItemLifecycleAction | null>(null);
@@ -203,6 +205,8 @@ const personalStatusAction = ref<Api.PersonalItem.PersonalItemLifecycleAction |
const taskStatusVisible = ref(false);
const taskStatusRow = ref<Api.Project.MyTaskItem | null>(null);
const taskStatusAction = ref<Api.Project.LifecycleAction<Api.Project.ProjectTaskActionCode> | null>(null);
const taskPreviewMap = ref<Record<string, Api.Project.ProjectTask>>({});
const taskPreviewLoadingIds = ref<string[]>([]);
// 任务填报工时:复用任务工作区的工时弹层;弹层需要完整任务详情,点击时按需拉取
const taskWorklogVisible = ref(false);
const taskWorklogTask = ref<Api.Project.ProjectTask | null>(null);
@@ -222,9 +226,7 @@ const currentPerformanceActionType = ref<PerformanceApprovalActionType>('confirm
// 批量审批选中状态(存原始加班申请 id避免映射转换
const selectedOvertimeIds = ref<Set<string>>(new Set());
const OVERTIME_APPROVAL_ACTION_ICONS = {
detail: markRaw(IconMdiEyeOutline)
};
const APPROVAL_ENTRY_ICON = markRaw(IconMdiClipboardCheckOutline);
function getApprovalCategoryLabel(bizType: ApprovalBizType) {
if (bizType === 'weekly') return '周报';
@@ -287,13 +289,13 @@ function getPersonalCompleteAction(row: Api.PersonalItem.PersonalItem) {
return row.availableActions?.find(action => action.actionCode === 'complete') || null;
}
// 仅当个人事项当前可完成availableActions 含 complete时才渲染完成图标
// 仅当我的事项当前可完成availableActions 含 complete时才渲染完成图标
function canCompletePersonalItem(item: WorkbenchTodoItem) {
const row = findPersonalItemRow(item);
return Boolean(row && getPersonalCompleteAction(row));
}
// 详情:复用个人事项操作弹层的 view 只读模式
// 详情:复用我的事项操作弹层的 view 只读模式
function openPersonalDetail(item: WorkbenchTodoItem) {
const row = findPersonalItemRow(item);
if (!row) return;
@@ -350,6 +352,52 @@ function findMyTaskRow(item: WorkbenchTodoItem) {
return myTaskRows.value.find(row => `task-${row.id}` === item.id) || null;
}
function canPreviewTaskInfo(item: WorkbenchTodoItem) {
const row = findMyTaskRow(item);
return Boolean(item.category === 'task' && row?.projectId && row.executionId);
}
function getTaskPreviewTask(item: WorkbenchTodoItem) {
const row = findMyTaskRow(item);
if (!row) return null;
return taskPreviewMap.value[row.id] || null;
}
function getTaskPreviewTaskOptions(item: WorkbenchTodoItem) {
const task = getTaskPreviewTask(item);
return task ? [task] : [];
}
function isTaskPreviewLoading(item: WorkbenchTodoItem) {
const row = findMyTaskRow(item);
if (!row) return false;
return taskPreviewLoadingIds.value.includes(row.id);
}
async function handleTaskPreviewShow(item: WorkbenchTodoItem) {
const row = findMyTaskRow(item);
if (!row?.executionId) return;
if (taskPreviewMap.value[row.id] || taskPreviewLoadingIds.value.includes(row.id)) {
return;
}
taskPreviewLoadingIds.value = [...taskPreviewLoadingIds.value, row.id];
const { error, data } = await fetchGetProjectTask(row.projectId, row.executionId, row.id);
taskPreviewLoadingIds.value = taskPreviewLoadingIds.value.filter(id => id !== row.id);
if (error || !data) {
return;
}
taskPreviewMap.value = {
...taskPreviewMap.value,
[row.id]: data
};
}
// 状态变更接口挂在执行路径下,未挂执行的任务不渲染动作按钮
function getTaskActions(item: WorkbenchTodoItem) {
const row = findMyTaskRow(item);
@@ -362,6 +410,23 @@ function canReportTaskWorklog(item: WorkbenchTodoItem) {
return Boolean(findMyTaskRow(item)?.executionId);
}
async function refreshTaskWorklogTaskDetail(
target?:
| Pick<Api.Project.ProjectTask, 'id' | 'projectId' | 'executionId'>
| Pick<Api.Project.MyTaskItem, 'id' | 'projectId' | 'executionId'>
| null
) {
const task = target ?? taskWorklogTask.value;
if (!task?.executionId) return;
const { error, data } = await fetchGetProjectTask(task.projectId, task.executionId, task.id);
if (error || !data) return;
if (taskWorklogTask.value?.id === data.id) {
taskWorklogTask.value = data;
}
}
// 填报:工时弹层需要完整任务详情(负责人/状态/日期),按需拉一次详情再打开
async function openTaskWorklog(item: WorkbenchTodoItem) {
const row = findMyTaskRow(item);
@@ -378,6 +443,9 @@ async function openTaskWorklog(item: WorkbenchTodoItem) {
async function handleTaskWorklogChanged(payload: WorklogChangedPayload) {
notifyWorklogChanged();
await loadMyTaskItems();
if (taskWorklogTask.value?.id === payload.taskId) {
await refreshTaskWorklogTaskDetail();
}
// 与任务工作区联动一致:进度填到 100 且我是任务负责人时提示完成(仅单任务,不做级联)
if (payload.mode === 'delete' || payload.progressRate !== 100) return;
@@ -459,6 +527,9 @@ async function submitTaskStatusChange(reason: string | null) {
taskStatusVisible.value = false;
window.$message?.success(`${action.actionName}成功`);
await loadMyTaskItems();
if (taskWorklogTask.value?.id === row.id) {
await refreshTaskWorklogTaskDetail(row);
}
}
const tabCounts = computed(() => {
@@ -733,6 +804,9 @@ function mapTaskTodoPriority(priority: string) {
async function loadMyTaskItems() {
const { error, data } = await fetchGetMyTaskPage({ pageNo: 1, pageSize: -1 });
taskPreviewMap.value = {};
taskPreviewLoadingIds.value = [];
if (error || !data) {
myTaskRows.value = [];
myTaskItems.value = [];
@@ -756,7 +830,7 @@ async function loadMyTaskItems() {
);
}
// 待办口径:未到终态的个人事项pending / active / pausedterminal 态completed / cancelled不进待办
// 待办口径:未到终态的我的事项pending / active / pausedterminal 态completed / cancelled不进待办
const PERSONAL_TODO_STATUSES: Api.PersonalItem.PersonalItemStatusCode[] = ['pending', 'active', 'paused'];
async function loadPersonalTodoItems() {
@@ -777,7 +851,7 @@ async function loadPersonalTodoItems() {
title: item.taskTitle,
createdTime: item.createTime,
deadline: item.plannedEndDate,
source: '个人事项',
source: '我的事项',
progressRate: item.progressRate,
routeKey: 'personal-center_my-item'
}))
@@ -1061,7 +1135,7 @@ onActivated(refresh);
<button type="button" class="workbench-todo__add" @click="handleOpenAdd">
<SvgIcon icon="mdi:plus" class="workbench-todo__add-icon" />
<span>个人事项</span>
<span>我的事项</span>
</button>
</div>
@@ -1179,7 +1253,42 @@ onActivated(refresh);
<div class="workbench-todo__body">
<h4 class="workbench-todo__item-title">
<ElPopover
v-if="canPreviewTaskInfo(item)"
placement="right-start"
trigger="hover"
:width="960"
popper-class="workbench-task-preview-popover"
@show="handleTaskPreviewShow(item)"
>
<template #reference>
<span
class="workbench-todo__item-title-text"
:class="{
'workbench-todo__item-title-text--clickable': Boolean(
item.routeKey || item.approvalBizType || item.projectId
)
}"
@click="handleClickItem(item)"
>
{{ item.title }}
</span>
</template>
<div class="workbench-task-preview">
<div class="workbench-task-preview__header">任务信息</div>
<div v-loading="isTaskPreviewLoading(item)" class="workbench-task-preview__body">
<TaskInfoReadonly
v-if="getTaskPreviewTask(item)"
:task="getTaskPreviewTask(item)"
:task-options="getTaskPreviewTaskOptions(item)"
/>
<ElEmpty v-else description="暂无任务信息" :image-size="56" />
</div>
</div>
</ElPopover>
<span
v-else
class="workbench-todo__item-title-text"
:class="{
'workbench-todo__item-title-text--clickable': Boolean(
@@ -1212,16 +1321,16 @@ onActivated(refresh);
<div class="workbench-todo__trailing">
<div v-if="item.approvalBizType === 'overtime_application'" class="workbench-todo__actions" @click.stop>
<ElTooltip content="详情">
<ElTooltip content="审批">
<ElButton link type="primary" class="workbench-todo__action-btn" @click="openOvertimeDetail(item)">
<component :is="OVERTIME_APPROVAL_ACTION_ICONS.detail" class="text-15px" />
<component :is="APPROVAL_ENTRY_ICON" class="text-15px" />
</ElButton>
</ElTooltip>
</div>
<div v-else-if="item.approvalBizType === 'performance'" class="workbench-todo__actions" @click.stop>
<ElTooltip content="查看绩效">
<ElTooltip content="审批">
<ElButton link type="primary" class="workbench-todo__action-btn" @click="openPerformanceDetail(item)">
<IconMdiEyeOutline class="text-15px" />
<component :is="APPROVAL_ENTRY_ICON" class="text-15px" />
</ElButton>
</ElTooltip>
</div>
@@ -1230,9 +1339,9 @@ onActivated(refresh);
class="workbench-todo__actions"
@click.stop
>
<ElTooltip content="详情">
<ElTooltip content="审批">
<ElButton link type="primary" class="workbench-todo__action-btn" @click="openWorkReportDetail(item)">
<component :is="OVERTIME_APPROVAL_ACTION_ICONS.detail" class="text-15px" />
<component :is="APPROVAL_ENTRY_ICON" class="text-15px" />
</ElButton>
</ElTooltip>
</div>
@@ -1241,7 +1350,7 @@ onActivated(refresh);
class="workbench-todo__actions"
@click.stop
>
<ElTooltip content="填报工时">
<ElTooltip content="填报">
<ElButton link type="primary" class="workbench-todo__action-btn" @click="openTaskWorklog(item)">
<component :is="PERSONAL_ACTION_ICONS.worklog" class="text-15px" />
</ElButton>
@@ -1723,6 +1832,32 @@ onActivated(refresh);
text-decoration: underline;
}
:deep(.workbench-task-preview-popover) {
padding: 0;
overflow: hidden;
}
.workbench-task-preview {
background: var(--el-bg-color);
}
.workbench-task-preview__header {
display: flex;
align-items: center;
min-height: 48px;
padding: 0 18px;
border-bottom: 1px solid var(--el-border-color-lighter);
font-size: 15px;
font-weight: 600;
color: var(--el-text-color-primary);
}
.workbench-task-preview__body {
max-height: min(72vh, 760px);
padding: 18px;
overflow: auto;
}
.workbench-todo__meta {
display: flex;
align-items: center;