@@ -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 / paused) , terminal 态( completed / cancelled) 不进待办
// 待办口径:未到终态的我的 事项( pending / active / paused) , terminal 态( 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)">
<I conMdiEyeOutline 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 : 48 px ;
padding : 0 18 px ;
border - bottom : 1 px solid var ( -- el - border - color - lighter ) ;
font - size : 15 px ;
font - weight : 600 ;
color : var ( -- el - text - color - primary ) ;
}
. workbench - task - preview _ _body {
max - height : min ( 72 vh , 760 px ) ;
padding : 18 px ;
overflow : auto ;
}
. workbench - todo _ _meta {
display : flex ;
align - items : center ;