czh-20260630-优化计划,增加分页、滚动加载等
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
import { ref } from 'vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
interface OptimisticOptions {
|
||||
/** 调用前乐观修改本地数据 */
|
||||
onOptimistic: () => void;
|
||||
/** 失败时回滚本地数据 */
|
||||
onRollback: () => void;
|
||||
/** 失败提示消息 */
|
||||
errorMsg: string;
|
||||
/** 成功回调 */
|
||||
onSuccess?: () => void;
|
||||
}
|
||||
|
||||
export function useOptimisticMutation() {
|
||||
const loading = ref(false);
|
||||
|
||||
async function execute<TResult>(
|
||||
apiCall: () => Promise<TResult>,
|
||||
options: OptimisticOptions
|
||||
): Promise<TResult | null> {
|
||||
const { onOptimistic, onRollback, errorMsg, onSuccess } = options;
|
||||
|
||||
onOptimistic();
|
||||
|
||||
try {
|
||||
const result = await apiCall();
|
||||
onSuccess?.();
|
||||
return result;
|
||||
} catch (e: any) {
|
||||
console.error('[OptimisticMutation] 操作失败', e);
|
||||
message.error(errorMsg);
|
||||
onRollback();
|
||||
throw e;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
return { execute, loading };
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import { ref, computed, type Ref } from 'vue';
|
||||
|
||||
export interface VirtualScrollOptions {
|
||||
items: Ref<any[]>;
|
||||
itemHeight: number;
|
||||
buffer: number;
|
||||
containerRef: Ref<HTMLElement | null>;
|
||||
}
|
||||
|
||||
export function useVirtualScroll(options: VirtualScrollOptions) {
|
||||
const { items, itemHeight, buffer, containerRef } = options;
|
||||
|
||||
const scrollTop = ref(0);
|
||||
const containerHeight = ref(0);
|
||||
|
||||
const totalHeight = computed(() => items.value.length * itemHeight);
|
||||
|
||||
const startIndex = computed(() => {
|
||||
const idx = Math.floor(scrollTop.value / itemHeight) - buffer;
|
||||
return Math.max(0, idx);
|
||||
});
|
||||
|
||||
const endIndex = computed(() => {
|
||||
const visible = Math.ceil(containerHeight.value / itemHeight);
|
||||
const idx = Math.floor(scrollTop.value / itemHeight) + visible + buffer;
|
||||
return Math.min(items.value.length, idx);
|
||||
});
|
||||
|
||||
const visibleItems = computed(() => {
|
||||
return items.value.slice(startIndex.value, endIndex.value).map((item, idx) => ({
|
||||
item,
|
||||
index: startIndex.value + idx,
|
||||
style: {
|
||||
position: 'absolute' as const,
|
||||
top: `${(startIndex.value + idx) * itemHeight}px`,
|
||||
width: '100%',
|
||||
height: `${itemHeight}px`,
|
||||
},
|
||||
}));
|
||||
});
|
||||
|
||||
const containerStyle = computed(() => ({
|
||||
height: `${totalHeight.value}px`,
|
||||
position: 'relative' as const,
|
||||
overflow: 'hidden',
|
||||
}));
|
||||
|
||||
function onScroll(event: Event) {
|
||||
const target = event.target as HTMLElement;
|
||||
scrollTop.value = target.scrollTop;
|
||||
}
|
||||
|
||||
function updateContainer() {
|
||||
if (containerRef.value) {
|
||||
containerHeight.value = containerRef.value.clientHeight;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
visibleItems,
|
||||
containerStyle,
|
||||
onScroll,
|
||||
updateContainer,
|
||||
totalHeight,
|
||||
scrollTop,
|
||||
};
|
||||
}
|
||||
@@ -16,6 +16,7 @@
|
||||
:class="[prefixCls, `${prefixCls}--${getHeaderTheme}`, 'headerIntroductionClass']"
|
||||
>
|
||||
<img src="../../../assets/images/header-brand.png" class="header-logo" />
|
||||
<span class="header-secrecy-level">机密级</span>
|
||||
{{ t('layout.header.welcomeIn') }} {{ title }}
|
||||
</span>
|
||||
</div>
|
||||
@@ -256,6 +257,13 @@
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.header-secrecy-level {
|
||||
color: #ffffff;
|
||||
font-weight: bold;
|
||||
font-size: 20px;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
.header-logo {
|
||||
height: 32px;
|
||||
width: auto;
|
||||
|
||||
@@ -106,11 +106,14 @@ export const renameGroup = (params) => defHttp.post({ url: Api.renameGroup, para
|
||||
|
||||
export const renameTaskList = (params) => defHttp.post({ url: Api.renameTaskList, params });
|
||||
|
||||
export const getMyOwnLists = () => defHttp.get({ url: Api.myOwnLists });
|
||||
export const getMyOwnLists = (params?: { pageNo?: number; pageSize?: number; keyword?: string }) =>
|
||||
defHttp.get({ url: Api.myOwnLists, params });
|
||||
|
||||
export const getAllLists = () => defHttp.get({ url: Api.allLists });
|
||||
export const getAllLists = (params?: { pageNo?: number; pageSize?: number; keyword?: string }) =>
|
||||
defHttp.get({ url: Api.allLists, params });
|
||||
|
||||
export const getMyCollabLists = () => defHttp.get({ url: Api.myCollabLists });
|
||||
export const getMyCollabLists = (params?: { pageNo?: number; pageSize?: number; keyword?: string }) =>
|
||||
defHttp.get({ url: Api.myCollabLists, params });
|
||||
|
||||
export const addTask = (params) => defHttp.post({ url: Api.addTask, params });
|
||||
|
||||
@@ -146,10 +149,13 @@ export const loadSubTasks = (params) => defHttp.get({ url: Api.loadSubTasks, par
|
||||
|
||||
export const listByMainId = (params) => defHttp.get({ url: Api.listByMainId, params });
|
||||
|
||||
export const listAllByMainId = (params) => defHttp.get({ url: Api.listAllByMainId, params });
|
||||
export const listAllByMainId = (params: { mainId: string }) =>
|
||||
defHttp.get({ url: Api.listAllByMainId, params });
|
||||
|
||||
export const myResponsibleTasks = () => defHttp.get({ url: Api.myResponsibleTasks });
|
||||
export const myResponsibleTasks = (params?: { pageNo?: number; pageSize?: number }) =>
|
||||
defHttp.get({ url: Api.myResponsibleTasks, params });
|
||||
|
||||
export const myFollowedTasks = () => defHttp.get({ url: Api.myFollowedTasks });
|
||||
export const myFollowedTasks = (params?: { pageNo?: number; pageSize?: number }) =>
|
||||
defHttp.get({ url: Api.myFollowedTasks, params });
|
||||
|
||||
export const getCurrentUserSecurityLevel = () => defHttp.get({ url: Api.getCurrentUserSecurityLevel });
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
placeholder="搜索计划名称"
|
||||
class="list-search-input"
|
||||
allow-clear
|
||||
@search="onSearch"
|
||||
/>
|
||||
</div>
|
||||
<a-tooltip title="使用帮助">
|
||||
@@ -265,7 +266,7 @@
|
||||
|
||||
<div class="content-body">
|
||||
<template v-if="showListGroupView">
|
||||
<div class="list-group-view">
|
||||
<div ref="listGroupViewRef" class="list-group-view" @scroll="onListGroupScroll">
|
||||
<div class="list-group-table-header">
|
||||
<div class="list-group-col list-group-col-name">名称</div>
|
||||
<div class="list-group-col list-group-col-owner">所有者</div>
|
||||
@@ -316,6 +317,13 @@
|
||||
<Icon icon="ant-design:inbox-outlined" class="empty-icon" />
|
||||
<p>暂无可见计划</p>
|
||||
</div>
|
||||
<div v-if="props.hasMore && allTaskLists.length > 0" class="load-more-trigger">
|
||||
<template v-if="props.loadingMore">
|
||||
<a-spin size="small" />
|
||||
<span class="load-more-text">加载中...</span>
|
||||
</template>
|
||||
<a-button v-else type="link" size="small" @click="emit('load-more')">加载更多</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -435,14 +443,14 @@
|
||||
<draggable
|
||||
:list="flatListCache[group.id]"
|
||||
:item-key="(ft: any) => ft.task.id"
|
||||
group="tasks"
|
||||
:group="dragGroupConfig"
|
||||
handle=".drag-handle"
|
||||
:animation="200"
|
||||
ghost-class="sortable-ghost"
|
||||
chosen-class="sortable-chosen"
|
||||
@start="onTaskDragStart"
|
||||
@end="(evt: any) => onTaskDragEnd(evt)"
|
||||
@move="onTaskMove"
|
||||
:move="onTaskMove"
|
||||
>
|
||||
<template #item="{ element: ft }">
|
||||
<div class="task-row" :data-task-id="ft.task.id" :style="{ minWidth: totalFieldsWidth + 'px' }" v-show="!isChildHidden(ft)">
|
||||
@@ -863,6 +871,8 @@
|
||||
FilterOperator,
|
||||
} from '../types';
|
||||
|
||||
const { createMessage } = useMessage();
|
||||
|
||||
const props = defineProps<{
|
||||
currentList: TaskList | null;
|
||||
taskListGroups: TaskListGroup[];
|
||||
@@ -874,6 +884,8 @@
|
||||
showListGroupView?: boolean;
|
||||
favoriteIdMap?: Map<string, string>;
|
||||
loading?: boolean;
|
||||
hasMore?: boolean;
|
||||
loadingMore?: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -901,6 +913,8 @@
|
||||
'rename-group': [groupId: string, newName: string];
|
||||
'delete-group': [groupId: string];
|
||||
'create-list': [];
|
||||
'load-more': [];
|
||||
'search-keyword': [keyword: string];
|
||||
}>();
|
||||
|
||||
const userStore = useUserStore();
|
||||
@@ -919,6 +933,15 @@
|
||||
const renamingGroupId = ref<string | null>(null);
|
||||
const renamingGroupName = ref('');
|
||||
const renamingGroupInputRef = ref<HTMLInputElement | null>(null);
|
||||
const listGroupViewRef = ref<HTMLElement | null>(null);
|
||||
|
||||
function onListGroupScroll(event: Event) {
|
||||
const target = event.target as HTMLElement;
|
||||
const nearBottom = target.scrollHeight - target.scrollTop - target.clientHeight < 200;
|
||||
if (nearBottom && props.hasMore) {
|
||||
emit('load-more');
|
||||
}
|
||||
}
|
||||
|
||||
watch(renamingGroupId, async (val) => {
|
||||
if (val) {
|
||||
@@ -935,6 +958,29 @@
|
||||
const expandedTaskIds = ref<Set<string>>(new Set());
|
||||
const draggingTaskId = ref('');
|
||||
const dragBlockedOnce = ref(false);
|
||||
|
||||
const dragGroupConfig = computed(() => ({
|
||||
name: 'tasks',
|
||||
put(to: HTMLElement, _from: HTMLElement, dragEl: HTMLElement) {
|
||||
const taskId = dragEl.dataset?.taskId;
|
||||
if (!taskId) return true;
|
||||
|
||||
const task = findTaskById(taskId);
|
||||
if (!task) return true;
|
||||
|
||||
const groupIds = new Set(computedGroups.value.map((g) => g.id));
|
||||
const isSubtask = !!(task.pid && !groupIds.has(task.pid));
|
||||
if (!isSubtask) return true;
|
||||
|
||||
const toContainer = to.closest('[data-group-id]') as HTMLElement;
|
||||
const targetGroupId = toContainer?.dataset?.groupId;
|
||||
if (!targetGroupId) return false;
|
||||
|
||||
const parentGroupId = findGroupContainingTask(task.pid);
|
||||
return targetGroupId === parentGroupId;
|
||||
},
|
||||
}));
|
||||
|
||||
const newTaskInputRef = ref<HTMLInputElement | null>(null);
|
||||
const newGroupInputRef = ref<HTMLInputElement | null>(null);
|
||||
|
||||
@@ -1148,19 +1194,29 @@
|
||||
|
||||
const allTaskLists = computed(() => {
|
||||
const lists: TaskList[] = [];
|
||||
const keyword = searchKeyword.value.trim().toLowerCase();
|
||||
for (const group of props.taskListGroups) {
|
||||
for (const list of group.taskLists) {
|
||||
if (list.delFlag !== 1) {
|
||||
if (!keyword || list.name.toLowerCase().includes(keyword)) {
|
||||
lists.push(list);
|
||||
}
|
||||
lists.push(list);
|
||||
}
|
||||
}
|
||||
}
|
||||
return lists;
|
||||
});
|
||||
|
||||
function onSearch(keyword: string) {
|
||||
if (props.showListGroupView) {
|
||||
emit('search-keyword', keyword || '');
|
||||
}
|
||||
}
|
||||
|
||||
// 用户点击清除按钮时触发搜索重置
|
||||
watch(searchKeyword, (val) => {
|
||||
if (props.showListGroupView && !val) {
|
||||
emit('search-keyword', '');
|
||||
}
|
||||
});
|
||||
|
||||
const collaboratorDisplayNames = computed(() => {
|
||||
if (!props.currentList) return '';
|
||||
const names: string[] = [];
|
||||
@@ -1378,6 +1434,13 @@
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function findGroupContainingTask(taskId: string): string | undefined {
|
||||
for (const group of props.currentList.groups) {
|
||||
if (group.tasks.some((t) => t.id === taskId)) return group.id;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function openUserSelectModal(type: 'assignee' | 'followers' | 'participant', taskId: string) {
|
||||
userSelectModalType.value = type;
|
||||
userSelectModalTaskId.value = taskId;
|
||||
@@ -1546,6 +1609,23 @@
|
||||
|
||||
const isSubtask = !!(draggedTask.pid && !groupIds.has(draggedTask.pid));
|
||||
|
||||
// 子任务不允许跨父级移动:检测是否被拖到了原父级所在分组之外
|
||||
if (isSubtask) {
|
||||
const parentGroupId = findGroupContainingTask(draggedTask.pid);
|
||||
if (parentGroupId && parentGroupId !== targetGroupId) {
|
||||
// 回退:从目标分组移除,放回原分组末尾
|
||||
const targetFlatList = flatListCache.value[targetGroupId] || [];
|
||||
const movedIdx = targetFlatList.findIndex((f) => f.task.id === taskId);
|
||||
if (movedIdx >= 0) {
|
||||
const [moved] = targetFlatList.splice(movedIdx, 1);
|
||||
const originalFlatList = flatListCache.value[parentGroupId] || [];
|
||||
originalFlatList.push(moved);
|
||||
}
|
||||
createMessage.warning('子事项不能移动到其他分组');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let targetPid: string;
|
||||
let targetSortOrder = 0;
|
||||
|
||||
@@ -1573,47 +1653,47 @@
|
||||
}
|
||||
|
||||
function onTaskMove(evt: any) {
|
||||
const relatedEl = evt.related as HTMLElement;
|
||||
const draggedTaskId = draggingTaskId.value;
|
||||
if (!draggedTaskId) {
|
||||
return true;
|
||||
}
|
||||
if (!draggedTaskId) return true;
|
||||
|
||||
const draggedTask = findTaskById(draggedTaskId);
|
||||
if (!draggedTask) {
|
||||
return true;
|
||||
}
|
||||
if (!draggedTask) return true;
|
||||
|
||||
const groupIds = new Set(computedGroups.value.map((g) => g.id));
|
||||
const isSubtask = !!(draggedTask.pid && !groupIds.has(draggedTask.pid));
|
||||
if (!isSubtask) return true;
|
||||
|
||||
if (!isSubtask) {
|
||||
return true;
|
||||
}
|
||||
// 子事项必须在原父级所在分组内移动
|
||||
const toContainer = evt.to.closest('[data-group-id]') as HTMLElement;
|
||||
const targetGroupId = toContainer?.dataset?.groupId;
|
||||
if (!targetGroupId) return false;
|
||||
|
||||
if (!relatedEl) {
|
||||
dragBlockedOnce.value = true;
|
||||
return false;
|
||||
const parentGroupId = findGroupContainingTask(draggedTask.pid);
|
||||
if (targetGroupId !== parentGroupId) return false;
|
||||
|
||||
// 在 flatList 中定位有效插入范围(仅允许在同父级子节点区间内)
|
||||
const flatList = flatListCache.value[targetGroupId] || [];
|
||||
let minIdx = -1;
|
||||
let maxIdx = -1;
|
||||
for (let i = 0; i < flatList.length; i++) {
|
||||
if (flatList[i].task.pid === draggedTask.pid) {
|
||||
if (minIdx === -1) minIdx = i;
|
||||
maxIdx = i;
|
||||
}
|
||||
}
|
||||
if (minIdx === -1) return false;
|
||||
|
||||
const relatedEl = evt.related as HTMLElement;
|
||||
if (!relatedEl) return false;
|
||||
|
||||
const relatedTaskId = relatedEl.dataset?.taskId;
|
||||
if (!relatedTaskId) {
|
||||
dragBlockedOnce.value = true;
|
||||
return false;
|
||||
}
|
||||
if (!relatedTaskId) return false;
|
||||
|
||||
const relatedTask = findTaskById(relatedTaskId);
|
||||
if (!relatedTask) {
|
||||
dragBlockedOnce.value = true;
|
||||
return false;
|
||||
}
|
||||
const relatedIdx = flatList.findIndex((f) => f.task.id === relatedTaskId);
|
||||
if (relatedIdx === -1) return false;
|
||||
|
||||
if (relatedTask.pid !== draggedTask.pid) {
|
||||
dragBlockedOnce.value = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
const insertIdx = evt.willInsertAfter ? relatedIdx + 1 : relatedIdx;
|
||||
return insertIdx >= minIdx && insertIdx <= maxIdx + 1;
|
||||
}
|
||||
|
||||
function isChildHidden(ft: FlatTaskItem): boolean {
|
||||
@@ -1776,14 +1856,38 @@
|
||||
}
|
||||
|
||||
function computeDynamicGroups(tasks: TaskItem[]): TaskGroup[] {
|
||||
const groupMap = new Map<string, TaskItem[]>();
|
||||
for (const task of tasks) {
|
||||
const key = getGroupKey(task, props.groupDimension);
|
||||
if (!groupMap.has(key)) groupMap.set(key, []);
|
||||
groupMap.get(key)!.push(task);
|
||||
const taskMap = new Map<string, TaskItem>();
|
||||
for (const t of tasks) {
|
||||
taskMap.set(t.id, t);
|
||||
}
|
||||
|
||||
// 找出所有一级节点(子节点跟随父节点分组)
|
||||
const groupIds = new Set((props.currentList?.groups || []).map((g) => g.id));
|
||||
const topTasks = tasks.filter((t) => !t.pid || groupIds.has(t.pid));
|
||||
|
||||
// 收集每个一级节点及其所有子孙
|
||||
const topWithDescendants = new Map<string, TaskItem[]>();
|
||||
for (const top of topTasks) {
|
||||
const key = getGroupKey(top, props.groupDimension);
|
||||
if (!topWithDescendants.has(key)) topWithDescendants.set(key, []);
|
||||
topWithDescendants.get(key)!.push(top);
|
||||
collectDescendants(top.id, tasks, topWithDescendants.get(key)!);
|
||||
}
|
||||
|
||||
// 处理没有父节点归属的游离节点(直接按自身分组)
|
||||
const groupedIds = new Set<string>();
|
||||
for (const groupTasks of topWithDescendants.values()) {
|
||||
for (const t of groupTasks) groupedIds.add(t.id);
|
||||
}
|
||||
const orphans = tasks.filter((t) => !groupedIds.has(t.id));
|
||||
for (const orphan of orphans) {
|
||||
const key = getGroupKey(orphan, props.groupDimension);
|
||||
if (!topWithDescendants.has(key)) topWithDescendants.set(key, []);
|
||||
topWithDescendants.get(key)!.push(orphan);
|
||||
}
|
||||
|
||||
const groups: TaskGroup[] = [];
|
||||
groupMap.forEach((groupTasks, key) => {
|
||||
topWithDescendants.forEach((groupTasks, key) => {
|
||||
groups.push({
|
||||
id: `dynamic-${key}`,
|
||||
name: key || '未分组',
|
||||
@@ -1795,6 +1899,14 @@
|
||||
return groups;
|
||||
}
|
||||
|
||||
function collectDescendants(parentId: string, allTasks: TaskItem[], result: TaskItem[]) {
|
||||
const children = allTasks.filter((t) => t.pid === parentId);
|
||||
for (const child of children) {
|
||||
result.push(child);
|
||||
collectDescendants(child.id, allTasks, result);
|
||||
}
|
||||
}
|
||||
|
||||
function getGroupKey(task: TaskItem, dimension: GroupDimension): string {
|
||||
switch (dimension) {
|
||||
case 'assignee':
|
||||
@@ -2408,6 +2520,7 @@
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
height: 46px;
|
||||
user-select: none;
|
||||
padding: 0 8px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
font-size: 14px;
|
||||
@@ -2941,4 +3054,18 @@
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.load-more-trigger {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px 0;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.load-more-text {
|
||||
color: #8f959e;
|
||||
font-size: 13px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<a-spin :spinning="props.loading" tip="加载中...">
|
||||
<div class="flat-view-header">
|
||||
<h2 class="flat-view-title">{{ title }}</h2>
|
||||
<span class="task-count">({{ flattenedTasks.length }})</span>
|
||||
<span class="task-count">({{ props.totalCount ?? flattenedTasks.length }})</span>
|
||||
</div>
|
||||
|
||||
<div class="flat-view-toolbar">
|
||||
@@ -75,7 +75,7 @@
|
||||
</a-popover>
|
||||
</div>
|
||||
|
||||
<div class="flat-view-body">
|
||||
<div ref="flatBodyRef" class="flat-view-body" @scroll="onFlatScroll">
|
||||
<div class="flat-view-table-header" :style="{ minWidth: totalFieldsWidth + 'px' }">
|
||||
<div class="table-col table-col-drag-handle"></div>
|
||||
<div class="table-col table-col-title">
|
||||
@@ -103,7 +103,13 @@
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div v-for="ft in flattenedTasks" :key="ft.task.id" class="task-row" :style="{ minWidth: totalFieldsWidth + 'px' }">
|
||||
<div :style="virtualContainerStyle" class="virtual-container">
|
||||
<div
|
||||
v-for="{ item: ft, index, style } in visibleTasks"
|
||||
:key="ft.task.id"
|
||||
class="task-row"
|
||||
:style="{ ...style, minWidth: totalFieldsWidth + 'px' }"
|
||||
>
|
||||
<div class="table-col table-col-drag-handle"></div>
|
||||
<div class="table-col table-col-title" @click.stop="emit('task-click', ft.task.id)">
|
||||
<div class="task-indent" :style="{ marginLeft: Math.min(ft.level, 4) * 20 + 'px' }">
|
||||
@@ -258,7 +264,7 @@
|
||||
'overdue-date': field.key === 'endTime' && isOverdue(ft.task),
|
||||
}"
|
||||
>
|
||||
{{ field.key === 'startTime' ? ft.task.startTime : ft.task.endTime }}
|
||||
{{ (field.key === 'startTime' ? ft.task.startTime : ft.task.endTime).substring(0, 10) }}
|
||||
</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
@@ -330,10 +336,18 @@
|
||||
</template>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div v-if="flattenedTasks.length === 0" class="empty-state">
|
||||
<Icon icon="ant-design:inbox-outlined" class="empty-icon" />
|
||||
<p>暂无事项</p>
|
||||
</div>
|
||||
<div v-if="props.hasMore && flattenedTasks.length > 0" class="load-more-trigger">
|
||||
<template v-if="props.loadingMore">
|
||||
<a-spin size="small" />
|
||||
<span class="load-more-text">加载中</span>
|
||||
</template>
|
||||
<a-button v-else type="link" size="small" @click="emit('load-more')">加载更多</a-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SzDeptUserModal
|
||||
@@ -351,13 +365,14 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, nextTick } from 'vue';
|
||||
import { ref, computed, nextTick, watch, onMounted } from 'vue';
|
||||
import dayjs from 'dayjs';
|
||||
import Icon from '/@/components/Icon/index';
|
||||
import AvatarDisplay from './AvatarDisplay.vue';
|
||||
import SzDeptUserModal from '/@/components/semri/deptUserComponent/SzDeptUserModal.vue';
|
||||
import { FIELD_CONFIG, SORT_OPTIONS, MAX_TASK_DEPTH } from '../types';
|
||||
import { PRIORITY_OPTIONS, getPriorityBg, getPriorityLabel, isOverdue } from '../utils/taskUtils';
|
||||
import { useVirtualScroll } from '/@/hooks/useVirtualScroll';
|
||||
import type { TaskItem, StatusFilter, SortField, SortDirection, FieldKey, FlatTaskItem } from '../types';
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -370,6 +385,9 @@
|
||||
editable?: boolean;
|
||||
taskEditMap?: Map<string, boolean>;
|
||||
loading?: boolean;
|
||||
hasMore?: boolean;
|
||||
loadingMore?: boolean;
|
||||
totalCount?: number;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -382,8 +400,12 @@
|
||||
'update:sortDirection': [value: SortDirection];
|
||||
'update:visibleFields': [value: FieldKey[]];
|
||||
'load-subtasks': [taskId: string, mainId: string];
|
||||
'load-more': [];
|
||||
}>();
|
||||
|
||||
const ITEM_HEIGHT = 46;
|
||||
const flatBodyRef = ref<HTMLElement | null>(null);
|
||||
|
||||
const fieldConfigVisible = ref(false);
|
||||
const expandedTaskIds = ref<Set<string>>(new Set());
|
||||
const loadedChildrenIds = ref<Set<string>>(new Set());
|
||||
@@ -495,6 +517,23 @@
|
||||
return result;
|
||||
});
|
||||
|
||||
const { visibleItems: allVisibleTasks, containerStyle: virtualContainerStyle, updateContainer, scrollTop } = useVirtualScroll({
|
||||
items: flattenedTasks,
|
||||
itemHeight: ITEM_HEIGHT,
|
||||
buffer: 5,
|
||||
containerRef: flatBodyRef,
|
||||
});
|
||||
|
||||
const visibleTasks = computed(() => allVisibleTasks.value as { item: FlatTaskItem; index: number; style: Record<string, string> }[]);
|
||||
|
||||
watch(() => [flattenedTasks.value.length, flattenedTasks.value], () => {
|
||||
nextTick(() => updateContainer());
|
||||
}, { deep: true });
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => updateContainer());
|
||||
});
|
||||
|
||||
function collectFlatTasks(task: TaskItem, level: number, result: FlatTaskItem[], visited: Set<string>) {
|
||||
if (visited.has(task.id)) return;
|
||||
if (level >= MAX_TASK_DEPTH) return;
|
||||
@@ -540,6 +579,17 @@
|
||||
return document.body;
|
||||
}
|
||||
|
||||
function onFlatScroll(event: Event) {
|
||||
// 先更新虚拟滚动位置
|
||||
scrollTop.value = (event.target as HTMLElement).scrollTop;
|
||||
// 触底检测
|
||||
const target = event.target as HTMLElement;
|
||||
const nearBottom = target.scrollHeight - target.scrollTop - target.clientHeight < 200;
|
||||
if (nearBottom && props.hasMore) {
|
||||
emit('load-more');
|
||||
}
|
||||
}
|
||||
|
||||
function onStatusFilterChange({ key }: { key: string }) {
|
||||
emit('update:statusFilter', key as StatusFilter);
|
||||
}
|
||||
@@ -845,6 +895,7 @@
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
height: 46px;
|
||||
user-select: none;
|
||||
padding: 0 8px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
font-size: 14px;
|
||||
@@ -1133,6 +1184,24 @@
|
||||
}
|
||||
}
|
||||
|
||||
.virtual-container {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.load-more-trigger {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px 0;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.load-more-text {
|
||||
color: #8f959e;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
<Icon icon="tasklist-task-list|svg" class="sidebar-favorites-icon" />
|
||||
<span class="sidebar-favorites-name">常用计划</span>
|
||||
<a-dropdown :trigger="['click']">
|
||||
<Icon icon="ant-design:plus-outlined" class="sidebar-group-add" @click.stop />
|
||||
<Icon icon="ant-design:plus-outlined" class="sidebar-group-add header-add-btn" @click.stop />
|
||||
<template #overlay>
|
||||
<a-menu @click="onAddMenuClick">
|
||||
<a-menu-item key="new-list">
|
||||
@@ -62,7 +62,8 @@
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</div>
|
||||
<div class="sidebar-favorites-items">
|
||||
<div class="sidebar-favorites-scroll">
|
||||
<div class="sidebar-favorites-items">
|
||||
<draggable :list="localUngroupedTaskLists" group="tasklists" item-key="id" handle=".list-drag-handle" :animation="200" @end="onDragEnd">
|
||||
<template #item="{ element: taskList }">
|
||||
<div
|
||||
@@ -214,6 +215,7 @@
|
||||
<span>新建分组</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -559,7 +561,9 @@
|
||||
|
||||
.sidebar-lists-section {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
@@ -569,6 +573,12 @@
|
||||
gap: 4px;
|
||||
padding: 6px 8px 2px;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.sidebar-favorites-scroll {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.sidebar-favorites-icon {
|
||||
@@ -657,7 +667,6 @@
|
||||
}
|
||||
|
||||
.sidebar-group-add {
|
||||
margin-left: auto;
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
cursor: pointer;
|
||||
|
||||
+216
-67
@@ -32,6 +32,8 @@
|
||||
:show-list-group-view="showListGroupView"
|
||||
:favorite-id-map="favoriteIdMap"
|
||||
:loading="pageLoading"
|
||||
:has-more="showListGroupView ? listViewHasMore : false"
|
||||
:loading-more="showListGroupView ? loadingMore : false"
|
||||
@create-task="onCreateTask"
|
||||
@toggle-task="onToggleTask"
|
||||
@task-click="onTaskClick"
|
||||
@@ -55,6 +57,8 @@
|
||||
@rename-group="onRenameTaskGroup"
|
||||
@delete-group="onDeleteTaskGroup"
|
||||
@create-list="onCreateListModal('')"
|
||||
@load-more="onLoadMoreListViews"
|
||||
@search-keyword="onSearchKeywordChange"
|
||||
/>
|
||||
<TaskFlatView
|
||||
v-else
|
||||
@@ -67,6 +71,9 @@
|
||||
:editable="flatViewEditable"
|
||||
:task-edit-map="flatTaskEditMap"
|
||||
:loading="pageLoading"
|
||||
:has-more="hasMore"
|
||||
:loading-more="loadingMore"
|
||||
:total-count="totalCount"
|
||||
@toggle-task="onToggleTask"
|
||||
@task-click="onTaskClick"
|
||||
@update-task-field="onUpdateTaskField"
|
||||
@@ -76,6 +83,7 @@
|
||||
@update:sort-direction="(v) => (sortDirection = v)"
|
||||
@update:visible-fields="(v) => (visibleFields = v)"
|
||||
@load-subtasks="onLoadSubtasks"
|
||||
@load-more="loadMoreFlatTasks"
|
||||
/>
|
||||
<CreateTaskModal @register="registerTaskModal" @success="onTaskCreated" />
|
||||
<CreateListModal @register="registerListModal" @success="onListCreated" />
|
||||
@@ -168,6 +176,19 @@
|
||||
const visibleFields = ref<FieldKey[]>([...DEFAULT_VISIBLE_FIELDS]);
|
||||
const currentDrawerTaskId = ref<string>('');
|
||||
const pageLoading = ref(false);
|
||||
const loadingMore = ref(false);
|
||||
const pageNo = ref(1);
|
||||
const pageSize = 50;
|
||||
const totalCount = ref(0);
|
||||
const hasMore = computed(() => flatTasks.value.length < totalCount.value);
|
||||
const searchKeyword = ref('');
|
||||
const listViewPageNo = ref(1);
|
||||
const listViewPageSize = 30;
|
||||
const listViewTotalCount = ref(0);
|
||||
const listViewHasMore = computed(() => {
|
||||
const allLists = quickAccessGroups.value.flatMap((g) => g.taskLists);
|
||||
return allLists.length < listViewTotalCount.value;
|
||||
});
|
||||
|
||||
const [registerTaskModal, { openModal: openTaskModal }] = useModal();
|
||||
const [registerListModal, { openModal: openListModal }] = useModal();
|
||||
@@ -366,8 +387,7 @@
|
||||
|
||||
async function loadTasks(listId: string) {
|
||||
try {
|
||||
const res = await listAllByMainId({ mainId: listId });
|
||||
const data: any[] = Array.isArray(res) ? res : Array.isArray(res?.result) ? res.result : [];
|
||||
const data: any[] = (await listAllByMainId({ mainId: listId })) || [];
|
||||
const list = findListById(listId);
|
||||
if (!list) return;
|
||||
|
||||
@@ -393,58 +413,72 @@
|
||||
defaultGroupRealIdMap.value[listId] = gr.id;
|
||||
}
|
||||
}
|
||||
|
||||
if (!defaultGroup) {
|
||||
defaultGroup = {
|
||||
id: '__default__',
|
||||
name: '默认分组',
|
||||
taskListId: listId,
|
||||
collapsed: false,
|
||||
tasks: [],
|
||||
sortOrder: 0,
|
||||
};
|
||||
defaultGroup = { id: '__default__', name: '默认分组', taskListId: listId, collapsed: false, tasks: [], sortOrder: 0 };
|
||||
}
|
||||
|
||||
const groupIds = new Set(groupRecords.map((g: any) => g.id));
|
||||
const newTasks = taskRecords.map((tr: any) => buildTaskItem(tr));
|
||||
|
||||
const allTaskItems = new Map<string, TaskItem>();
|
||||
for (const tr of taskRecords) {
|
||||
allTaskItems.set(tr.id, buildTaskItem(tr));
|
||||
}
|
||||
|
||||
function getGroupForTask(taskId: string): { groupId: string; groupName: string } {
|
||||
const item = allTaskItems.get(taskId);
|
||||
if (!item) return { groupId: defaultGroup!.id, groupName: defaultGroup!.name };
|
||||
if (!item.pid) return { groupId: defaultGroup!.id, groupName: defaultGroup!.name };
|
||||
if (groupIds.has(item.pid)) {
|
||||
const tg = groupMap.get(item.pid);
|
||||
return { groupId: tg!.id, groupName: tg!.name };
|
||||
}
|
||||
return getGroupForTask(item.pid);
|
||||
}
|
||||
|
||||
for (const [id, taskItem] of allTaskItems) {
|
||||
const { groupId, groupName } = getGroupForTask(id);
|
||||
taskItem.groupId = groupId;
|
||||
taskItem.groupName = groupName;
|
||||
const targetGroup = groupMap.get(groupId) || defaultGroup!;
|
||||
for (const taskItem of newTasks) {
|
||||
taskItem.groupId = resolveGroupId(taskItem, groupIds, groupMap, defaultGroup!);
|
||||
taskItem.groupName = groupMap.get(taskItem.groupId)?.name || defaultGroup!.name;
|
||||
const targetGroup = groupMap.get(taskItem.groupId) || defaultGroup!;
|
||||
targetGroup.tasks = targetGroup.tasks.filter((t) => t.id !== taskItem.id);
|
||||
targetGroup.tasks.push(taskItem);
|
||||
}
|
||||
|
||||
const resultGroups = Array.from(groupMap.values());
|
||||
resultGroups.sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0));
|
||||
|
||||
list.groups = resultGroups;
|
||||
list.groups = Array.from(groupMap.values()).sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0));
|
||||
} catch (e) {
|
||||
console.error('[TaskList] 加载任务失败', e);
|
||||
}
|
||||
}
|
||||
|
||||
function resolveGroupId(taskItem: TaskItem, groupIds: Set<string>, groupMap: Map<string, TaskGroup>, defaultGroup: TaskGroup): string {
|
||||
if (!taskItem.pid) return defaultGroup.id;
|
||||
if (groupIds.has(taskItem.pid)) return taskItem.pid;
|
||||
return defaultGroup.id;
|
||||
}
|
||||
|
||||
async function loadMoreFlatTasks() {
|
||||
if (!hasMore.value || loadingMore.value) return;
|
||||
loadingMore.value = true;
|
||||
try {
|
||||
pageNo.value++;
|
||||
if (currentView.value === 'my-tasks') {
|
||||
const res: any = await myResponsibleTasks({ pageNo: pageNo.value, pageSize });
|
||||
const data: any[] = Array.isArray(res?.records) ? res.records : Array.isArray(res?.result?.records) ? res.result.records : Array.isArray(res?.result) ? res.result : [];
|
||||
totalCount.value = res?.total || res?.result?.total || data.length;
|
||||
flatTasks.value = [...flatTasks.value, ...data.map(buildFlatTaskItem)];
|
||||
} else if (currentView.value === 'followed') {
|
||||
const res: any = await myFollowedTasks({ pageNo: pageNo.value, pageSize });
|
||||
const data: any[] = Array.isArray(res?.records) ? res.records : Array.isArray(res?.result?.records) ? res.result.records : Array.isArray(res?.result) ? res.result : [];
|
||||
totalCount.value = res?.total || res?.result?.total || data.length;
|
||||
const items = data.map(buildFlatTaskItem);
|
||||
flatTasks.value = [...flatTasks.value, ...items];
|
||||
const editMap = new Map(flatTaskEditMap.value);
|
||||
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;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[TaskList] 加载更多任务失败', e);
|
||||
} finally {
|
||||
loadingMore.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function onViewSelect(view: ViewType) {
|
||||
currentView.value = view;
|
||||
showListGroupView.value = false;
|
||||
currentListId.value = '';
|
||||
flatTasks.value = [];
|
||||
pageNo.value = 1;
|
||||
totalCount.value = 0;
|
||||
groupDimension.value = 'none';
|
||||
sortField.value = 'createTime';
|
||||
sortDirection.value = 'asc';
|
||||
@@ -455,13 +489,17 @@
|
||||
flatViewTitle.value = '我的事项';
|
||||
flatViewEditable.value = true;
|
||||
flatTaskEditMap.value = new Map();
|
||||
const data: any[] = await myResponsibleTasks();
|
||||
flatTasks.value = (data || []).map(buildFlatTaskItem);
|
||||
const res: any = await myResponsibleTasks({ pageNo: 1, pageSize });
|
||||
const data: any[] = Array.isArray(res?.records) ? res.records : Array.isArray(res?.result?.records) ? res.result.records : Array.isArray(res?.result) ? res.result : [];
|
||||
totalCount.value = res?.total || res?.result?.total || data.length;
|
||||
flatTasks.value = data.map(buildFlatTaskItem);
|
||||
} else if (view === 'followed') {
|
||||
flatViewTitle.value = '关注事项';
|
||||
flatViewEditable.value = false;
|
||||
const data: any[] = await myFollowedTasks();
|
||||
const items = (data || []).map(buildFlatTaskItem);
|
||||
const res: any = await myFollowedTasks({ pageNo: 1, pageSize });
|
||||
const data: any[] = Array.isArray(res?.records) ? res.records : Array.isArray(res?.result?.records) ? res.result.records : Array.isArray(res?.result) ? res.result : [];
|
||||
totalCount.value = res?.total || res?.result?.total || data.length;
|
||||
const items = data.map(buildFlatTaskItem);
|
||||
const editMap = new Map<string, boolean>();
|
||||
const userId = userStore.getUserInfo?.id || '';
|
||||
for (const item of items) {
|
||||
@@ -604,18 +642,23 @@
|
||||
currentListId.value = '';
|
||||
groupDimension.value = 'custom';
|
||||
flatTasks.value = [];
|
||||
listViewPageNo.value = 1;
|
||||
pageLoading.value = true;
|
||||
|
||||
try {
|
||||
const kw = searchKeyword.value || undefined;
|
||||
if (tabKey === 'all') {
|
||||
const data: TaskList[] = await getAllLists();
|
||||
quickAccessGroups.value = wrapAsVirtualGroupsWithDetails(data);
|
||||
const res: any = await getAllLists({ pageNo: 1, pageSize: listViewPageSize, keyword: kw });
|
||||
quickAccessGroups.value = wrapAsVirtualGroupsWithDetails(res?.records || []);
|
||||
listViewTotalCount.value = res?.total || 0;
|
||||
} else if (tabKey === 'own') {
|
||||
const data: TaskList[] = await getMyOwnLists();
|
||||
quickAccessGroups.value = wrapAsVirtualGroupsWithDetails(data);
|
||||
const res: any = await getMyOwnLists({ pageNo: 1, pageSize: listViewPageSize, keyword: kw });
|
||||
quickAccessGroups.value = wrapAsVirtualGroupsWithDetails(res?.records || []);
|
||||
listViewTotalCount.value = res?.total || 0;
|
||||
} else if (tabKey === 'collab') {
|
||||
const data: TaskList[] = await getMyCollabLists();
|
||||
quickAccessGroups.value = wrapAsVirtualGroupsWithDetails(data);
|
||||
const res: any = await getMyCollabLists({ pageNo: 1, pageSize: listViewPageSize, keyword: kw });
|
||||
quickAccessGroups.value = wrapAsVirtualGroupsWithDetails(res?.records || []);
|
||||
listViewTotalCount.value = res?.total || 0;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[TaskList] 加载清单失败', e);
|
||||
@@ -624,6 +667,60 @@
|
||||
}
|
||||
}
|
||||
|
||||
let searchTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
function onSearchKeywordChange(keyword: string) {
|
||||
searchKeyword.value = keyword;
|
||||
if (searchTimer) clearTimeout(searchTimer);
|
||||
searchTimer = setTimeout(() => {
|
||||
listViewPageNo.value = 1;
|
||||
onQuickAccessTabClick(getCurrentTabKey());
|
||||
}, 300);
|
||||
}
|
||||
|
||||
function getCurrentTabKey(): string {
|
||||
const tabKeyMap: Record<string, string> = {
|
||||
'all-tasks': 'all',
|
||||
created: 'own',
|
||||
assigned: 'collab',
|
||||
};
|
||||
return tabKeyMap[currentView.value] || 'all';
|
||||
}
|
||||
|
||||
async function onLoadMoreListViews() {
|
||||
if (!listViewHasMore.value || loadingMore.value) return;
|
||||
loadingMore.value = true;
|
||||
listViewPageNo.value++;
|
||||
const tabKeyMap: Record<string, string> = {
|
||||
'all-tasks': 'all',
|
||||
created: 'own',
|
||||
assigned: 'collab',
|
||||
};
|
||||
const tabKey = tabKeyMap[currentView.value] || '';
|
||||
try {
|
||||
const kw = searchKeyword.value || undefined;
|
||||
let res: any;
|
||||
if (tabKey === 'all') {
|
||||
res = await getAllLists({ pageNo: listViewPageNo.value, pageSize: listViewPageSize, keyword: kw });
|
||||
} else if (tabKey === 'own') {
|
||||
res = await getMyOwnLists({ pageNo: listViewPageNo.value, pageSize: listViewPageSize, keyword: kw });
|
||||
} else if (tabKey === 'collab') {
|
||||
res = await getMyCollabLists({ pageNo: listViewPageNo.value, pageSize: listViewPageSize, keyword: kw });
|
||||
}
|
||||
if (res?.records) {
|
||||
const existingIds = new Set(quickAccessGroups.value.flatMap((g) => g.taskLists).map((l) => l.id));
|
||||
const newLists = (res.records as TaskList[]).filter((item) => !existingIds.has(item.id));
|
||||
if (newLists.length > 0) {
|
||||
quickAccessGroups.value = [...quickAccessGroups.value, ...wrapAsVirtualGroupsWithDetails(newLists)];
|
||||
}
|
||||
listViewTotalCount.value = res?.total || 0;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[TaskList] 加载更多清单失败', e);
|
||||
} finally {
|
||||
loadingMore.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function wrapAsVirtualGroupsWithDetails(lists: TaskList[]): TaskListGroup[] {
|
||||
return lists.map((item: TaskList) => ({
|
||||
id: `_virtual_${item.id}`,
|
||||
@@ -663,9 +760,21 @@
|
||||
type: '1',
|
||||
pid: groupId && groupId !== '__default__' ? groupId : '',
|
||||
};
|
||||
await addTask(params);
|
||||
const created: any = await addTask(params);
|
||||
if (currentListId.value) {
|
||||
await loadTasks(currentListId.value);
|
||||
const newTask = buildTaskItem(created?.result || created || {});
|
||||
if (newTask.id) {
|
||||
newTask.groupId = groupId || '__default__';
|
||||
newTask.groupName = groupId && groupId !== '__default__'
|
||||
? (currentList.value?.groups?.find((g: TaskGroup) => g.id === groupId)?.name || '')
|
||||
: (currentList.value?.groups?.[0]?.name || '');
|
||||
const list = findListById(currentListId.value);
|
||||
if (list) {
|
||||
const targetGroup = list.groups.find((g: TaskGroup) => g.id === (groupId || list.groups[0]?.id));
|
||||
if (targetGroup) targetGroup.tasks.unshift(newTask);
|
||||
else if (list.groups.length > 0) list.groups[0].tasks.unshift(newTask);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[TaskList] 创建任务失败', e);
|
||||
@@ -697,15 +806,16 @@
|
||||
|
||||
async function refreshQuickAccessView() {
|
||||
try {
|
||||
const viewMap: Record<string, () => Promise<TaskList[]>> = {
|
||||
const viewMap: Record<string, (params?: any) => Promise<any>> = {
|
||||
'all-tasks': getAllLists,
|
||||
created: getMyOwnLists,
|
||||
assigned: getMyCollabLists,
|
||||
};
|
||||
const fetcher = viewMap[currentView.value];
|
||||
if (fetcher) {
|
||||
const data = await fetcher();
|
||||
quickAccessGroups.value = wrapAsVirtualGroupsWithDetails(data);
|
||||
const res = await fetcher({ pageNo: 1, pageSize: listViewPageSize });
|
||||
quickAccessGroups.value = wrapAsVirtualGroupsWithDetails(res?.records || []);
|
||||
listViewTotalCount.value = res?.total || 0;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[TaskList] 刷新计划列表失败', e);
|
||||
@@ -981,18 +1091,33 @@
|
||||
return;
|
||||
}
|
||||
}
|
||||
// 乐观更新
|
||||
let oldValue: any = undefined;
|
||||
if (isFlatView.value) {
|
||||
const idx = flatTasks.value.findIndex((t) => t.id === taskId);
|
||||
if (idx >= 0) {
|
||||
oldValue = (flatTasks.value[idx] as any)[field];
|
||||
(flatTasks.value[idx] as any)[field] = value;
|
||||
}
|
||||
} else {
|
||||
const found = findTaskInGroups(taskId);
|
||||
if (found) {
|
||||
oldValue = (found.task as any)[field];
|
||||
(found.task as any)[field] = value;
|
||||
}
|
||||
}
|
||||
try {
|
||||
await editTask({ id: taskId, [field]: 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);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[TaskList] 更新任务字段失败', e);
|
||||
// 回滚
|
||||
if (isFlatView.value) {
|
||||
const idx = flatTasks.value.findIndex((t) => t.id === taskId);
|
||||
if (idx >= 0) (flatTasks.value[idx] as any)[field] = oldValue;
|
||||
} else {
|
||||
const found = findTaskInGroups(taskId);
|
||||
if (found) (found.task as any)[field] = oldValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1008,18 +1133,33 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
const oldFields: Record<string, any> = {};
|
||||
if (isFlatView.value) {
|
||||
const idx = flatTasks.value.findIndex((t) => t.id === taskId);
|
||||
if (idx >= 0) {
|
||||
for (const f of Object.keys(fields)) oldFields[f] = (flatTasks.value[idx] as any)[f];
|
||||
flatTasks.value[idx] = { ...flatTasks.value[idx], ...fields };
|
||||
}
|
||||
} else {
|
||||
const found = findTaskInGroups(taskId);
|
||||
if (found) {
|
||||
for (const f of Object.keys(fields)) oldFields[f] = (found.task as any)[f];
|
||||
Object.assign(found.task, fields);
|
||||
}
|
||||
}
|
||||
try {
|
||||
await editTask({ id: taskId, ...fields });
|
||||
} catch (e) {
|
||||
console.error('[TaskList] 批量更新任务字段失败', e);
|
||||
if (isFlatView.value) {
|
||||
const idx = flatTasks.value.findIndex((t) => t.id === taskId);
|
||||
if (idx >= 0) {
|
||||
flatTasks.value[idx] = { ...flatTasks.value[idx], ...fields };
|
||||
for (const f of Object.keys(oldFields)) (flatTasks.value[idx] as any)[f] = oldFields[f];
|
||||
}
|
||||
} else if (currentListId.value) {
|
||||
await loadTasks(currentListId.value);
|
||||
} else {
|
||||
const found = findTaskInGroups(taskId);
|
||||
if (found) Object.assign(found.task, oldFields);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[TaskList] 批量更新任务字段失败', e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1134,15 +1274,24 @@
|
||||
}
|
||||
|
||||
async function onDeleteTask(taskId: string) {
|
||||
let removed: { idx: number; item: TaskItem } | null = null;
|
||||
if (isFlatView.value) {
|
||||
const idx = flatTasks.value.findIndex((t) => t.id === taskId);
|
||||
if (idx >= 0) {
|
||||
removed = { idx, item: flatTasks.value[idx] };
|
||||
flatTasks.value.splice(idx, 1);
|
||||
}
|
||||
}
|
||||
try {
|
||||
await deleteTaskApi({ id: taskId }, () => {});
|
||||
if (isFlatView.value) {
|
||||
flatTasks.value = flatTasks.value.filter((t) => t.id !== taskId);
|
||||
} else if (currentListId.value) {
|
||||
if (!isFlatView.value && currentListId.value) {
|
||||
await loadTasks(currentListId.value);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[TaskList] 删除任务失败', e);
|
||||
if (removed) {
|
||||
flatTasks.value.splice(removed.idx, 0, removed.item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user