czh-20260430-修复任务管理相关bug

This commit is contained in:
zhihao
2026-04-30 22:43:59 +08:00
parent 2641522dcd
commit d9d5ab70cc
8 changed files with 1925 additions and 556 deletions
+94 -28
View File
@@ -47,22 +47,24 @@
<div class="sidebar-favorites-header">
<Icon icon="tasklist-task-list|svg" class="sidebar-favorites-icon" />
<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 class="sidebar-favorites-items">
<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>
<draggable :model-value="ungroupedTaskLists" group="tasklists" item-key="id" handle=".list-drag-handle" :animation="200" @end="onDragEnd">
<draggable :list="localUngroupedTaskLists" group="tasklists" item-key="id" handle=".list-drag-handle" :animation="200" @end="onDragEnd">
<template #item="{ element: taskList }">
<div
v-if="renamingId !== taskList.id"
@@ -97,13 +99,25 @@
<div v-else class="sidebar-item sidebar-item-renaming">
<span class="list-drag-handle-placeholder"></span>
<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>
</template>
</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>
<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 }">
<div :data-group-id="group.id" :class="{ 'group-being-dragged': draggingGroupId === group.id }">
<div
@@ -134,7 +148,7 @@
</div>
<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 }" />
<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>
<template v-if="group.delFlag !== 1 && !collapsedGroups[group.id]">
<div class="sidebar-group-items" :data-group-id="group.id">
@@ -151,7 +165,7 @@
/>
</div>
<draggable
:model-value="getGroupTaskLists(group)"
:list="getLocalGroupTaskLists(group)"
group="tasklists"
item-key="id"
handle=".list-drag-handle"
@@ -192,12 +206,12 @@
<div v-else class="sidebar-item sidebar-item-renaming">
<span class="list-drag-handle-placeholder"></span>
<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>
</template>
</draggable>
<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"
>
可拖拽清单加入该分组
@@ -229,7 +243,7 @@
</template>
<script lang="ts" setup>
import { ref, computed } from 'vue';
import { ref, computed, watch, nextTick } from 'vue';
import Icon from '/@/components/Icon/index';
import draggable from 'vuedraggable';
import { useMessage } from '/@/hooks/web/useMessage';
@@ -253,6 +267,7 @@
'create-group': [name: string];
'rename-list': [listId: string, newName: string];
'delete-list': [listId: string];
'remove-favorite': [listId: string];
'rename-group': [groupId: string, newName: string];
'delete-group': [groupId: string];
'quick-access-tab-click': [tabKey: string];
@@ -277,14 +292,46 @@
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(() => {
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 collapsedGroups = ref<Record<string, boolean>>({});
const showNewGroupInput = ref(false);
@@ -295,6 +342,14 @@
const renamingId = ref('');
const renamingType = ref<'list' | 'group'>('list');
const renameValue = ref('');
const renameInputRef = ref<HTMLInputElement | null>(null);
watch(renamingId, async (val) => {
if (val) {
await nextTick();
renameInputRef.value?.focus();
}
});
const draggingGroupId = ref('');
const dragStartX = ref(0);
@@ -363,6 +418,8 @@
const itemEl = evt.item;
const groupId = itemEl?.dataset?.groupId || itemEl?.closest('[data-group-id]')?.dataset?.groupId;
if (!groupId) return;
// 拖拽结束后保持分组折叠状态,防止展开跳动
collapsedGroups.value[groupId] = true;
const newIndex = evt.newIndex ?? 0;
emit('move-group', groupId, newIndex + 1);
}
@@ -372,6 +429,15 @@
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) {
showNewListInput.value = true;
newListGroupId.value = groupId;
@@ -397,12 +463,12 @@
} else if (key === 'remove') {
createConfirm({
iconType: 'warning',
title: '确认除',
content: '删除任务清单后,其下所有任务也将被删除,确定要删除吗?',
title: '确认除',
content: '确定从收藏清单中移除吗?',
okText: '确认',
cancelText: '取消',
onOk: () => {
emit('delete-list', listId);
emit('remove-favorite', listId);
},
});
}