czh-20260429-实现任务管理的前端功能
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
import { defHttp } from '/@/utils/http/axios';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
|
||||
const { createConfirm } = useMessage();
|
||||
|
||||
enum Api {
|
||||
list = '/tasklist/taskList/list',
|
||||
save = '/tasklist/taskList/add',
|
||||
edit = '/tasklist/taskList/edit',
|
||||
deleteOne = '/tasklist/taskList/delete',
|
||||
deleteBatch = '/tasklist/taskList/deleteBatch',
|
||||
importExcel = '/tasklist/taskList/importExcel',
|
||||
exportXls = '/tasklist/taskList/exportXls',
|
||||
taskListDetialList = '/tasklist/taskList/queryTaskListDetialByMainId',
|
||||
treeList = '/tasklist/taskList/treeList',
|
||||
myFavorites = '/tasklist/taskList/myFavorites',
|
||||
addTaskList = '/tasklist/taskList/addTaskList',
|
||||
addTaskListGroup = '/tasklist/taskList/addTaskListGroup',
|
||||
moveTaskList = '/tasklist/taskList/moveTaskList',
|
||||
moveGroup = '/tasklist/taskList/moveGroup',
|
||||
deleteTaskList = '/tasklist/taskList/deleteTaskList',
|
||||
removeFavorite = '/tasklist/taskList/removeFavorite',
|
||||
removeFavoriteGroup = '/tasklist/taskList/removeFavoriteGroup',
|
||||
addToFavorites = '/tasklist/taskList/addToFavorites',
|
||||
renameGroup = '/tasklist/taskList/renameGroup',
|
||||
renameTaskList = '/tasklist/taskList/renameTaskList',
|
||||
myOwnLists = '/tasklist/taskList/myOwnLists',
|
||||
allLists = '/tasklist/taskList/allLists',
|
||||
myCollabLists = '/tasklist/taskList/myCollabLists',
|
||||
addTask = '/tasklist/taskListDetial/add',
|
||||
editTask = '/tasklist/taskListDetial/edit',
|
||||
deleteTask = '/tasklist/taskListDetial/delete',
|
||||
toggleTaskStatus = '/tasklist/taskListDetial/toggleStatus',
|
||||
getCollaborators = '/tasklist/taskList/getCollaborators',
|
||||
addCollaborator = '/tasklist/taskList/addCollaborator',
|
||||
removeCollaborator = '/tasklist/taskList/removeCollaborator',
|
||||
updateCollaboratorPermission = '/tasklist/taskList/updateCollaboratorPermission',
|
||||
getMyPermission = '/tasklist/taskList/getMyPermission',
|
||||
moveTask = '/tasklist/taskListDetial/moveTask',
|
||||
moveTaskGroup = '/tasklist/taskListDetial/moveTaskGroup',
|
||||
followTask = '/tasklist/taskListDetial/follow',
|
||||
unfollowTask = '/tasklist/taskListDetial/unfollow',
|
||||
loadSubTasks = '/tasklist/taskListDetial/loadSubTasks',
|
||||
listByMainId = '/tasklist/taskListDetial/listByMainId',
|
||||
listAllByMainId = '/tasklist/taskListDetial/listAllByMainId',
|
||||
myResponsibleTasks = '/tasklist/taskList/myResponsibleTasks',
|
||||
myFollowedTasks = '/tasklist/taskList/myFollowedTasks',
|
||||
}
|
||||
|
||||
export const getExportUrl = Api.exportXls;
|
||||
export const getImportUrl = Api.importExcel;
|
||||
export const taskListDetialList = Api.taskListDetialList;
|
||||
|
||||
export const list = (params) => defHttp.get({ url: Api.list, params });
|
||||
|
||||
export const treeList = (params) => defHttp.get({ url: Api.treeList, params });
|
||||
|
||||
export const deleteOne = (params, handleSuccess) => {
|
||||
return defHttp.delete({ url: Api.deleteOne, params }, { joinParamsToUrl: true }).then(() => {
|
||||
handleSuccess();
|
||||
});
|
||||
};
|
||||
|
||||
export const batchDelete = (params, handleSuccess) => {
|
||||
createConfirm({
|
||||
iconType: 'warning',
|
||||
title: '确认删除',
|
||||
content: '是否删除选中数据',
|
||||
okText: '确认',
|
||||
cancelText: '取消',
|
||||
onOk: () => {
|
||||
return defHttp.delete({ url: Api.deleteBatch, data: params }, { joinParamsToUrl: true }).then(() => {
|
||||
handleSuccess();
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const saveOrUpdate = (params, isUpdate) => {
|
||||
const url = isUpdate ? Api.edit : Api.save;
|
||||
return defHttp.post({ url: url, params });
|
||||
};
|
||||
|
||||
export const addGroup = (params) => defHttp.post({ url: Api.addTaskListGroup, params });
|
||||
|
||||
export const addTaskList = (params) => defHttp.post({ url: Api.addTaskList, params });
|
||||
|
||||
export const addTaskListGroup = (params) => defHttp.post({ url: Api.addTaskListGroup, params });
|
||||
|
||||
export const moveTaskList = (params) => defHttp.post({ url: Api.moveTaskList, params });
|
||||
|
||||
export const moveGroup = (params) => defHttp.post({ url: Api.moveGroup, params });
|
||||
|
||||
export const getMyFavorites = () => defHttp.get({ url: Api.myFavorites });
|
||||
|
||||
export const deleteTaskListApi = (params) => defHttp.post({ url: Api.deleteTaskList, params });
|
||||
|
||||
export const removeFavorite = (params) => defHttp.post({ url: Api.removeFavorite, params });
|
||||
|
||||
export const addToFavorites = (params) => defHttp.post({ url: Api.addToFavorites, params });
|
||||
|
||||
export const removeFavoriteGroup = (params) => defHttp.post({ url: Api.removeFavoriteGroup, params });
|
||||
|
||||
export const renameGroup = (params) => defHttp.post({ url: Api.renameGroup, params });
|
||||
|
||||
export const renameTaskList = (params) => defHttp.post({ url: Api.renameTaskList, params });
|
||||
|
||||
export const getMyOwnLists = () => defHttp.get({ url: Api.myOwnLists });
|
||||
|
||||
export const getAllLists = () => defHttp.get({ url: Api.allLists });
|
||||
|
||||
export const getMyCollabLists = () => defHttp.get({ url: Api.myCollabLists });
|
||||
|
||||
export const addTask = (params) => defHttp.post({ url: Api.addTask, params });
|
||||
|
||||
export const editTask = (params) => defHttp.post({ url: Api.editTask, params });
|
||||
|
||||
export const deleteTask = (params, handleSuccess) => {
|
||||
return defHttp.delete({ url: Api.deleteTask, params }, { joinParamsToUrl: true }).then(() => {
|
||||
handleSuccess();
|
||||
});
|
||||
};
|
||||
|
||||
export const toggleTaskStatus = (params) => defHttp.post({ url: Api.toggleTaskStatus, params });
|
||||
|
||||
export const getCollaborators = (params) => defHttp.get({ url: Api.getCollaborators, params });
|
||||
|
||||
export const addCollaborator = (params) => defHttp.post({ url: Api.addCollaborator, params });
|
||||
|
||||
export const removeCollaborator = (params) => defHttp.post({ url: Api.removeCollaborator, params });
|
||||
|
||||
export const updateCollaboratorPermission = (params) => defHttp.post({ url: Api.updateCollaboratorPermission, params });
|
||||
|
||||
export const getMyPermission = (params) => defHttp.get({ url: Api.getMyPermission, params });
|
||||
|
||||
export const moveTask = (params) => defHttp.post({ url: Api.moveTask, params });
|
||||
|
||||
export const moveTaskGroup = (params) => defHttp.post({ url: Api.moveTaskGroup, params });
|
||||
|
||||
export const followTask = (params) => defHttp.post({ url: Api.followTask, params });
|
||||
|
||||
export const unfollowTask = (params) => defHttp.post({ url: Api.unfollowTask, params });
|
||||
|
||||
export const loadSubTasks = (params) => defHttp.get({ url: Api.loadSubTasks, params });
|
||||
|
||||
export const listByMainId = (params) => defHttp.get({ url: Api.listByMainId, params });
|
||||
|
||||
export const listAllByMainId = (params) => defHttp.get({ url: Api.listAllByMainId, params });
|
||||
|
||||
export const myResponsibleTasks = () => defHttp.get({ url: Api.myResponsibleTasks });
|
||||
|
||||
export const myFollowedTasks = () => defHttp.get({ url: Api.myFollowedTasks });
|
||||
@@ -0,0 +1,587 @@
|
||||
import { BasicColumn } from '/@/components/Table';
|
||||
import { FormSchema } from '/@/components/Table';
|
||||
import { JVxeTypes, JVxeColumn } from '/@/components/jeecg/JVxeTable/types';
|
||||
import type { TaskListGroup } from './types';
|
||||
|
||||
export const columns: BasicColumn[] = [
|
||||
{
|
||||
title: '任务清单名称',
|
||||
align: 'center',
|
||||
dataIndex: 'tasklistName',
|
||||
},
|
||||
{
|
||||
title: '父级节点',
|
||||
align: 'center',
|
||||
dataIndex: 'pid',
|
||||
},
|
||||
{
|
||||
title: '是否有子节点',
|
||||
align: 'center',
|
||||
dataIndex: 'hasChild',
|
||||
},
|
||||
{
|
||||
title: '排序号',
|
||||
align: 'center',
|
||||
dataIndex: 'sortOrder',
|
||||
},
|
||||
{
|
||||
title: '类型',
|
||||
align: 'center',
|
||||
dataIndex: 'type',
|
||||
},
|
||||
];
|
||||
export const searchFormSchema: FormSchema[] = [];
|
||||
|
||||
export const formSchema: FormSchema[] = [
|
||||
{
|
||||
label: '任务清单名称',
|
||||
field: 'tasklistName',
|
||||
component: 'Input',
|
||||
},
|
||||
{
|
||||
label: '父级节点',
|
||||
field: 'pid',
|
||||
component: 'Input',
|
||||
},
|
||||
{
|
||||
label: '是否有子节点',
|
||||
field: 'hasChild',
|
||||
component: 'Input',
|
||||
},
|
||||
{
|
||||
label: '排序号',
|
||||
field: 'sortOrder',
|
||||
component: 'InputNumber',
|
||||
},
|
||||
{
|
||||
label: '类型',
|
||||
field: 'type',
|
||||
component: 'Input',
|
||||
},
|
||||
{
|
||||
label: '',
|
||||
field: 'id',
|
||||
component: 'Input',
|
||||
show: false,
|
||||
},
|
||||
];
|
||||
|
||||
export const taskListDetialColumns: JVxeColumn[] = [
|
||||
{
|
||||
title: '父节点ID',
|
||||
key: 'pid',
|
||||
type: JVxeTypes.input,
|
||||
width: '200px',
|
||||
placeholder: '请输入${title}',
|
||||
defaultValue: '',
|
||||
},
|
||||
{
|
||||
title: '是否有子节点',
|
||||
key: 'hasChild',
|
||||
type: JVxeTypes.input,
|
||||
width: '200px',
|
||||
placeholder: '请输入${title}',
|
||||
defaultValue: '',
|
||||
},
|
||||
{
|
||||
title: '排序号',
|
||||
key: 'sortOrder',
|
||||
type: JVxeTypes.inputNumber,
|
||||
width: '200px',
|
||||
placeholder: '请输入${title}',
|
||||
defaultValue: '',
|
||||
},
|
||||
{
|
||||
title: '任务名称',
|
||||
key: 'taskName',
|
||||
type: JVxeTypes.input,
|
||||
width: '200px',
|
||||
placeholder: '请输入${title}',
|
||||
defaultValue: '',
|
||||
},
|
||||
{
|
||||
title: '任务描述',
|
||||
key: 'taskDesc',
|
||||
type: JVxeTypes.input,
|
||||
width: '200px',
|
||||
placeholder: '请输入${title}',
|
||||
defaultValue: '',
|
||||
},
|
||||
{
|
||||
title: '优先级',
|
||||
key: 'priority',
|
||||
type: JVxeTypes.select,
|
||||
options: [],
|
||||
dictCode: 'task_priority',
|
||||
width: '200px',
|
||||
placeholder: '请输入${title}',
|
||||
defaultValue: '',
|
||||
},
|
||||
{
|
||||
title: '完成状态',
|
||||
key: 'taskStatus',
|
||||
type: JVxeTypes.inputNumber,
|
||||
width: '200px',
|
||||
placeholder: '请输入${title}',
|
||||
defaultValue: '',
|
||||
},
|
||||
{
|
||||
title: '负责人ID',
|
||||
key: 'assigneeId',
|
||||
type: JVxeTypes.input,
|
||||
width: '200px',
|
||||
placeholder: '请输入${title}',
|
||||
defaultValue: '',
|
||||
},
|
||||
{
|
||||
title: '关注人ID',
|
||||
key: 'followersId',
|
||||
type: JVxeTypes.input,
|
||||
width: '200px',
|
||||
placeholder: '请输入${title}',
|
||||
defaultValue: '',
|
||||
},
|
||||
{
|
||||
title: '分配人ID',
|
||||
key: 'assignId',
|
||||
type: JVxeTypes.input,
|
||||
width: '200px',
|
||||
placeholder: '请输入${title}',
|
||||
defaultValue: '',
|
||||
},
|
||||
{
|
||||
title: '开始时间',
|
||||
key: 'startTime',
|
||||
type: JVxeTypes.datetime,
|
||||
width: '200px',
|
||||
placeholder: '请输入${title}',
|
||||
defaultValue: '',
|
||||
},
|
||||
{
|
||||
title: '结束时间',
|
||||
key: 'endTime',
|
||||
type: JVxeTypes.datetime,
|
||||
width: '200px',
|
||||
placeholder: '请输入${title}',
|
||||
defaultValue: '',
|
||||
},
|
||||
{
|
||||
title: '完成时间',
|
||||
key: 'completeTime',
|
||||
type: JVxeTypes.datetime,
|
||||
width: '200px',
|
||||
placeholder: '请输入${title}',
|
||||
defaultValue: '',
|
||||
},
|
||||
{
|
||||
title: '类型',
|
||||
key: 'type',
|
||||
type: JVxeTypes.input,
|
||||
width: '200px',
|
||||
placeholder: '请输入${title}',
|
||||
defaultValue: '',
|
||||
},
|
||||
{
|
||||
title: '子任务数',
|
||||
key: 'subTaskCount',
|
||||
type: JVxeTypes.inputNumber,
|
||||
width: '200px',
|
||||
placeholder: '请输入${title}',
|
||||
defaultValue: '',
|
||||
},
|
||||
{
|
||||
title: '子任务完成数',
|
||||
key: 'completedSubTaskCount',
|
||||
type: JVxeTypes.inputNumber,
|
||||
width: '200px',
|
||||
placeholder: '请输入${title}',
|
||||
defaultValue: '',
|
||||
},
|
||||
];
|
||||
|
||||
export const superQuerySchema = {
|
||||
tasklistName: { title: '任务清单名称', order: 0, view: 'text', type: 'string' },
|
||||
pid: { title: '父级节点', order: 1, view: 'text', type: 'string' },
|
||||
hasChild: { title: '是否有子节点', order: 2, view: 'text', type: 'string' },
|
||||
sortOrder: { title: '排序号', order: 3, view: 'number', type: 'number' },
|
||||
type: { title: '类型', order: 4, view: 'text', type: 'string' },
|
||||
taskListDetial: {
|
||||
title: '工作任务清单详情',
|
||||
view: 'table',
|
||||
fields: {
|
||||
mainId: { title: '外键', order: 0, view: 'text', type: 'string' },
|
||||
pid: { title: '父节点ID', order: 1, view: 'text', type: 'string' },
|
||||
hasChild: { title: '是否有子节点', order: 2, view: 'text', type: 'string' },
|
||||
sortOrder: { title: '排序号', order: 3, view: 'number', type: 'number' },
|
||||
taskName: { title: '任务名称', order: 4, view: 'text', type: 'string' },
|
||||
taskDesc: { title: '任务描述', order: 5, view: 'text', type: 'string' },
|
||||
priority: { title: '优先级', order: 6, view: 'list', type: 'string', dictCode: 'task_priority' },
|
||||
taskStatus: { title: '完成状态', order: 7, view: 'number', type: 'number' },
|
||||
assigneeId: { title: '负责人ID', order: 8, view: 'text', type: 'string' },
|
||||
followersId: { title: '关注人ID', order: 9, view: 'text', type: 'string' },
|
||||
assignId: { title: '分配人ID', order: 10, view: 'text', type: 'string' },
|
||||
startTime: { title: '开始时间', order: 11, view: 'datetime', type: 'string' },
|
||||
endTime: { title: '结束时间', order: 12, view: 'datetime', type: 'string' },
|
||||
completeTime: { title: '完成时间', order: 13, view: 'datetime', type: 'string' },
|
||||
type: { title: '类型', order: 14, view: 'text', type: 'string' },
|
||||
subTaskCount: { title: '子任务数', order: 15, view: 'number', type: 'number' },
|
||||
completedSubTaskCount: { title: '子任务完成数', order: 16, view: 'number', type: 'number' },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export function getBpmFormSchema(_formData): FormSchema[] {
|
||||
return formSchema;
|
||||
}
|
||||
|
||||
export const createTaskFormSchema: FormSchema[] = [
|
||||
{
|
||||
label: '任务标题',
|
||||
field: 'taskName',
|
||||
component: 'Input',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
label: '任务描述',
|
||||
field: 'taskDesc',
|
||||
component: 'InputTextArea',
|
||||
componentProps: { rows: 3 },
|
||||
},
|
||||
{
|
||||
label: '优先级',
|
||||
field: 'priority',
|
||||
component: 'JDictSelectTag',
|
||||
componentProps: {
|
||||
dictCode: 'task_priority',
|
||||
placeholder: '请选择优先级',
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '负责人',
|
||||
field: 'assigneeId',
|
||||
component: 'JSelectUser',
|
||||
componentProps: {
|
||||
labelKey: 'realname',
|
||||
rowKey: 'id',
|
||||
isRadioSelection: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '分配人',
|
||||
field: 'assignId',
|
||||
component: 'JSelectUser',
|
||||
componentProps: {
|
||||
labelKey: 'realname',
|
||||
rowKey: 'id',
|
||||
isRadioSelection: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '开始日期',
|
||||
field: 'startTime',
|
||||
component: 'DatePicker',
|
||||
componentProps: {
|
||||
style: { width: '100%' },
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '截止日期',
|
||||
field: 'endTime',
|
||||
component: 'DatePicker',
|
||||
componentProps: {
|
||||
style: { width: '100%' },
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '',
|
||||
field: 'groupId',
|
||||
component: 'Input',
|
||||
show: false,
|
||||
},
|
||||
];
|
||||
|
||||
export const createListFormSchema: FormSchema[] = [
|
||||
{
|
||||
label: '清单名称',
|
||||
field: 'tasklistName',
|
||||
component: 'Input',
|
||||
required: true,
|
||||
},
|
||||
];
|
||||
|
||||
export const mockTaskListGroups: TaskListGroup[] = [
|
||||
{
|
||||
id: 'tlg-1',
|
||||
name: '任务清单',
|
||||
pid: '0',
|
||||
hasChild: '1',
|
||||
type: 0,
|
||||
sortOrder: 1,
|
||||
delFlag: 0,
|
||||
taskLists: [
|
||||
{
|
||||
id: 'tl-1',
|
||||
name: '测试',
|
||||
pid: 'tlg-1',
|
||||
hasChild: '0',
|
||||
type: 1,
|
||||
sortOrder: 1,
|
||||
delFlag: 0,
|
||||
groups: [
|
||||
{
|
||||
id: 'g-1',
|
||||
name: '默认分组',
|
||||
taskListId: 'tl-1',
|
||||
collapsed: false,
|
||||
tasks: [
|
||||
{
|
||||
id: 't-1',
|
||||
taskName: '导入数据、说明书编写、bug调试',
|
||||
taskDesc: '',
|
||||
completed: true,
|
||||
priority: 'high',
|
||||
assigneeId: 'u1',
|
||||
assigneeName: '惠灵佳',
|
||||
assignId: 'u2',
|
||||
assignName: '管理员',
|
||||
followersId: '',
|
||||
followersName: '',
|
||||
createBy: '惠灵佳',
|
||||
startTime: '2025-12-15',
|
||||
endTime: '2025-12-19',
|
||||
completeTime: '2025-12-18',
|
||||
createTime: '2025-12-15',
|
||||
updateTime: '2025-12-18',
|
||||
groupId: 'g-1',
|
||||
groupName: '默认分组',
|
||||
childCount: 2,
|
||||
completedChildCount: 1,
|
||||
pid: '',
|
||||
hasChild: '1',
|
||||
},
|
||||
{
|
||||
id: 't-1-1',
|
||||
taskName: '数据导入脚本',
|
||||
taskDesc: '编写Python导入脚本',
|
||||
completed: true,
|
||||
priority: 'high',
|
||||
assigneeId: 'u1',
|
||||
assigneeName: '惠灵佳',
|
||||
assignId: 'u2',
|
||||
assignName: '管理员',
|
||||
followersId: '',
|
||||
followersName: '',
|
||||
createBy: '惠灵佳',
|
||||
startTime: '2025-12-15',
|
||||
endTime: '2025-12-16',
|
||||
completeTime: '2025-12-16',
|
||||
createTime: '2025-12-15',
|
||||
updateTime: '2025-12-16',
|
||||
groupId: 'g-1',
|
||||
groupName: '默认分组',
|
||||
childCount: 0,
|
||||
completedChildCount: 0,
|
||||
pid: 't-1',
|
||||
hasChild: '0',
|
||||
},
|
||||
{
|
||||
id: 't-1-2',
|
||||
taskName: '说明书编写',
|
||||
taskDesc: '',
|
||||
completed: false,
|
||||
priority: 'medium',
|
||||
assigneeId: 'u1',
|
||||
assigneeName: '惠灵佳',
|
||||
assignId: 'u2',
|
||||
assignName: '管理员',
|
||||
followersId: '',
|
||||
followersName: '',
|
||||
createBy: '惠灵佳',
|
||||
startTime: '2025-12-17',
|
||||
endTime: '2025-12-19',
|
||||
completeTime: '',
|
||||
createTime: '2025-12-15',
|
||||
updateTime: '2025-12-17',
|
||||
groupId: 'g-1',
|
||||
groupName: '默认分组',
|
||||
childCount: 0,
|
||||
completedChildCount: 0,
|
||||
pid: 't-1',
|
||||
hasChild: '0',
|
||||
},
|
||||
{
|
||||
id: 't-2',
|
||||
taskName: '完成新能体开发与调试',
|
||||
taskDesc: '',
|
||||
completed: true,
|
||||
priority: 'medium',
|
||||
assigneeId: 'u1',
|
||||
assigneeName: '惠灵佳,张三,李四,王五,赵六,钱七',
|
||||
assignId: 'u2',
|
||||
assignName: '管理员',
|
||||
followersId: '',
|
||||
followersName: '',
|
||||
createBy: '惠灵佳',
|
||||
startTime: '2025-12-11',
|
||||
endTime: '2025-12-12',
|
||||
completeTime: '2025-12-12',
|
||||
createTime: '2025-12-09',
|
||||
updateTime: '2025-12-12',
|
||||
groupId: 'g-1',
|
||||
groupName: '默认分组',
|
||||
childCount: 0,
|
||||
completedChildCount: 0,
|
||||
pid: '',
|
||||
hasChild: '0',
|
||||
},
|
||||
{
|
||||
id: 't-3',
|
||||
taskName: '完成其他数据的导入与知识库构建',
|
||||
taskDesc: '',
|
||||
completed: false,
|
||||
priority: 'high',
|
||||
assigneeId: 'u1',
|
||||
assigneeName: '惠灵佳,张三,李四,王五,赵六,钱七,孙八,周九,吴十,郑十一',
|
||||
assignId: 'u2',
|
||||
assignName: '管理员',
|
||||
followersId: '',
|
||||
followersName: '',
|
||||
createBy: '惠灵佳',
|
||||
startTime: '2025-12-10',
|
||||
endTime: '2025-12-20',
|
||||
completeTime: '',
|
||||
createTime: '2025-12-09',
|
||||
updateTime: '2025-12-10',
|
||||
groupId: 'g-1',
|
||||
groupName: '默认分组',
|
||||
childCount: 0,
|
||||
completedChildCount: 0,
|
||||
pid: '',
|
||||
hasChild: '0',
|
||||
},
|
||||
{
|
||||
id: 't-4',
|
||||
taskName: '简历筛选脚本编写',
|
||||
taskDesc: '',
|
||||
completed: false,
|
||||
priority: 'low',
|
||||
assigneeId: 'u3',
|
||||
assigneeName: '张三,惠灵佳',
|
||||
assignId: 'u2',
|
||||
assignName: '管理员',
|
||||
followersId: 'u1',
|
||||
followersName: '惠灵佳',
|
||||
createBy: '张三',
|
||||
startTime: '2025-11-27',
|
||||
endTime: '2025-12-27',
|
||||
completeTime: '',
|
||||
createTime: '2025-11-27',
|
||||
updateTime: '2025-11-27',
|
||||
groupId: 'g-1',
|
||||
groupName: '默认分组',
|
||||
childCount: 0,
|
||||
completedChildCount: 0,
|
||||
pid: '',
|
||||
hasChild: '0',
|
||||
},
|
||||
{
|
||||
id: 't-5',
|
||||
taskName: '使用HiAgent开发AI应用',
|
||||
taskDesc: '保罗办需求',
|
||||
completed: false,
|
||||
priority: 'high',
|
||||
assigneeId: 'u1',
|
||||
assigneeName: '惠灵佳,张三,李四,王五',
|
||||
assignId: 'u2',
|
||||
assignName: '管理员',
|
||||
followersId: '',
|
||||
followersName: '',
|
||||
createBy: '惠灵佳',
|
||||
startTime: '2025-10-20',
|
||||
endTime: '2025-12-31',
|
||||
completeTime: '',
|
||||
createTime: '2025-10-03',
|
||||
updateTime: '2025-10-20',
|
||||
groupId: 'g-1',
|
||||
groupName: '默认分组',
|
||||
childCount: 0,
|
||||
completedChildCount: 0,
|
||||
pid: '',
|
||||
hasChild: '0',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'g-2',
|
||||
name: '高优先级',
|
||||
taskListId: 'tl-1',
|
||||
collapsed: false,
|
||||
tasks: [
|
||||
{
|
||||
id: 't-6',
|
||||
taskName: '考研关系梳理配置',
|
||||
taskDesc: '',
|
||||
completed: false,
|
||||
priority: 'high',
|
||||
assigneeId: 'u1',
|
||||
assigneeName: '惠灵佳,李四,王五,赵六,钱七,孙八',
|
||||
assignId: 'u2',
|
||||
assignName: '管理员',
|
||||
followersId: '',
|
||||
followersName: '',
|
||||
createBy: '惠灵佳',
|
||||
startTime: '2025-12-11',
|
||||
endTime: '2025-12-24',
|
||||
completeTime: '',
|
||||
createTime: '2025-11-27',
|
||||
updateTime: '2025-12-11',
|
||||
groupId: 'g-2',
|
||||
groupName: '高优先级',
|
||||
childCount: 0,
|
||||
completedChildCount: 0,
|
||||
pid: '',
|
||||
hasChild: '0',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'tl-2',
|
||||
name: '2025年所有工作登记',
|
||||
pid: 'tlg-1',
|
||||
hasChild: '0',
|
||||
type: 1,
|
||||
sortOrder: 2,
|
||||
delFlag: 0,
|
||||
groups: [
|
||||
{
|
||||
id: 'g-tl2-1',
|
||||
name: '默认分组',
|
||||
taskListId: 'tl-2',
|
||||
collapsed: false,
|
||||
tasks: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'tl-3',
|
||||
name: 'AI应用开发与实施',
|
||||
pid: 'tlg-1',
|
||||
hasChild: '0',
|
||||
type: 1,
|
||||
sortOrder: 3,
|
||||
delFlag: 0,
|
||||
groups: [
|
||||
{
|
||||
id: 'g-tl3-1',
|
||||
name: '默认分组',
|
||||
taskListId: 'tl-3',
|
||||
collapsed: false,
|
||||
tasks: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,155 @@
|
||||
<template>
|
||||
<div class="avatar-display" :class="{ 'avatar-display-completed': completed }">
|
||||
<template v-if="names.length === 0">
|
||||
<span class="avatar-empty">-</span>
|
||||
</template>
|
||||
<template v-else-if="names.length === 1">
|
||||
<div class="avatar-single-wrap">
|
||||
<div class="avatar-circle" :style="{ background: getColor(names[0]), width: size + 'px', height: size + 'px' }">
|
||||
<span class="avatar-text" :style="{ fontSize: fontSize + 'px' }">{{ names[0].charAt(0) }}</span>
|
||||
</div>
|
||||
<span v-if="showName" class="avatar-name">{{ names[0] }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="names.length <= maxShow">
|
||||
<div
|
||||
v-for="(name, idx) in names"
|
||||
:key="idx"
|
||||
class="avatar-circle"
|
||||
:style="{ background: getColor(name), width: size + 'px', height: size + 'px', marginLeft: idx > 0 ? overlap + 'px' : '0', zIndex: idx + 1 }"
|
||||
>
|
||||
<span class="avatar-text" :style="{ fontSize: fontSize + 'px' }">{{ name.charAt(0) }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div
|
||||
v-for="(name, idx) in names.slice(0, maxShow - 1)"
|
||||
:key="idx"
|
||||
class="avatar-circle"
|
||||
:style="{ background: getColor(name), width: size + 'px', height: size + 'px', marginLeft: idx > 0 ? overlap + 'px' : '0', zIndex: idx + 1 }"
|
||||
>
|
||||
<span class="avatar-text" :style="{ fontSize: fontSize + 'px' }">{{ name.charAt(0) }}</span>
|
||||
</div>
|
||||
<div
|
||||
class="avatar-overflow"
|
||||
:style="{ width: size + 'px', height: size + 'px', marginLeft: overlap + 'px', fontSize: overflowFontSize + 'px', zIndex: names.length }"
|
||||
>
|
||||
+{{ names.length - (maxShow - 1) }}
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import { getAvatarColor } from '../types';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
names: string;
|
||||
size?: number;
|
||||
maxShow?: number;
|
||||
overlap?: number;
|
||||
showName?: boolean;
|
||||
completed?: boolean;
|
||||
}>(),
|
||||
{
|
||||
size: 24,
|
||||
maxShow: 5,
|
||||
overlap: -8,
|
||||
showName: true,
|
||||
completed: false,
|
||||
},
|
||||
);
|
||||
|
||||
const fontSize = computed(() => Math.max(10, Math.round(props.size * 0.54)));
|
||||
const overflowFontSize = computed(() => Math.max(8, Math.round(props.size * 0.42)));
|
||||
|
||||
const parsedNames = computed(() => {
|
||||
if (!props.names) return [];
|
||||
return props.names
|
||||
.split(/[,,、]/)
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean);
|
||||
});
|
||||
|
||||
const names = computed(() => parsedNames.value);
|
||||
|
||||
function getColor(name: string): string {
|
||||
return getAvatarColor(name);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.avatar-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.avatar-display-completed {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.avatar-single-wrap {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 2px 8px 2px 2px;
|
||||
background: #f5f6f7;
|
||||
border-radius: 14px;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.avatar-circle {
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
box-shadow: 0 0 0 2px #fff;
|
||||
}
|
||||
|
||||
.avatar-text {
|
||||
color: #fff;
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
font-size: 14px !important;
|
||||
}
|
||||
|
||||
.avatar-name {
|
||||
color: #646a73;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.avatar-count {
|
||||
font-size: 14px;
|
||||
color: #646a73;
|
||||
margin-left: 4px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.avatar-overflow {
|
||||
border-radius: 50%;
|
||||
background: #f0f0f0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #8c8c8c;
|
||||
font-weight: 500;
|
||||
position: relative;
|
||||
box-shadow: 0 0 0 2px #fff;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.avatar-empty {
|
||||
color: #c0c4cc;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,430 @@
|
||||
<template>
|
||||
<a-modal
|
||||
v-model:open="visible"
|
||||
title="协作人管理"
|
||||
:footer="null"
|
||||
width="520px"
|
||||
:bodyStyle="{ padding: '0' }"
|
||||
:centered="true"
|
||||
:destroyOnClose="true"
|
||||
@cancel="onClose"
|
||||
>
|
||||
<div class="collab-modal">
|
||||
<div class="collab-body">
|
||||
<div class="member-list">
|
||||
<div v-for="item in collaborators" :key="item.permissionId" class="member-item" @mouseenter="hoverId = item.permissionId" @mouseleave="hoverId = ''">
|
||||
<div class="member-avatar" :style="{ background: getColor(item.username) }">
|
||||
{{ (item.username || '?').charAt(0) }}
|
||||
</div>
|
||||
<div class="member-main">
|
||||
<div class="member-name-row">
|
||||
<span class="member-name">{{ item.username }}</span>
|
||||
<span v-if="item.permission === '1'" class="role-badge is-owner">所有者</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="member-right">
|
||||
<template v-if="item.permission === '1'">
|
||||
<span class="role-label">所有者</span>
|
||||
</template>
|
||||
<template v-else-if="isOwner">
|
||||
<a-dropdown :trigger="['click']" :overlayStyle="{ minWidth: '120px' }">
|
||||
<span class="role-label clickable" @click.stop>
|
||||
{{ item.permission === '2' ? '可编辑' : '可阅读' }}
|
||||
<Icon icon="ant-design:down-outlined" style="font-size: 10px; margin-left: 2px" />
|
||||
</span>
|
||||
<template #overlay>
|
||||
<a-menu @click="({ key }) => onPermissionChange(item.permissionId, key)">
|
||||
<a-menu-item key="2" :class="{ 'active-key': item.permission === '2' }">
|
||||
<span>可编辑</span>
|
||||
<Icon v-if="item.permission === '2'" icon="ant-design:check-outlined" class="check-icon" />
|
||||
</a-menu-item>
|
||||
<a-menu-item key="3" :class="{ 'active-key': item.permission === '3' }">
|
||||
<span>可阅读</span>
|
||||
<Icon v-if="item.permission === '3'" icon="ant-design:check-outlined" class="check-icon" />
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
<span v-show="hoverId === item.permissionId" class="remove-icon" @click="onRemove(item.permissionId)">
|
||||
<Icon icon="ant-design:close-outlined" />
|
||||
</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="role-label">{{ item.permission === '2' ? '可编辑' : '可阅读' }}</span>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="collaborators.length === 0" class="empty-state">
|
||||
<Icon icon="ant-design:team-outlined" style="font-size: 36px; color: #d9d9d9; margin-bottom: 8px" />
|
||||
<span>暂无协作人</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="showAddPanel && isOwner" class="add-panel">
|
||||
<div class="add-panel-header">
|
||||
<span>添加协作人</span>
|
||||
<span class="add-panel-close" @click="showAddPanel = false">
|
||||
<Icon icon="ant-design:close-outlined" />
|
||||
</span>
|
||||
</div>
|
||||
<div class="add-panel-body">
|
||||
<JSelectUser
|
||||
v-model:value="addForm.userIds"
|
||||
:label-key="'realname'"
|
||||
:row-key="'id'"
|
||||
placeholder="搜索并选择用户(支持多选)"
|
||||
/>
|
||||
<div class="add-panel-row">
|
||||
<span class="add-panel-label">权限</span>
|
||||
<a-radio-group v-model:value="addForm.permission" size="small">
|
||||
<a-radio-button value="2">可编辑</a-radio-button>
|
||||
<a-radio-button value="3">可阅读</a-radio-button>
|
||||
</a-radio-group>
|
||||
</div>
|
||||
</div>
|
||||
<div class="add-panel-footer">
|
||||
<a-button size="small" @click="showAddPanel = false">取消</a-button>
|
||||
<a-button size="small" type="primary" :disabled="!addForm.userIds" @click="onBatchAdd">
|
||||
确认添加
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="isOwner && !showAddPanel" class="collab-footer">
|
||||
<a-button type="text" size="small" @click="showAddPanel = true">
|
||||
<Icon icon="ant-design:user-add-outlined" style="margin-right: 4px" />
|
||||
添加协作人
|
||||
</a-button>
|
||||
<a-button type="primary" size="small" @click="onClose">完成</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive } from 'vue';
|
||||
import Icon from '/@/components/Icon/index';
|
||||
import JSelectUser from '/@/components/Form/src/jeecg/components/JSelectUser.vue';
|
||||
import { getCollaborators, addCollaborator, removeCollaborator, updateCollaboratorPermission } from '../TaskList.api';
|
||||
import { getAvatarColor } from '../types';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
|
||||
const { createMessage } = useMessage();
|
||||
|
||||
interface CollaboratorItem {
|
||||
permissionId: string;
|
||||
userId: string;
|
||||
username: string;
|
||||
permission: string;
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
taskListId: string;
|
||||
isOwner: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
changed: [];
|
||||
}>();
|
||||
|
||||
const visible = ref(false);
|
||||
const collaborators = ref<CollaboratorItem[]>([]);
|
||||
const hoverId = ref('');
|
||||
const showAddPanel = ref(false);
|
||||
const addForm = reactive({
|
||||
userIds: '',
|
||||
permission: '2',
|
||||
});
|
||||
|
||||
function getColor(name: string): string {
|
||||
return getAvatarColor(name || '');
|
||||
}
|
||||
|
||||
async function open() {
|
||||
visible.value = true;
|
||||
showAddPanel.value = false;
|
||||
await loadCollaborators();
|
||||
}
|
||||
|
||||
function onClose() {
|
||||
visible.value = false;
|
||||
showAddPanel.value = false;
|
||||
addForm.userIds = '';
|
||||
addForm.permission = '2';
|
||||
}
|
||||
|
||||
async function loadCollaborators() {
|
||||
try {
|
||||
const data: any[] = await getCollaborators({ taskListId: props.taskListId });
|
||||
collaborators.value = data;
|
||||
} catch (e) {
|
||||
console.error('[CollaboratorModal] load failed', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function onBatchAdd() {
|
||||
if (!addForm.userIds) return;
|
||||
const ids = addForm.userIds.split(',').filter(Boolean);
|
||||
if (ids.length === 0) return;
|
||||
|
||||
let successCount = 0;
|
||||
let failCount = 0;
|
||||
for (const uid of ids) {
|
||||
try {
|
||||
await addCollaborator({
|
||||
taskListId: props.taskListId,
|
||||
userId: uid,
|
||||
permission: addForm.permission,
|
||||
});
|
||||
successCount++;
|
||||
} catch {
|
||||
failCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (successCount > 0) {
|
||||
createMessage.success(`成功添加 ${successCount} 位协作人${failCount > 0 ? `,${failCount} 位已存在` : ''}`);
|
||||
} else {
|
||||
createMessage.warning('所选用户均已是协作人');
|
||||
}
|
||||
|
||||
addForm.userIds = '';
|
||||
addForm.permission = '2';
|
||||
showAddPanel.value = false;
|
||||
await loadCollaborators();
|
||||
emit('changed');
|
||||
}
|
||||
|
||||
async function onRemove(permissionId: string) {
|
||||
try {
|
||||
await removeCollaborator({ permissionId });
|
||||
await loadCollaborators();
|
||||
emit('changed');
|
||||
} catch (e: any) {
|
||||
createMessage.error(e?.message || '移除失败');
|
||||
}
|
||||
}
|
||||
|
||||
async function onPermissionChange(permissionId: string, newPermission: string) {
|
||||
try {
|
||||
await updateCollaboratorPermission({ permissionId, permission: newPermission });
|
||||
await loadCollaborators();
|
||||
emit('changed');
|
||||
} catch (e: any) {
|
||||
createMessage.error(e?.message || '修改失败');
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({ open });
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.collab-modal {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.collab-body {
|
||||
position: relative;
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.member-list {
|
||||
padding: 8px 0;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.member-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 8px 20px;
|
||||
transition: background 0.12s;
|
||||
|
||||
&:hover {
|
||||
background: #f5f6f7;
|
||||
}
|
||||
}
|
||||
|
||||
.member-avatar {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
color: #fff;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.member-main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.member-name-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.member-name {
|
||||
font-size: 14px;
|
||||
color: #1f2329;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.role-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0 6px;
|
||||
font-size: 11px;
|
||||
line-height: 18px;
|
||||
border-radius: 3px;
|
||||
flex-shrink: 0;
|
||||
|
||||
&.is-owner {
|
||||
background: #e8f3ff;
|
||||
color: #3370ff;
|
||||
}
|
||||
}
|
||||
|
||||
.member-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
flex-shrink: 0;
|
||||
min-width: 70px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.role-label {
|
||||
font-size: 13px;
|
||||
color: #8f959e;
|
||||
|
||||
&.clickable {
|
||||
cursor: pointer;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
transition: background 0.12s;
|
||||
|
||||
&:hover {
|
||||
background: #e8e8e8;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.remove-icon {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
color: #8f959e;
|
||||
font-size: 12px;
|
||||
transition: all 0.12s;
|
||||
|
||||
&:hover {
|
||||
background: #f0f0f0;
|
||||
color: #f5222d;
|
||||
}
|
||||
}
|
||||
|
||||
.check-icon {
|
||||
margin-left: auto;
|
||||
color: #3370ff;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.active-key {
|
||||
color: #3370ff;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40px 0;
|
||||
color: #bfbfbf;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.add-panel {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: #fff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.add-panel-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 20px 12px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #1f2329;
|
||||
}
|
||||
|
||||
.add-panel-close {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
color: #8f959e;
|
||||
|
||||
&:hover {
|
||||
background: #f0f0f0;
|
||||
}
|
||||
}
|
||||
|
||||
.add-panel-body {
|
||||
padding: 0 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.add-panel-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.add-panel-label {
|
||||
font-size: 13px;
|
||||
color: #595959;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.add-panel-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
padding: 16px 20px;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.collab-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10px 20px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,43 @@
|
||||
<template>
|
||||
<BasicModal v-bind="$attrs" @register="registerModal" destroy-on-close title="创建清单" :width="500" @ok="handleSubmit">
|
||||
<BasicForm @register="registerForm" />
|
||||
</BasicModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { BasicModal, useModalInner } from '/@/components/Modal';
|
||||
import { BasicForm, useForm } from '/@/components/Form/index';
|
||||
import { createListFormSchema } from '../TaskList.data';
|
||||
import { addTaskList } from '../TaskList.api';
|
||||
|
||||
const emit = defineEmits(['register', 'success']);
|
||||
|
||||
const [registerForm, { resetFields, validate }] = useForm({
|
||||
labelWidth: 100,
|
||||
schemas: createListFormSchema,
|
||||
showActionButtonGroup: false,
|
||||
baseColProps: { span: 24 },
|
||||
});
|
||||
|
||||
let pendingGroupId = '';
|
||||
|
||||
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
|
||||
await resetFields();
|
||||
pendingGroupId = data?.groupId || '';
|
||||
});
|
||||
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
setModalProps({ confirmLoading: true });
|
||||
const values = await validate();
|
||||
await addTaskList({
|
||||
tasklistName: values.tasklistName || values.name,
|
||||
pid: pendingGroupId || undefined,
|
||||
});
|
||||
closeModal();
|
||||
emit('success');
|
||||
} finally {
|
||||
setModalProps({ confirmLoading: false });
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,55 @@
|
||||
<template>
|
||||
<BasicModal v-bind="$attrs" @register="registerModal" destroy-on-close title="创建任务" :width="600" @ok="handleSubmit">
|
||||
<BasicForm @register="registerForm" />
|
||||
</BasicModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { BasicModal, useModalInner } from '/@/components/Modal';
|
||||
import { BasicForm, useForm } from '/@/components/Form/index';
|
||||
import { createTaskFormSchema } from '../TaskList.data';
|
||||
import { addTask } from '../TaskList.api';
|
||||
|
||||
const emit = defineEmits(['register', 'success']);
|
||||
const groupId = ref('');
|
||||
const mainId = ref('');
|
||||
|
||||
const [registerForm, { setFieldsValue, resetFields, validate }] = useForm({
|
||||
labelWidth: 100,
|
||||
schemas: createTaskFormSchema,
|
||||
showActionButtonGroup: false,
|
||||
baseColProps: { span: 24 },
|
||||
});
|
||||
|
||||
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
|
||||
await resetFields();
|
||||
groupId.value = data?.groupId || '';
|
||||
mainId.value = data?.mainId || '';
|
||||
if (groupId.value) {
|
||||
await setFieldsValue({ groupId: groupId.value });
|
||||
}
|
||||
});
|
||||
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
setModalProps({ confirmLoading: true });
|
||||
const values = await validate();
|
||||
await addTask({
|
||||
mainId: mainId.value,
|
||||
taskName: values.taskName,
|
||||
taskDesc: values.taskDesc || '',
|
||||
priority: values.priority || '',
|
||||
type: '1',
|
||||
pid: groupId.value || '',
|
||||
assigneeId: values.assigneeId || '',
|
||||
startTime: values.startTime || null,
|
||||
endTime: values.endTime || null,
|
||||
});
|
||||
closeModal();
|
||||
emit('success');
|
||||
} finally {
|
||||
setModalProps({ confirmLoading: false });
|
||||
}
|
||||
}
|
||||
</script>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,123 @@
|
||||
<template>
|
||||
<a-date-picker
|
||||
v-if="mode === 'start' || mode === 'range'"
|
||||
v-model:value="startDayjs"
|
||||
placeholder="选择开始时间"
|
||||
format="YYYY-MM-DD"
|
||||
:allow-clear="true"
|
||||
:open="startOpen"
|
||||
@change="onStartChange"
|
||||
@open-change="onStartOpenChange"
|
||||
/>
|
||||
<a-date-picker
|
||||
v-if="mode === 'end' || mode === 'range'"
|
||||
v-model:value="endDayjs"
|
||||
placeholder="选择结束时间"
|
||||
format="YYYY-MM-DD"
|
||||
:allow-clear="true"
|
||||
:open="endOpen"
|
||||
@change="onEndChange"
|
||||
@open-change="onEndOpenChange"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch, nextTick } from 'vue';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
type DateRangeValue = [string | null, string | null];
|
||||
type PickerMode = 'start' | 'end' | 'range';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
startValue?: string | null;
|
||||
endValue?: string | null;
|
||||
visible?: boolean;
|
||||
triggerRef?: HTMLElement | null;
|
||||
mode?: PickerMode;
|
||||
}>(),
|
||||
{
|
||||
startValue: null,
|
||||
endValue: null,
|
||||
visible: false,
|
||||
triggerRef: null,
|
||||
mode: 'range',
|
||||
}
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
change: [value: DateRangeValue];
|
||||
'update:visible': [value: boolean];
|
||||
}>();
|
||||
|
||||
const startDayjs = ref<any>(null);
|
||||
const endDayjs = ref<any>(null);
|
||||
const startOpen = ref(false);
|
||||
const endOpen = ref(false);
|
||||
|
||||
watch(
|
||||
() => props.startValue,
|
||||
(val) => {
|
||||
startDayjs.value = val ? dayjs(val) : null;
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.endValue,
|
||||
(val) => {
|
||||
endDayjs.value = val ? dayjs(val) : null;
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
async (visible) => {
|
||||
if (visible) {
|
||||
await nextTick();
|
||||
if (props.mode === 'start' || props.mode === 'range') {
|
||||
startOpen.value = true;
|
||||
}
|
||||
if (props.mode === 'end') {
|
||||
endOpen.value = true;
|
||||
}
|
||||
} else {
|
||||
startOpen.value = false;
|
||||
endOpen.value = false;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
function onStartOpenChange(open: boolean) {
|
||||
startOpen.value = open;
|
||||
if (!open) {
|
||||
emit('update:visible', false);
|
||||
}
|
||||
}
|
||||
|
||||
function onEndOpenChange(open: boolean) {
|
||||
endOpen.value = open;
|
||||
if (!open) {
|
||||
emit('update:visible', false);
|
||||
}
|
||||
}
|
||||
|
||||
function onStartChange(date: any) {
|
||||
const value = date ? date.format('YYYY-MM-DD') : null;
|
||||
emit('change', [value, props.endValue]);
|
||||
if (props.mode === 'start') {
|
||||
startOpen.value = false;
|
||||
emit('update:visible', false);
|
||||
}
|
||||
}
|
||||
|
||||
function onEndChange(date: any) {
|
||||
const value = date ? date.format('YYYY-MM-DD') : null;
|
||||
emit('change', [props.startValue, value]);
|
||||
if (props.mode === 'end') {
|
||||
endOpen.value = false;
|
||||
emit('update:visible', false);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,123 @@
|
||||
<template>
|
||||
<div>
|
||||
<BasicForm @register="registerForm" ref="formRef" />
|
||||
<!-- 子表单区域 -->
|
||||
<a-tabs v-model:activeKey="activeKey" animated @change="handleChangeTabs">
|
||||
<a-tab-pane tab="工作任务清单详情" key="taskListDetial" :forceRender="true">
|
||||
<JVxeTable
|
||||
keep-source
|
||||
resizable
|
||||
ref="taskListDetial"
|
||||
v-if="taskListDetialTable.show"
|
||||
:loading="taskListDetialTable.loading"
|
||||
:columns="taskListDetialTable.columns"
|
||||
:dataSource="taskListDetialTable.dataSource"
|
||||
:height="340"
|
||||
:rowNumber="true"
|
||||
:rowSelection="true"
|
||||
:disabled="formDisabled"
|
||||
:toolbar="true"
|
||||
/>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
|
||||
<div style="width: 100%; text-align: center" v-if="!formDisabled">
|
||||
<a-button @click="handleSubmit" pre-icon="ant-design:check" type="primary">提 交</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { BasicForm, useForm } from '/@/components/Form/index';
|
||||
import { computed, defineComponent, reactive, ref } from 'vue';
|
||||
import { defHttp } from '/@/utils/http/axios';
|
||||
import { propTypes } from '/@/utils/propTypes';
|
||||
import { useJvxeMethod } from '/@/hooks/system/useJvxeMethods';
|
||||
import { getBpmFormSchema, taskListDetialColumns } from '../TaskList.data';
|
||||
import { saveOrUpdate, taskListDetialList } from '../TaskList.api';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'TaskListForm',
|
||||
components: {
|
||||
BasicForm,
|
||||
},
|
||||
props: {
|
||||
formData: propTypes.object.def({}),
|
||||
formBpm: propTypes.bool.def(true),
|
||||
},
|
||||
setup(props) {
|
||||
const [registerForm, { setFieldsValue, setProps }] = useForm({
|
||||
labelWidth: 150,
|
||||
schemas: getBpmFormSchema(props.formData),
|
||||
showActionButtonGroup: false,
|
||||
baseColProps: { span: 24 },
|
||||
});
|
||||
|
||||
const formDisabled = computed(() => {
|
||||
if (props.formData.disabled === false) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
const refKeys = ref(['taskListDetial']);
|
||||
const activeKey = ref('taskListDetial');
|
||||
const taskListDetial = ref();
|
||||
const tableRefs = { taskListDetial };
|
||||
const taskListDetialTable = reactive({
|
||||
loading: false,
|
||||
dataSource: [],
|
||||
columns: taskListDetialColumns,
|
||||
show: false,
|
||||
});
|
||||
|
||||
const [handleChangeTabs, handleSubmit, requestSubTableData, formRef] = useJvxeMethod(
|
||||
requestAddOrEdit,
|
||||
classifyIntoFormData,
|
||||
tableRefs,
|
||||
activeKey,
|
||||
refKeys,
|
||||
validateSubForm
|
||||
);
|
||||
|
||||
function classifyIntoFormData(allValues) {
|
||||
let main = Object.assign({}, allValues.formValue);
|
||||
return {
|
||||
...main, // 展开
|
||||
taskListDetialList: allValues.tablesValue[0].tableData,
|
||||
};
|
||||
}
|
||||
|
||||
//表单提交事件
|
||||
async function requestAddOrEdit(values) {
|
||||
await saveOrUpdate(values, true);
|
||||
}
|
||||
|
||||
const queryByIdUrl = '/tasklist/taskList/queryById';
|
||||
async function initFormData() {
|
||||
let params = { id: props.formData.dataId };
|
||||
const data = await defHttp.get({ url: queryByIdUrl, params });
|
||||
//设置表单的值
|
||||
await setFieldsValue({ ...data });
|
||||
requestSubTableData(taskListDetialList, { id: data.id }, taskListDetialTable, () => {
|
||||
taskListDetialTable.show = true;
|
||||
});
|
||||
//默认是禁用
|
||||
await setProps({ disabled: formDisabled.value });
|
||||
}
|
||||
|
||||
initFormData();
|
||||
|
||||
return {
|
||||
registerForm,
|
||||
formDisabled,
|
||||
formRef,
|
||||
handleSubmit,
|
||||
activeKey,
|
||||
handleChangeTabs,
|
||||
taskListDetial,
|
||||
taskListDetialTable,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,119 @@
|
||||
<template>
|
||||
<BasicModal v-bind="$attrs" @register="registerModal" destroyOnClose :title="title" :width="800" @ok="handleSubmit">
|
||||
<BasicForm @register="registerForm" ref="formRef" name="TaskListForm" />
|
||||
<!-- 子表单区域 -->
|
||||
<a-tabs v-model:activeKey="activeKey" animated @change="handleChangeTabs">
|
||||
<a-tab-pane tab="工作任务清单详情" key="taskListDetial" :forceRender="true">
|
||||
<JVxeTable
|
||||
keep-source
|
||||
resizable
|
||||
ref="taskListDetial"
|
||||
:loading="taskListDetialTable.loading"
|
||||
:columns="taskListDetialTable.columns"
|
||||
:dataSource="taskListDetialTable.dataSource"
|
||||
:height="340"
|
||||
:rowNumber="true"
|
||||
:rowSelection="true"
|
||||
:disabled="formDisabled"
|
||||
:toolbar="true"
|
||||
/>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</BasicModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, unref, reactive } from 'vue';
|
||||
import { BasicModal, useModalInner } from '/@/components/Modal';
|
||||
import { BasicForm, useForm } from '/@/components/Form/index';
|
||||
import { JVxeTable } from '/@/components/jeecg/JVxeTable';
|
||||
import { useJvxeMethod } from '/@/hooks/system/useJvxeMethods.ts';
|
||||
import { formSchema, taskListDetialColumns } from '../TaskList.data';
|
||||
import { saveOrUpdate, taskListDetialList } from '../TaskList.api';
|
||||
// Emits声明
|
||||
const emit = defineEmits(['register', 'success']);
|
||||
const isUpdate = ref(true);
|
||||
const formDisabled = ref(false);
|
||||
const refKeys = ref(['taskListDetial']);
|
||||
const activeKey = ref('taskListDetial');
|
||||
const taskListDetial = ref();
|
||||
const tableRefs = { taskListDetial };
|
||||
const taskListDetialTable = reactive({
|
||||
loading: false,
|
||||
dataSource: [],
|
||||
columns: taskListDetialColumns,
|
||||
});
|
||||
//表单配置
|
||||
const [registerForm, { setProps, resetFields, setFieldsValue }] = useForm({
|
||||
labelWidth: 150,
|
||||
schemas: formSchema,
|
||||
showActionButtonGroup: false,
|
||||
baseColProps: { span: 24 },
|
||||
});
|
||||
//表单赋值
|
||||
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
|
||||
//重置表单
|
||||
await reset();
|
||||
setModalProps({ confirmLoading: false, showCancelBtn: data?.showFooter, showOkBtn: data?.showFooter });
|
||||
isUpdate.value = !!data?.isUpdate;
|
||||
formDisabled.value = !data?.showFooter;
|
||||
if (unref(isUpdate)) {
|
||||
//表单赋值
|
||||
await setFieldsValue({
|
||||
...data.record,
|
||||
});
|
||||
requestSubTableData(taskListDetialList, { id: data?.record?.id }, taskListDetialTable);
|
||||
}
|
||||
// 隐藏底部时禁用整个表单
|
||||
setProps({ disabled: !data?.showFooter });
|
||||
});
|
||||
//方法配置
|
||||
const [handleChangeTabs, handleSubmit, requestSubTableData, formRef] = useJvxeMethod(
|
||||
requestAddOrEdit,
|
||||
classifyIntoFormData,
|
||||
tableRefs,
|
||||
activeKey,
|
||||
refKeys
|
||||
);
|
||||
|
||||
//设置标题
|
||||
const title = computed(() => (!unref(isUpdate) ? '新增' : !unref(formDisabled) ? '编辑' : '详情'));
|
||||
|
||||
async function reset() {
|
||||
await resetFields();
|
||||
activeKey.value = 'taskListDetial';
|
||||
taskListDetialTable.dataSource = [];
|
||||
}
|
||||
function classifyIntoFormData(allValues) {
|
||||
let main = Object.assign({}, allValues.formValue);
|
||||
return {
|
||||
...main, // 展开
|
||||
taskListDetialList: allValues.tablesValue[0].tableData,
|
||||
};
|
||||
}
|
||||
//表单提交事件
|
||||
async function requestAddOrEdit(values) {
|
||||
try {
|
||||
setModalProps({ confirmLoading: true });
|
||||
//提交表单
|
||||
await saveOrUpdate(values, isUpdate.value);
|
||||
//关闭弹窗
|
||||
closeModal();
|
||||
//刷新列表
|
||||
emit('success');
|
||||
} finally {
|
||||
setModalProps({ confirmLoading: false });
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
/** 时间和数字输入框样式 */
|
||||
:deep(.ant-input-number) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:deep(.ant-calendar-picker) {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,794 @@
|
||||
<template>
|
||||
<div class="task-sidebar">
|
||||
<div class="sidebar-header">
|
||||
<span class="sidebar-title">
|
||||
<Icon icon="ant-design:menu-outlined" class="mr-1" />
|
||||
任务
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-nav">
|
||||
<div
|
||||
v-for="item in NAV_ITEMS"
|
||||
:key="item.id"
|
||||
class="sidebar-item"
|
||||
:class="{ active: currentView === item.id }"
|
||||
@click="emit('view-select', item.id)"
|
||||
>
|
||||
<Icon :icon="item.icon" class="sidebar-item-icon" />
|
||||
<span>{{ item.label }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-divider"></div>
|
||||
|
||||
<div class="sidebar-section">
|
||||
<div class="sidebar-quick-header" @click="quickAccessExpanded = !quickAccessExpanded">
|
||||
<Icon icon="tasklist-arrow-down|svg" class="quick-access-arrow" :class="{ expanded: quickAccessExpanded }" />
|
||||
<span class="sidebar-quick-title">快速访问</span>
|
||||
</div>
|
||||
<template v-if="quickAccessExpanded">
|
||||
<div
|
||||
v-for="item in QUICK_ACCESS_ITEMS"
|
||||
:key="item.id"
|
||||
class="sidebar-item sidebar-quick-access-sub"
|
||||
:class="{ active: currentView === item.id }"
|
||||
@click="onQuickAccessClick(item)"
|
||||
>
|
||||
<Icon :icon="item.icon" class="sidebar-item-icon" />
|
||||
<span>{{ item.label }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-divider"></div>
|
||||
|
||||
<div class="sidebar-section sidebar-lists-section">
|
||||
<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('')" />
|
||||
</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">
|
||||
<template #item="{ element: taskList }">
|
||||
<div
|
||||
v-if="renamingId !== taskList.id"
|
||||
class="sidebar-item"
|
||||
:class="{ active: currentListId === taskList.id }"
|
||||
:data-list-id="taskList.id"
|
||||
@click="emit('list-select', taskList.id)"
|
||||
@mouseenter="hoveredListId = taskList.id"
|
||||
@mouseleave="hoveredListId = ''"
|
||||
>
|
||||
<span class="list-drag-handle">
|
||||
<Icon icon="tasklist-list-drag-handle|svg" />
|
||||
</span>
|
||||
<Icon icon="ant-design:file-text-outlined" class="sidebar-item-icon" />
|
||||
<span class="sidebar-item-name">{{ taskList.name }}</span>
|
||||
<a-dropdown :trigger="['click']">
|
||||
<Icon v-show="hoveredListId === taskList.id" icon="ant-design:ellipsis-outlined" class="sidebar-item-more" @click.stop />
|
||||
<template #overlay>
|
||||
<a-menu @click="onListMenuClick($event, taskList.id, taskList.name)">
|
||||
<a-menu-item v-if="canEditList(taskList.id)" key="rename">
|
||||
<Icon icon="ant-design:edit-outlined" class="mr-1" />
|
||||
重命名
|
||||
</a-menu-item>
|
||||
<a-menu-item key="remove">
|
||||
<Icon icon="ant-design:minus-circle-outlined" class="mr-1" />
|
||||
移除
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</div>
|
||||
<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" />
|
||||
</div>
|
||||
</template>
|
||||
</draggable>
|
||||
</div>
|
||||
|
||||
<draggable :model-value="userGroups" 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
|
||||
v-if="group.delFlag !== 1 && renamingId !== group.id"
|
||||
class="sidebar-group-header"
|
||||
@mousedown="onGroupMouseDown($event, group.id)"
|
||||
@mousemove="onGroupMouseMove($event)"
|
||||
@mouseup="onGroupMouseUp($event, group.id)"
|
||||
>
|
||||
<Icon icon="tasklist-arrow-down|svg" class="group-collapse-arrow" :class="{ collapsed: collapsedGroups[group.id] || draggingGroupId === group.id }" />
|
||||
<span class="sidebar-group-name">{{ group.name }}</span>
|
||||
<a-dropdown :trigger="['click']" @click.stop>
|
||||
<Icon icon="ant-design:ellipsis-outlined" class="sidebar-group-more" @click.stop />
|
||||
<template #overlay>
|
||||
<a-menu @click="onGroupMenuClick($event, group.id, group.name)">
|
||||
<a-menu-item key="rename">
|
||||
<Icon icon="ant-design:edit-outlined" class="mr-1" />
|
||||
重命名
|
||||
</a-menu-item>
|
||||
<a-menu-item key="delete" danger>
|
||||
<Icon icon="ant-design:delete-outlined" class="mr-1" />
|
||||
删除
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
<Icon icon="ant-design:plus-outlined" class="sidebar-group-add" @click.stop="startNewList(group.id)" />
|
||||
</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" />
|
||||
</div>
|
||||
<template v-if="group.delFlag !== 1 && !collapsedGroups[group.id]">
|
||||
<div class="sidebar-group-items" :data-group-id="group.id">
|
||||
<div v-if="showNewListInput && newListGroupId === group.id" 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="getGroupTaskLists(group)"
|
||||
group="tasklists"
|
||||
item-key="id"
|
||||
handle=".list-drag-handle"
|
||||
:animation="200"
|
||||
@end="onDragEnd"
|
||||
>
|
||||
<template #item="{ element: taskList }">
|
||||
<div
|
||||
v-if="renamingId !== taskList.id"
|
||||
class="sidebar-item"
|
||||
:class="{ active: currentListId === taskList.id }"
|
||||
:data-list-id="taskList.id"
|
||||
@click="emit('list-select', taskList.id)"
|
||||
@mouseenter="hoveredListId = taskList.id"
|
||||
@mouseleave="hoveredListId = ''"
|
||||
>
|
||||
<span class="list-drag-handle">
|
||||
<Icon icon="tasklist-list-drag-handle|svg" />
|
||||
</span>
|
||||
<Icon icon="ant-design:file-text-outlined" class="sidebar-item-icon" />
|
||||
<span class="sidebar-item-name">{{ taskList.name }}</span>
|
||||
<a-dropdown :trigger="['click']">
|
||||
<Icon v-show="hoveredListId === taskList.id" icon="ant-design:ellipsis-outlined" class="sidebar-item-more" @click.stop />
|
||||
<template #overlay>
|
||||
<a-menu @click="onListMenuClick($event, taskList.id, taskList.name)">
|
||||
<a-menu-item v-if="canEditList(taskList.id)" key="rename">
|
||||
<Icon icon="ant-design:edit-outlined" class="mr-1" />
|
||||
重命名
|
||||
</a-menu-item>
|
||||
<a-menu-item key="remove">
|
||||
<Icon icon="ant-design:minus-circle-outlined" class="mr-1" />
|
||||
移除
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</div>
|
||||
<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" />
|
||||
</div>
|
||||
</template>
|
||||
</draggable>
|
||||
<div
|
||||
v-if="group.taskLists.filter((l) => l.delFlag !== 1).length === 0 && !(showNewListInput && newListGroupId === group.id)"
|
||||
class="sidebar-empty-hint"
|
||||
>
|
||||
可拖拽清单加入该分组
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</draggable>
|
||||
|
||||
<div v-if="showNewGroupInput" class="sidebar-new-group-row">
|
||||
<Icon icon="ant-design:plus-outlined" class="sidebar-new-icon" />
|
||||
<a-input
|
||||
v-model:value="newGroupName"
|
||||
size="small"
|
||||
placeholder="输入分组名称..."
|
||||
autofocus
|
||||
style="flex: 1"
|
||||
@press-enter="submitNewGroup"
|
||||
@blur="showNewGroupInput = false"
|
||||
/>
|
||||
</div>
|
||||
<div v-else class="sidebar-new-group-btn" @click="showNewGroupInput = true">
|
||||
<Icon icon="ant-design:plus-outlined" class="sidebar-new-icon" />
|
||||
<span>新建分组</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import Icon from '/@/components/Icon/index';
|
||||
import draggable from 'vuedraggable';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
import { NAV_ITEMS, QUICK_ACCESS_ITEMS, QUICK_ACCESS_TAB_MAP } from '../types';
|
||||
import type { TaskListGroup, ViewType } from '../types';
|
||||
|
||||
const { createConfirm } = useMessage();
|
||||
|
||||
const props = defineProps<{
|
||||
taskListGroups: TaskListGroup[];
|
||||
currentView: ViewType;
|
||||
currentListId: string;
|
||||
favoriteIdMap: Map<string, string>;
|
||||
permissionMap: Map<string, string>;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
'view-select': [view: ViewType];
|
||||
'list-select': [listId: string];
|
||||
'create-list': [name: string, groupId: string];
|
||||
'create-group': [name: string];
|
||||
'rename-list': [listId: string, newName: string];
|
||||
'delete-list': [listId: string];
|
||||
'rename-group': [groupId: string, newName: string];
|
||||
'delete-group': [groupId: string];
|
||||
'quick-access-tab-click': [tabKey: string];
|
||||
'move-list': [favoriteId: string, targetGroupId: string, sortOrder: number];
|
||||
'move-group': [groupId: string, targetSortOrder: number];
|
||||
}>();
|
||||
|
||||
function onQuickAccessClick(item: { id: string }) {
|
||||
const tabKey = QUICK_ACCESS_TAB_MAP[item.id];
|
||||
if (tabKey) {
|
||||
emit('quick-access-tab-click', tabKey);
|
||||
}
|
||||
}
|
||||
|
||||
const quickAccessExpanded = ref(true);
|
||||
|
||||
const ungroupedTaskLists = computed(() => {
|
||||
const group = props.taskListGroups.find((g) => g.id === '__ungrouped__');
|
||||
if (group) {
|
||||
return [...group.taskLists].filter((l) => l.delFlag !== 1).sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0));
|
||||
}
|
||||
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));
|
||||
});
|
||||
|
||||
const hoveredListId = ref('');
|
||||
const collapsedGroups = ref<Record<string, boolean>>({});
|
||||
const showNewGroupInput = ref(false);
|
||||
const newGroupName = ref('');
|
||||
const showNewListInput = ref(false);
|
||||
const newListGroupId = ref('');
|
||||
const newListName = ref('');
|
||||
const renamingId = ref('');
|
||||
const renamingType = ref<'list' | 'group'>('list');
|
||||
const renameValue = ref('');
|
||||
|
||||
const draggingGroupId = ref('');
|
||||
const dragStartX = ref(0);
|
||||
const dragStartY = ref(0);
|
||||
const isDragging = ref(false);
|
||||
const DRAG_THRESHOLD = 5;
|
||||
|
||||
function toggleGroup(groupId: string) {
|
||||
collapsedGroups.value = {
|
||||
...collapsedGroups.value,
|
||||
[groupId]: !collapsedGroups.value[groupId],
|
||||
};
|
||||
}
|
||||
|
||||
function onDragEnd(evt: any) {
|
||||
const itemEl = evt.item;
|
||||
const listId = itemEl?.dataset?.listId;
|
||||
if (!listId) return;
|
||||
|
||||
const favoriteId = props.favoriteIdMap.get(listId);
|
||||
if (!favoriteId) return;
|
||||
|
||||
const toContainer = evt.to;
|
||||
const targetGroupId = toContainer?.closest('[data-group-id]')?.dataset?.groupId || '';
|
||||
|
||||
const newIndex = evt.newIndex ?? 0;
|
||||
|
||||
emit('move-list', favoriteId, targetGroupId, newIndex + 1);
|
||||
}
|
||||
|
||||
function onGroupMouseDown(evt: MouseEvent, groupId: string) {
|
||||
dragStartX.value = evt.clientX;
|
||||
dragStartY.value = evt.clientY;
|
||||
isDragging.value = false;
|
||||
}
|
||||
|
||||
function onGroupMouseMove(evt: MouseEvent) {
|
||||
const dx = Math.abs(evt.clientX - dragStartX.value);
|
||||
const dy = Math.abs(evt.clientY - dragStartY.value);
|
||||
|
||||
if (dx > DRAG_THRESHOLD || dy > DRAG_THRESHOLD) {
|
||||
isDragging.value = true;
|
||||
}
|
||||
}
|
||||
|
||||
function onGroupMouseUp(evt: MouseEvent, groupId: string) {
|
||||
if (!isDragging.value) {
|
||||
toggleGroup(groupId);
|
||||
}
|
||||
isDragging.value = false;
|
||||
dragStartX.value = 0;
|
||||
dragStartY.value = 0;
|
||||
}
|
||||
|
||||
function onGroupDragStart(evt: any) {
|
||||
const itemEl = evt.item;
|
||||
const groupId = itemEl?.dataset?.groupId;
|
||||
if (groupId) {
|
||||
draggingGroupId.value = groupId;
|
||||
}
|
||||
}
|
||||
|
||||
function onGroupDragEnd(evt: any) {
|
||||
draggingGroupId.value = '';
|
||||
isDragging.value = false;
|
||||
const itemEl = evt.item;
|
||||
const groupId = itemEl?.dataset?.groupId || itemEl?.closest('[data-group-id]')?.dataset?.groupId;
|
||||
if (!groupId) return;
|
||||
const newIndex = evt.newIndex ?? 0;
|
||||
emit('move-group', groupId, newIndex + 1);
|
||||
}
|
||||
|
||||
function canEditList(listId: string): boolean {
|
||||
const perm = props.permissionMap.get(listId);
|
||||
return perm === '1' || perm === '2';
|
||||
}
|
||||
|
||||
function startNewList(groupId: string) {
|
||||
showNewListInput.value = true;
|
||||
newListGroupId.value = groupId;
|
||||
newListName.value = '';
|
||||
}
|
||||
|
||||
function submitNewList() {
|
||||
const name = newListName.value.trim();
|
||||
if (!name) {
|
||||
showNewListInput.value = false;
|
||||
return;
|
||||
}
|
||||
emit('create-list', name, newListGroupId.value);
|
||||
showNewListInput.value = false;
|
||||
newListName.value = '';
|
||||
}
|
||||
|
||||
function onListMenuClick({ key }: { key: string }, listId: string, currentName: string) {
|
||||
if (key === 'rename') {
|
||||
renamingId.value = listId;
|
||||
renamingType.value = 'list';
|
||||
renameValue.value = currentName;
|
||||
} else if (key === 'remove') {
|
||||
createConfirm({
|
||||
iconType: 'warning',
|
||||
title: '确认删除',
|
||||
content: '删除任务清单后,其下所有任务也将被删除,确定要删除吗?',
|
||||
okText: '确认',
|
||||
cancelText: '取消',
|
||||
onOk: () => {
|
||||
emit('delete-list', listId);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function onGroupMenuClick({ key }: { key: string }, groupId: string, currentName: string) {
|
||||
if (key === 'rename') {
|
||||
renamingId.value = groupId;
|
||||
renamingType.value = 'group';
|
||||
renameValue.value = currentName;
|
||||
} else if (key === 'delete') {
|
||||
emit('delete-group', groupId);
|
||||
}
|
||||
}
|
||||
|
||||
function confirmInlineRename() {
|
||||
const name = renameValue.value.trim();
|
||||
if (name && renamingId.value) {
|
||||
if (renamingType.value === 'list') {
|
||||
emit('rename-list', renamingId.value, name);
|
||||
} else {
|
||||
emit('rename-group', renamingId.value, name);
|
||||
}
|
||||
}
|
||||
renamingId.value = '';
|
||||
renameValue.value = '';
|
||||
}
|
||||
|
||||
function submitNewGroup() {
|
||||
if (newGroupName.value.trim()) {
|
||||
emit('create-group', newGroupName.value.trim());
|
||||
newGroupName.value = '';
|
||||
showNewGroupInput.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.task-sidebar {
|
||||
width: 240px;
|
||||
min-width: 240px;
|
||||
height: 100%;
|
||||
background: #fff;
|
||||
border-right: 1px solid #f0f0f0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
padding: 14px 16px 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.sidebar-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #1f1f1f;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.sidebar-nav {
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
.sidebar-divider {
|
||||
height: 1px;
|
||||
background: #f0f0f0;
|
||||
margin: 4px 12px;
|
||||
}
|
||||
|
||||
.sidebar-section {
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
.sidebar-quick-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 10px;
|
||||
cursor: pointer;
|
||||
color: #8c8c8c;
|
||||
font-size: 14px;
|
||||
user-select: none;
|
||||
|
||||
&:hover {
|
||||
color: #595959;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-quick-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #595959;
|
||||
}
|
||||
|
||||
.quick-access-arrow {
|
||||
font-size: 12px;
|
||||
color: #8c8c8c;
|
||||
transition: transform 0.2s ease;
|
||||
transform: rotate(-90deg);
|
||||
|
||||
&.expanded {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-item.sidebar-quick-access-sub {
|
||||
padding-left: 30px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.sidebar-section-header {
|
||||
padding: 6px 8px 2px;
|
||||
}
|
||||
|
||||
.sidebar-section-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #8c8c8c;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.sidebar-lists-section {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.sidebar-favorites-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 6px 8px 2px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.sidebar-favorites-icon {
|
||||
color: #8f959e;
|
||||
flex-shrink: 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.sidebar-favorites-name {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #595959;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.sidebar-favorites-items {
|
||||
.sidebar-item {
|
||||
padding-left: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-group-items {
|
||||
.sidebar-item {
|
||||
padding-left: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-group-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 6px 8px 2px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
|
||||
&:hover .sidebar-group-name {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.sidebar-group-more,
|
||||
.sidebar-group-add {
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
|
||||
&:hover .sidebar-group-more,
|
||||
&:hover .sidebar-group-add {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.group-collapse-arrow {
|
||||
font-size: 12px;
|
||||
color: #8f959e;
|
||||
transition: transform 0.2s ease;
|
||||
flex-shrink: 0;
|
||||
width: 16px;
|
||||
text-align: center;
|
||||
|
||||
&.collapsed {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-group-name {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #595959;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.sidebar-group-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
font-size: 10px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.sidebar-group-add {
|
||||
margin-left: auto;
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
cursor: pointer;
|
||||
padding: 2px 4px;
|
||||
border-radius: 4px;
|
||||
&:hover {
|
||||
color: #1677ff;
|
||||
background: #e6f4ff;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-group-more {
|
||||
font-size: 16px;
|
||||
color: #999;
|
||||
padding: 2px;
|
||||
border-radius: 4px;
|
||||
flex-shrink: 0;
|
||||
&:hover {
|
||||
color: #1677ff;
|
||||
background: #e6f4ff;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 10px;
|
||||
cursor: pointer;
|
||||
border-radius: 6px;
|
||||
margin: 1px 0;
|
||||
font-size: 14px;
|
||||
color: #595959;
|
||||
transition: all 0.15s ease;
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
background: #f5f5f5;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: #e6f4ff;
|
||||
color: #1677ff;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-item-icon {
|
||||
font-size: 14px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.sidebar-item-name {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.sidebar-item-more {
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
padding: 2px;
|
||||
border-radius: 4px;
|
||||
flex-shrink: 0;
|
||||
&:hover {
|
||||
color: #1677ff;
|
||||
background: #e6f4ff;
|
||||
}
|
||||
}
|
||||
|
||||
.list-drag-handle {
|
||||
cursor: grab;
|
||||
font-size: 12px;
|
||||
color: transparent;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
flex-shrink: 0;
|
||||
transition: color 0.15s;
|
||||
&:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-item:hover .list-drag-handle {
|
||||
color: #c0c4cc;
|
||||
&:hover {
|
||||
color: #8c8c8c;
|
||||
}
|
||||
}
|
||||
|
||||
.list-drag-handle-placeholder {
|
||||
width: 14px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.sidebar-empty-hint {
|
||||
padding: 8px 10px 4px 38px;
|
||||
font-size: 12px;
|
||||
color: #bfbfbf;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.sidebar-new-group-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 10px;
|
||||
margin: 4px 0;
|
||||
cursor: pointer;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
color: #8c8c8c;
|
||||
&:hover {
|
||||
background: #f5f5f5;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-new-group-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 10px;
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
.sidebar-new-item-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 4px 10px 4px 8px;
|
||||
margin: 2px 0;
|
||||
}
|
||||
|
||||
.group-being-dragged > .sidebar-group-items {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.group-drag-ghost {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.sidebar-item-renaming {
|
||||
background: #f5f6f7;
|
||||
}
|
||||
|
||||
.sidebar-group-renaming {
|
||||
background: #f5f6f7;
|
||||
}
|
||||
|
||||
.sidebar-new-icon {
|
||||
font-size: 12px;
|
||||
color: #8c8c8c;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,366 @@
|
||||
<template>
|
||||
<a-modal
|
||||
v-model:open="visible"
|
||||
:title="title"
|
||||
:footer="null"
|
||||
width="520px"
|
||||
:bodyStyle="{ padding: '0' }"
|
||||
:centered="true"
|
||||
:destroyOnClose="true"
|
||||
@cancel="onClose"
|
||||
>
|
||||
<div class="user-modal">
|
||||
<div class="user-body">
|
||||
<div class="member-list">
|
||||
<div
|
||||
v-for="item in memberList"
|
||||
:key="item.userId"
|
||||
class="member-item"
|
||||
@mouseenter="hoverId = item.userId"
|
||||
@mouseleave="hoverId = ''"
|
||||
>
|
||||
<div class="member-avatar" :style="{ background: getColor(item.username) }">
|
||||
{{ (item.username || '?').charAt(0) }}
|
||||
</div>
|
||||
<div class="member-main">
|
||||
<div class="member-name-row">
|
||||
<span class="member-name">{{ item.username }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="member-right">
|
||||
<span v-show="hoverId === item.userId" class="remove-icon" @click="onRemove(item.userId)">
|
||||
<Icon icon="ant-design:close-outlined" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="memberList.length === 0" class="empty-state">
|
||||
<Icon icon="ant-design:team-outlined" style="font-size: 36px; color: #d9d9d9; margin-bottom: 8px" />
|
||||
<span>暂未选择</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="showAddPanel" class="add-panel">
|
||||
<div class="add-panel-header">
|
||||
<span>{{ addPanelTitle }}</span>
|
||||
<span class="add-panel-close" @click="showAddPanel = false">
|
||||
<Icon icon="ant-design:close-outlined" />
|
||||
</span>
|
||||
</div>
|
||||
<div class="add-panel-body">
|
||||
<JSelectUser
|
||||
v-model:value="addForm.userIds"
|
||||
:label-key="'realname'"
|
||||
:row-key="'id'"
|
||||
multiple="multiple"
|
||||
placeholder="搜索并选择用户(支持多选)"
|
||||
/>
|
||||
</div>
|
||||
<div class="add-panel-footer">
|
||||
<a-button size="small" @click="showAddPanel = false">取消</a-button>
|
||||
<a-button size="small" type="primary" :disabled="!addForm.userIds" @click="onConfirmAdd">
|
||||
确认添加
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="!showAddPanel" class="user-footer">
|
||||
<a-button type="text" size="small" @click="showAddPanel = true">
|
||||
<Icon icon="ant-design:user-add-outlined" style="margin-right: 4px" />
|
||||
{{ addPanelTitle }}
|
||||
</a-button>
|
||||
<a-button type="primary" size="small" @click="onDone">完成</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, computed } from 'vue';
|
||||
import Icon from '/@/components/Icon/index';
|
||||
import JSelectUser from '/@/components/Form/src/jeecg/components/JSelectUser.vue';
|
||||
import { getAvatarColor } from '../types';
|
||||
import { getUserList } from '/@/api/common/api';
|
||||
|
||||
interface MemberItem {
|
||||
userId: string;
|
||||
username: string;
|
||||
}
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
title?: string;
|
||||
userIds: string;
|
||||
userNames: string;
|
||||
multiple?: boolean;
|
||||
}>(),
|
||||
{
|
||||
title: '选择人员',
|
||||
multiple: true,
|
||||
}
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
confirm: [userIds: string, userNames: string];
|
||||
}>();
|
||||
|
||||
const visible = ref(false);
|
||||
const hoverId = ref('');
|
||||
const showAddPanel = ref(false);
|
||||
const addForm = reactive({
|
||||
userIds: '',
|
||||
});
|
||||
|
||||
const memberList = ref<MemberItem[]>([]);
|
||||
|
||||
const addPanelTitle = computed(() => {
|
||||
return props.title === '选择负责人' ? '添加负责人' : props.title === '选择参与人' ? '添加参与人' : props.title === '选择关注人' ? '添加关注人' : '添加人员';
|
||||
});
|
||||
|
||||
function getColor(name: string): string {
|
||||
return getAvatarColor(name || '');
|
||||
}
|
||||
|
||||
function parseMembers(userIds: string, userNames: string): MemberItem[] {
|
||||
if (!userIds) return [];
|
||||
const ids = userIds.split(',').filter(Boolean);
|
||||
const names = userNames ? userNames.split(',').filter(Boolean) : [];
|
||||
return ids.map((id, i) => ({
|
||||
userId: id.trim(),
|
||||
username: (names[i] || id).trim(),
|
||||
}));
|
||||
}
|
||||
|
||||
async function open() {
|
||||
memberList.value = parseMembers(props.userIds, props.userNames);
|
||||
const needFetch = memberList.value.filter((m) => m.username === m.userId);
|
||||
if (needFetch.length > 0) {
|
||||
try {
|
||||
const ids = needFetch.map((m) => m.userId);
|
||||
const res = await getUserList({ id: ids.join(','), pageNo: 1, pageSize: ids.length * 2 });
|
||||
if (res.records && res.records.length > 0) {
|
||||
const nameMap: Record<string, string> = {};
|
||||
res.records.forEach((u: any) => {
|
||||
nameMap[u.id] = u.realname || u.username;
|
||||
});
|
||||
memberList.value.forEach((m) => {
|
||||
if (nameMap[m.userId]) {
|
||||
m.username = nameMap[m.userId];
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('获取用户名失败', e);
|
||||
}
|
||||
}
|
||||
showAddPanel.value = false;
|
||||
addForm.userIds = '';
|
||||
visible.value = true;
|
||||
}
|
||||
|
||||
function onClose() {
|
||||
visible.value = false;
|
||||
showAddPanel.value = false;
|
||||
addForm.userIds = '';
|
||||
}
|
||||
|
||||
function onRemove(userId: string) {
|
||||
memberList.value = memberList.value.filter((m) => m.userId !== userId);
|
||||
}
|
||||
|
||||
async function onConfirmAdd() {
|
||||
if (!addForm.userIds) return;
|
||||
const newIds = addForm.userIds.split(',').filter(Boolean);
|
||||
const existingIds = new Set(memberList.value.map((m) => m.userId));
|
||||
if (!props.multiple) {
|
||||
memberList.value = [];
|
||||
}
|
||||
const nameMap: Record<string, string> = {};
|
||||
try {
|
||||
const res = await getUserList({ id: newIds.join(','), pageNo: 1, pageSize: newIds.length * 2 });
|
||||
if (res.records && res.records.length > 0) {
|
||||
res.records.forEach((u: any) => {
|
||||
nameMap[u.id] = u.realname || u.username;
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('获取用户名失败,使用ID作为名称', e);
|
||||
}
|
||||
for (const nid of newIds) {
|
||||
if (!existingIds.has(nid) || !props.multiple) {
|
||||
memberList.value.push({
|
||||
userId: nid,
|
||||
username: nameMap[nid] || nid,
|
||||
});
|
||||
}
|
||||
}
|
||||
addForm.userIds = '';
|
||||
showAddPanel.value = false;
|
||||
}
|
||||
|
||||
function onDone() {
|
||||
const ids = memberList.value.map((m) => m.userId).join(',');
|
||||
const names = memberList.value.map((m) => m.username).join(',');
|
||||
emit('confirm', ids, names);
|
||||
visible.value = false;
|
||||
}
|
||||
|
||||
defineExpose({ open });
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.user-modal {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.user-body {
|
||||
position: relative;
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.member-list {
|
||||
padding: 8px 0;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.member-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 8px 20px;
|
||||
transition: background 0.12s;
|
||||
|
||||
&:hover {
|
||||
background: #f5f6f7;
|
||||
}
|
||||
}
|
||||
|
||||
.member-avatar {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
color: #fff;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.member-main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.member-name-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.member-name {
|
||||
font-size: 14px;
|
||||
color: #1f2329;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.member-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
flex-shrink: 0;
|
||||
min-width: 30px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.remove-icon {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
color: #8f959e;
|
||||
font-size: 12px;
|
||||
transition: all 0.12s;
|
||||
|
||||
&:hover {
|
||||
background: #f0f0f0;
|
||||
color: #f5222d;
|
||||
}
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40px 0;
|
||||
color: #bfbfbf;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.add-panel {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: #fff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.add-panel-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 20px 12px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #1f2329;
|
||||
}
|
||||
|
||||
.add-panel-close {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
color: #8f959e;
|
||||
|
||||
&:hover {
|
||||
background: #f0f0f0;
|
||||
}
|
||||
}
|
||||
|
||||
.add-panel-body {
|
||||
padding: 0 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.add-panel-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
padding: 16px 20px;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.user-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10px 20px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,982 @@
|
||||
<!-- eslint-disable vue/multi-word-component-names -->
|
||||
<template>
|
||||
<div class="task-page">
|
||||
<TaskSidebar
|
||||
:task-list-groups="taskListGroups"
|
||||
:current-view="currentView"
|
||||
:current-list-id="currentListId"
|
||||
:favorite-id-map="favoriteIdMap"
|
||||
:permission-map="permissionMap"
|
||||
@view-select="onViewSelect"
|
||||
@list-select="onListSelect"
|
||||
@create-list="onCreateList"
|
||||
@create-group="onCreateGroup"
|
||||
@rename-list="onRenameList"
|
||||
@delete-list="onDeleteList"
|
||||
@rename-group="onRenameGroup"
|
||||
@delete-group="onDeleteGroup"
|
||||
@quick-access-tab-click="onQuickAccessTabClick"
|
||||
@move-list="onMoveList"
|
||||
@move-group="onMoveGroup"
|
||||
/>
|
||||
<TaskContent
|
||||
:current-list="currentList"
|
||||
:task-list-groups="showListGroupView ? quickAccessGroups : taskListGroups"
|
||||
:status-filter="statusFilter"
|
||||
:group-dimension="groupDimension"
|
||||
:sort-field="sortField"
|
||||
:sort-direction="sortDirection"
|
||||
:visible-fields="visibleFields"
|
||||
:show-list-group-view="showListGroupView"
|
||||
:list-group-tab="listGroupTab"
|
||||
:favorite-id-map="favoriteIdMap"
|
||||
@create-task="onCreateTask"
|
||||
@toggle-task="onToggleTask"
|
||||
@task-click="onTaskClick"
|
||||
@create-group="onCreateGroupFromContent"
|
||||
@add-group="onAddGroup"
|
||||
@update-task-field="onUpdateTaskField"
|
||||
@update-task-fields="onUpdateTaskFields"
|
||||
@update:status-filter="(v) => (statusFilter = v)"
|
||||
@update:group-dimension="(v) => (groupDimension = v)"
|
||||
@update:sort-field="(v) => (sortField = v)"
|
||||
@update:sort-direction="(v) => (sortDirection = v)"
|
||||
@update:visible-fields="(v) => (visibleFields = v)"
|
||||
@update:list-group-tab="onQuickAccessTabClick"
|
||||
@list-select="onListSelect"
|
||||
@open-collaborator="onOpenCollaborator"
|
||||
@add-to-favorites="onAddToFavorites"
|
||||
@remove-from-favorites="onRemoveFromFavorites"
|
||||
@move-task="onMoveTask"
|
||||
@move-task-group="onMoveTaskGroup"
|
||||
@rename-list="onRenameList"
|
||||
@delete-list="onDeleteList"
|
||||
@rename-group="onRenameTaskGroup"
|
||||
@delete-group="onDeleteTaskGroup"
|
||||
/>
|
||||
<CreateTaskModal @register="registerTaskModal" @success="onTaskCreated" />
|
||||
<CreateListModal @register="registerListModal" @success="onListCreated" />
|
||||
<TaskDetailDrawer
|
||||
@register="registerDetailDrawer"
|
||||
@save-task="onSaveTask"
|
||||
@create-sub-task="onCreateSubTask"
|
||||
@toggle-sub-task="onToggleSubTask"
|
||||
@toggle-task="onToggleTask"
|
||||
@navigate-task="onNavigateTask"
|
||||
@delete-task="onDeleteTask"
|
||||
@delete-sub-task="onDeleteSubTaskRecord"
|
||||
@update-sub-task-field="onUpdateSubTaskField"
|
||||
@update-sub-task-fields="onUpdateSubTaskFields"
|
||||
@follow-task="onFollowTask"
|
||||
@unfollow-task="onUnfollowTask"
|
||||
@move-task="onMoveTask"
|
||||
/>
|
||||
<CollaboratorModal ref="collaboratorModalRef" :task-list-id="currentListId" :is-owner="isCurrentOwner" @changed="onCollaboratorsChanged" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" name="tasklist-index-page" setup>
|
||||
import { ref, reactive, computed, onMounted } from 'vue';
|
||||
import { useModal } from '/@/components/Modal';
|
||||
import { useDrawer } from '/@/components/Drawer';
|
||||
import TaskSidebar from './components/TaskSidebar.vue';
|
||||
import TaskContent from './components/TaskContent.vue';
|
||||
import CreateTaskModal from './components/CreateTaskModal.vue';
|
||||
import CreateListModal from './components/CreateListModal.vue';
|
||||
import TaskDetailDrawer from './components/TaskDetailDrawer.vue';
|
||||
import CollaboratorModal from './components/CollaboratorModal.vue';
|
||||
import {
|
||||
getMyFavorites,
|
||||
addTaskListGroup,
|
||||
addTaskList,
|
||||
removeFavorite,
|
||||
removeFavoriteGroup,
|
||||
deleteTaskListApi,
|
||||
renameTaskList,
|
||||
saveOrUpdate,
|
||||
renameGroup,
|
||||
getMyOwnLists,
|
||||
getMyCollabLists,
|
||||
getAllLists,
|
||||
moveTaskList,
|
||||
moveGroup,
|
||||
getMyPermission,
|
||||
getCollaborators,
|
||||
addToFavorites,
|
||||
addTask,
|
||||
editTask,
|
||||
deleteTask as deleteTaskApi,
|
||||
toggleTaskStatus,
|
||||
followTask as followTaskApi,
|
||||
unfollowTask as unfollowTaskApi,
|
||||
listAllByMainId,
|
||||
myResponsibleTasks,
|
||||
myFollowedTasks,
|
||||
moveTask,
|
||||
moveTaskGroup,
|
||||
} from './TaskList.api';
|
||||
import { DEFAULT_VISIBLE_FIELDS } from './types';
|
||||
import type {
|
||||
TaskListGroup,
|
||||
StatusFilter,
|
||||
GroupDimension,
|
||||
SortField,
|
||||
SortDirection,
|
||||
FieldKey,
|
||||
TaskList,
|
||||
ViewType,
|
||||
TaskItem,
|
||||
TaskGroup,
|
||||
} from './types';
|
||||
|
||||
const taskListGroups = ref<TaskListGroup[]>([]);
|
||||
const quickAccessGroups = ref<TaskListGroup[]>([]);
|
||||
const currentView = ref<ViewType>('my-tasks');
|
||||
const currentListId = ref<string>('');
|
||||
const statusFilter = ref<StatusFilter>('all');
|
||||
const groupDimension = ref<GroupDimension>('custom');
|
||||
const sortField = ref<SortField>('sortOrder');
|
||||
const sortDirection = ref<SortDirection>('asc');
|
||||
const showListGroupView = ref(false);
|
||||
const listGroupTab = ref('all');
|
||||
const visibleFields = ref<FieldKey[]>([...DEFAULT_VISIBLE_FIELDS]);
|
||||
const currentDrawerTaskId = ref<string>('');
|
||||
|
||||
const [registerTaskModal, { openModal: openTaskModal }] = useModal();
|
||||
const [registerListModal, { openModal: _openListModal }] = useModal();
|
||||
const [registerDetailDrawer, { openDrawer: openDetailDrawer }] = useDrawer();
|
||||
|
||||
const collaboratorModalRef = ref<InstanceType<typeof CollaboratorModal> | null>(null);
|
||||
|
||||
const favoriteIdMap = reactive(new Map<string, string>());
|
||||
const permissionMap = reactive(new Map<string, string>());
|
||||
|
||||
async function loadFavorites() {
|
||||
try {
|
||||
const data: any[] = await getMyFavorites();
|
||||
favoriteIdMap.clear();
|
||||
permissionMap.clear();
|
||||
for (const fav of data) {
|
||||
if (fav.type === '1' && fav.mainId) {
|
||||
favoriteIdMap.set(fav.mainId, fav.id);
|
||||
if (fav.permission) {
|
||||
permissionMap.set(fav.mainId, fav.permission);
|
||||
}
|
||||
}
|
||||
}
|
||||
taskListGroups.value = buildTree(data);
|
||||
} catch (e) {
|
||||
console.error('[TaskList] 加载收藏列表失败', e);
|
||||
}
|
||||
}
|
||||
|
||||
function buildTree(favorites: any[]): TaskListGroup[] {
|
||||
const groups: TaskListGroup[] = [];
|
||||
const ungroupedLists: TaskList[] = [];
|
||||
|
||||
for (const fav of favorites) {
|
||||
if (fav.type === '0') {
|
||||
groups.push({
|
||||
id: fav.id,
|
||||
name: fav.tasklistName || '',
|
||||
pid: '0',
|
||||
hasChild: fav.hasChild || '0',
|
||||
type: 0,
|
||||
sortOrder: fav.sortOrder || 0,
|
||||
delFlag: 0,
|
||||
taskLists: [],
|
||||
});
|
||||
} else if (fav.type === '1') {
|
||||
const list: TaskList = {
|
||||
id: fav.mainId,
|
||||
name: fav.tasklistName || '',
|
||||
pid: fav.pid || '',
|
||||
hasChild: '0',
|
||||
type: 1,
|
||||
sortOrder: fav.sortOrder || 0,
|
||||
delFlag: 0,
|
||||
groups: [],
|
||||
};
|
||||
|
||||
if (fav.pid) {
|
||||
const parentGroup = groups.find((g) => g.id === fav.pid);
|
||||
if (parentGroup) {
|
||||
parentGroup.taskLists.push(list);
|
||||
} else {
|
||||
ungroupedLists.push(list);
|
||||
}
|
||||
} else {
|
||||
ungroupedLists.push(list);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (ungroupedLists.length > 0) {
|
||||
const virtualGroup: TaskListGroup = {
|
||||
id: '__ungrouped__',
|
||||
name: '__ungrouped__',
|
||||
pid: '0',
|
||||
hasChild: '0',
|
||||
type: 0,
|
||||
sortOrder: -1,
|
||||
delFlag: 0,
|
||||
taskLists: ungroupedLists,
|
||||
};
|
||||
groups.unshift(virtualGroup);
|
||||
}
|
||||
|
||||
groups.sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0));
|
||||
|
||||
return groups;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadFavorites();
|
||||
});
|
||||
|
||||
function findListInGroups(groups: TaskListGroup[], listId: string): TaskList | null {
|
||||
for (const group of groups) {
|
||||
for (const list of group.taskLists) {
|
||||
if (list.id === listId) return list;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const currentList = computed<TaskList | null>(() => {
|
||||
if (!currentListId.value) return null;
|
||||
return findListInGroups(taskListGroups.value, currentListId.value) || findListInGroups(quickAccessGroups.value, currentListId.value);
|
||||
});
|
||||
|
||||
const isCurrentOwner = computed(() => currentList.value?.myPermission === '1');
|
||||
|
||||
async function loadPermissionForList(listId: string) {
|
||||
try {
|
||||
const perm = await getMyPermission({ taskListId: listId });
|
||||
const list = findListById(listId);
|
||||
if (list) {
|
||||
list.myPermission = perm || '';
|
||||
}
|
||||
const collabs: any[] = await getCollaborators({ taskListId: listId });
|
||||
const names = collabs.map((c: any) => c.username).filter(Boolean);
|
||||
if (list) {
|
||||
list.collaboratorNames = names.join(',');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[TaskList] 获取权限失败', e);
|
||||
}
|
||||
}
|
||||
|
||||
function buildTaskItem(tr: any): TaskItem {
|
||||
return {
|
||||
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.createByName || tr.createBy_dictText || 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,
|
||||
};
|
||||
}
|
||||
|
||||
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 list = findListById(listId);
|
||||
if (!list) return;
|
||||
|
||||
const groupRecords = data.filter((d: any) => d.type === '0');
|
||||
const taskRecords = data.filter((d: any) => d.type === '1');
|
||||
|
||||
const groupMap = new Map<string, TaskGroup>();
|
||||
let defaultGroup: TaskGroup | null = null;
|
||||
|
||||
for (const gr of groupRecords) {
|
||||
const tg: TaskGroup = {
|
||||
id: gr.id,
|
||||
name: gr.taskName || '',
|
||||
taskListId: listId,
|
||||
collapsed: false,
|
||||
tasks: [],
|
||||
};
|
||||
groupMap.set(gr.id, tg);
|
||||
if (gr.taskName === '默认分组') {
|
||||
defaultGroup = tg;
|
||||
}
|
||||
}
|
||||
|
||||
if (!defaultGroup) {
|
||||
defaultGroup = {
|
||||
id: '__default__',
|
||||
name: '默认分组',
|
||||
taskListId: listId,
|
||||
collapsed: false,
|
||||
tasks: [],
|
||||
};
|
||||
}
|
||||
|
||||
const groupIds = new Set(groupRecords.map((g: any) => g.id));
|
||||
|
||||
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!;
|
||||
targetGroup.tasks.push(taskItem);
|
||||
}
|
||||
|
||||
const resultGroups: TaskGroup[] = [];
|
||||
if (defaultGroup) {
|
||||
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;
|
||||
} catch (e) {
|
||||
console.error('[TaskList] 加载任务失败', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function onViewSelect(view: ViewType) {
|
||||
currentView.value = view;
|
||||
showListGroupView.value = false;
|
||||
currentListId.value = '';
|
||||
|
||||
if (view === 'my-tasks') {
|
||||
try {
|
||||
const data: any[] = await myResponsibleTasks();
|
||||
quickAccessGroups.value = buildGroupsFromTasks(data, '我负责的任务');
|
||||
showListGroupView.value = true;
|
||||
} catch (e) {
|
||||
console.error('[TaskList] 加载我负责的任务失败', e);
|
||||
}
|
||||
} else if (view === 'followed') {
|
||||
try {
|
||||
const data: any[] = await myFollowedTasks();
|
||||
quickAccessGroups.value = buildGroupsFromTasks(data, '我关注的任务');
|
||||
showListGroupView.value = true;
|
||||
} catch (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) {
|
||||
currentView.value = '';
|
||||
currentListId.value = listId;
|
||||
showListGroupView.value = false;
|
||||
loadPermissionForList(listId);
|
||||
loadTasks(listId);
|
||||
}
|
||||
|
||||
function onOpenCollaborator() {
|
||||
collaboratorModalRef.value?.open();
|
||||
}
|
||||
|
||||
async function onCollaboratorsChanged() {
|
||||
if (currentListId.value) {
|
||||
await loadPermissionForList(currentListId.value);
|
||||
}
|
||||
}
|
||||
|
||||
async function onAddToFavorites(taskListId: string) {
|
||||
try {
|
||||
await addToFavorites({ taskListId });
|
||||
await loadFavorites();
|
||||
} catch (e) {
|
||||
console.error('[TaskList] 添加收藏失败', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function onRemoveFromFavorites(favoriteId: string) {
|
||||
try {
|
||||
await removeFavorite({ favoriteId });
|
||||
await loadFavorites();
|
||||
} catch (e) {
|
||||
console.error('[TaskList] 取消收藏失败', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function onQuickAccessTabClick(tabKey: string) {
|
||||
const quickAccessViewMap: Record<string, ViewType> = {
|
||||
all: 'all-tasks',
|
||||
own: 'created',
|
||||
collab: 'assigned',
|
||||
};
|
||||
currentView.value = quickAccessViewMap[tabKey] || currentView.value;
|
||||
showListGroupView.value = true;
|
||||
currentListId.value = '';
|
||||
listGroupTab.value = tabKey;
|
||||
|
||||
try {
|
||||
if (tabKey === 'all') {
|
||||
const data: TaskList[] = await getAllLists();
|
||||
quickAccessGroups.value = wrapAsVirtualGroupsWithDetails(data);
|
||||
} else if (tabKey === 'own') {
|
||||
const data: TaskList[] = await getMyOwnLists();
|
||||
quickAccessGroups.value = wrapAsVirtualGroupsWithDetails(data);
|
||||
} else if (tabKey === 'collab') {
|
||||
const data: TaskList[] = await getMyCollabLists();
|
||||
quickAccessGroups.value = wrapAsVirtualGroupsWithDetails(data);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[TaskList] 加载清单失败', e);
|
||||
}
|
||||
}
|
||||
|
||||
function wrapAsVirtualGroupsWithDetails(lists: TaskList[]): TaskListGroup[] {
|
||||
return lists.map((item: TaskList) => ({
|
||||
id: `_virtual_${item.id}`,
|
||||
name: item.tasklistName || '',
|
||||
pid: '0',
|
||||
hasChild: '0',
|
||||
type: 0,
|
||||
sortOrder: 0,
|
||||
delFlag: 0,
|
||||
taskLists: [
|
||||
{
|
||||
id: item.id,
|
||||
name: item.tasklistName || '',
|
||||
pid: '',
|
||||
hasChild: '0',
|
||||
type: 1,
|
||||
sortOrder: 0,
|
||||
delFlag: 0,
|
||||
groups: [],
|
||||
ownerName: item.ownerName || '',
|
||||
collaboratorNames: item.collaboratorNames || '',
|
||||
createTimeStr: item.createTimeStr || '',
|
||||
},
|
||||
],
|
||||
}));
|
||||
}
|
||||
|
||||
async function onCreateTask(groupId: string, taskName: string, sortOrder?: number) {
|
||||
if (taskName && taskName.trim()) {
|
||||
try {
|
||||
console.log('[TaskList] onCreateTask:', { groupId, taskName, mainId: currentListId.value, sortOrder });
|
||||
const params: any = {
|
||||
mainId: currentListId.value,
|
||||
taskName: taskName.trim(),
|
||||
type: '1',
|
||||
pid: groupId && groupId !== '__default__' ? groupId : '',
|
||||
};
|
||||
if (sortOrder !== undefined) {
|
||||
params.sortOrder = sortOrder;
|
||||
}
|
||||
await addTask(params);
|
||||
if (currentListId.value) {
|
||||
await loadTasks(currentListId.value);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[TaskList] 创建任务失败', e);
|
||||
}
|
||||
} else {
|
||||
openTaskModal(true, { groupId, mainId: currentListId.value });
|
||||
}
|
||||
}
|
||||
|
||||
async function onCreateList(name: string, groupId: string) {
|
||||
if (!name || !name.trim()) return;
|
||||
try {
|
||||
await addTaskList({
|
||||
tasklistName: name.trim(),
|
||||
pid: groupId || undefined,
|
||||
sortOrder: 1,
|
||||
});
|
||||
await loadFavorites();
|
||||
} catch (e) {
|
||||
console.error('[TaskList] 创建清单失败', e);
|
||||
}
|
||||
}
|
||||
|
||||
function onListCreated() {
|
||||
loadFavorites();
|
||||
}
|
||||
|
||||
async function onTaskCreated() {
|
||||
if (currentListId.value) {
|
||||
await loadTasks(currentListId.value);
|
||||
}
|
||||
}
|
||||
|
||||
async function onCreateGroup(name: string) {
|
||||
const groupName = name?.trim() || '新建分组';
|
||||
try {
|
||||
await addTaskListGroup({ tasklistName: groupName });
|
||||
await loadFavorites();
|
||||
} catch (e) {
|
||||
console.error('[TaskList] 创建分组失败', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function onCreateGroupFromContent(_name: string) {
|
||||
const groupName = '新建分组';
|
||||
try {
|
||||
if (!currentListId.value) return;
|
||||
await addTask({
|
||||
mainId: currentListId.value,
|
||||
taskName: groupName,
|
||||
type: '0',
|
||||
sortOrder: 1,
|
||||
});
|
||||
await loadTasks(currentListId.value);
|
||||
} catch (e) {
|
||||
console.error('[TaskList] 创建任务分组失败', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function onRenameGroup(groupId: string, newName: string) {
|
||||
try {
|
||||
await renameGroup({ groupId, newName });
|
||||
await loadFavorites();
|
||||
} catch (e) {
|
||||
console.error('[TaskList] 重命名分组失败', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function onDeleteGroup(groupId: string) {
|
||||
try {
|
||||
await removeFavoriteGroup({ groupId });
|
||||
await loadFavorites();
|
||||
const hasCurrentList = taskListGroups.value.some((g) => g.taskLists.some((l) => l.id === currentListId.value));
|
||||
if (!hasCurrentList) {
|
||||
const firstAvailable = findFirstAvailableList();
|
||||
currentListId.value = firstAvailable?.id || '';
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[TaskList] 移除分组失败', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function onMoveList(favoriteId: string, targetGroupId: string, sortOrder: number) {
|
||||
try {
|
||||
await moveTaskList({ favoriteId, targetGroupId: targetGroupId || undefined, sortOrder });
|
||||
await loadFavorites();
|
||||
} catch (e) {
|
||||
console.error('[TaskList] 移动清单失败', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function onMoveGroup(groupId: string, sortOrder: number) {
|
||||
try {
|
||||
await moveGroup({ groupId, sortOrder });
|
||||
await loadFavorites();
|
||||
} catch (e) {
|
||||
console.error('[TaskList] 移动分组失败', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function onMoveTask(taskId: string, targetPid: string, targetSortOrder: number) {
|
||||
try {
|
||||
await moveTask({ taskId, targetPid, targetSortOrder });
|
||||
if (currentListId.value) {
|
||||
await loadTasks(currentListId.value);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[TaskList] 移动任务失败', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function onMoveTaskGroup(taskGroupId: string, targetSortOrder: number) {
|
||||
try {
|
||||
await moveTaskGroup({ taskGroupId, targetSortOrder });
|
||||
if (currentListId.value) {
|
||||
await loadTasks(currentListId.value);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[TaskList] 移动任务分组失败', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function onToggleTask(taskId: string) {
|
||||
try {
|
||||
await toggleTaskStatus({ id: taskId });
|
||||
if (currentListId.value) {
|
||||
await loadTasks(currentListId.value);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[TaskList] 切换任务状态失败', e);
|
||||
}
|
||||
}
|
||||
|
||||
function onTaskClick(taskId: string) {
|
||||
currentDrawerTaskId.value = taskId;
|
||||
const allTasks = getAllTasks();
|
||||
const task = allTasks.find((t) => t.id === taskId);
|
||||
if (task) {
|
||||
openDetailDrawer(true, { task, allTasks });
|
||||
}
|
||||
}
|
||||
|
||||
function refreshDrawer() {
|
||||
if (!currentDrawerTaskId.value) return;
|
||||
const allTasks = getAllTasks();
|
||||
const task = allTasks.find((t) => t.id === currentDrawerTaskId.value);
|
||||
if (task) {
|
||||
openDetailDrawer(true, { task, allTasks });
|
||||
}
|
||||
}
|
||||
|
||||
async function onUpdateTaskField(taskId: string, field: string, value: any) {
|
||||
try {
|
||||
await editTask({ id: taskId, [field]: value });
|
||||
if (currentListId.value) {
|
||||
await loadTasks(currentListId.value);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[TaskList] 更新任务字段失败', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function onUpdateTaskFields(taskId: string, fields: Record<string, any>) {
|
||||
try {
|
||||
await editTask({ id: taskId, ...fields });
|
||||
if (currentListId.value) {
|
||||
await loadTasks(currentListId.value);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[TaskList] 批量更新任务字段失败', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function onSaveTask(updatedTask: TaskItem) {
|
||||
try {
|
||||
await editTask({
|
||||
id: updatedTask.id,
|
||||
taskName: updatedTask.taskName,
|
||||
taskDesc: updatedTask.taskDesc,
|
||||
priority: updatedTask.priority,
|
||||
assigneeId: updatedTask.assigneeId,
|
||||
assigneeName: updatedTask.assigneeName,
|
||||
participantId: updatedTask.participantId,
|
||||
participantName: updatedTask.participantName,
|
||||
followersId: updatedTask.followersId,
|
||||
followersName: updatedTask.followersName,
|
||||
startTime: updatedTask.startTime || null,
|
||||
endTime: updatedTask.endTime || null,
|
||||
type: updatedTask.type,
|
||||
remark: updatedTask.remark,
|
||||
});
|
||||
if (currentListId.value) {
|
||||
await loadTasks(currentListId.value);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[TaskList] 保存任务失败', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function onCreateSubTask(parentTaskId: string, taskName: string) {
|
||||
try {
|
||||
await addTask({
|
||||
mainId: currentListId.value,
|
||||
taskName,
|
||||
type: '1',
|
||||
pid: parentTaskId,
|
||||
});
|
||||
if (currentListId.value) {
|
||||
await loadTasks(currentListId.value);
|
||||
refreshDrawer();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[TaskList] 创建子任务失败', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function onToggleSubTask(taskId: string) {
|
||||
try {
|
||||
await toggleTaskStatus({ id: taskId });
|
||||
if (currentListId.value) {
|
||||
await loadTasks(currentListId.value);
|
||||
refreshDrawer();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[TaskList] 切换子任务状态失败', e);
|
||||
}
|
||||
}
|
||||
|
||||
function onNavigateTask(taskId: string) {
|
||||
currentDrawerTaskId.value = taskId;
|
||||
const allTasks = getAllTasks();
|
||||
const task = allTasks.find((t) => t.id === taskId);
|
||||
if (task) {
|
||||
openDetailDrawer(true, { task, allTasks });
|
||||
}
|
||||
}
|
||||
|
||||
async function onDeleteTask(taskId: string) {
|
||||
try {
|
||||
await deleteTaskApi({ id: taskId }, () => {});
|
||||
if (currentListId.value) {
|
||||
await loadTasks(currentListId.value);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[TaskList] 删除任务失败', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function onDeleteSubTaskRecord(taskId: string) {
|
||||
try {
|
||||
await deleteTaskApi({ id: taskId }, () => {});
|
||||
if (currentListId.value) {
|
||||
await loadTasks(currentListId.value);
|
||||
refreshDrawer();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[TaskList] 删除子任务失败', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function onUpdateSubTaskField(taskId: string, field: string, value: any) {
|
||||
try {
|
||||
await editTask({ id: taskId, [field]: value || '' });
|
||||
if (currentListId.value) {
|
||||
await loadTasks(currentListId.value);
|
||||
refreshDrawer();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[TaskList] 更新子任务字段失败', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function onUpdateSubTaskFields(taskId: string, fields: Record<string, any>) {
|
||||
try {
|
||||
await editTask({ id: taskId, ...fields });
|
||||
if (currentListId.value) {
|
||||
await loadTasks(currentListId.value);
|
||||
refreshDrawer();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[TaskList] 批量更新子任务字段失败', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function onFollowTask(taskId: string) {
|
||||
try {
|
||||
await followTaskApi({ id: taskId });
|
||||
if (currentListId.value) {
|
||||
await loadTasks(currentListId.value);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[TaskList] 关注任务失败', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function onUnfollowTask(taskId: string) {
|
||||
try {
|
||||
await unfollowTaskApi({ id: taskId });
|
||||
if (currentListId.value) {
|
||||
await loadTasks(currentListId.value);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[TaskList] 取消关注失败', e);
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
const list = findListById(listId);
|
||||
if (list) {
|
||||
list.name = newName;
|
||||
}
|
||||
try {
|
||||
await renameTaskList({ taskListId: listId, newName });
|
||||
} catch (e) {
|
||||
console.error('[TaskList] 重命名清单失败', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function onDeleteList(listId: string) {
|
||||
try {
|
||||
if (showListGroupView.value) {
|
||||
const favId = favoriteIdMap.get(listId);
|
||||
if (favId) {
|
||||
await removeFavorite({ favoriteId: favId });
|
||||
}
|
||||
} else {
|
||||
await deleteTaskListApi({ id: listId });
|
||||
}
|
||||
if (currentListId.value === listId) {
|
||||
const firstAvailable = findFirstAvailableList();
|
||||
currentListId.value = firstAvailable?.id || '';
|
||||
}
|
||||
await loadFavorites();
|
||||
} catch (e) {
|
||||
console.error('[TaskList] 删除清单失败', e);
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
for (const group of taskListGroups.value) {
|
||||
for (const list of group.taskLists) {
|
||||
if (list.id === listId) return list;
|
||||
}
|
||||
}
|
||||
for (const group of quickAccessGroups.value) {
|
||||
for (const list of group.taskLists) {
|
||||
if (list.id === listId) return list;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function findFirstAvailableList(): TaskList | null {
|
||||
for (const group of taskListGroups.value) {
|
||||
for (const list of group.taskLists) {
|
||||
if (list.delFlag !== 1) return list;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.task-page {
|
||||
display: flex;
|
||||
height: calc(100vh - 120px);
|
||||
background: #f5f5f5;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,240 @@
|
||||
export type ViewType = 'my-tasks' | 'followed' | 'all-tasks' | 'created' | 'assigned' | '';
|
||||
|
||||
export interface NavItem {
|
||||
id: ViewType;
|
||||
label: string;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
export const NAV_ITEMS: NavItem[] = [
|
||||
{ id: 'my-tasks', label: '我负责的', icon: 'ant-design:user-outlined' },
|
||||
{ id: 'followed', label: '我关注的', icon: 'ant-design:star-outlined' },
|
||||
];
|
||||
|
||||
export const QUICK_ACCESS_ITEMS: NavItem[] = [
|
||||
{ id: 'all-tasks', label: '全部清单', icon: 'ant-design:unordered-list-outlined' },
|
||||
{ id: 'created', label: '我所有的', icon: 'ant-design:edit-outlined' },
|
||||
{ id: 'assigned', label: '我协作的', icon: 'ant-design:send-outlined' },
|
||||
];
|
||||
|
||||
export const QUICK_ACCESS_TAB_MAP: Record<string, string> = {
|
||||
'all-tasks': 'all',
|
||||
created: 'own',
|
||||
assigned: 'collab',
|
||||
};
|
||||
|
||||
export const MAX_TASK_DEPTH = 5;
|
||||
|
||||
const AVATAR_COLORS = ['#3370ff', '#14c9c9', '#f77234', '#722ed1', '#eb2f96', '#f53f3f', '#0fc6c2', '#3491fa', '#9fdb1d', '#f7ba1e'];
|
||||
|
||||
export function getAvatarColor(name: string): string {
|
||||
let hash = 0;
|
||||
for (let i = 0; i < name.length; i++) {
|
||||
hash = name.charCodeAt(i) + ((hash << 5) - hash);
|
||||
}
|
||||
return AVATAR_COLORS[Math.abs(hash) % AVATAR_COLORS.length];
|
||||
}
|
||||
|
||||
export interface TaskItem {
|
||||
id: string;
|
||||
taskName: string;
|
||||
taskDesc: string;
|
||||
completed: boolean;
|
||||
priority: string;
|
||||
assigneeId: string;
|
||||
assigneeName: string;
|
||||
assignId: string;
|
||||
assignName: string;
|
||||
participantId: string;
|
||||
participantName: string;
|
||||
followersId: string;
|
||||
followersName: string;
|
||||
createBy: string;
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
completeTime: string;
|
||||
createTime: string;
|
||||
updateTime: string;
|
||||
type: string;
|
||||
subTaskCount: number;
|
||||
completedSubTaskCount: number;
|
||||
remark: string;
|
||||
groupId: string;
|
||||
groupName: string;
|
||||
childCount: number;
|
||||
completedChildCount: number;
|
||||
pid: string;
|
||||
hasChild: string;
|
||||
sortOrder: number;
|
||||
}
|
||||
|
||||
export interface TaskGroup {
|
||||
id: string;
|
||||
name: string;
|
||||
taskListId: string;
|
||||
collapsed: boolean;
|
||||
tasks: TaskItem[];
|
||||
}
|
||||
|
||||
export interface TaskList {
|
||||
id: string;
|
||||
name: string;
|
||||
pid: string;
|
||||
hasChild: string;
|
||||
type: number;
|
||||
sortOrder: number;
|
||||
delFlag: number;
|
||||
icon?: string;
|
||||
groups: TaskGroup[];
|
||||
ownerName?: string;
|
||||
collaboratorNames?: string;
|
||||
createTimeStr?: string;
|
||||
myPermission?: string;
|
||||
}
|
||||
|
||||
export interface TaskListGroup {
|
||||
id: string;
|
||||
name: string;
|
||||
pid: string;
|
||||
hasChild: string;
|
||||
type: number;
|
||||
sortOrder: number;
|
||||
delFlag: number;
|
||||
taskLists: TaskList[];
|
||||
}
|
||||
|
||||
export type StatusFilter = 'all' | 'completed' | 'uncompleted';
|
||||
|
||||
export type GroupDimension = 'custom' | 'assignee' | 'startTime' | 'endTime' | 'createBy' | 'priority' | 'none';
|
||||
|
||||
export type SortField = 'sortOrder' | 'startTime' | 'endTime' | 'createTime' | 'completeTime' | 'updateTime' | 'priority';
|
||||
|
||||
export type SortDirection = 'asc' | 'desc';
|
||||
|
||||
export type FieldKey =
|
||||
| 'taskDesc'
|
||||
| 'assigneeName'
|
||||
| 'participantName'
|
||||
| 'startTime'
|
||||
| 'endTime'
|
||||
| 'subTaskProgress'
|
||||
| 'createBy'
|
||||
| 'assignName'
|
||||
| 'followersName'
|
||||
| 'createTime'
|
||||
| 'completeTime'
|
||||
| 'updateTime'
|
||||
| 'priority'
|
||||
| 'remark';
|
||||
|
||||
export interface FieldConfig {
|
||||
key: FieldKey;
|
||||
label: string;
|
||||
defaultVisible: boolean;
|
||||
}
|
||||
|
||||
export const FIELD_CONFIG: FieldConfig[] = [
|
||||
{ key: 'taskDesc', label: '任务描述', defaultVisible: false },
|
||||
{ key: 'assigneeName', label: '负责人', defaultVisible: true },
|
||||
{ key: 'participantName', label: '参与人', defaultVisible: true },
|
||||
{ key: 'startTime', label: '开始时间', defaultVisible: true },
|
||||
{ key: 'endTime', label: '截止时间', defaultVisible: true },
|
||||
{ key: 'subTaskProgress', label: '子任务进度', defaultVisible: true },
|
||||
{ key: 'createBy', label: '创建人', defaultVisible: true },
|
||||
{ key: 'assignName', label: '分配人', defaultVisible: false },
|
||||
{ key: 'followersName', label: '关注人', defaultVisible: false },
|
||||
{ key: 'createTime', label: '创建时间', defaultVisible: false },
|
||||
{ key: 'completeTime', label: '完成时间', defaultVisible: true },
|
||||
{ key: 'updateTime', label: '更新时间', defaultVisible: false },
|
||||
{ key: 'priority', label: '优先级', defaultVisible: true },
|
||||
{ key: 'remark', label: '其他事项说明', defaultVisible: true },
|
||||
];
|
||||
|
||||
export const DEFAULT_VISIBLE_FIELDS: FieldKey[] = FIELD_CONFIG.filter((f) => f.defaultVisible).map((f) => f.key);
|
||||
|
||||
export interface GroupOption {
|
||||
value: GroupDimension;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export const GROUP_OPTIONS: GroupOption[] = [
|
||||
{ value: 'custom', label: '自定义分组' },
|
||||
{ value: 'assignee', label: '按负责人' },
|
||||
{ value: 'startTime', label: '按开始时间' },
|
||||
{ value: 'endTime', label: '按截止时间' },
|
||||
{ value: 'createBy', label: '按创建人' },
|
||||
{ value: 'priority', label: '按优先级' },
|
||||
{ value: 'none', label: '无分组' },
|
||||
];
|
||||
|
||||
export interface SortOption {
|
||||
value: SortField;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export const SORT_OPTIONS: SortOption[] = [
|
||||
{ value: 'sortOrder', label: '拖拽自定义' },
|
||||
{ value: 'createTime', label: '创建时间' },
|
||||
{ value: 'startTime', label: '开始时间' },
|
||||
{ value: 'endTime', label: '截止时间' },
|
||||
{ value: 'completeTime', label: '完成时间' },
|
||||
{ value: 'updateTime', label: '更新时间' },
|
||||
{ value: 'priority', label: '优先级' },
|
||||
];
|
||||
|
||||
export type FilterFieldType =
|
||||
| 'assigneeName'
|
||||
| 'createBy'
|
||||
| 'assignName'
|
||||
| 'followersName'
|
||||
| 'priority'
|
||||
| 'startTime'
|
||||
| 'endTime'
|
||||
| 'createTime'
|
||||
| 'completeTime'
|
||||
| 'updateTime';
|
||||
|
||||
export type FilterOperator = 'contains' | 'equals' | 'before' | 'after' | 'between' | 'isEmpty' | 'isNotEmpty';
|
||||
|
||||
export interface FilterFieldConfig {
|
||||
value: FilterFieldType;
|
||||
label: string;
|
||||
type: 'text' | 'select' | 'date';
|
||||
operators: FilterOperator[];
|
||||
}
|
||||
|
||||
export const FILTER_FIELD_CONFIGS: FilterFieldConfig[] = [
|
||||
{ value: 'assigneeName', label: '负责人', type: 'text', operators: ['contains', 'isEmpty', 'isNotEmpty'] },
|
||||
{ value: 'createBy', label: '创建人', type: 'text', operators: ['contains', 'isEmpty', 'isNotEmpty'] },
|
||||
{ value: 'assignName', label: '分配人', type: 'text', operators: ['contains', 'isEmpty', 'isNotEmpty'] },
|
||||
{ value: 'followersName', label: '关注人', type: 'text', operators: ['contains', 'isEmpty', 'isNotEmpty'] },
|
||||
{ value: 'priority', label: '优先级', type: 'select', operators: ['equals', 'isEmpty', 'isNotEmpty'] },
|
||||
{ value: 'startTime', label: '开始时间', type: 'date', operators: ['before', 'after', 'between', 'isEmpty', 'isNotEmpty'] },
|
||||
{ value: 'endTime', label: '截止时间', type: 'date', operators: ['before', 'after', 'between', 'isEmpty', 'isNotEmpty'] },
|
||||
{ value: 'createTime', label: '创建时间', type: 'date', operators: ['before', 'after', 'between', 'isEmpty', 'isNotEmpty'] },
|
||||
{ value: 'completeTime', label: '完成时间', type: 'date', operators: ['before', 'after', 'between', 'isEmpty', 'isNotEmpty'] },
|
||||
{ value: 'updateTime', label: '更新时间', type: 'date', operators: ['before', 'after', 'between', 'isEmpty', 'isNotEmpty'] },
|
||||
];
|
||||
|
||||
export const FILTER_OPERATOR_LABELS: Record<FilterOperator, string> = {
|
||||
contains: '包含',
|
||||
equals: '等于',
|
||||
before: '早于',
|
||||
after: '晚于',
|
||||
between: '介于',
|
||||
isEmpty: '为空',
|
||||
isNotEmpty: '不为空',
|
||||
};
|
||||
|
||||
export interface FilterCondition {
|
||||
id: string;
|
||||
field: FilterFieldType;
|
||||
operator: FilterOperator;
|
||||
value: any;
|
||||
valueEnd?: any;
|
||||
}
|
||||
|
||||
export interface FlatTaskItem {
|
||||
task: TaskItem;
|
||||
level: number;
|
||||
}
|
||||
Reference in New Issue
Block a user