czh-20260430-修复任务管理相关bug
This commit is contained in:
@@ -30,12 +30,12 @@
|
|||||||
try {
|
try {
|
||||||
setModalProps({ confirmLoading: true });
|
setModalProps({ confirmLoading: true });
|
||||||
const values = await validate();
|
const values = await validate();
|
||||||
await addTaskList({
|
const listId = await addTaskList({
|
||||||
tasklistName: values.tasklistName || values.name,
|
tasklistName: values.tasklistName || values.name,
|
||||||
pid: pendingGroupId || undefined,
|
pid: pendingGroupId || undefined,
|
||||||
});
|
});
|
||||||
closeModal();
|
closeModal();
|
||||||
emit('success');
|
emit('success', listId);
|
||||||
} finally {
|
} finally {
|
||||||
setModalProps({ confirmLoading: false });
|
setModalProps({ confirmLoading: false });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,9 +8,9 @@
|
|||||||
<div class="content-header-left">
|
<div class="content-header-left">
|
||||||
<div v-if="headerRenaming" class="content-header-rename-row">
|
<div v-if="headerRenaming" class="content-header-rename-row">
|
||||||
<a-input
|
<a-input
|
||||||
|
ref="headerRenameInputRef"
|
||||||
v-model:value="headerRenameValue"
|
v-model:value="headerRenameValue"
|
||||||
size="small"
|
size="small"
|
||||||
autofocus
|
|
||||||
style="width: 200px"
|
style="width: 200px"
|
||||||
@press-enter="confirmHeaderRename"
|
@press-enter="confirmHeaderRename"
|
||||||
@blur="confirmHeaderRename"
|
@blur="confirmHeaderRename"
|
||||||
@@ -53,39 +53,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="showListGroupView" class="content-tabs">
|
|
||||||
<div
|
|
||||||
v-for="tab in LIST_TAB_ITEMS"
|
|
||||||
:key="tab.key"
|
|
||||||
class="content-tab"
|
|
||||||
:class="{ active: listGroupTab === tab.key }"
|
|
||||||
@click="emit('update:list-group-tab', tab.key)"
|
|
||||||
>
|
|
||||||
{{ tab.label }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="!showListGroupView" class="content-toolbar">
|
<div v-if="!showListGroupView" class="content-toolbar">
|
||||||
<div v-if="canEdit" class="feishu-btn-group">
|
|
||||||
<a-button size="small" class="feishu-btn-main" @click="handleCreateTask()">
|
|
||||||
<Icon icon="ant-design:plus-outlined" class="mr-1" />
|
|
||||||
新建任务
|
|
||||||
</a-button>
|
|
||||||
<a-dropdown>
|
|
||||||
<a-button size="small" class="feishu-btn-arrow">
|
|
||||||
<Icon icon="ant-design:down-outlined" class="feishu-arrow-icon" />
|
|
||||||
</a-button>
|
|
||||||
<template #overlay>
|
|
||||||
<a-menu @click="onCreateMenuClick">
|
|
||||||
<a-menu-item key="group">
|
|
||||||
<Icon icon="ant-design:folder-add-outlined" class="mr-1" />
|
|
||||||
新建分组
|
|
||||||
</a-menu-item>
|
|
||||||
</a-menu>
|
|
||||||
</template>
|
|
||||||
</a-dropdown>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<a-dropdown>
|
<a-dropdown>
|
||||||
<template #overlay>
|
<template #overlay>
|
||||||
<a-menu @click="onStatusFilterChange">
|
<a-menu @click="onStatusFilterChange">
|
||||||
@@ -282,12 +250,14 @@
|
|||||||
</div>
|
</div>
|
||||||
<div v-for="list in allTaskLists" :key="list.id" class="list-group-row" @click="emit('list-select', list.id)">
|
<div v-for="list in allTaskLists" :key="list.id" class="list-group-row" @click="emit('list-select', list.id)">
|
||||||
<div class="list-group-col list-group-col-name">
|
<div class="list-group-col list-group-col-name">
|
||||||
<Icon icon="tasklist-list-file|svg" class="list-icon" size="16" />
|
<Icon icon="ant-design:file-text-outlined" class="list-icon" size="16" />
|
||||||
<span class="list-name-text">{{ list.name }}</span>
|
<span class="list-name-text">{{ list.name }}</span>
|
||||||
|
<a-tooltip :title="isFavorited(list.id) ? '取消收藏' : '收藏'">
|
||||||
<span class="pin-btn-wrap" @click.stop="toggleFavorite(list.id)">
|
<span class="pin-btn-wrap" @click.stop="toggleFavorite(list.id)">
|
||||||
<Icon v-if="!isFavorited(list.id)" icon="tasklist-star|svg" class="pin-svg pin-svg-outlined" />
|
<Icon v-if="!isFavorited(list.id)" icon="tasklist-star|svg" class="pin-svg pin-svg-outlined" />
|
||||||
<Icon v-else icon="tasklist-star-filled|svg" class="pin-svg pin-svg-filled" />
|
<Icon v-else icon="tasklist-star-filled|svg" class="pin-svg pin-svg-filled" />
|
||||||
</span>
|
</span>
|
||||||
|
</a-tooltip>
|
||||||
</div>
|
</div>
|
||||||
<div class="list-group-col list-group-col-owner">
|
<div class="list-group-col list-group-col-owner">
|
||||||
<template v-if="getFirstAssignee(list)">
|
<template v-if="getFirstAssignee(list)">
|
||||||
@@ -344,7 +314,7 @@
|
|||||||
|
|
||||||
<template v-if="currentList">
|
<template v-if="currentList">
|
||||||
<draggable
|
<draggable
|
||||||
:model-value="computedGroups"
|
:list="localGroups"
|
||||||
item-key="id"
|
item-key="id"
|
||||||
group="taskGroups"
|
group="taskGroups"
|
||||||
handle=".group-drag-handle"
|
handle=".group-drag-handle"
|
||||||
@@ -360,10 +330,16 @@
|
|||||||
:style="{ minWidth: totalFieldsWidth + 'px' }"
|
:style="{ minWidth: totalFieldsWidth + 'px' }"
|
||||||
@click="toggleGroupCollapse(group.id)"
|
@click="toggleGroupCollapse(group.id)"
|
||||||
>
|
>
|
||||||
<span v-if="canEdit && groupDimension === 'custom'" class="group-drag-handle">
|
<span v-if="canEdit && groupDimension === 'custom'" class="group-drag-handle table-col-drag-handle">
|
||||||
<Icon icon="tasklist-drag-handle|svg" />
|
<Icon icon="tasklist-drag-handle|svg" />
|
||||||
</span>
|
</span>
|
||||||
<Icon :icon="group.collapsed ? 'ant-design:caret-right-outlined' : 'ant-design:caret-down-outlined'" class="group-arrow" />
|
<span v-else class="table-col-drag-handle"></span>
|
||||||
|
<span class="table-col table-col-title">
|
||||||
|
<div class="task-indent" style="margin-left: 0">
|
||||||
|
<span class="expand-arrow">
|
||||||
|
<Icon :icon="group.collapsed ? 'ant-design:caret-right-outlined' : 'ant-design:caret-down-outlined'" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
<template v-if="groupDimension === 'assignee' || groupDimension === 'createBy'">
|
<template v-if="groupDimension === 'assignee' || groupDimension === 'createBy'">
|
||||||
<template v-if="group.name && group.name !== '未分配' && group.name !== '未知'">
|
<template v-if="group.name && group.name !== '未分配' && group.name !== '未知'">
|
||||||
<AvatarDisplay :names="group.name" :size="24" :show-name="true" />
|
<AvatarDisplay :names="group.name" :size="24" :show-name="true" />
|
||||||
@@ -382,10 +358,10 @@
|
|||||||
</template>
|
</template>
|
||||||
<template v-else-if="renamingGroupId === group.id">
|
<template v-else-if="renamingGroupId === group.id">
|
||||||
<a-input
|
<a-input
|
||||||
|
ref="renamingGroupInputRef"
|
||||||
v-model:value="renamingGroupName"
|
v-model:value="renamingGroupName"
|
||||||
size="small"
|
size="small"
|
||||||
style="width: 160px"
|
style="width: 160px"
|
||||||
autofocus
|
|
||||||
@press-enter="confirmRenameGroup"
|
@press-enter="confirmRenameGroup"
|
||||||
@blur="confirmRenameGroup"
|
@blur="confirmRenameGroup"
|
||||||
@click.stop
|
@click.stop
|
||||||
@@ -404,41 +380,23 @@
|
|||||||
</span>
|
</span>
|
||||||
<a-dropdown v-if="canEdit && groupDimension === 'custom'">
|
<a-dropdown v-if="canEdit && groupDimension === 'custom'">
|
||||||
<span class="group-action-btn group-action-more" title="更多操作" @click.stop>
|
<span class="group-action-btn group-action-more" title="更多操作" @click.stop>
|
||||||
<Icon icon="ant-design:more-outlined" />
|
<Icon icon="ant-design:ellipsis-outlined" />
|
||||||
</span>
|
</span>
|
||||||
<template #overlay>
|
<template #overlay>
|
||||||
<a-menu @click="({ key }) => onGroupMenuClick(key, group.id, group.name)">
|
<a-menu @click="({ key }) => onGroupMenuClick(key, group.id, group.name)">
|
||||||
<a-menu-item key="rename">重命名</a-menu-item>
|
<a-menu-item key="rename">重命名</a-menu-item>
|
||||||
<a-menu-item v-if="group.id !== '__default__'" key="delete" danger>删除</a-menu-item>
|
<a-tooltip v-if="group.id === '__default__'" title="系统内置分组不可删除" placement="right">
|
||||||
|
<a-menu-item key="delete" disabled style="color: #ccc; cursor: not-allowed">删除</a-menu-item>
|
||||||
|
</a-tooltip>
|
||||||
|
<a-menu-item v-else key="delete" danger>删除</a-menu-item>
|
||||||
</a-menu>
|
</a-menu>
|
||||||
</template>
|
</template>
|
||||||
</a-dropdown>
|
</a-dropdown>
|
||||||
</span>
|
</span>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<template v-if="!group.collapsed">
|
<div v-show="!group.collapsed">
|
||||||
<div
|
|
||||||
v-if="showNewTaskInput === group.id && canEdit"
|
|
||||||
class="task-row new-task-input-row"
|
|
||||||
:style="{ minWidth: totalFieldsWidth + 'px' }"
|
|
||||||
>
|
|
||||||
<div class="table-col table-col-drag-handle"></div>
|
|
||||||
<div class="table-col table-col-title">
|
|
||||||
<div class="task-indent">
|
|
||||||
<span class="expand-arrow-placeholder"></span>
|
|
||||||
<div class="task-checkbox"></div>
|
|
||||||
</div>
|
|
||||||
<a-input
|
|
||||||
v-model:value="newTaskTitle"
|
|
||||||
size="small"
|
|
||||||
placeholder="输入任务标题,按回车确认..."
|
|
||||||
autofocus
|
|
||||||
@press-enter="submitNewTask(group.id)"
|
|
||||||
@blur="submitNewTask(group.id)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<draggable
|
<draggable
|
||||||
:model-value="flattenGroupTasks(group.tasks)"
|
:model-value="flattenGroupTasks(group.tasks)"
|
||||||
:item-key="(ft: any) => ft.task.id"
|
:item-key="(ft: any) => ft.task.id"
|
||||||
@@ -453,7 +411,7 @@
|
|||||||
>
|
>
|
||||||
<template #item="{ element: ft }">
|
<template #item="{ element: ft }">
|
||||||
<div class="task-row" :data-task-id="ft.task.id" :style="{ minWidth: totalFieldsWidth + 'px' }" v-show="!isChildHidden(ft)">
|
<div class="task-row" :data-task-id="ft.task.id" :style="{ minWidth: totalFieldsWidth + 'px' }" v-show="!isChildHidden(ft)">
|
||||||
<div class="table-col table-col-drag-handle" v-if="canEdit">
|
<div class="table-col table-col-drag-handle" v-if="_canEditTask(ft.task)">
|
||||||
<span class="drag-handle">
|
<span class="drag-handle">
|
||||||
<Icon icon="tasklist-drag-handle|svg" />
|
<Icon icon="tasklist-drag-handle|svg" />
|
||||||
</span>
|
</span>
|
||||||
@@ -468,8 +426,8 @@
|
|||||||
<button
|
<button
|
||||||
class="task-checkbox"
|
class="task-checkbox"
|
||||||
:class="{ checked: ft.task.completed }"
|
:class="{ checked: ft.task.completed }"
|
||||||
:disabled="isReadOnly"
|
:disabled="!_canEditTask(ft.task)"
|
||||||
@click.stop="!isReadOnly && emit('toggle-task', ft.task.id)"
|
@click.stop="_canEditTask(ft.task) && emit('toggle-task', ft.task.id)"
|
||||||
>
|
>
|
||||||
<Icon v-if="ft.task.completed" icon="ant-design:check-outlined" />
|
<Icon v-if="ft.task.completed" icon="ant-design:check-outlined" />
|
||||||
</button>
|
</button>
|
||||||
@@ -483,7 +441,7 @@
|
|||||||
<template v-for="field in visibleFieldConfigs" :key="field.key">
|
<template v-for="field in visibleFieldConfigs" :key="field.key">
|
||||||
<div class="table-col task-field" :style="{ width: fieldWidthMap[field.key] || '100px' }">
|
<div class="table-col task-field" :style="{ width: fieldWidthMap[field.key] || '100px' }">
|
||||||
<template v-if="field.key === 'subTaskProgress'">
|
<template v-if="field.key === 'subTaskProgress'">
|
||||||
<div class="cell-inner">
|
<div class="cell-inner readonly-cell">
|
||||||
<div class="progress-cell" v-if="ft.task.childCount > 0">
|
<div class="progress-cell" v-if="ft.task.childCount > 0">
|
||||||
<div class="progress-bar">
|
<div class="progress-bar">
|
||||||
<div
|
<div
|
||||||
@@ -500,7 +458,7 @@
|
|||||||
</template>
|
</template>
|
||||||
<template v-else-if="field.key === 'priority'">
|
<template v-else-if="field.key === 'priority'">
|
||||||
<a-dropdown
|
<a-dropdown
|
||||||
v-if="canEdit"
|
v-if="_canEditTask(ft.task)"
|
||||||
:trigger="['click']"
|
:trigger="['click']"
|
||||||
:get-popup-container="getPopupContainer"
|
:get-popup-container="getPopupContainer"
|
||||||
:overlay-style="{ minWidth: '100px' }"
|
:overlay-style="{ minWidth: '100px' }"
|
||||||
@@ -537,7 +495,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="field.key === 'assigneeName'">
|
<template v-else-if="field.key === 'assigneeName'">
|
||||||
<div v-if="canEdit" class="cell-inner editable-cell" @click.stop="openUserSelectModal('assignee', ft.task.id)">
|
<div v-if="_canEditTask(ft.task)" class="cell-inner editable-cell" @click.stop="openUserSelectModal('assignee', ft.task.id)">
|
||||||
<template v-if="ft.task.assigneeName">
|
<template v-if="ft.task.assigneeName">
|
||||||
<a-tooltip :title="ft.task.assigneeName" placement="top"
|
<a-tooltip :title="ft.task.assigneeName" placement="top"
|
||||||
><AvatarDisplay :names="ft.task.assigneeName" :size="24" :completed="ft.task.completed"
|
><AvatarDisplay :names="ft.task.assigneeName" :size="24" :completed="ft.task.completed"
|
||||||
@@ -561,8 +519,8 @@
|
|||||||
<template v-else-if="field.key === 'startTime' || field.key === 'endTime'">
|
<template v-else-if="field.key === 'startTime' || field.key === 'endTime'">
|
||||||
<div
|
<div
|
||||||
class="cell-inner date-cell-wrapper"
|
class="cell-inner date-cell-wrapper"
|
||||||
:class="{ 'editable-cell': canEdit }"
|
:class="{ 'editable-cell': _canEditTask(ft.task) }"
|
||||||
@click.stop="canEdit && !datePickerVisible && startEditCell(ft.task.id, field.key)"
|
@click.stop="_canEditTask(ft.task) && !datePickerVisible && startEditCell(ft.task.id, field.key)"
|
||||||
>
|
>
|
||||||
<a-date-picker
|
<a-date-picker
|
||||||
v-if="editingCell?.taskId === ft.task.id && editingCell?.field === field.key"
|
v-if="editingCell?.taskId === ft.task.id && editingCell?.field === field.key"
|
||||||
@@ -603,7 +561,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="field.key === 'followersName'">
|
<template v-else-if="field.key === 'followersName'">
|
||||||
<div v-if="canEdit" class="cell-inner editable-cell" @click.stop="openUserSelectModal('followers', ft.task.id)">
|
<div
|
||||||
|
v-if="_canEditTask(ft.task)"
|
||||||
|
class="cell-inner editable-cell"
|
||||||
|
@click.stop="openUserSelectModal('followers', ft.task.id)"
|
||||||
|
>
|
||||||
<template v-if="ft.task.followersName">
|
<template v-if="ft.task.followersName">
|
||||||
<a-tooltip :title="ft.task.followersName" placement="top"
|
<a-tooltip :title="ft.task.followersName" placement="top"
|
||||||
><AvatarDisplay :names="ft.task.followersName" :size="24" :completed="ft.task.completed"
|
><AvatarDisplay :names="ft.task.followersName" :size="24" :completed="ft.task.completed"
|
||||||
@@ -625,7 +587,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="field.key === 'participantName'">
|
<template v-else-if="field.key === 'participantName'">
|
||||||
<div v-if="canEdit" class="cell-inner editable-cell" @click.stop="openUserSelectModal('participant', ft.task.id)">
|
<div
|
||||||
|
v-if="_canEditTask(ft.task)"
|
||||||
|
class="cell-inner editable-cell"
|
||||||
|
@click.stop="openUserSelectModal('participant', ft.task.id)"
|
||||||
|
>
|
||||||
<template v-if="ft.task.participantName">
|
<template v-if="ft.task.participantName">
|
||||||
<a-tooltip :title="ft.task.participantName" placement="top"
|
<a-tooltip :title="ft.task.participantName" placement="top"
|
||||||
><AvatarDisplay :names="ft.task.participantName" :size="24" :completed="ft.task.completed"
|
><AvatarDisplay :names="ft.task.participantName" :size="24" :completed="ft.task.completed"
|
||||||
@@ -647,7 +613,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="field.key === 'completeTime'">
|
<template v-else-if="field.key === 'completeTime'">
|
||||||
<div class="cell-inner">
|
<div class="cell-inner readonly-cell">
|
||||||
<template v-if="ft.task.completeTime">
|
<template v-if="ft.task.completeTime">
|
||||||
<span class="field-value" :class="{ 'task-field-completed': ft.task.completed }">
|
<span class="field-value" :class="{ 'task-field-completed': ft.task.completed }">
|
||||||
{{ ft.task.completeTime.substring(0, 10) }}
|
{{ ft.task.completeTime.substring(0, 10) }}
|
||||||
@@ -657,7 +623,27 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="field.key === 'remark'">
|
<template v-else-if="field.key === 'remark'">
|
||||||
<div class="cell-inner">
|
<div
|
||||||
|
class="cell-inner"
|
||||||
|
:class="{ 'editable-cell': canEdit }"
|
||||||
|
@click.stop="canEdit && startEditTextCell(ft.task.id, field.key)"
|
||||||
|
>
|
||||||
|
<a-input
|
||||||
|
v-if="editingCell?.taskId === ft.task.id && editingCell?.field === field.key"
|
||||||
|
v-model:value="editingValue"
|
||||||
|
size="small"
|
||||||
|
:maxlength="200"
|
||||||
|
class="inline-text-editor"
|
||||||
|
:ref="
|
||||||
|
(el: any) => {
|
||||||
|
if (el) textEditInputRef = el;
|
||||||
|
}
|
||||||
|
"
|
||||||
|
@press-enter="confirmEditTextCell"
|
||||||
|
@blur="confirmEditTextCell"
|
||||||
|
@click.stop
|
||||||
|
/>
|
||||||
|
<template v-else>
|
||||||
<span
|
<span
|
||||||
v-if="ft.task.remark"
|
v-if="ft.task.remark"
|
||||||
class="field-value"
|
class="field-value"
|
||||||
@@ -666,19 +652,54 @@
|
|||||||
>
|
>
|
||||||
{{ ft.task.remark.length > 20 ? ft.task.remark.substring(0, 20) + '...' : ft.task.remark }}
|
{{ ft.task.remark.length > 20 ? ft.task.remark.substring(0, 20) + '...' : ft.task.remark }}
|
||||||
</span>
|
</span>
|
||||||
<span v-else class="field-placeholder">-</span>
|
<span v-else class="field-icon-placeholder" :class="{ 'task-field-completed': ft.task.completed }">
|
||||||
|
<Icon icon="ant-design:edit-outlined" />
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="field.key === 'assignName' || field.key === 'createBy'">
|
<template v-else-if="field.key === 'assignName' || field.key === 'createBy'">
|
||||||
<div class="cell-inner">
|
<div class="cell-inner readonly-cell">
|
||||||
<template v-if="getFieldValue(ft.task, field.key)">
|
<template v-if="getFieldValue(ft.task, field.key)">
|
||||||
<a-tooltip :title="getFieldValue(ft.task, field.key)" placement="top"
|
<a-tooltip :title="getFieldValue(ft.task, field.key)" placement="top"
|
||||||
><AvatarDisplay :names="getFieldValue(ft.task, field.key)" :size="24" :completed="ft.task.completed"
|
><AvatarDisplay :names="getFieldValue(ft.task, field.key)" :size="24" :completed="ft.task.completed"
|
||||||
/></a-tooltip>
|
/></a-tooltip>
|
||||||
</template>
|
</template>
|
||||||
|
<span v-else class="field-placeholder">-</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="field.key === 'taskDesc'">
|
||||||
|
<div
|
||||||
|
class="cell-inner"
|
||||||
|
:class="{ 'editable-cell': canEdit }"
|
||||||
|
@click.stop="canEdit && startEditTextCell(ft.task.id, field.key)"
|
||||||
|
>
|
||||||
|
<a-input
|
||||||
|
v-if="editingCell?.taskId === ft.task.id && editingCell?.field === field.key"
|
||||||
|
v-model:value="editingValue"
|
||||||
|
size="small"
|
||||||
|
:maxlength="200"
|
||||||
|
class="inline-text-editor"
|
||||||
|
:ref="
|
||||||
|
(el: any) => {
|
||||||
|
if (el) textEditInputRef = el;
|
||||||
|
}
|
||||||
|
"
|
||||||
|
@press-enter="confirmEditTextCell"
|
||||||
|
@blur="confirmEditTextCell"
|
||||||
|
@click.stop
|
||||||
|
/>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<span class="field-icon-placeholder" :class="{ 'task-field-completed': ft.task.completed }">
|
<span
|
||||||
<Icon icon="tasklist-add-user|svg" />
|
v-if="ft.task.taskDesc"
|
||||||
|
class="field-value"
|
||||||
|
:class="{ 'task-field-completed': ft.task.completed }"
|
||||||
|
:title="ft.task.taskDesc"
|
||||||
|
>
|
||||||
|
{{ ft.task.taskDesc.length > 20 ? ft.task.taskDesc.substring(0, 20) + '...' : ft.task.taskDesc }}
|
||||||
|
</span>
|
||||||
|
<span v-else class="field-icon-placeholder" :class="{ 'task-field-completed': ft.task.completed }">
|
||||||
|
<Icon icon="ant-design:edit-outlined" />
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
@@ -694,6 +715,29 @@
|
|||||||
</template>
|
</template>
|
||||||
</draggable>
|
</draggable>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="showNewTaskInput === group.id && canEdit"
|
||||||
|
class="task-row new-task-input-row"
|
||||||
|
:style="{ minWidth: totalFieldsWidth + 'px' }"
|
||||||
|
>
|
||||||
|
<div class="table-col table-col-drag-handle"></div>
|
||||||
|
<div class="table-col table-col-title">
|
||||||
|
<div class="task-indent">
|
||||||
|
<span class="expand-arrow-placeholder"></span>
|
||||||
|
<div class="task-checkbox"></div>
|
||||||
|
</div>
|
||||||
|
<a-input
|
||||||
|
ref="newTaskInputRef"
|
||||||
|
v-model:value="newTaskTitle"
|
||||||
|
size="small"
|
||||||
|
placeholder="输入任务标题,按回车确认..."
|
||||||
|
autofocus
|
||||||
|
@press-enter="submitNewTask(group.id)"
|
||||||
|
@blur="submitNewTask(group.id)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="showNewTaskInput !== group.id && canEdit"
|
v-if="showNewTaskInput !== group.id && canEdit"
|
||||||
class="task-row new-task-btn"
|
class="task-row new-task-btn"
|
||||||
@@ -709,7 +753,7 @@
|
|||||||
<span class="new-task-text">新建任务</span>
|
<span class="new-task-text">新建任务</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</draggable>
|
</draggable>
|
||||||
@@ -717,6 +761,7 @@
|
|||||||
<div v-if="showNewGroupInput && canEdit" class="new-group-row">
|
<div v-if="showNewGroupInput && canEdit" class="new-group-row">
|
||||||
<Icon icon="ant-design:plus-outlined" class="new-task-plus" />
|
<Icon icon="ant-design:plus-outlined" class="new-task-plus" />
|
||||||
<a-input
|
<a-input
|
||||||
|
ref="newGroupInputRef"
|
||||||
v-model:value="newGroupName"
|
v-model:value="newGroupName"
|
||||||
size="small"
|
size="small"
|
||||||
placeholder="输入分组名称,按回车确认..."
|
placeholder="输入分组名称,按回车确认..."
|
||||||
@@ -751,7 +796,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, computed, nextTick } from 'vue';
|
import { ref, computed, watch, nextTick } from 'vue';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import Icon from '/@/components/Icon/index';
|
import Icon from '/@/components/Icon/index';
|
||||||
import AvatarDisplay from './AvatarDisplay.vue';
|
import AvatarDisplay from './AvatarDisplay.vue';
|
||||||
@@ -760,6 +805,7 @@
|
|||||||
import { useUserStore } from '/@/store/modules/user';
|
import { useUserStore } from '/@/store/modules/user';
|
||||||
import { useMessage } from '/@/hooks/web/useMessage';
|
import { useMessage } from '/@/hooks/web/useMessage';
|
||||||
import { FIELD_CONFIG, GROUP_OPTIONS, SORT_OPTIONS, MAX_TASK_DEPTH, FILTER_FIELD_CONFIGS, FILTER_OPERATOR_LABELS } from '../types';
|
import { FIELD_CONFIG, GROUP_OPTIONS, SORT_OPTIONS, MAX_TASK_DEPTH, FILTER_FIELD_CONFIGS, FILTER_OPERATOR_LABELS } from '../types';
|
||||||
|
import { PRIORITY_OPTIONS, getPriorityBg, getPriorityLabel, isOverdue } from '../utils/taskUtils';
|
||||||
import type {
|
import type {
|
||||||
TaskList,
|
TaskList,
|
||||||
TaskGroup,
|
TaskGroup,
|
||||||
@@ -785,12 +831,11 @@
|
|||||||
sortDirection: SortDirection;
|
sortDirection: SortDirection;
|
||||||
visibleFields: FieldKey[];
|
visibleFields: FieldKey[];
|
||||||
showListGroupView?: boolean;
|
showListGroupView?: boolean;
|
||||||
listGroupTab?: string;
|
|
||||||
favoriteIdMap?: Map<string, string>;
|
favoriteIdMap?: Map<string, string>;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
'create-task': [groupId: string, taskName: string, sortOrder?: number];
|
'create-task': [groupId: string, taskName: string];
|
||||||
'toggle-task': [taskId: string];
|
'toggle-task': [taskId: string];
|
||||||
'task-click': [taskId: string];
|
'task-click': [taskId: string];
|
||||||
'add-group': [listId: string, groupName: string];
|
'add-group': [listId: string, groupName: string];
|
||||||
@@ -803,7 +848,6 @@
|
|||||||
'update:visibleFields': [value: FieldKey[]];
|
'update:visibleFields': [value: FieldKey[]];
|
||||||
'update:list-group-tab': [value: string];
|
'update:list-group-tab': [value: string];
|
||||||
'list-select': [listId: string];
|
'list-select': [listId: string];
|
||||||
'create-group': [name: string];
|
|
||||||
'open-collaborator': [];
|
'open-collaborator': [];
|
||||||
'add-to-favorites': [taskListId: string];
|
'add-to-favorites': [taskListId: string];
|
||||||
'remove-from-favorites': [favoriteId: string];
|
'remove-from-favorites': [favoriteId: string];
|
||||||
@@ -818,18 +862,20 @@
|
|||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const { createConfirm } = useMessage();
|
const { createConfirm } = useMessage();
|
||||||
|
|
||||||
const LIST_TAB_ITEMS = [
|
|
||||||
{ key: 'all', label: '全部' },
|
|
||||||
{ key: 'own', label: '我所有的' },
|
|
||||||
{ key: 'collab', label: '我协作的' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const showNewTaskInput = ref<string | null>(null);
|
const showNewTaskInput = ref<string | null>(null);
|
||||||
const newTaskTitle = ref('');
|
const newTaskTitle = ref('');
|
||||||
const showNewGroupInput = ref(false);
|
const showNewGroupInput = ref(false);
|
||||||
const newGroupName = ref('');
|
const newGroupName = ref('');
|
||||||
const renamingGroupId = ref<string | null>(null);
|
const renamingGroupId = ref<string | null>(null);
|
||||||
const renamingGroupName = ref('');
|
const renamingGroupName = ref('');
|
||||||
|
const renamingGroupInputRef = ref<HTMLInputElement | null>(null);
|
||||||
|
|
||||||
|
watch(renamingGroupId, async (val) => {
|
||||||
|
if (val) {
|
||||||
|
await nextTick();
|
||||||
|
renamingGroupInputRef.value?.focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
const submittingGroup = ref(false);
|
const submittingGroup = ref(false);
|
||||||
const submittingRename = ref(false);
|
const submittingRename = ref(false);
|
||||||
const filterVisible = ref(false);
|
const filterVisible = ref(false);
|
||||||
@@ -838,6 +884,24 @@
|
|||||||
const expandedTaskIds = ref<Set<string>>(new Set());
|
const expandedTaskIds = ref<Set<string>>(new Set());
|
||||||
const draggingTaskId = ref('');
|
const draggingTaskId = ref('');
|
||||||
const dragBlockedOnce = ref(false);
|
const dragBlockedOnce = ref(false);
|
||||||
|
const newTaskInputRef = ref<HTMLInputElement | null>(null);
|
||||||
|
const newGroupInputRef = ref<HTMLInputElement | null>(null);
|
||||||
|
|
||||||
|
watch(showNewTaskInput, async (val) => {
|
||||||
|
if (val) {
|
||||||
|
await nextTick();
|
||||||
|
newTaskInputRef.value?.focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(showNewGroupInput, async (val) => {
|
||||||
|
if (val) {
|
||||||
|
await nextTick();
|
||||||
|
newGroupInputRef.value?.focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const localGroups = ref<TaskGroup[]>([]);
|
||||||
|
|
||||||
function showDragBlockedMessage() {
|
function showDragBlockedMessage() {
|
||||||
import('ant-design-vue').then(({ message }) => {
|
import('ant-design-vue').then(({ message }) => {
|
||||||
@@ -845,29 +909,11 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const priorityOptions = ref<{ label: string; value: string }[]>([
|
const priorityOptions = PRIORITY_OPTIONS;
|
||||||
{ label: '高', value: 'high' },
|
|
||||||
{ label: '中', value: 'medium' },
|
|
||||||
{ label: '低', value: 'low' },
|
|
||||||
]);
|
|
||||||
|
|
||||||
const priorityColorMap: Record<string, string> = {
|
|
||||||
high: '#ffccc7',
|
|
||||||
medium: '#ffe58f',
|
|
||||||
low: '#91d5ff',
|
|
||||||
};
|
|
||||||
|
|
||||||
function getPriorityBg(value: string) {
|
|
||||||
return priorityColorMap[value] || '#f5f5f5';
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPriorityLabel(value: string): string {
|
|
||||||
const opt = priorityOptions.value.find((o) => o.value === value);
|
|
||||||
return opt?.label || value;
|
|
||||||
}
|
|
||||||
|
|
||||||
const editingCell = ref<{ taskId: string; field: string } | null>(null);
|
const editingCell = ref<{ taskId: string; field: string } | null>(null);
|
||||||
const editingValue = ref<any>('');
|
const editingValue = ref<any>('');
|
||||||
|
const textEditInputRef = ref<any>(null);
|
||||||
const datePickerVisible = ref(false);
|
const datePickerVisible = ref(false);
|
||||||
const pickerEpoch = ref(0);
|
const pickerEpoch = ref(0);
|
||||||
const editingDateDayjs = computed(() => {
|
const editingDateDayjs = computed(() => {
|
||||||
@@ -1084,6 +1130,14 @@
|
|||||||
|
|
||||||
const headerRenaming = ref(false);
|
const headerRenaming = ref(false);
|
||||||
const headerRenameValue = ref('');
|
const headerRenameValue = ref('');
|
||||||
|
const headerRenameInputRef = ref<HTMLInputElement | null>(null);
|
||||||
|
|
||||||
|
watch(headerRenaming, async (val) => {
|
||||||
|
if (val) {
|
||||||
|
await nextTick();
|
||||||
|
headerRenameInputRef.value?.focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
function onHeaderMenuClick({ key }: { key: string }) {
|
function onHeaderMenuClick({ key }: { key: string }) {
|
||||||
if (key === 'rename') {
|
if (key === 'rename') {
|
||||||
@@ -1113,15 +1167,6 @@
|
|||||||
headerRenaming.value = false;
|
headerRenaming.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isOverdue(task: TaskItem): boolean {
|
|
||||||
if (!task.endTime || task.completed) return false;
|
|
||||||
const end = new Date(task.endTime);
|
|
||||||
const now = new Date();
|
|
||||||
now.setHours(0, 0, 0, 0);
|
|
||||||
end.setHours(0, 0, 0, 0);
|
|
||||||
return now > end;
|
|
||||||
}
|
|
||||||
|
|
||||||
function _canEditTask(task: TaskItem): boolean {
|
function _canEditTask(task: TaskItem): boolean {
|
||||||
if (canEdit.value) return true;
|
if (canEdit.value) return true;
|
||||||
if (isReadOnly.value && task.assigneeId) {
|
if (isReadOnly.value && task.assigneeId) {
|
||||||
@@ -1210,6 +1255,24 @@
|
|||||||
datePickerVisible.value = false;
|
datePickerVisible.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function startEditTextCell(taskId: string, field: string) {
|
||||||
|
cancelEdit();
|
||||||
|
const task = findTaskById(taskId);
|
||||||
|
editingValue.value = (task as any)?.[field] || '';
|
||||||
|
editingCell.value = { taskId, field };
|
||||||
|
nextTick(() => {
|
||||||
|
textEditInputRef.value?.focus();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function confirmEditTextCell() {
|
||||||
|
if (!editingCell.value) return;
|
||||||
|
const { taskId, field } = editingCell.value;
|
||||||
|
const value = editingValue.value?.trim() ?? '';
|
||||||
|
emit('update-task-field', taskId, field, value);
|
||||||
|
cancelEdit();
|
||||||
|
}
|
||||||
|
|
||||||
function onPriorityChange(taskId: string, value: any) {
|
function onPriorityChange(taskId: string, value: any) {
|
||||||
if (value !== undefined && value !== null) {
|
if (value !== undefined && value !== null) {
|
||||||
emit('update-task-field', taskId, 'priority', value);
|
emit('update-task-field', taskId, 'priority', value);
|
||||||
@@ -1472,10 +1535,12 @@
|
|||||||
const { oldIndex, newIndex } = evt;
|
const { oldIndex, newIndex } = evt;
|
||||||
if (oldIndex === newIndex) return;
|
if (oldIndex === newIndex) return;
|
||||||
|
|
||||||
const movedElement = evt.item;
|
const groupId = localGroups.value[newIndex]?.id;
|
||||||
const groupId = movedElement?.dataset?.groupId;
|
|
||||||
if (!groupId) return;
|
if (!groupId) return;
|
||||||
|
|
||||||
|
// 拖拽结束后收起分组,防止展开跳动
|
||||||
|
collapsedGroups.value.add(groupId);
|
||||||
|
|
||||||
emit('move-task-group', groupId, newIndex + 1);
|
emit('move-task-group', groupId, newIndex + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1501,11 +1566,7 @@
|
|||||||
tasks: allTasks.filter((t) => t.groupId === g.id).sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0)),
|
tasks: allTasks.filter((t) => t.groupId === g.id).sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0)),
|
||||||
collapsed: collapsedGroups.value.has(g.id),
|
collapsed: collapsedGroups.value.has(g.id),
|
||||||
}));
|
}));
|
||||||
groups.sort((a, b) => {
|
groups.sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0));
|
||||||
if (a.id === '__default__') return -1;
|
|
||||||
if (b.id === '__default__') return 1;
|
|
||||||
return (a.sortOrder ?? 0) - (b.sortOrder ?? 0);
|
|
||||||
});
|
|
||||||
return groups;
|
return groups;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1526,6 +1587,14 @@
|
|||||||
return computeDynamicGroups(allTasks);
|
return computeDynamicGroups(allTasks);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
computedGroups,
|
||||||
|
(newGroups) => {
|
||||||
|
localGroups.value = [...newGroups];
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
function flattenGroupTasks(tasks?: TaskItem[]): FlatTaskItem[] {
|
function flattenGroupTasks(tasks?: TaskItem[]): FlatTaskItem[] {
|
||||||
const sourceTasks = tasks || [];
|
const sourceTasks = tasks || [];
|
||||||
const result: FlatTaskItem[] = [];
|
const result: FlatTaskItem[] = [];
|
||||||
@@ -1665,20 +1734,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleCreateTask(groupId?: string) {
|
|
||||||
const targetGroupId = groupId || props.currentList?.groups[0]?.id;
|
|
||||||
if (targetGroupId) {
|
|
||||||
showNewTaskInput.value = targetGroupId;
|
|
||||||
newTaskTitle.value = '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function submitNewTask(groupId: string) {
|
function submitNewTask(groupId: string) {
|
||||||
const title = newTaskTitle.value.trim();
|
const title = newTaskTitle.value.trim();
|
||||||
newTaskTitle.value = '';
|
newTaskTitle.value = '';
|
||||||
showNewTaskInput.value = null;
|
showNewTaskInput.value = null;
|
||||||
if (title) {
|
if (title) {
|
||||||
emit('create-task', groupId, title, 1);
|
emit('create-task', groupId, title);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1696,13 +1757,6 @@
|
|||||||
}, 300);
|
}, 300);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onCreateMenuClick({ key }: { key: string }) {
|
|
||||||
if (key === 'group') {
|
|
||||||
showNewGroupInput.value = true;
|
|
||||||
newGroupName.value = '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onGroupMenuClick(key: string, groupId: string, groupName: string) {
|
function onGroupMenuClick(key: string, groupId: string, groupName: string) {
|
||||||
if (key === 'rename') {
|
if (key === 'rename') {
|
||||||
renamingGroupId.value = groupId;
|
renamingGroupId.value = groupId;
|
||||||
@@ -1856,75 +1910,6 @@
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.feishu-btn-group {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
.feishu-btn-main {
|
|
||||||
border-radius: 6px 0 0 6px;
|
|
||||||
border-right: none;
|
|
||||||
padding: 0 7px;
|
|
||||||
height: 24px;
|
|
||||||
font-size: 14px;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 4px;
|
|
||||||
background: #fff;
|
|
||||||
border: 1px solid #d9d9d9;
|
|
||||||
color: rgba(0, 0, 0, 0.88);
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s;
|
|
||||||
line-height: 1;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: #4096ff;
|
|
||||||
border-color: #4096ff;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
color: #0958d9;
|
|
||||||
border-color: #0958d9;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.app-iconify) {
|
|
||||||
font-size: 14px !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.feishu-btn-arrow {
|
|
||||||
border-radius: 0 6px 6px 0;
|
|
||||||
padding: 0 4px;
|
|
||||||
height: 24px;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
background: #fff;
|
|
||||||
border: 1px solid #d9d9d9;
|
|
||||||
color: rgba(0, 0, 0, 0.88);
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s;
|
|
||||||
line-height: 1;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: #4096ff;
|
|
||||||
border-color: #4096ff;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
color: #0958d9;
|
|
||||||
border-color: #0958d9;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.app-iconify) {
|
|
||||||
font-size: 10px !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.feishu-arrow-icon {
|
|
||||||
font-size: 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-badge {
|
.filter-badge {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-left: 4px;
|
margin-left: 4px;
|
||||||
@@ -2076,7 +2061,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.content-table-header {
|
.content-table-header {
|
||||||
padding: 0 24px;
|
padding: 0 8px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
background: #fafafa;
|
background: #fafafa;
|
||||||
border-bottom: 1px solid #e5e6eb;
|
border-bottom: 1px solid #e5e6eb;
|
||||||
@@ -2123,11 +2108,8 @@
|
|||||||
|
|
||||||
.drag-handle {
|
.drag-handle {
|
||||||
cursor: grab;
|
cursor: grab;
|
||||||
color: #c0c4cc;
|
color: transparent;
|
||||||
opacity: 0;
|
transition: color 0.15s;
|
||||||
transition:
|
|
||||||
opacity 0.15s,
|
|
||||||
color 0.15s;
|
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -2139,7 +2121,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.task-row:hover .drag-handle {
|
.task-row:hover .drag-handle {
|
||||||
opacity: 1;
|
color: #c0c4cc;
|
||||||
}
|
}
|
||||||
|
|
||||||
.drag-handle:hover {
|
.drag-handle:hover {
|
||||||
@@ -2162,7 +2144,6 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
overflow: hidden;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2174,46 +2155,64 @@
|
|||||||
.task-group-header {
|
.task-group-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
padding: 6px 8px;
|
||||||
padding: 8px 24px;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border-bottom: 1px solid #fafafa;
|
border-bottom: 1px solid #f0f0f0;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 500;
|
font-weight: 600;
|
||||||
|
color: #1f2329;
|
||||||
&:hover {
|
&:hover {
|
||||||
background: #fafafa;
|
background: #f5f6f7;
|
||||||
}
|
|
||||||
.group-arrow {
|
|
||||||
font-size: 12px;
|
|
||||||
color: #999;
|
|
||||||
width: 16px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
}
|
||||||
.group-drag-handle {
|
.group-drag-handle {
|
||||||
cursor: grab;
|
cursor: grab;
|
||||||
font-size: 12px;
|
font-size: 14px;
|
||||||
color: #c0c4cc;
|
color: transparent;
|
||||||
|
transition: color 0.15s;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
&:hover {
|
justify-content: center;
|
||||||
color: #8c8c8c;
|
width: 24px;
|
||||||
}
|
min-width: 24px;
|
||||||
|
flex-shrink: 0;
|
||||||
&:active {
|
&:active {
|
||||||
cursor: grabbing;
|
cursor: grabbing;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
&:hover .group-drag-handle {
|
||||||
|
color: #c0c4cc;
|
||||||
|
}
|
||||||
|
.group-drag-handle:hover {
|
||||||
|
color: #8c8c8c;
|
||||||
|
}
|
||||||
|
.table-col-title {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1f2329;
|
||||||
|
.expand-arrow {
|
||||||
|
color: #8f959e;
|
||||||
|
}
|
||||||
|
}
|
||||||
.group-name {
|
.group-name {
|
||||||
color: #333;
|
color: #1f2329;
|
||||||
|
font-weight: 600;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-shrink: 1;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
.group-count {
|
.group-count {
|
||||||
color: #999;
|
color: #8f959e;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
|
font-size: 13px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-left: 4px;
|
||||||
}
|
}
|
||||||
.group-actions {
|
.group-actions {
|
||||||
margin-left: auto;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 4px;
|
gap: 2px;
|
||||||
|
flex-shrink: 0;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: opacity 0.15s;
|
transition: opacity 0.15s;
|
||||||
}
|
}
|
||||||
@@ -2228,7 +2227,7 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: #8c8c8c;
|
color: #8f959e;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
transition: all 0.15s;
|
transition: all 0.15s;
|
||||||
&:hover {
|
&:hover {
|
||||||
@@ -2242,7 +2241,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
height: 46px;
|
height: 46px;
|
||||||
padding: 0 24px;
|
padding: 0 8px;
|
||||||
border-bottom: 1px solid #f0f0f0;
|
border-bottom: 1px solid #f0f0f0;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
@@ -2406,6 +2405,13 @@
|
|||||||
min-width: 80px;
|
min-width: 80px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.inline-text-editor {
|
||||||
|
width: 100%;
|
||||||
|
:deep(.ant-input) {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdown-cell {
|
.dropdown-cell {
|
||||||
@@ -2427,6 +2433,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.readonly-cell {
|
||||||
|
&:hover {
|
||||||
|
border-color: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.date-picker-footer {
|
.date-picker-footer {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
@@ -2584,7 +2596,7 @@
|
|||||||
.new-group-row {
|
.new-group-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 12px 24px;
|
padding: 12px 8px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
&.new-group-btn {
|
&.new-group-btn {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -2595,40 +2607,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.content-tabs {
|
|
||||||
display: flex;
|
|
||||||
gap: 0;
|
|
||||||
border-bottom: 1px solid #f0f0f0;
|
|
||||||
padding: 0 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content-tab {
|
|
||||||
padding: 8px 16px;
|
|
||||||
font-size: 14px;
|
|
||||||
color: #8c8c8c;
|
|
||||||
cursor: pointer;
|
|
||||||
position: relative;
|
|
||||||
transition: color 0.2s;
|
|
||||||
&:hover {
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
&.active {
|
|
||||||
color: #1677ff;
|
|
||||||
font-weight: 500;
|
|
||||||
&::after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
bottom: -1px;
|
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
width: 24px;
|
|
||||||
height: 2px;
|
|
||||||
background: #1677ff;
|
|
||||||
border-radius: 1px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.list-group-view {
|
.list-group-view {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
>
|
>
|
||||||
<template v-if="taskData">
|
<template v-if="taskData">
|
||||||
<div class="detail-toolbar">
|
<div class="detail-toolbar">
|
||||||
<a-button size="small" :type="taskData.completed ? 'primary' : 'default'" @click="toggleComplete">
|
<a-button size="small" :type="taskData.completed ? 'primary' : 'default'" :disabled="isFullReadOnly" @click="toggleComplete">
|
||||||
<Icon icon="ant-design:check-outlined" class="mr-1" />
|
<Icon icon="ant-design:check-outlined" class="mr-1" />
|
||||||
{{ taskData.completed ? '已完成' : '完成任务' }}
|
{{ taskData.completed ? '已完成' : '完成任务' }}
|
||||||
</a-button>
|
</a-button>
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
<Icon :icon="isFollowed ? 'ant-design:eye-filled' : 'ant-design:eye-outlined'" />
|
<Icon :icon="isFollowed ? 'ant-design:eye-filled' : 'ant-design:eye-outlined'" />
|
||||||
</a-button>
|
</a-button>
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
<a-tooltip title="删除">
|
<a-tooltip v-if="!isReadOnly" title="删除">
|
||||||
<a-button type="text" size="small" @click="handleDelete">
|
<a-button type="text" size="small" @click="handleDelete">
|
||||||
<Icon icon="ant-design:delete-outlined" />
|
<Icon icon="ant-design:delete-outlined" />
|
||||||
</a-button>
|
</a-button>
|
||||||
@@ -63,7 +63,7 @@
|
|||||||
@blur="onTitleBlur"
|
@blur="onTitleBlur"
|
||||||
@press-enter="editingTitle = false"
|
@press-enter="editingTitle = false"
|
||||||
/>
|
/>
|
||||||
<div v-else class="detail-title-text" @click="editingTitle = true">
|
<div v-else class="detail-title-text" :class="{ 'readonly-text': isFullReadOnly }" @click="!isFullReadOnly && (editingTitle = true)">
|
||||||
{{ taskData.taskName }}
|
{{ taskData.taskName }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -76,12 +76,13 @@
|
|||||||
<div class="detail-row-content">
|
<div class="detail-row-content">
|
||||||
<div class="detail-field-row">
|
<div class="detail-field-row">
|
||||||
<span class="detail-field-label">负责人</span>
|
<span class="detail-field-label">负责人</span>
|
||||||
<div v-if="taskData.assigneeName" class="assignee-info" @click="openDrawerUserSelect('assignee')">
|
<div v-if="taskData.assigneeName" class="assignee-info" :class="{ 'readonly-field': isReadOnly }" @click="!isReadOnly && openDrawerUserSelect('assignee')">
|
||||||
<a-tooltip :title="taskData.assigneeName" placement="top">
|
<a-tooltip :title="taskData.assigneeName" placement="top">
|
||||||
<AvatarDisplay :names="taskData.assigneeName" :size="28" :show-name="true" />
|
<AvatarDisplay :names="taskData.assigneeName" :size="28" :show-name="true" />
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
</div>
|
</div>
|
||||||
<a-button v-else type="text" class="add-field-btn" @click="openDrawerUserSelect('assignee')">添加</a-button>
|
<a-button v-else-if="!isReadOnly" type="text" class="add-field-btn" @click="openDrawerUserSelect('assignee')">添加</a-button>
|
||||||
|
<span v-else class="detail-field-value">-</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -93,12 +94,13 @@
|
|||||||
<div class="detail-row-content">
|
<div class="detail-row-content">
|
||||||
<div class="detail-field-row">
|
<div class="detail-field-row">
|
||||||
<span class="detail-field-label">参与人</span>
|
<span class="detail-field-label">参与人</span>
|
||||||
<div v-if="taskData.participantName" class="assignee-info" @click="openDrawerUserSelect('participant')">
|
<div v-if="taskData.participantName" class="assignee-info" :class="{ 'readonly-field': isFullReadOnly }" @click="!isFullReadOnly && openDrawerUserSelect('participant')">
|
||||||
<a-tooltip :title="taskData.participantName" placement="top">
|
<a-tooltip :title="taskData.participantName" placement="top">
|
||||||
<AvatarDisplay :names="taskData.participantName" :size="28" :show-name="true" />
|
<AvatarDisplay :names="taskData.participantName" :size="28" :show-name="true" />
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
</div>
|
</div>
|
||||||
<a-button v-else type="text" class="add-field-btn" @click="openDrawerUserSelect('participant')">添加</a-button>
|
<a-button v-else-if="!isFullReadOnly" type="text" class="add-field-btn" @click="openDrawerUserSelect('participant')">添加</a-button>
|
||||||
|
<span v-else class="detail-field-value">-</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -111,7 +113,7 @@
|
|||||||
<div class="detail-field-row">
|
<div class="detail-field-row">
|
||||||
<span class="detail-field-label">开始时间</span>
|
<span class="detail-field-label">开始时间</span>
|
||||||
<a-date-picker
|
<a-date-picker
|
||||||
v-if="drawerDateEditMode === 'start'"
|
v-if="drawerDateEditMode === 'start' && !isFullReadOnly"
|
||||||
:value="startTimeDayjs ? dayjs(startTimeDayjs) : null"
|
:value="startTimeDayjs ? dayjs(startTimeDayjs) : null"
|
||||||
:open="drawerDatePickerVisible"
|
:open="drawerDatePickerVisible"
|
||||||
placeholder="选择开始时间"
|
placeholder="选择开始时间"
|
||||||
@@ -127,7 +129,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</a-date-picker>
|
</a-date-picker>
|
||||||
<span v-else class="detail-field-value detail-field-clickable" @click="openDrawerDatePicker('start')">{{
|
<span v-else class="detail-field-value detail-field-clickable" :class="{ 'readonly-field': isFullReadOnly }" @click="!isFullReadOnly && openDrawerDatePicker('start')">{{
|
||||||
startTimeDisplay || '未设置'
|
startTimeDisplay || '未设置'
|
||||||
}}</span>
|
}}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -142,7 +144,7 @@
|
|||||||
<div class="detail-field-row">
|
<div class="detail-field-row">
|
||||||
<span class="detail-field-label">截止时间</span>
|
<span class="detail-field-label">截止时间</span>
|
||||||
<a-date-picker
|
<a-date-picker
|
||||||
v-if="drawerDateEditMode === 'end'"
|
v-if="drawerDateEditMode === 'end' && !isFullReadOnly"
|
||||||
:value="endTimeDayjs ? dayjs(endTimeDayjs) : null"
|
:value="endTimeDayjs ? dayjs(endTimeDayjs) : null"
|
||||||
:open="drawerDatePickerVisible"
|
:open="drawerDatePickerVisible"
|
||||||
placeholder="选择截止时间"
|
placeholder="选择截止时间"
|
||||||
@@ -162,8 +164,8 @@
|
|||||||
<span
|
<span
|
||||||
v-else
|
v-else
|
||||||
class="detail-field-value detail-field-clickable"
|
class="detail-field-value detail-field-clickable"
|
||||||
:class="{ 'overdue-text': isEndTimeOverdue }"
|
:class="{ 'overdue-text': isEndTimeOverdue, 'readonly-field': isFullReadOnly }"
|
||||||
@click="openDrawerDatePicker('end')"
|
@click="!isFullReadOnly && openDrawerDatePicker('end')"
|
||||||
>{{ endTimeDisplay || '未设置' }}</span
|
>{{ endTimeDisplay || '未设置' }}</span
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
@@ -189,8 +191,8 @@
|
|||||||
<Icon icon="tasklist-desc|svg" />
|
<Icon icon="tasklist-desc|svg" />
|
||||||
</span>
|
</span>
|
||||||
<div class="detail-row-content">
|
<div class="detail-row-content">
|
||||||
<a-textarea v-if="showDescEditor" v-model:value="taskData.taskDesc" :rows="3" placeholder="添加描述..." autofocus @blur="onDescBlur" />
|
<a-textarea v-if="showDescEditor && !isFullReadOnly" v-model:value="taskData.taskDesc" :rows="3" placeholder="添加描述..." autofocus @blur="onDescBlur" />
|
||||||
<span v-else class="desc-trigger" @click="showDescEditor = true">
|
<span v-else class="desc-trigger" :class="{ 'readonly-field': isFullReadOnly }" @click="!isFullReadOnly && (showDescEditor = true)">
|
||||||
{{ taskData.taskDesc || '添加描述' }}
|
{{ taskData.taskDesc || '添加描述' }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -202,14 +204,14 @@
|
|||||||
</span>
|
</span>
|
||||||
<div class="detail-row-content">
|
<div class="detail-row-content">
|
||||||
<a-textarea
|
<a-textarea
|
||||||
v-if="showRemarkEditor"
|
v-if="showRemarkEditor && !isFullReadOnly"
|
||||||
v-model:value="taskData.remark"
|
v-model:value="taskData.remark"
|
||||||
:rows="3"
|
:rows="3"
|
||||||
placeholder="添加其他事项说明..."
|
placeholder="添加其他事项说明..."
|
||||||
autofocus
|
autofocus
|
||||||
@blur="onRemarkBlur"
|
@blur="onRemarkBlur"
|
||||||
/>
|
/>
|
||||||
<span v-else class="desc-trigger" @click="showRemarkEditor = true">
|
<span v-else class="desc-trigger" :class="{ 'readonly-field': isFullReadOnly }" @click="!isFullReadOnly && (showRemarkEditor = true)">
|
||||||
{{ taskData.remark || '添加其他事项说明' }}
|
{{ taskData.remark || '添加其他事项说明' }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -242,7 +244,7 @@
|
|||||||
|
|
||||||
<template v-if="subtaskExpanded">
|
<template v-if="subtaskExpanded">
|
||||||
<draggable
|
<draggable
|
||||||
:model-value="subTasks"
|
:list="localSubTasks"
|
||||||
:item-key="(child: any) => child.id"
|
:item-key="(child: any) => child.id"
|
||||||
handle=".feishu-subtask-drag"
|
handle=".feishu-subtask-drag"
|
||||||
:animation="200"
|
:animation="200"
|
||||||
@@ -252,14 +254,14 @@
|
|||||||
>
|
>
|
||||||
<template #item="{ element: child }">
|
<template #item="{ element: child }">
|
||||||
<div class="feishu-subtask-row" :data-task-id="child.id">
|
<div class="feishu-subtask-row" :data-task-id="child.id">
|
||||||
<span class="feishu-subtask-drag">
|
<span v-if="!isFullReadOnly" class="feishu-subtask-drag">
|
||||||
<Icon icon="tasklist-subtask-drag|svg" />
|
<Icon icon="tasklist-subtask-drag|svg" />
|
||||||
</span>
|
</span>
|
||||||
<button class="feishu-subtask-check" :class="{ checked: child.completed }" @click="emit('toggle-sub-task', child.id)">
|
<button class="feishu-subtask-check" :class="{ checked: child.completed }" :disabled="isFullReadOnly" @click="emit('toggle-sub-task', child.id)">
|
||||||
<Icon v-if="child.completed" icon="ant-design:check-outlined" />
|
<Icon v-if="child.completed" icon="ant-design:check-outlined" />
|
||||||
</button>
|
</button>
|
||||||
<span class="feishu-subtask-name" :class="{ done: child.completed }">{{ child.taskName }}</span>
|
<span class="feishu-subtask-name" :class="{ done: child.completed }">{{ child.taskName }}</span>
|
||||||
<div class="feishu-subtask-actions">
|
<div v-if="!isFullReadOnly" class="feishu-subtask-actions">
|
||||||
<a-tooltip title="设置日期">
|
<a-tooltip title="设置日期">
|
||||||
<span class="feishu-subtask-action-btn" @click.stop="openSubTaskDateEditor(child.id)">
|
<span class="feishu-subtask-action-btn" @click.stop="openSubTaskDateEditor(child.id)">
|
||||||
<Icon icon="ant-design:calendar-outlined" />
|
<Icon icon="ant-design:calendar-outlined" />
|
||||||
@@ -276,7 +278,10 @@
|
|||||||
</span>
|
</span>
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="child.assigneeName" class="feishu-subtask-avatar" @click.stop="openSubTaskUserSelect(child)">
|
<div v-if="child.assigneeName && !isFullReadOnly" class="feishu-subtask-avatar" @click.stop="openSubTaskUserSelect(child)">
|
||||||
|
<a-tooltip :title="child.assigneeName" placement="top"><AvatarDisplay :names="child.assigneeName" :size="20" /></a-tooltip>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="child.assigneeName && isFullReadOnly" class="feishu-subtask-avatar">
|
||||||
<a-tooltip :title="child.assigneeName" placement="top"><AvatarDisplay :names="child.assigneeName" :size="20" /></a-tooltip>
|
<a-tooltip :title="child.assigneeName" placement="top"><AvatarDisplay :names="child.assigneeName" :size="20" /></a-tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -293,7 +298,7 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="showNewSubTaskInput" class="feishu-subtask-row feishu-subtask-input-row">
|
<div v-if="showNewSubTaskInput && !isFullReadOnly" class="feishu-subtask-row feishu-subtask-input-row">
|
||||||
<span class="feishu-subtask-drag-placeholder"></span>
|
<span class="feishu-subtask-drag-placeholder"></span>
|
||||||
<span class="feishu-subtask-check feishu-subtask-check-empty"></span>
|
<span class="feishu-subtask-check feishu-subtask-check-empty"></span>
|
||||||
<a-input
|
<a-input
|
||||||
@@ -306,7 +311,7 @@
|
|||||||
@blur="showNewSubTaskInput = false"
|
@blur="showNewSubTaskInput = false"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div v-else v-if="canAddSubTask" class="feishu-subtask-add" @click="showNewSubTaskInput = true">
|
<div v-else-if="canAddSubTask && !isFullReadOnly" class="feishu-subtask-add" @click="showNewSubTaskInput = true">
|
||||||
<Icon icon="ant-design:plus-outlined" class="feishu-subtask-add-icon" />
|
<Icon icon="ant-design:plus-outlined" class="feishu-subtask-add-icon" />
|
||||||
<span>添加子任务</span>
|
<span>添加子任务</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -322,7 +327,7 @@
|
|||||||
<AvatarDisplay :names="taskData.followersName" :size="24" :show-name="false" />
|
<AvatarDisplay :names="taskData.followersName" :size="24" :show-name="false" />
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
<span v-else class="follower-empty">将通知 0 人</span>
|
<span v-else class="follower-empty">将通知 0 人</span>
|
||||||
<a-button type="text" size="small" @click="openDrawerUserSelect('followers')">
|
<a-button v-if="!isFullReadOnly" type="text" size="small" @click="openDrawerUserSelect('followers')">
|
||||||
<Icon icon="ant-design:plus-outlined" />
|
<Icon icon="ant-design:plus-outlined" />
|
||||||
</a-button>
|
</a-button>
|
||||||
</div>
|
</div>
|
||||||
@@ -348,7 +353,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, computed, nextTick } from 'vue';
|
import { ref, computed, watch, nextTick } from 'vue';
|
||||||
import { BasicDrawer, useDrawerInner } from '/@/components/Drawer';
|
import { BasicDrawer, useDrawerInner } from '/@/components/Drawer';
|
||||||
import Icon from '/@/components/Icon/index';
|
import Icon from '/@/components/Icon/index';
|
||||||
import AvatarDisplay from './AvatarDisplay.vue';
|
import AvatarDisplay from './AvatarDisplay.vue';
|
||||||
@@ -386,6 +391,13 @@
|
|||||||
|
|
||||||
const allTasks = ref<TaskItem[]>([]);
|
const allTasks = ref<TaskItem[]>([]);
|
||||||
const taskData = ref<TaskItem | null>(null);
|
const taskData = ref<TaskItem | null>(null);
|
||||||
|
const isReadOnly = ref(false);
|
||||||
|
const isAssigneeEdit = computed(() => {
|
||||||
|
if (!isReadOnly.value || !taskData.value) return false;
|
||||||
|
const assigneeIds = (taskData.value.assigneeId || '').split(',').filter(Boolean);
|
||||||
|
return assigneeIds.includes(CURRENT_USER.value);
|
||||||
|
});
|
||||||
|
const isFullReadOnly = computed(() => isReadOnly.value && !isAssigneeEdit.value);
|
||||||
const showNewSubTaskInput = ref(false);
|
const showNewSubTaskInput = ref(false);
|
||||||
const newSubTaskName = ref('');
|
const newSubTaskName = ref('');
|
||||||
const startTimeDayjs = ref<string | null>(null);
|
const startTimeDayjs = ref<string | null>(null);
|
||||||
@@ -451,6 +463,12 @@
|
|||||||
return allTasks.value.filter((t) => t.pid === taskData.value!.id).sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0));
|
return allTasks.value.filter((t) => t.pid === taskData.value!.id).sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const localSubTasks = ref<TaskItem[]>([]);
|
||||||
|
|
||||||
|
watch(subTasks, (val) => {
|
||||||
|
localSubTasks.value = [...val];
|
||||||
|
}, { immediate: true });
|
||||||
|
|
||||||
const subtaskProgress = computed(() => {
|
const subtaskProgress = computed(() => {
|
||||||
const total = subTasks.value.length;
|
const total = subTasks.value.length;
|
||||||
if (total === 0) return { text: '', percent: 0 };
|
if (total === 0) return { text: '', percent: 0 };
|
||||||
@@ -489,6 +507,7 @@
|
|||||||
const [registerDrawer, { closeDrawer }] = useDrawerInner(async (data) => {
|
const [registerDrawer, { closeDrawer }] = useDrawerInner(async (data) => {
|
||||||
taskData.value = data?.task ? { ...data.task } : null;
|
taskData.value = data?.task ? { ...data.task } : null;
|
||||||
allTasks.value = data?.allTasks ? [...data.allTasks] : [];
|
allTasks.value = data?.allTasks ? [...data.allTasks] : [];
|
||||||
|
isReadOnly.value = data?.readOnly === true;
|
||||||
showNewSubTaskInput.value = false;
|
showNewSubTaskInput.value = false;
|
||||||
newSubTaskName.value = '';
|
newSubTaskName.value = '';
|
||||||
editingTitle.value = false;
|
editingTitle.value = false;
|
||||||
@@ -839,6 +858,12 @@
|
|||||||
background: #f5f6f7;
|
background: #f5f6f7;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
&.readonly-text {
|
||||||
|
cursor: default;
|
||||||
|
&:hover {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-title-input {
|
.detail-title-input {
|
||||||
@@ -1014,6 +1039,19 @@
|
|||||||
&:hover {
|
&:hover {
|
||||||
color: #3370ff;
|
color: #3370ff;
|
||||||
}
|
}
|
||||||
|
&.readonly-field {
|
||||||
|
cursor: default;
|
||||||
|
&:hover {
|
||||||
|
color: #8c8c8c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.readonly-field {
|
||||||
|
cursor: default !important;
|
||||||
|
&:hover {
|
||||||
|
color: inherit !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-checkbox {
|
.task-checkbox {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -47,22 +47,24 @@
|
|||||||
<div class="sidebar-favorites-header">
|
<div class="sidebar-favorites-header">
|
||||||
<Icon icon="tasklist-task-list|svg" class="sidebar-favorites-icon" />
|
<Icon icon="tasklist-task-list|svg" class="sidebar-favorites-icon" />
|
||||||
<span class="sidebar-favorites-name">收藏清单</span>
|
<span class="sidebar-favorites-name">收藏清单</span>
|
||||||
<Icon icon="ant-design:plus-outlined" class="sidebar-group-add" @click.stop="startNewList('')" />
|
<a-dropdown :trigger="['click']">
|
||||||
|
<Icon icon="ant-design:plus-outlined" class="sidebar-group-add" @click.stop />
|
||||||
|
<template #overlay>
|
||||||
|
<a-menu @click="onAddMenuClick">
|
||||||
|
<a-menu-item key="new-list">
|
||||||
|
<Icon icon="ant-design:file-add-outlined" class="mr-1" />
|
||||||
|
新建清单
|
||||||
|
</a-menu-item>
|
||||||
|
<a-menu-item key="new-group">
|
||||||
|
<Icon icon="ant-design:folder-add-outlined" class="mr-1" />
|
||||||
|
新建分组
|
||||||
|
</a-menu-item>
|
||||||
|
</a-menu>
|
||||||
|
</template>
|
||||||
|
</a-dropdown>
|
||||||
</div>
|
</div>
|
||||||
<div class="sidebar-favorites-items">
|
<div class="sidebar-favorites-items">
|
||||||
<div v-if="showNewListInput && newListGroupId === ''" class="sidebar-new-item-row">
|
<draggable :list="localUngroupedTaskLists" group="tasklists" item-key="id" handle=".list-drag-handle" :animation="200" @end="onDragEnd">
|
||||||
<Icon icon="ant-design:file-text-outlined" class="sidebar-item-icon" style="opacity: 0.4" />
|
|
||||||
<a-input
|
|
||||||
v-model:value="newListName"
|
|
||||||
size="small"
|
|
||||||
placeholder="输入清单名称..."
|
|
||||||
autofocus
|
|
||||||
style="flex: 1"
|
|
||||||
@press-enter="submitNewList"
|
|
||||||
@blur="showNewListInput = false"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<draggable :model-value="ungroupedTaskLists" group="tasklists" item-key="id" handle=".list-drag-handle" :animation="200" @end="onDragEnd">
|
|
||||||
<template #item="{ element: taskList }">
|
<template #item="{ element: taskList }">
|
||||||
<div
|
<div
|
||||||
v-if="renamingId !== taskList.id"
|
v-if="renamingId !== taskList.id"
|
||||||
@@ -97,13 +99,25 @@
|
|||||||
<div v-else class="sidebar-item sidebar-item-renaming">
|
<div v-else class="sidebar-item sidebar-item-renaming">
|
||||||
<span class="list-drag-handle-placeholder"></span>
|
<span class="list-drag-handle-placeholder"></span>
|
||||||
<Icon icon="ant-design:file-text-outlined" class="sidebar-item-icon" style="opacity: 0.4" />
|
<Icon icon="ant-design:file-text-outlined" class="sidebar-item-icon" style="opacity: 0.4" />
|
||||||
<a-input v-model:value="renameValue" size="small" style="flex: 1" @press-enter="confirmInlineRename" @blur="confirmInlineRename" />
|
<a-input ref="renameInputRef" v-model:value="renameValue" size="small" style="flex: 1" @press-enter="confirmInlineRename" @blur="confirmInlineRename" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</draggable>
|
</draggable>
|
||||||
|
<div v-if="showNewListInput && newListGroupId === ''" class="sidebar-new-item-row">
|
||||||
|
<Icon icon="ant-design:file-text-outlined" class="sidebar-item-icon" style="opacity: 0.4" />
|
||||||
|
<a-input
|
||||||
|
v-model:value="newListName"
|
||||||
|
size="small"
|
||||||
|
placeholder="输入清单名称..."
|
||||||
|
autofocus
|
||||||
|
style="flex: 1"
|
||||||
|
@press-enter="submitNewList"
|
||||||
|
@blur="showNewListInput = false"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<draggable :model-value="userGroups" group="tasklistgroups" item-key="id" :animation="200" drag-class="group-drag-ghost" @start="onGroupDragStart" @end="onGroupDragEnd">
|
<draggable :list="localUserGroups" group="tasklistgroups" item-key="id" :animation="200" drag-class="group-drag-ghost" @start="onGroupDragStart" @end="onGroupDragEnd">
|
||||||
<template #item="{ element: group }">
|
<template #item="{ element: group }">
|
||||||
<div :data-group-id="group.id" :class="{ 'group-being-dragged': draggingGroupId === group.id }">
|
<div :data-group-id="group.id" :class="{ 'group-being-dragged': draggingGroupId === group.id }">
|
||||||
<div
|
<div
|
||||||
@@ -134,7 +148,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div v-if="group.delFlag !== 1 && renamingId === group.id" class="sidebar-group-header sidebar-group-renaming" @click.stop>
|
<div v-if="group.delFlag !== 1 && renamingId === group.id" class="sidebar-group-header sidebar-group-renaming" @click.stop>
|
||||||
<Icon icon="tasklist-arrow-down|svg" class="group-collapse-arrow" :class="{ collapsed: collapsedGroups[group.id] || draggingGroupId === group.id }" />
|
<Icon icon="tasklist-arrow-down|svg" class="group-collapse-arrow" :class="{ collapsed: collapsedGroups[group.id] || draggingGroupId === group.id }" />
|
||||||
<a-input v-model:value="renameValue" size="small" style="flex: 1" @press-enter="confirmInlineRename" @blur="confirmInlineRename" />
|
<a-input ref="renameInputRef" v-model:value="renameValue" size="small" style="flex: 1" @press-enter="confirmInlineRename" @blur="confirmInlineRename" />
|
||||||
</div>
|
</div>
|
||||||
<template v-if="group.delFlag !== 1 && !collapsedGroups[group.id]">
|
<template v-if="group.delFlag !== 1 && !collapsedGroups[group.id]">
|
||||||
<div class="sidebar-group-items" :data-group-id="group.id">
|
<div class="sidebar-group-items" :data-group-id="group.id">
|
||||||
@@ -151,7 +165,7 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<draggable
|
<draggable
|
||||||
:model-value="getGroupTaskLists(group)"
|
:list="getLocalGroupTaskLists(group)"
|
||||||
group="tasklists"
|
group="tasklists"
|
||||||
item-key="id"
|
item-key="id"
|
||||||
handle=".list-drag-handle"
|
handle=".list-drag-handle"
|
||||||
@@ -192,12 +206,12 @@
|
|||||||
<div v-else class="sidebar-item sidebar-item-renaming">
|
<div v-else class="sidebar-item sidebar-item-renaming">
|
||||||
<span class="list-drag-handle-placeholder"></span>
|
<span class="list-drag-handle-placeholder"></span>
|
||||||
<Icon icon="ant-design:file-text-outlined" class="sidebar-item-icon" style="opacity: 0.4" />
|
<Icon icon="ant-design:file-text-outlined" class="sidebar-item-icon" style="opacity: 0.4" />
|
||||||
<a-input v-model:value="renameValue" size="small" style="flex: 1" @press-enter="confirmInlineRename" @blur="confirmInlineRename" />
|
<a-input ref="renameInputRef" v-model:value="renameValue" size="small" style="flex: 1" @press-enter="confirmInlineRename" @blur="confirmInlineRename" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</draggable>
|
</draggable>
|
||||||
<div
|
<div
|
||||||
v-if="group.taskLists.filter((l) => l.delFlag !== 1).length === 0 && !(showNewListInput && newListGroupId === group.id)"
|
v-if="getLocalGroupTaskLists(group).length === 0 && !(showNewListInput && newListGroupId === group.id)"
|
||||||
class="sidebar-empty-hint"
|
class="sidebar-empty-hint"
|
||||||
>
|
>
|
||||||
可拖拽清单加入该分组
|
可拖拽清单加入该分组
|
||||||
@@ -229,7 +243,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, computed } from 'vue';
|
import { ref, computed, watch, nextTick } from 'vue';
|
||||||
import Icon from '/@/components/Icon/index';
|
import Icon from '/@/components/Icon/index';
|
||||||
import draggable from 'vuedraggable';
|
import draggable from 'vuedraggable';
|
||||||
import { useMessage } from '/@/hooks/web/useMessage';
|
import { useMessage } from '/@/hooks/web/useMessage';
|
||||||
@@ -253,6 +267,7 @@
|
|||||||
'create-group': [name: string];
|
'create-group': [name: string];
|
||||||
'rename-list': [listId: string, newName: string];
|
'rename-list': [listId: string, newName: string];
|
||||||
'delete-list': [listId: string];
|
'delete-list': [listId: string];
|
||||||
|
'remove-favorite': [listId: string];
|
||||||
'rename-group': [groupId: string, newName: string];
|
'rename-group': [groupId: string, newName: string];
|
||||||
'delete-group': [groupId: string];
|
'delete-group': [groupId: string];
|
||||||
'quick-access-tab-click': [tabKey: string];
|
'quick-access-tab-click': [tabKey: string];
|
||||||
@@ -277,14 +292,46 @@
|
|||||||
return [];
|
return [];
|
||||||
});
|
});
|
||||||
|
|
||||||
function getGroupTaskLists(group: TaskListGroup) {
|
|
||||||
return [...group.taskLists].filter((l) => l.delFlag !== 1).sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0));
|
|
||||||
}
|
|
||||||
|
|
||||||
const userGroups = computed(() => {
|
const userGroups = computed(() => {
|
||||||
return props.taskListGroups.filter((g) => g.id !== '__ungrouped__' && g.delFlag !== 1).sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0));
|
return props.taskListGroups.filter((g) => g.id !== '__ungrouped__' && g.delFlag !== 1).sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 本地可变副本,供 draggable :list 模式直接修改,实现乐观更新
|
||||||
|
const localUngroupedTaskLists = ref<any[]>([]);
|
||||||
|
const localUserGroups = ref<any[]>([]);
|
||||||
|
const localGroupTaskListsMap = ref<Record<string, any[]>>({});
|
||||||
|
|
||||||
|
watch(ungroupedTaskLists, (val) => {
|
||||||
|
localUngroupedTaskLists.value = [...val];
|
||||||
|
}, { immediate: true });
|
||||||
|
|
||||||
|
watch(userGroups, (val) => {
|
||||||
|
localUserGroups.value = val.map((g) => ({
|
||||||
|
...g,
|
||||||
|
taskLists: [...(g.taskLists || [])],
|
||||||
|
}));
|
||||||
|
// 同步分组内清单到 map
|
||||||
|
const newMap: Record<string, any[]> = {};
|
||||||
|
for (const g of val) {
|
||||||
|
newMap[g.id] = [...(g.taskLists || [])].filter((l) => l.delFlag !== 1).sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0));
|
||||||
|
}
|
||||||
|
localGroupTaskListsMap.value = newMap;
|
||||||
|
}, { immediate: true });
|
||||||
|
|
||||||
|
function getGroupTaskLists(group: any) {
|
||||||
|
return [...group.taskLists].filter((l) => l.delFlag !== 1).sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLocalGroupTaskLists(group: any) {
|
||||||
|
if (localGroupTaskListsMap.value[group.id]) {
|
||||||
|
return localGroupTaskListsMap.value[group.id];
|
||||||
|
}
|
||||||
|
// 兜底:如果没有本地缓存,从原始数据计算
|
||||||
|
const list = getGroupTaskLists(group);
|
||||||
|
localGroupTaskListsMap.value[group.id] = list;
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
const hoveredListId = ref('');
|
const hoveredListId = ref('');
|
||||||
const collapsedGroups = ref<Record<string, boolean>>({});
|
const collapsedGroups = ref<Record<string, boolean>>({});
|
||||||
const showNewGroupInput = ref(false);
|
const showNewGroupInput = ref(false);
|
||||||
@@ -295,6 +342,14 @@
|
|||||||
const renamingId = ref('');
|
const renamingId = ref('');
|
||||||
const renamingType = ref<'list' | 'group'>('list');
|
const renamingType = ref<'list' | 'group'>('list');
|
||||||
const renameValue = ref('');
|
const renameValue = ref('');
|
||||||
|
const renameInputRef = ref<HTMLInputElement | null>(null);
|
||||||
|
|
||||||
|
watch(renamingId, async (val) => {
|
||||||
|
if (val) {
|
||||||
|
await nextTick();
|
||||||
|
renameInputRef.value?.focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const draggingGroupId = ref('');
|
const draggingGroupId = ref('');
|
||||||
const dragStartX = ref(0);
|
const dragStartX = ref(0);
|
||||||
@@ -363,6 +418,8 @@
|
|||||||
const itemEl = evt.item;
|
const itemEl = evt.item;
|
||||||
const groupId = itemEl?.dataset?.groupId || itemEl?.closest('[data-group-id]')?.dataset?.groupId;
|
const groupId = itemEl?.dataset?.groupId || itemEl?.closest('[data-group-id]')?.dataset?.groupId;
|
||||||
if (!groupId) return;
|
if (!groupId) return;
|
||||||
|
// 拖拽结束后保持分组折叠状态,防止展开跳动
|
||||||
|
collapsedGroups.value[groupId] = true;
|
||||||
const newIndex = evt.newIndex ?? 0;
|
const newIndex = evt.newIndex ?? 0;
|
||||||
emit('move-group', groupId, newIndex + 1);
|
emit('move-group', groupId, newIndex + 1);
|
||||||
}
|
}
|
||||||
@@ -372,6 +429,15 @@
|
|||||||
return perm === '1' || perm === '2';
|
return perm === '1' || perm === '2';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onAddMenuClick({ key }: { key: string }) {
|
||||||
|
if (key === 'new-list') {
|
||||||
|
startNewList('');
|
||||||
|
} else if (key === 'new-group') {
|
||||||
|
showNewGroupInput.value = true;
|
||||||
|
newGroupName.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function startNewList(groupId: string) {
|
function startNewList(groupId: string) {
|
||||||
showNewListInput.value = true;
|
showNewListInput.value = true;
|
||||||
newListGroupId.value = groupId;
|
newListGroupId.value = groupId;
|
||||||
@@ -397,12 +463,12 @@
|
|||||||
} else if (key === 'remove') {
|
} else if (key === 'remove') {
|
||||||
createConfirm({
|
createConfirm({
|
||||||
iconType: 'warning',
|
iconType: 'warning',
|
||||||
title: '确认删除',
|
title: '确认移除',
|
||||||
content: '删除任务清单后,其下所有任务也将被删除,确定要删除吗?',
|
content: '确定从收藏清单中移除吗?',
|
||||||
okText: '确认',
|
okText: '确认',
|
||||||
cancelText: '取消',
|
cancelText: '取消',
|
||||||
onOk: () => {
|
onOk: () => {
|
||||||
emit('delete-list', listId);
|
emit('remove-favorite', listId);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
+272
-168
@@ -18,8 +18,10 @@
|
|||||||
@quick-access-tab-click="onQuickAccessTabClick"
|
@quick-access-tab-click="onQuickAccessTabClick"
|
||||||
@move-list="onMoveList"
|
@move-list="onMoveList"
|
||||||
@move-group="onMoveGroup"
|
@move-group="onMoveGroup"
|
||||||
|
@remove-favorite="onRemoveFavorite"
|
||||||
/>
|
/>
|
||||||
<TaskContent
|
<TaskContent
|
||||||
|
v-if="!isFlatView"
|
||||||
:current-list="currentList"
|
:current-list="currentList"
|
||||||
:task-list-groups="showListGroupView ? quickAccessGroups : taskListGroups"
|
:task-list-groups="showListGroupView ? quickAccessGroups : taskListGroups"
|
||||||
:status-filter="statusFilter"
|
:status-filter="statusFilter"
|
||||||
@@ -28,12 +30,10 @@
|
|||||||
:sort-direction="sortDirection"
|
:sort-direction="sortDirection"
|
||||||
:visible-fields="visibleFields"
|
:visible-fields="visibleFields"
|
||||||
:show-list-group-view="showListGroupView"
|
:show-list-group-view="showListGroupView"
|
||||||
:list-group-tab="listGroupTab"
|
|
||||||
:favorite-id-map="favoriteIdMap"
|
:favorite-id-map="favoriteIdMap"
|
||||||
@create-task="onCreateTask"
|
@create-task="onCreateTask"
|
||||||
@toggle-task="onToggleTask"
|
@toggle-task="onToggleTask"
|
||||||
@task-click="onTaskClick"
|
@task-click="onTaskClick"
|
||||||
@create-group="onCreateGroupFromContent"
|
|
||||||
@add-group="onAddGroup"
|
@add-group="onAddGroup"
|
||||||
@update-task-field="onUpdateTaskField"
|
@update-task-field="onUpdateTaskField"
|
||||||
@update-task-fields="onUpdateTaskFields"
|
@update-task-fields="onUpdateTaskFields"
|
||||||
@@ -42,7 +42,6 @@
|
|||||||
@update:sort-field="(v) => (sortField = v)"
|
@update:sort-field="(v) => (sortField = v)"
|
||||||
@update:sort-direction="(v) => (sortDirection = v)"
|
@update:sort-direction="(v) => (sortDirection = v)"
|
||||||
@update:visible-fields="(v) => (visibleFields = v)"
|
@update:visible-fields="(v) => (visibleFields = v)"
|
||||||
@update:list-group-tab="onQuickAccessTabClick"
|
|
||||||
@list-select="onListSelect"
|
@list-select="onListSelect"
|
||||||
@open-collaborator="onOpenCollaborator"
|
@open-collaborator="onOpenCollaborator"
|
||||||
@add-to-favorites="onAddToFavorites"
|
@add-to-favorites="onAddToFavorites"
|
||||||
@@ -54,6 +53,26 @@
|
|||||||
@rename-group="onRenameTaskGroup"
|
@rename-group="onRenameTaskGroup"
|
||||||
@delete-group="onDeleteTaskGroup"
|
@delete-group="onDeleteTaskGroup"
|
||||||
/>
|
/>
|
||||||
|
<TaskFlatView
|
||||||
|
v-else
|
||||||
|
:title="flatViewTitle"
|
||||||
|
:tasks="flatTasks"
|
||||||
|
:status-filter="statusFilter"
|
||||||
|
:sort-field="sortField"
|
||||||
|
:sort-direction="sortDirection"
|
||||||
|
:visible-fields="visibleFields"
|
||||||
|
:editable="flatViewEditable"
|
||||||
|
:task-edit-map="flatTaskEditMap"
|
||||||
|
@toggle-task="onToggleTask"
|
||||||
|
@task-click="onTaskClick"
|
||||||
|
@update-task-field="onUpdateTaskField"
|
||||||
|
@update-task-fields="onUpdateTaskFields"
|
||||||
|
@update:status-filter="(v) => (statusFilter = v)"
|
||||||
|
@update:sort-field="(v) => (sortField = v)"
|
||||||
|
@update:sort-direction="(v) => (sortDirection = v)"
|
||||||
|
@update:visible-fields="(v) => (visibleFields = v)"
|
||||||
|
@load-subtasks="onLoadSubtasks"
|
||||||
|
/>
|
||||||
<CreateTaskModal @register="registerTaskModal" @success="onTaskCreated" />
|
<CreateTaskModal @register="registerTaskModal" @success="onTaskCreated" />
|
||||||
<CreateListModal @register="registerListModal" @success="onListCreated" />
|
<CreateListModal @register="registerListModal" @success="onListCreated" />
|
||||||
<TaskDetailDrawer
|
<TaskDetailDrawer
|
||||||
@@ -76,11 +95,13 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" name="tasklist-index-page" setup>
|
<script lang="ts" name="tasklist-index-page" setup>
|
||||||
import { ref, reactive, computed, onMounted } from 'vue';
|
import { ref, computed, onMounted } from 'vue';
|
||||||
import { useModal } from '/@/components/Modal';
|
import { useModal } from '/@/components/Modal';
|
||||||
import { useDrawer } from '/@/components/Drawer';
|
import { useDrawer } from '/@/components/Drawer';
|
||||||
|
import { useUserStore } from '/@/store/modules/user';
|
||||||
import TaskSidebar from './components/TaskSidebar.vue';
|
import TaskSidebar from './components/TaskSidebar.vue';
|
||||||
import TaskContent from './components/TaskContent.vue';
|
import TaskContent from './components/TaskContent.vue';
|
||||||
|
import TaskFlatView from './components/TaskFlatView.vue';
|
||||||
import CreateTaskModal from './components/CreateTaskModal.vue';
|
import CreateTaskModal from './components/CreateTaskModal.vue';
|
||||||
import CreateListModal from './components/CreateListModal.vue';
|
import CreateListModal from './components/CreateListModal.vue';
|
||||||
import TaskDetailDrawer from './components/TaskDetailDrawer.vue';
|
import TaskDetailDrawer from './components/TaskDetailDrawer.vue';
|
||||||
@@ -93,7 +114,6 @@
|
|||||||
removeFavoriteGroup,
|
removeFavoriteGroup,
|
||||||
deleteTaskListApi,
|
deleteTaskListApi,
|
||||||
renameTaskList,
|
renameTaskList,
|
||||||
saveOrUpdate,
|
|
||||||
renameGroup,
|
renameGroup,
|
||||||
getMyOwnLists,
|
getMyOwnLists,
|
||||||
getMyCollabLists,
|
getMyCollabLists,
|
||||||
@@ -138,7 +158,6 @@
|
|||||||
const sortField = ref<SortField>('sortOrder');
|
const sortField = ref<SortField>('sortOrder');
|
||||||
const sortDirection = ref<SortDirection>('asc');
|
const sortDirection = ref<SortDirection>('asc');
|
||||||
const showListGroupView = ref(false);
|
const showListGroupView = ref(false);
|
||||||
const listGroupTab = ref('all');
|
|
||||||
const visibleFields = ref<FieldKey[]>([...DEFAULT_VISIBLE_FIELDS]);
|
const visibleFields = ref<FieldKey[]>([...DEFAULT_VISIBLE_FIELDS]);
|
||||||
const currentDrawerTaskId = ref<string>('');
|
const currentDrawerTaskId = ref<string>('');
|
||||||
|
|
||||||
@@ -147,23 +166,35 @@
|
|||||||
const [registerDetailDrawer, { openDrawer: openDetailDrawer }] = useDrawer();
|
const [registerDetailDrawer, { openDrawer: openDetailDrawer }] = useDrawer();
|
||||||
|
|
||||||
const collaboratorModalRef = ref<InstanceType<typeof CollaboratorModal> | null>(null);
|
const collaboratorModalRef = ref<InstanceType<typeof CollaboratorModal> | null>(null);
|
||||||
|
const defaultGroupRealIdMap = ref<Record<string, string>>({});
|
||||||
|
|
||||||
const favoriteIdMap = reactive(new Map<string, string>());
|
const userStore = useUserStore();
|
||||||
const permissionMap = reactive(new Map<string, string>());
|
|
||||||
|
const flatTasks = ref<TaskItem[]>([]);
|
||||||
|
const flatViewTitle = ref('');
|
||||||
|
const flatViewEditable = ref(false);
|
||||||
|
const flatTaskEditMap = ref<Map<string, boolean>>(new Map());
|
||||||
|
|
||||||
|
const isFlatView = computed(() => currentView.value === 'my-tasks' || currentView.value === 'followed');
|
||||||
|
|
||||||
|
const favoriteIdMap = ref(new Map<string, string>());
|
||||||
|
const permissionMap = ref(new Map<string, string>());
|
||||||
|
|
||||||
async function loadFavorites() {
|
async function loadFavorites() {
|
||||||
try {
|
try {
|
||||||
const data: any[] = await getMyFavorites();
|
const data: any[] = await getMyFavorites();
|
||||||
favoriteIdMap.clear();
|
const newFavMap = new Map<string, string>();
|
||||||
permissionMap.clear();
|
const newPermMap = new Map<string, string>();
|
||||||
for (const fav of data) {
|
for (const fav of data) {
|
||||||
if (fav.type === '1' && fav.mainId) {
|
if (fav.type === '1' && fav.mainId) {
|
||||||
favoriteIdMap.set(fav.mainId, fav.id);
|
newFavMap.set(fav.mainId, fav.id);
|
||||||
if (fav.permission) {
|
if (fav.permission) {
|
||||||
permissionMap.set(fav.mainId, fav.permission);
|
newPermMap.set(fav.mainId, fav.permission);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
favoriteIdMap.value = newFavMap;
|
||||||
|
permissionMap.value = newPermMap;
|
||||||
taskListGroups.value = buildTree(data);
|
taskListGroups.value = buildTree(data);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('[TaskList] 加载收藏列表失败', e);
|
console.error('[TaskList] 加载收藏列表失败', e);
|
||||||
@@ -232,6 +263,7 @@
|
|||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadFavorites();
|
loadFavorites();
|
||||||
|
onViewSelect('my-tasks');
|
||||||
});
|
});
|
||||||
|
|
||||||
function findListInGroups(groups: TaskListGroup[], listId: string): TaskList | null {
|
function findListInGroups(groups: TaskListGroup[], listId: string): TaskList | null {
|
||||||
@@ -299,6 +331,17 @@
|
|||||||
pid: tr.pid || '',
|
pid: tr.pid || '',
|
||||||
hasChild: tr.hasChild || '0',
|
hasChild: tr.hasChild || '0',
|
||||||
sortOrder: tr.sortOrder ?? 0,
|
sortOrder: tr.sortOrder ?? 0,
|
||||||
|
isDefault: tr.isDefault,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildFlatTaskItem(tr: any): TaskItem {
|
||||||
|
const base = buildTaskItem(tr);
|
||||||
|
return {
|
||||||
|
...base,
|
||||||
|
listName: tr.listName || '',
|
||||||
|
mainId: tr.mainId || '',
|
||||||
|
myPermission: tr.myPermission || '',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -322,10 +365,13 @@
|
|||||||
taskListId: listId,
|
taskListId: listId,
|
||||||
collapsed: false,
|
collapsed: false,
|
||||||
tasks: [],
|
tasks: [],
|
||||||
|
isDefault: gr.isDefault,
|
||||||
|
sortOrder: gr.sortOrder ?? 0,
|
||||||
};
|
};
|
||||||
groupMap.set(gr.id, tg);
|
groupMap.set(gr.id, tg);
|
||||||
if (gr.taskName === '默认分组') {
|
if (gr.isDefault === 1) {
|
||||||
defaultGroup = tg;
|
defaultGroup = tg;
|
||||||
|
defaultGroupRealIdMap.value[listId] = gr.id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -336,6 +382,7 @@
|
|||||||
taskListId: listId,
|
taskListId: listId,
|
||||||
collapsed: false,
|
collapsed: false,
|
||||||
tasks: [],
|
tasks: [],
|
||||||
|
sortOrder: 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -365,21 +412,8 @@
|
|||||||
targetGroup.tasks.push(taskItem);
|
targetGroup.tasks.push(taskItem);
|
||||||
}
|
}
|
||||||
|
|
||||||
const resultGroups: TaskGroup[] = [];
|
const resultGroups = Array.from(groupMap.values());
|
||||||
if (defaultGroup) {
|
resultGroups.sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0));
|
||||||
resultGroups.push(defaultGroup);
|
|
||||||
}
|
|
||||||
for (const [_gid, tg] of groupMap) {
|
|
||||||
if (tg !== defaultGroup) {
|
|
||||||
resultGroups.push(tg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
resultGroups.sort((a, b) => {
|
|
||||||
if (a.id === '__default__') return -1;
|
|
||||||
if (b.id === '__default__') return 1;
|
|
||||||
return (a.sortOrder ?? 0) - (b.sortOrder ?? 0);
|
|
||||||
});
|
|
||||||
|
|
||||||
list.groups = resultGroups;
|
list.groups = resultGroups;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -391,97 +425,47 @@
|
|||||||
currentView.value = view;
|
currentView.value = view;
|
||||||
showListGroupView.value = false;
|
showListGroupView.value = false;
|
||||||
currentListId.value = '';
|
currentListId.value = '';
|
||||||
|
flatTasks.value = [];
|
||||||
|
groupDimension.value = 'none';
|
||||||
|
sortField.value = 'createTime';
|
||||||
|
sortDirection.value = 'asc';
|
||||||
|
|
||||||
if (view === 'my-tasks') {
|
if (view === 'my-tasks') {
|
||||||
try {
|
try {
|
||||||
|
flatViewTitle.value = '我负责的任务';
|
||||||
|
flatViewEditable.value = true;
|
||||||
|
flatTaskEditMap.value = new Map();
|
||||||
const data: any[] = await myResponsibleTasks();
|
const data: any[] = await myResponsibleTasks();
|
||||||
quickAccessGroups.value = buildGroupsFromTasks(data, '我负责的任务');
|
flatTasks.value = (data || []).map(buildFlatTaskItem);
|
||||||
showListGroupView.value = true;
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('[TaskList] 加载我负责的任务失败', e);
|
console.error('[TaskList] 加载我负责的任务失败', e);
|
||||||
}
|
}
|
||||||
} else if (view === 'followed') {
|
} else if (view === 'followed') {
|
||||||
try {
|
try {
|
||||||
|
flatViewTitle.value = '我关注的任务';
|
||||||
|
flatViewEditable.value = false;
|
||||||
const data: any[] = await myFollowedTasks();
|
const data: any[] = await myFollowedTasks();
|
||||||
quickAccessGroups.value = buildGroupsFromTasks(data, '我关注的任务');
|
const items = (data || []).map(buildFlatTaskItem);
|
||||||
showListGroupView.value = true;
|
const editMap = new Map<string, boolean>();
|
||||||
|
const userId = userStore.getUserInfo?.id || '';
|
||||||
|
for (const item of items) {
|
||||||
|
const isAssignee = item.assigneeId?.includes(userId);
|
||||||
|
const hasListPerm = item.myPermission === '1' || item.myPermission === '2';
|
||||||
|
editMap.set(item.id, !!isAssignee || hasListPerm);
|
||||||
|
}
|
||||||
|
flatTaskEditMap.value = editMap;
|
||||||
|
flatTasks.value = items;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('[TaskList] 加载我关注的任务失败', e);
|
console.error('[TaskList] 加载我关注的任务失败', e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildGroupsFromTasks(tasks: any[], virtualGroupName: string): TaskListGroup[] {
|
|
||||||
const taskItems: TaskItem[] = (tasks || []).map((tr: any) => ({
|
|
||||||
id: tr.id,
|
|
||||||
taskName: tr.taskName || '',
|
|
||||||
taskDesc: tr.taskDesc || '',
|
|
||||||
completed: tr.taskStatus === 1,
|
|
||||||
priority: tr.priority || '',
|
|
||||||
assigneeId: tr.assigneeId || '',
|
|
||||||
assigneeName: tr.assigneeName || '',
|
|
||||||
assignId: tr.assignId || '',
|
|
||||||
assignName: tr.assignName || '',
|
|
||||||
participantId: tr.participantId || '',
|
|
||||||
participantName: tr.participantName || '',
|
|
||||||
followersId: tr.followersId || '',
|
|
||||||
followersName: tr.followersName || '',
|
|
||||||
createBy: tr.createBy || '',
|
|
||||||
startTime: tr.startTime ? tr.startTime.split(' ')[0] : '',
|
|
||||||
endTime: tr.endTime ? tr.endTime.split(' ')[0] : '',
|
|
||||||
completeTime: tr.completeTime ? tr.completeTime.split(' ')[0] : '',
|
|
||||||
createTime: tr.createTime ? tr.createTime.split(' ')[0] : '',
|
|
||||||
updateTime: tr.updateTime ? tr.updateTime.split(' ')[0] : '',
|
|
||||||
type: tr.type || '',
|
|
||||||
subTaskCount: tr.subTaskCount || 0,
|
|
||||||
completedSubTaskCount: tr.completedSubTaskCount || 0,
|
|
||||||
remark: tr.remark || '',
|
|
||||||
groupId: '',
|
|
||||||
groupName: '',
|
|
||||||
childCount: tr.subTaskCount || 0,
|
|
||||||
completedChildCount: tr.completedSubTaskCount || 0,
|
|
||||||
pid: tr.pid || '',
|
|
||||||
hasChild: tr.hasChild || '0',
|
|
||||||
sortOrder: tr.sortOrder ?? 0,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const taskList: TaskList = {
|
|
||||||
id: '__virtual_view__',
|
|
||||||
name: virtualGroupName,
|
|
||||||
pid: '',
|
|
||||||
hasChild: '0',
|
|
||||||
type: 1,
|
|
||||||
sortOrder: 0,
|
|
||||||
delFlag: 0,
|
|
||||||
groups: [
|
|
||||||
{
|
|
||||||
id: '__default__',
|
|
||||||
name: virtualGroupName,
|
|
||||||
taskListId: '__virtual_view__',
|
|
||||||
collapsed: false,
|
|
||||||
tasks: taskItems,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
id: '__virtual_view_group__',
|
|
||||||
name: virtualGroupName,
|
|
||||||
pid: '0',
|
|
||||||
hasChild: '0',
|
|
||||||
type: 0,
|
|
||||||
sortOrder: 0,
|
|
||||||
delFlag: 0,
|
|
||||||
taskLists: [taskList],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
function onListSelect(listId: string) {
|
function onListSelect(listId: string) {
|
||||||
currentView.value = '';
|
currentView.value = '';
|
||||||
currentListId.value = listId;
|
currentListId.value = listId;
|
||||||
showListGroupView.value = false;
|
showListGroupView.value = false;
|
||||||
|
groupDimension.value = 'custom';
|
||||||
loadPermissionForList(listId);
|
loadPermissionForList(listId);
|
||||||
loadTasks(listId);
|
loadTasks(listId);
|
||||||
}
|
}
|
||||||
@@ -496,9 +480,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onAddToFavorites(taskListId: string) {
|
async function onAddToFavorites(taskListId: string, pid?: string) {
|
||||||
try {
|
try {
|
||||||
await addToFavorites({ taskListId });
|
await addToFavorites({ taskListId, pid: pid || undefined });
|
||||||
await loadFavorites();
|
await loadFavorites();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('[TaskList] 添加收藏失败', e);
|
console.error('[TaskList] 添加收藏失败', e);
|
||||||
@@ -514,6 +498,23 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function onRemoveFavorite(listId: string) {
|
||||||
|
try {
|
||||||
|
const favId = favoriteIdMap.value.get(listId);
|
||||||
|
if (favId) {
|
||||||
|
await removeFavorite({ favoriteId: favId });
|
||||||
|
const isCurrent = currentListId.value === listId;
|
||||||
|
await loadFavorites();
|
||||||
|
if (isCurrent) {
|
||||||
|
const firstAvailable = findFirstAvailableList();
|
||||||
|
currentListId.value = firstAvailable?.id || '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[TaskList] 从收藏移除失败', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function onQuickAccessTabClick(tabKey: string) {
|
async function onQuickAccessTabClick(tabKey: string) {
|
||||||
const quickAccessViewMap: Record<string, ViewType> = {
|
const quickAccessViewMap: Record<string, ViewType> = {
|
||||||
all: 'all-tasks',
|
all: 'all-tasks',
|
||||||
@@ -523,7 +524,8 @@
|
|||||||
currentView.value = quickAccessViewMap[tabKey] || currentView.value;
|
currentView.value = quickAccessViewMap[tabKey] || currentView.value;
|
||||||
showListGroupView.value = true;
|
showListGroupView.value = true;
|
||||||
currentListId.value = '';
|
currentListId.value = '';
|
||||||
listGroupTab.value = tabKey;
|
groupDimension.value = 'custom';
|
||||||
|
flatTasks.value = [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (tabKey === 'all') {
|
if (tabKey === 'all') {
|
||||||
@@ -568,19 +570,15 @@
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onCreateTask(groupId: string, taskName: string, sortOrder?: number) {
|
async function onCreateTask(groupId: string, taskName: string) {
|
||||||
if (taskName && taskName.trim()) {
|
if (taskName && taskName.trim()) {
|
||||||
try {
|
try {
|
||||||
console.log('[TaskList] onCreateTask:', { groupId, taskName, mainId: currentListId.value, sortOrder });
|
|
||||||
const params: any = {
|
const params: any = {
|
||||||
mainId: currentListId.value,
|
mainId: currentListId.value,
|
||||||
taskName: taskName.trim(),
|
taskName: taskName.trim(),
|
||||||
type: '1',
|
type: '1',
|
||||||
pid: groupId && groupId !== '__default__' ? groupId : '',
|
pid: groupId && groupId !== '__default__' ? groupId : '',
|
||||||
};
|
};
|
||||||
if (sortOrder !== undefined) {
|
|
||||||
params.sortOrder = sortOrder;
|
|
||||||
}
|
|
||||||
await addTask(params);
|
await addTask(params);
|
||||||
if (currentListId.value) {
|
if (currentListId.value) {
|
||||||
await loadTasks(currentListId.value);
|
await loadTasks(currentListId.value);
|
||||||
@@ -596,18 +594,24 @@
|
|||||||
async function onCreateList(name: string, groupId: string) {
|
async function onCreateList(name: string, groupId: string) {
|
||||||
if (!name || !name.trim()) return;
|
if (!name || !name.trim()) return;
|
||||||
try {
|
try {
|
||||||
await addTaskList({
|
const listId = await addTaskList({
|
||||||
tasklistName: name.trim(),
|
tasklistName: name.trim(),
|
||||||
pid: groupId || undefined,
|
pid: groupId || undefined,
|
||||||
sortOrder: 1,
|
sortOrder: 1,
|
||||||
});
|
});
|
||||||
|
if (listId) {
|
||||||
|
await addToFavorites({ taskListId: listId, pid: groupId || undefined });
|
||||||
|
}
|
||||||
await loadFavorites();
|
await loadFavorites();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('[TaskList] 创建清单失败', e);
|
console.error('[TaskList] 创建清单失败', e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onListCreated() {
|
function onListCreated(listId?: string) {
|
||||||
|
if (listId) {
|
||||||
|
addToFavorites({ taskListId: listId });
|
||||||
|
}
|
||||||
loadFavorites();
|
loadFavorites();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -627,17 +631,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onCreateGroupFromContent(_name: string) {
|
async function onAddGroup(listId: string, groupName: string) {
|
||||||
const groupName = '新建分组';
|
if (!groupName.trim() || !listId) return;
|
||||||
try {
|
try {
|
||||||
if (!currentListId.value) return;
|
|
||||||
await addTask({
|
await addTask({
|
||||||
mainId: currentListId.value,
|
mainId: listId,
|
||||||
taskName: groupName,
|
taskName: groupName.trim(),
|
||||||
type: '0',
|
type: '0',
|
||||||
sortOrder: 1,
|
|
||||||
});
|
});
|
||||||
|
if (currentListId.value) {
|
||||||
await loadTasks(currentListId.value);
|
await loadTasks(currentListId.value);
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('[TaskList] 创建任务分组失败', e);
|
console.error('[TaskList] 创建任务分组失败', e);
|
||||||
}
|
}
|
||||||
@@ -668,48 +672,83 @@
|
|||||||
|
|
||||||
async function onMoveList(favoriteId: string, targetGroupId: string, sortOrder: number) {
|
async function onMoveList(favoriteId: string, targetGroupId: string, sortOrder: number) {
|
||||||
try {
|
try {
|
||||||
await moveTaskList({ favoriteId, targetGroupId: targetGroupId || undefined, sortOrder });
|
await moveTaskList({ favoriteId, targetGroupId: targetGroupId || '', sortOrder });
|
||||||
await loadFavorites();
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('[TaskList] 移动清单失败', e);
|
console.error('[TaskList] 移动清单失败', e);
|
||||||
|
} finally {
|
||||||
|
await loadFavorites();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onMoveGroup(groupId: string, sortOrder: number) {
|
async function onMoveGroup(groupId: string, sortOrder: number) {
|
||||||
try {
|
try {
|
||||||
await moveGroup({ groupId, sortOrder });
|
await moveGroup({ groupId, sortOrder });
|
||||||
await loadFavorites();
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('[TaskList] 移动分组失败', e);
|
console.error('[TaskList] 移动分组失败', e);
|
||||||
|
} finally {
|
||||||
|
await loadFavorites();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onMoveTask(taskId: string, targetPid: string, targetSortOrder: number) {
|
async function onMoveTask(taskId: string, targetPid: string, targetSortOrder: number) {
|
||||||
try {
|
try {
|
||||||
await moveTask({ taskId, targetPid, targetSortOrder });
|
await moveTask({ taskId, targetPid, targetSortOrder });
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[TaskList] 移动任务失败', e);
|
||||||
|
} finally {
|
||||||
if (currentListId.value) {
|
if (currentListId.value) {
|
||||||
await loadTasks(currentListId.value);
|
await loadTasks(currentListId.value);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
|
||||||
console.error('[TaskList] 移动任务失败', e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onMoveTaskGroup(taskGroupId: string, targetSortOrder: number) {
|
async function onMoveTaskGroup(taskGroupId: string, targetSortOrder: number) {
|
||||||
try {
|
try {
|
||||||
await moveTaskGroup({ taskGroupId, targetSortOrder });
|
await moveTaskGroup({ taskGroupId, targetSortOrder });
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[TaskList] 移动任务分组失败', e);
|
||||||
|
} finally {
|
||||||
|
if (currentListId.value) {
|
||||||
|
await loadTasks(currentListId.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onDeleteTaskGroup(groupId: string) {
|
||||||
|
if (groupId === '__default__') return;
|
||||||
|
try {
|
||||||
|
await deleteTaskApi({ id: groupId }, () => {});
|
||||||
if (currentListId.value) {
|
if (currentListId.value) {
|
||||||
await loadTasks(currentListId.value);
|
await loadTasks(currentListId.value);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('[TaskList] 移动任务分组失败', e);
|
console.error('[TaskList] 删除任务分组失败', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onRenameTaskGroup(groupId: string, newName: string) {
|
||||||
|
if (!newName.trim()) return;
|
||||||
|
const realGroupId = groupId === '__default__' ? defaultGroupRealIdMap.value[currentListId.value] : groupId;
|
||||||
|
if (!realGroupId) return;
|
||||||
|
try {
|
||||||
|
await editTask({ id: realGroupId, taskName: newName.trim() });
|
||||||
|
if (currentListId.value) {
|
||||||
|
await loadTasks(currentListId.value);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[TaskList] 重命名任务分组失败', e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onToggleTask(taskId: string) {
|
async function onToggleTask(taskId: string) {
|
||||||
try {
|
try {
|
||||||
await toggleTaskStatus({ id: taskId });
|
await toggleTaskStatus({ id: taskId });
|
||||||
if (currentListId.value) {
|
if (isFlatView.value) {
|
||||||
|
const idx = flatTasks.value.findIndex((t) => t.id === taskId);
|
||||||
|
if (idx >= 0) {
|
||||||
|
flatTasks.value[idx] = { ...flatTasks.value[idx], completed: !flatTasks.value[idx].completed };
|
||||||
|
}
|
||||||
|
} else if (currentListId.value) {
|
||||||
await loadTasks(currentListId.value);
|
await loadTasks(currentListId.value);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -717,12 +756,75 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getAllTasks(): TaskItem[] {
|
||||||
|
if (isFlatView.value) return flatTasks.value;
|
||||||
|
const allTasks: TaskItem[] = [];
|
||||||
|
for (const group of taskListGroups.value) {
|
||||||
|
for (const list of group.taskLists) {
|
||||||
|
for (const taskGroup of list.groups) {
|
||||||
|
allTasks.push(...taskGroup.tasks);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return allTasks;
|
||||||
|
}
|
||||||
|
|
||||||
function onTaskClick(taskId: string) {
|
function onTaskClick(taskId: string) {
|
||||||
currentDrawerTaskId.value = taskId;
|
currentDrawerTaskId.value = taskId;
|
||||||
const allTasks = getAllTasks();
|
const allTasks = getAllTasks();
|
||||||
const task = allTasks.find((t) => t.id === taskId);
|
const task = allTasks.find((t) => t.id === taskId);
|
||||||
if (task) {
|
if (task) {
|
||||||
openDetailDrawer(true, { task, allTasks });
|
const readOnly = isFlatView.value ? !canEditFlatTask(task) : currentList.value?.myPermission === '3';
|
||||||
|
openDetailDrawer(true, { task, allTasks, readOnly });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function canEditFlatTask(task: TaskItem): boolean {
|
||||||
|
if (flatViewEditable.value) return true;
|
||||||
|
return flatTaskEditMap.value.has(task.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshFlatViewTasks() {
|
||||||
|
if (currentView.value === 'my-tasks') {
|
||||||
|
const data: any[] = await myResponsibleTasks();
|
||||||
|
flatTasks.value = (data || []).map(buildFlatTaskItem);
|
||||||
|
} else if (currentView.value === 'followed') {
|
||||||
|
const data: any[] = await myFollowedTasks();
|
||||||
|
const items = (data || []).map(buildFlatTaskItem);
|
||||||
|
const editMap = new Map<string, boolean>();
|
||||||
|
const userId = userStore.getUserInfo?.id || '';
|
||||||
|
for (const item of items) {
|
||||||
|
const isAssignee = item.assigneeId?.includes(userId);
|
||||||
|
const hasListPerm = item.myPermission === '1' || item.myPermission === '2';
|
||||||
|
editMap.set(item.id, !!isAssignee || hasListPerm);
|
||||||
|
}
|
||||||
|
flatTaskEditMap.value = editMap;
|
||||||
|
flatTasks.value = items;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onLoadSubtasks(taskId: string, mainId: string) {
|
||||||
|
try {
|
||||||
|
const res: any = await listAllByMainId({ mainId });
|
||||||
|
const records: any[] = Array.isArray(res) ? res : Array.isArray(res?.result) ? res.result : [];
|
||||||
|
const taskRecords = records.filter((r: any) => r.type === '1' && r.pid === taskId);
|
||||||
|
if (taskRecords.length === 0) return;
|
||||||
|
const newChildren = taskRecords.map((tr: any) => buildFlatTaskItem(tr));
|
||||||
|
flatTasks.value = [...flatTasks.value, ...newChildren];
|
||||||
|
if (currentView.value === 'followed') {
|
||||||
|
const editMap = new Map(flatTaskEditMap.value);
|
||||||
|
const userId = userStore.getUserInfo?.id || '';
|
||||||
|
for (const child of newChildren) {
|
||||||
|
const isAssignee = child.assigneeId?.includes(userId);
|
||||||
|
const hasListPerm = child.myPermission === '1' || child.myPermission === '2';
|
||||||
|
if (isAssignee || hasListPerm) {
|
||||||
|
editMap.set(child.id, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
flatTaskEditMap.value = editMap;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[TaskList] 加载子任务失败', e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -731,14 +833,20 @@
|
|||||||
const allTasks = getAllTasks();
|
const allTasks = getAllTasks();
|
||||||
const task = allTasks.find((t) => t.id === currentDrawerTaskId.value);
|
const task = allTasks.find((t) => t.id === currentDrawerTaskId.value);
|
||||||
if (task) {
|
if (task) {
|
||||||
openDetailDrawer(true, { task, allTasks });
|
const readOnly = isFlatView.value ? !canEditFlatTask(task) : currentList.value?.myPermission === '3';
|
||||||
|
openDetailDrawer(true, { task, allTasks, readOnly });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onUpdateTaskField(taskId: string, field: string, value: any) {
|
async function onUpdateTaskField(taskId: string, field: string, value: any) {
|
||||||
try {
|
try {
|
||||||
await editTask({ id: taskId, [field]: value });
|
await editTask({ id: taskId, [field]: value });
|
||||||
if (currentListId.value) {
|
if (isFlatView.value) {
|
||||||
|
const idx = flatTasks.value.findIndex((t) => t.id === taskId);
|
||||||
|
if (idx >= 0) {
|
||||||
|
flatTasks.value[idx] = { ...flatTasks.value[idx], [field]: value };
|
||||||
|
}
|
||||||
|
} else if (currentListId.value) {
|
||||||
await loadTasks(currentListId.value);
|
await loadTasks(currentListId.value);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -749,7 +857,12 @@
|
|||||||
async function onUpdateTaskFields(taskId: string, fields: Record<string, any>) {
|
async function onUpdateTaskFields(taskId: string, fields: Record<string, any>) {
|
||||||
try {
|
try {
|
||||||
await editTask({ id: taskId, ...fields });
|
await editTask({ id: taskId, ...fields });
|
||||||
if (currentListId.value) {
|
if (isFlatView.value) {
|
||||||
|
const idx = flatTasks.value.findIndex((t) => t.id === taskId);
|
||||||
|
if (idx >= 0) {
|
||||||
|
flatTasks.value[idx] = { ...flatTasks.value[idx], ...fields };
|
||||||
|
}
|
||||||
|
} else if (currentListId.value) {
|
||||||
await loadTasks(currentListId.value);
|
await loadTasks(currentListId.value);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -775,7 +888,12 @@
|
|||||||
type: updatedTask.type,
|
type: updatedTask.type,
|
||||||
remark: updatedTask.remark,
|
remark: updatedTask.remark,
|
||||||
});
|
});
|
||||||
if (currentListId.value) {
|
if (isFlatView.value) {
|
||||||
|
const idx = flatTasks.value.findIndex((t) => t.id === updatedTask.id);
|
||||||
|
if (idx >= 0) {
|
||||||
|
flatTasks.value[idx] = { ...flatTasks.value[idx], ...updatedTask };
|
||||||
|
}
|
||||||
|
} else if (currentListId.value) {
|
||||||
await loadTasks(currentListId.value);
|
await loadTasks(currentListId.value);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -785,13 +903,11 @@
|
|||||||
|
|
||||||
async function onCreateSubTask(parentTaskId: string, taskName: string) {
|
async function onCreateSubTask(parentTaskId: string, taskName: string) {
|
||||||
try {
|
try {
|
||||||
await addTask({
|
const mainId = isFlatView.value ? flatTasks.value.find((t) => t.id === parentTaskId)?.mainId || '' : currentListId.value;
|
||||||
mainId: currentListId.value,
|
await addTask({ mainId, taskName, type: '1', pid: parentTaskId });
|
||||||
taskName,
|
if (isFlatView.value) {
|
||||||
type: '1',
|
await refreshFlatViewTasks();
|
||||||
pid: parentTaskId,
|
} else if (currentListId.value) {
|
||||||
});
|
|
||||||
if (currentListId.value) {
|
|
||||||
await loadTasks(currentListId.value);
|
await loadTasks(currentListId.value);
|
||||||
refreshDrawer();
|
refreshDrawer();
|
||||||
}
|
}
|
||||||
@@ -803,7 +919,9 @@
|
|||||||
async function onToggleSubTask(taskId: string) {
|
async function onToggleSubTask(taskId: string) {
|
||||||
try {
|
try {
|
||||||
await toggleTaskStatus({ id: taskId });
|
await toggleTaskStatus({ id: taskId });
|
||||||
if (currentListId.value) {
|
if (isFlatView.value) {
|
||||||
|
await refreshFlatViewTasks();
|
||||||
|
} else if (currentListId.value) {
|
||||||
await loadTasks(currentListId.value);
|
await loadTasks(currentListId.value);
|
||||||
refreshDrawer();
|
refreshDrawer();
|
||||||
}
|
}
|
||||||
@@ -824,7 +942,9 @@
|
|||||||
async function onDeleteTask(taskId: string) {
|
async function onDeleteTask(taskId: string) {
|
||||||
try {
|
try {
|
||||||
await deleteTaskApi({ id: taskId }, () => {});
|
await deleteTaskApi({ id: taskId }, () => {});
|
||||||
if (currentListId.value) {
|
if (isFlatView.value) {
|
||||||
|
flatTasks.value = flatTasks.value.filter((t) => t.id !== taskId);
|
||||||
|
} else if (currentListId.value) {
|
||||||
await loadTasks(currentListId.value);
|
await loadTasks(currentListId.value);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -835,7 +955,9 @@
|
|||||||
async function onDeleteSubTaskRecord(taskId: string) {
|
async function onDeleteSubTaskRecord(taskId: string) {
|
||||||
try {
|
try {
|
||||||
await deleteTaskApi({ id: taskId }, () => {});
|
await deleteTaskApi({ id: taskId }, () => {});
|
||||||
if (currentListId.value) {
|
if (isFlatView.value) {
|
||||||
|
await refreshFlatViewTasks();
|
||||||
|
} else if (currentListId.value) {
|
||||||
await loadTasks(currentListId.value);
|
await loadTasks(currentListId.value);
|
||||||
refreshDrawer();
|
refreshDrawer();
|
||||||
}
|
}
|
||||||
@@ -847,7 +969,9 @@
|
|||||||
async function onUpdateSubTaskField(taskId: string, field: string, value: any) {
|
async function onUpdateSubTaskField(taskId: string, field: string, value: any) {
|
||||||
try {
|
try {
|
||||||
await editTask({ id: taskId, [field]: value || '' });
|
await editTask({ id: taskId, [field]: value || '' });
|
||||||
if (currentListId.value) {
|
if (isFlatView.value) {
|
||||||
|
await refreshFlatViewTasks();
|
||||||
|
} else if (currentListId.value) {
|
||||||
await loadTasks(currentListId.value);
|
await loadTasks(currentListId.value);
|
||||||
refreshDrawer();
|
refreshDrawer();
|
||||||
}
|
}
|
||||||
@@ -859,7 +983,9 @@
|
|||||||
async function onUpdateSubTaskFields(taskId: string, fields: Record<string, any>) {
|
async function onUpdateSubTaskFields(taskId: string, fields: Record<string, any>) {
|
||||||
try {
|
try {
|
||||||
await editTask({ id: taskId, ...fields });
|
await editTask({ id: taskId, ...fields });
|
||||||
if (currentListId.value) {
|
if (isFlatView.value) {
|
||||||
|
await refreshFlatViewTasks();
|
||||||
|
} else if (currentListId.value) {
|
||||||
await loadTasks(currentListId.value);
|
await loadTasks(currentListId.value);
|
||||||
refreshDrawer();
|
refreshDrawer();
|
||||||
}
|
}
|
||||||
@@ -871,7 +997,9 @@
|
|||||||
async function onFollowTask(taskId: string) {
|
async function onFollowTask(taskId: string) {
|
||||||
try {
|
try {
|
||||||
await followTaskApi({ id: taskId });
|
await followTaskApi({ id: taskId });
|
||||||
if (currentListId.value) {
|
if (isFlatView.value) {
|
||||||
|
refreshFlatViewTasks();
|
||||||
|
} else if (currentListId.value) {
|
||||||
await loadTasks(currentListId.value);
|
await loadTasks(currentListId.value);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -882,7 +1010,9 @@
|
|||||||
async function onUnfollowTask(taskId: string) {
|
async function onUnfollowTask(taskId: string) {
|
||||||
try {
|
try {
|
||||||
await unfollowTaskApi({ id: taskId });
|
await unfollowTaskApi({ id: taskId });
|
||||||
if (currentListId.value) {
|
if (isFlatView.value) {
|
||||||
|
refreshFlatViewTasks();
|
||||||
|
} else if (currentListId.value) {
|
||||||
await loadTasks(currentListId.value);
|
await loadTasks(currentListId.value);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -890,20 +1020,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onAddGroup(listId: string, groupName: string) {
|
|
||||||
try {
|
|
||||||
await addTask({
|
|
||||||
mainId: listId,
|
|
||||||
taskName: groupName,
|
|
||||||
type: '0',
|
|
||||||
sortOrder: 1,
|
|
||||||
});
|
|
||||||
await loadTasks(listId);
|
|
||||||
} catch (e) {
|
|
||||||
console.error('[TaskList] 创建任务分组失败', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function onRenameList(listId: string, newName: string) {
|
async function onRenameList(listId: string, newName: string) {
|
||||||
const list = findListById(listId);
|
const list = findListById(listId);
|
||||||
if (list) {
|
if (list) {
|
||||||
@@ -919,7 +1035,7 @@
|
|||||||
async function onDeleteList(listId: string) {
|
async function onDeleteList(listId: string) {
|
||||||
try {
|
try {
|
||||||
if (showListGroupView.value) {
|
if (showListGroupView.value) {
|
||||||
const favId = favoriteIdMap.get(listId);
|
const favId = favoriteIdMap.value.get(listId);
|
||||||
if (favId) {
|
if (favId) {
|
||||||
await removeFavorite({ favoriteId: favId });
|
await removeFavorite({ favoriteId: favId });
|
||||||
}
|
}
|
||||||
@@ -936,18 +1052,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAllTasks(): TaskItem[] {
|
|
||||||
const allTasks: TaskItem[] = [];
|
|
||||||
for (const group of taskListGroups.value) {
|
|
||||||
for (const list of group.taskLists) {
|
|
||||||
for (const taskGroup of list.groups) {
|
|
||||||
allTasks.push(...taskGroup.tasks);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return allTasks;
|
|
||||||
}
|
|
||||||
|
|
||||||
function findListById(listId: string): TaskList | null {
|
function findListById(listId: string): TaskList | null {
|
||||||
for (const group of taskListGroups.value) {
|
for (const group of taskListGroups.value) {
|
||||||
for (const list of group.taskLists) {
|
for (const list of group.taskLists) {
|
||||||
|
|||||||
@@ -66,6 +66,10 @@ export interface TaskItem {
|
|||||||
pid: string;
|
pid: string;
|
||||||
hasChild: string;
|
hasChild: string;
|
||||||
sortOrder: number;
|
sortOrder: number;
|
||||||
|
isDefault?: number;
|
||||||
|
listName: string;
|
||||||
|
mainId: string;
|
||||||
|
myPermission?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TaskGroup {
|
export interface TaskGroup {
|
||||||
@@ -74,6 +78,8 @@ export interface TaskGroup {
|
|||||||
taskListId: string;
|
taskListId: string;
|
||||||
collapsed: boolean;
|
collapsed: boolean;
|
||||||
tasks: TaskItem[];
|
tasks: TaskItem[];
|
||||||
|
isDefault?: number;
|
||||||
|
sortOrder?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TaskList {
|
export interface TaskList {
|
||||||
@@ -112,6 +118,7 @@ export type SortField = 'sortOrder' | 'startTime' | 'endTime' | 'createTime' | '
|
|||||||
export type SortDirection = 'asc' | 'desc';
|
export type SortDirection = 'asc' | 'desc';
|
||||||
|
|
||||||
export type FieldKey =
|
export type FieldKey =
|
||||||
|
| 'listName'
|
||||||
| 'taskDesc'
|
| 'taskDesc'
|
||||||
| 'assigneeName'
|
| 'assigneeName'
|
||||||
| 'participantName'
|
| 'participantName'
|
||||||
@@ -147,7 +154,8 @@ export const FIELD_CONFIG: FieldConfig[] = [
|
|||||||
{ key: 'completeTime', label: '完成时间', defaultVisible: true },
|
{ key: 'completeTime', label: '完成时间', defaultVisible: true },
|
||||||
{ key: 'updateTime', label: '更新时间', defaultVisible: false },
|
{ key: 'updateTime', label: '更新时间', defaultVisible: false },
|
||||||
{ key: 'priority', label: '优先级', defaultVisible: true },
|
{ key: 'priority', label: '优先级', defaultVisible: true },
|
||||||
{ key: 'remark', label: '其他事项说明', defaultVisible: true },
|
{ key: 'remark', label: '其他事项说明', defaultVisible: false },
|
||||||
|
{ key: 'listName', label: '来源', defaultVisible: true },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const DEFAULT_VISIBLE_FIELDS: FieldKey[] = FIELD_CONFIG.filter((f) => f.defaultVisible).map((f) => f.key);
|
export const DEFAULT_VISIBLE_FIELDS: FieldKey[] = FIELD_CONFIG.filter((f) => f.defaultVisible).map((f) => f.key);
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
export const PRIORITY_OPTIONS = [
|
||||||
|
{ label: '高', value: 'high' },
|
||||||
|
{ label: '中', value: 'medium' },
|
||||||
|
{ label: '低', value: 'low' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const PRIORITY_COLOR_MAP: Record<string, string> = {
|
||||||
|
high: '#ffccc7',
|
||||||
|
medium: '#ffe58f',
|
||||||
|
low: '#91d5ff',
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getPriorityBg(value: string): string {
|
||||||
|
return PRIORITY_COLOR_MAP[value] || '#f5f5f5';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPriorityLabel(value: string): string {
|
||||||
|
const opt = PRIORITY_OPTIONS.find((o) => o.value === value);
|
||||||
|
return opt?.label || value;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isOverdue(task: { endTime?: string; completed?: boolean }): boolean {
|
||||||
|
if (!task.endTime || task.completed) return false;
|
||||||
|
const end = new Date(task.endTime);
|
||||||
|
const now = new Date();
|
||||||
|
now.setHours(0, 0, 0, 0);
|
||||||
|
end.setHours(0, 0, 0, 0);
|
||||||
|
return now > end;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user