czh-20260430-修复任务管理相关bug
This commit is contained in:
@@ -8,9 +8,9 @@
|
||||
<div class="content-header-left">
|
||||
<div v-if="headerRenaming" class="content-header-rename-row">
|
||||
<a-input
|
||||
ref="headerRenameInputRef"
|
||||
v-model:value="headerRenameValue"
|
||||
size="small"
|
||||
autofocus
|
||||
style="width: 200px"
|
||||
@press-enter="confirmHeaderRename"
|
||||
@blur="confirmHeaderRename"
|
||||
@@ -53,39 +53,7 @@
|
||||
</template>
|
||||
</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="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>
|
||||
<template #overlay>
|
||||
<a-menu @click="onStatusFilterChange">
|
||||
@@ -282,12 +250,14 @@
|
||||
</div>
|
||||
<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">
|
||||
<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="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-else icon="tasklist-star-filled|svg" class="pin-svg pin-svg-filled" />
|
||||
</span>
|
||||
<a-tooltip :title="isFavorited(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-else icon="tasklist-star-filled|svg" class="pin-svg pin-svg-filled" />
|
||||
</span>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
<div class="list-group-col list-group-col-owner">
|
||||
<template v-if="getFirstAssignee(list)">
|
||||
@@ -344,7 +314,7 @@
|
||||
|
||||
<template v-if="currentList">
|
||||
<draggable
|
||||
:model-value="computedGroups"
|
||||
:list="localGroups"
|
||||
item-key="id"
|
||||
group="taskGroups"
|
||||
handle=".group-drag-handle"
|
||||
@@ -360,85 +330,73 @@
|
||||
:style="{ minWidth: totalFieldsWidth + 'px' }"
|
||||
@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" />
|
||||
</span>
|
||||
<Icon :icon="group.collapsed ? 'ant-design:caret-right-outlined' : 'ant-design:caret-down-outlined'" class="group-arrow" />
|
||||
<template v-if="groupDimension === 'assignee' || groupDimension === 'createBy'">
|
||||
<template v-if="group.name && group.name !== '未分配' && group.name !== '未知'">
|
||||
<AvatarDisplay :names="group.name" :size="24" :show-name="true" />
|
||||
<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="group.name && group.name !== '未分配' && group.name !== '未知'">
|
||||
<AvatarDisplay :names="group.name" :size="24" :show-name="true" />
|
||||
</template>
|
||||
<span v-else class="group-name">{{ group.name }}</span>
|
||||
</template>
|
||||
<template v-else-if="groupDimension === 'priority'">
|
||||
<span
|
||||
v-if="group.name && group.name !== '无优先级'"
|
||||
class="priority-tag"
|
||||
:style="{ backgroundColor: getPriorityBg(group.name), color: '#1f2329' }"
|
||||
>
|
||||
{{ getPriorityLabel(group.name) }}
|
||||
</span>
|
||||
<span v-else class="group-name">{{ group.name }}</span>
|
||||
</template>
|
||||
<template v-else-if="renamingGroupId === group.id">
|
||||
<a-input
|
||||
ref="renamingGroupInputRef"
|
||||
v-model:value="renamingGroupName"
|
||||
size="small"
|
||||
style="width: 160px"
|
||||
@press-enter="confirmRenameGroup"
|
||||
@blur="confirmRenameGroup"
|
||||
@click.stop
|
||||
/>
|
||||
</template>
|
||||
<span v-else class="group-name">{{ group.name }}</span>
|
||||
</template>
|
||||
<template v-else-if="groupDimension === 'priority'">
|
||||
<span
|
||||
v-if="group.name && group.name !== '无优先级'"
|
||||
class="priority-tag"
|
||||
:style="{ backgroundColor: getPriorityBg(group.name), color: '#1f2329' }"
|
||||
>
|
||||
{{ getPriorityLabel(group.name) }}
|
||||
</span>
|
||||
<span v-else class="group-name">{{ group.name }}</span>
|
||||
</template>
|
||||
<template v-else-if="renamingGroupId === group.id">
|
||||
<a-input
|
||||
v-model:value="renamingGroupName"
|
||||
size="small"
|
||||
style="width: 160px"
|
||||
autofocus
|
||||
@press-enter="confirmRenameGroup"
|
||||
@blur="confirmRenameGroup"
|
||||
@click.stop
|
||||
/>
|
||||
</template>
|
||||
<span v-else class="group-name">{{ group.name }}</span>
|
||||
<span class="group-count">({{ countTopLevelTasks(group.tasks) }})</span>
|
||||
<span class="group-actions" @click.stop>
|
||||
<span
|
||||
v-if="canEdit && groupDimension === 'custom'"
|
||||
class="group-action-btn group-action-add"
|
||||
title="新建任务"
|
||||
@click.stop="showNewTaskInput = group.id"
|
||||
>
|
||||
<Icon icon="ant-design:plus-outlined" />
|
||||
</span>
|
||||
<a-dropdown v-if="canEdit && groupDimension === 'custom'">
|
||||
<span class="group-action-btn group-action-more" title="更多操作" @click.stop>
|
||||
<Icon icon="ant-design:more-outlined" />
|
||||
<span class="group-count">({{ countTopLevelTasks(group.tasks) }})</span>
|
||||
<span class="group-actions" @click.stop>
|
||||
<span
|
||||
v-if="canEdit && groupDimension === 'custom'"
|
||||
class="group-action-btn group-action-add"
|
||||
title="新建任务"
|
||||
@click.stop="showNewTaskInput = group.id"
|
||||
>
|
||||
<Icon icon="ant-design:plus-outlined" />
|
||||
</span>
|
||||
<template #overlay>
|
||||
<a-menu @click="({ key }) => onGroupMenuClick(key, group.id, group.name)">
|
||||
<a-menu-item key="rename">重命名</a-menu-item>
|
||||
<a-menu-item v-if="group.id !== '__default__'" key="delete" danger>删除</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
<a-dropdown v-if="canEdit && groupDimension === 'custom'">
|
||||
<span class="group-action-btn group-action-more" title="更多操作" @click.stop>
|
||||
<Icon icon="ant-design:ellipsis-outlined" />
|
||||
</span>
|
||||
<template #overlay>
|
||||
<a-menu @click="({ key }) => onGroupMenuClick(key, group.id, group.name)">
|
||||
<a-menu-item key="rename">重命名</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>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<template v-if="!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>
|
||||
|
||||
<div v-show="!group.collapsed">
|
||||
<draggable
|
||||
:model-value="flattenGroupTasks(group.tasks)"
|
||||
:item-key="(ft: any) => ft.task.id"
|
||||
@@ -453,7 +411,7 @@
|
||||
>
|
||||
<template #item="{ element: 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">
|
||||
<Icon icon="tasklist-drag-handle|svg" />
|
||||
</span>
|
||||
@@ -468,8 +426,8 @@
|
||||
<button
|
||||
class="task-checkbox"
|
||||
:class="{ checked: ft.task.completed }"
|
||||
:disabled="isReadOnly"
|
||||
@click.stop="!isReadOnly && emit('toggle-task', ft.task.id)"
|
||||
:disabled="!_canEditTask(ft.task)"
|
||||
@click.stop="_canEditTask(ft.task) && emit('toggle-task', ft.task.id)"
|
||||
>
|
||||
<Icon v-if="ft.task.completed" icon="ant-design:check-outlined" />
|
||||
</button>
|
||||
@@ -483,7 +441,7 @@
|
||||
<template v-for="field in visibleFieldConfigs" :key="field.key">
|
||||
<div class="table-col task-field" :style="{ width: fieldWidthMap[field.key] || '100px' }">
|
||||
<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-bar">
|
||||
<div
|
||||
@@ -500,7 +458,7 @@
|
||||
</template>
|
||||
<template v-else-if="field.key === 'priority'">
|
||||
<a-dropdown
|
||||
v-if="canEdit"
|
||||
v-if="_canEditTask(ft.task)"
|
||||
:trigger="['click']"
|
||||
:get-popup-container="getPopupContainer"
|
||||
:overlay-style="{ minWidth: '100px' }"
|
||||
@@ -537,7 +495,7 @@
|
||||
</div>
|
||||
</template>
|
||||
<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">
|
||||
<a-tooltip :title="ft.task.assigneeName" placement="top"
|
||||
><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'">
|
||||
<div
|
||||
class="cell-inner date-cell-wrapper"
|
||||
:class="{ 'editable-cell': canEdit }"
|
||||
@click.stop="canEdit && !datePickerVisible && startEditCell(ft.task.id, field.key)"
|
||||
:class="{ 'editable-cell': _canEditTask(ft.task) }"
|
||||
@click.stop="_canEditTask(ft.task) && !datePickerVisible && startEditCell(ft.task.id, field.key)"
|
||||
>
|
||||
<a-date-picker
|
||||
v-if="editingCell?.taskId === ft.task.id && editingCell?.field === field.key"
|
||||
@@ -603,7 +561,11 @@
|
||||
</div>
|
||||
</template>
|
||||
<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">
|
||||
<a-tooltip :title="ft.task.followersName" placement="top"
|
||||
><AvatarDisplay :names="ft.task.followersName" :size="24" :completed="ft.task.completed"
|
||||
@@ -625,7 +587,11 @@
|
||||
</div>
|
||||
</template>
|
||||
<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">
|
||||
<a-tooltip :title="ft.task.participantName" placement="top"
|
||||
><AvatarDisplay :names="ft.task.participantName" :size="24" :completed="ft.task.completed"
|
||||
@@ -647,7 +613,7 @@
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="field.key === 'completeTime'">
|
||||
<div class="cell-inner">
|
||||
<div class="cell-inner readonly-cell">
|
||||
<template v-if="ft.task.completeTime">
|
||||
<span class="field-value" :class="{ 'task-field-completed': ft.task.completed }">
|
||||
{{ ft.task.completeTime.substring(0, 10) }}
|
||||
@@ -657,28 +623,83 @@
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="field.key === 'remark'">
|
||||
<div class="cell-inner">
|
||||
<span
|
||||
v-if="ft.task.remark"
|
||||
class="field-value"
|
||||
:class="{ 'task-field-completed': ft.task.completed }"
|
||||
:title="ft.task.remark"
|
||||
>
|
||||
{{ ft.task.remark.length > 20 ? ft.task.remark.substring(0, 20) + '...' : ft.task.remark }}
|
||||
</span>
|
||||
<span v-else class="field-placeholder">-</span>
|
||||
<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
|
||||
v-if="ft.task.remark"
|
||||
class="field-value"
|
||||
:class="{ 'task-field-completed': ft.task.completed }"
|
||||
:title="ft.task.remark"
|
||||
>
|
||||
{{ ft.task.remark.length > 20 ? ft.task.remark.substring(0, 20) + '...' : ft.task.remark }}
|
||||
</span>
|
||||
<span v-else class="field-icon-placeholder" :class="{ 'task-field-completed': ft.task.completed }">
|
||||
<Icon icon="ant-design:edit-outlined" />
|
||||
</span>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<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)">
|
||||
<a-tooltip :title="getFieldValue(ft.task, field.key)" placement="top"
|
||||
><AvatarDisplay :names="getFieldValue(ft.task, field.key)" :size="24" :completed="ft.task.completed"
|
||||
/></a-tooltip>
|
||||
</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>
|
||||
<span class="field-icon-placeholder" :class="{ 'task-field-completed': ft.task.completed }">
|
||||
<Icon icon="tasklist-add-user|svg" />
|
||||
<span
|
||||
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>
|
||||
</template>
|
||||
</div>
|
||||
@@ -694,6 +715,29 @@
|
||||
</template>
|
||||
</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
|
||||
v-if="showNewTaskInput !== group.id && canEdit"
|
||||
class="task-row new-task-btn"
|
||||
@@ -709,7 +753,7 @@
|
||||
<span class="new-task-text">新建任务</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</draggable>
|
||||
@@ -717,6 +761,7 @@
|
||||
<div v-if="showNewGroupInput && canEdit" class="new-group-row">
|
||||
<Icon icon="ant-design:plus-outlined" class="new-task-plus" />
|
||||
<a-input
|
||||
ref="newGroupInputRef"
|
||||
v-model:value="newGroupName"
|
||||
size="small"
|
||||
placeholder="输入分组名称,按回车确认..."
|
||||
@@ -751,7 +796,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, nextTick } from 'vue';
|
||||
import { ref, computed, watch, nextTick } from 'vue';
|
||||
import dayjs from 'dayjs';
|
||||
import Icon from '/@/components/Icon/index';
|
||||
import AvatarDisplay from './AvatarDisplay.vue';
|
||||
@@ -760,6 +805,7 @@
|
||||
import { useUserStore } from '/@/store/modules/user';
|
||||
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 { PRIORITY_OPTIONS, getPriorityBg, getPriorityLabel, isOverdue } from '../utils/taskUtils';
|
||||
import type {
|
||||
TaskList,
|
||||
TaskGroup,
|
||||
@@ -785,12 +831,11 @@
|
||||
sortDirection: SortDirection;
|
||||
visibleFields: FieldKey[];
|
||||
showListGroupView?: boolean;
|
||||
listGroupTab?: string;
|
||||
favoriteIdMap?: Map<string, string>;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
'create-task': [groupId: string, taskName: string, sortOrder?: number];
|
||||
'create-task': [groupId: string, taskName: string];
|
||||
'toggle-task': [taskId: string];
|
||||
'task-click': [taskId: string];
|
||||
'add-group': [listId: string, groupName: string];
|
||||
@@ -803,7 +848,6 @@
|
||||
'update:visibleFields': [value: FieldKey[]];
|
||||
'update:list-group-tab': [value: string];
|
||||
'list-select': [listId: string];
|
||||
'create-group': [name: string];
|
||||
'open-collaborator': [];
|
||||
'add-to-favorites': [taskListId: string];
|
||||
'remove-from-favorites': [favoriteId: string];
|
||||
@@ -818,18 +862,20 @@
|
||||
const userStore = useUserStore();
|
||||
const { createConfirm } = useMessage();
|
||||
|
||||
const LIST_TAB_ITEMS = [
|
||||
{ key: 'all', label: '全部' },
|
||||
{ key: 'own', label: '我所有的' },
|
||||
{ key: 'collab', label: '我协作的' },
|
||||
];
|
||||
|
||||
const showNewTaskInput = ref<string | null>(null);
|
||||
const newTaskTitle = ref('');
|
||||
const showNewGroupInput = ref(false);
|
||||
const newGroupName = ref('');
|
||||
const renamingGroupId = ref<string | null>(null);
|
||||
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 submittingRename = ref(false);
|
||||
const filterVisible = ref(false);
|
||||
@@ -838,6 +884,24 @@
|
||||
const expandedTaskIds = ref<Set<string>>(new Set());
|
||||
const draggingTaskId = ref('');
|
||||
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() {
|
||||
import('ant-design-vue').then(({ message }) => {
|
||||
@@ -845,29 +909,11 @@
|
||||
});
|
||||
}
|
||||
|
||||
const priorityOptions = ref<{ label: string; value: string }[]>([
|
||||
{ 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 priorityOptions = PRIORITY_OPTIONS;
|
||||
|
||||
const editingCell = ref<{ taskId: string; field: string } | null>(null);
|
||||
const editingValue = ref<any>('');
|
||||
const textEditInputRef = ref<any>(null);
|
||||
const datePickerVisible = ref(false);
|
||||
const pickerEpoch = ref(0);
|
||||
const editingDateDayjs = computed(() => {
|
||||
@@ -1084,6 +1130,14 @@
|
||||
|
||||
const headerRenaming = ref(false);
|
||||
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 }) {
|
||||
if (key === 'rename') {
|
||||
@@ -1113,15 +1167,6 @@
|
||||
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 {
|
||||
if (canEdit.value) return true;
|
||||
if (isReadOnly.value && task.assigneeId) {
|
||||
@@ -1210,6 +1255,24 @@
|
||||
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) {
|
||||
if (value !== undefined && value !== null) {
|
||||
emit('update-task-field', taskId, 'priority', value);
|
||||
@@ -1472,10 +1535,12 @@
|
||||
const { oldIndex, newIndex } = evt;
|
||||
if (oldIndex === newIndex) return;
|
||||
|
||||
const movedElement = evt.item;
|
||||
const groupId = movedElement?.dataset?.groupId;
|
||||
const groupId = localGroups.value[newIndex]?.id;
|
||||
if (!groupId) return;
|
||||
|
||||
// 拖拽结束后收起分组,防止展开跳动
|
||||
collapsedGroups.value.add(groupId);
|
||||
|
||||
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)),
|
||||
collapsed: collapsedGroups.value.has(g.id),
|
||||
}));
|
||||
groups.sort((a, b) => {
|
||||
if (a.id === '__default__') return -1;
|
||||
if (b.id === '__default__') return 1;
|
||||
return (a.sortOrder ?? 0) - (b.sortOrder ?? 0);
|
||||
});
|
||||
groups.sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0));
|
||||
return groups;
|
||||
}
|
||||
|
||||
@@ -1526,6 +1587,14 @@
|
||||
return computeDynamicGroups(allTasks);
|
||||
});
|
||||
|
||||
watch(
|
||||
computedGroups,
|
||||
(newGroups) => {
|
||||
localGroups.value = [...newGroups];
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
function flattenGroupTasks(tasks?: TaskItem[]): FlatTaskItem[] {
|
||||
const sourceTasks = tasks || [];
|
||||
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) {
|
||||
const title = newTaskTitle.value.trim();
|
||||
newTaskTitle.value = '';
|
||||
showNewTaskInput.value = null;
|
||||
if (title) {
|
||||
emit('create-task', groupId, title, 1);
|
||||
emit('create-task', groupId, title);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1696,13 +1757,6 @@
|
||||
}, 300);
|
||||
}
|
||||
|
||||
function onCreateMenuClick({ key }: { key: string }) {
|
||||
if (key === 'group') {
|
||||
showNewGroupInput.value = true;
|
||||
newGroupName.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
function onGroupMenuClick(key: string, groupId: string, groupName: string) {
|
||||
if (key === 'rename') {
|
||||
renamingGroupId.value = groupId;
|
||||
@@ -1856,75 +1910,6 @@
|
||||
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 {
|
||||
display: inline-block;
|
||||
margin-left: 4px;
|
||||
@@ -2076,7 +2061,7 @@
|
||||
}
|
||||
|
||||
.content-table-header {
|
||||
padding: 0 24px;
|
||||
padding: 0 8px;
|
||||
height: 40px;
|
||||
background: #fafafa;
|
||||
border-bottom: 1px solid #e5e6eb;
|
||||
@@ -2123,11 +2108,8 @@
|
||||
|
||||
.drag-handle {
|
||||
cursor: grab;
|
||||
color: #c0c4cc;
|
||||
opacity: 0;
|
||||
transition:
|
||||
opacity 0.15s,
|
||||
color 0.15s;
|
||||
color: transparent;
|
||||
transition: color 0.15s;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -2139,7 +2121,7 @@
|
||||
}
|
||||
|
||||
.task-row:hover .drag-handle {
|
||||
opacity: 1;
|
||||
color: #c0c4cc;
|
||||
}
|
||||
|
||||
.drag-handle:hover {
|
||||
@@ -2162,7 +2144,6 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@@ -2174,46 +2155,64 @@
|
||||
.task-group-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 24px;
|
||||
padding: 6px 8px;
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid #fafafa;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
font-weight: 600;
|
||||
color: #1f2329;
|
||||
&:hover {
|
||||
background: #fafafa;
|
||||
}
|
||||
.group-arrow {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
width: 16px;
|
||||
text-align: center;
|
||||
background: #f5f6f7;
|
||||
}
|
||||
.group-drag-handle {
|
||||
cursor: grab;
|
||||
font-size: 12px;
|
||||
color: #c0c4cc;
|
||||
font-size: 14px;
|
||||
color: transparent;
|
||||
transition: color 0.15s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
&:hover {
|
||||
color: #8c8c8c;
|
||||
}
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
min-width: 24px;
|
||||
flex-shrink: 0;
|
||||
&:active {
|
||||
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 {
|
||||
color: #333;
|
||||
color: #1f2329;
|
||||
font-weight: 600;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.group-count {
|
||||
color: #999;
|
||||
color: #8f959e;
|
||||
font-weight: 400;
|
||||
font-size: 13px;
|
||||
flex-shrink: 0;
|
||||
margin-left: 4px;
|
||||
}
|
||||
.group-actions {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
gap: 2px;
|
||||
flex-shrink: 0;
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
@@ -2228,7 +2227,7 @@
|
||||
justify-content: center;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
color: #8c8c8c;
|
||||
color: #8f959e;
|
||||
font-size: 14px;
|
||||
transition: all 0.15s;
|
||||
&:hover {
|
||||
@@ -2242,7 +2241,7 @@
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
height: 46px;
|
||||
padding: 0 24px;
|
||||
padding: 0 8px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
font-size: 14px;
|
||||
box-sizing: border-box;
|
||||
@@ -2406,6 +2405,13 @@
|
||||
min-width: 80px;
|
||||
}
|
||||
}
|
||||
|
||||
.inline-text-editor {
|
||||
width: 100%;
|
||||
:deep(.ant-input) {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-cell {
|
||||
@@ -2427,6 +2433,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
.readonly-cell {
|
||||
&:hover {
|
||||
border-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.date-picker-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
@@ -2584,7 +2596,7 @@
|
||||
.new-group-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px 24px;
|
||||
padding: 12px 8px;
|
||||
font-size: 14px;
|
||||
&.new-group-btn {
|
||||
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 {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
|
||||
Reference in New Issue
Block a user