!3 czh-20260501-增加密级相关功能
Merge pull request !3 from 陈志浩/feature/tasklist
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="1em" height="1em" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M15.5 6.5a5 5 0 1 0-10.002.002A5 5 0 0 0 15.5 6.5Zm-5 3a3.001 3.001 0 0 1 0-6 3.001 3.001 0 0 1 0 6ZM3 19c0-.346.045-.68.125-1 .445-1.725 2.01-3 3.875-3h5.98v-2H7a6 6 0 0 0-6 6v1c0 1.1.9 2 2 2h9.954v-2H3v-1Zm15.5 3a1 1 0 0 1-1-1v-2h-2a1 1 0 1 1 0-2h2v-2a1 1 0 1 1 2 0v2h2a1 1 0 1 1 0 2h-2v2a1 1 0 0 1-1 1Z" fill="currentColor"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 444 B |
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="1em" height="1em" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M11.22 18.46a1 1 0 0 0 1.56 0l8.305-10.334a1 1 0 0 0-.78-1.626H3.696a1 1 0 0 0-.78 1.626L11.22 18.46Z" fill="currentColor"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 240 B |
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="1em" height="1em" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M12.304 7.315a1 1 0 0 1 1.414 1.414L8.13 14.317a1.485 1.485 0 0 0 0 2.1l.01.011a1.5 1.5 0 0 0 2.117-.005l7.43-7.43a3.5 3.5 0 0 0 0-4.95l-.036-.037a3.5 3.5 0 0 0-4.95 0l-7.778 7.777a5.521 5.521 0 0 0 7.808 7.809l7.07-7.07a1 1 0 0 1 1.415 1.414l-7.07 7.07A7.521 7.521 0 0 1 3.509 10.37l7.778-7.778a5.5 5.5 0 0 1 7.778 0l.037.037a5.5 5.5 0 0 1 0 7.778l-7.43 7.43a3.5 3.5 0 0 1-4.939.012l-.006-.006-.012-.012a3.485 3.485 0 0 1 0-4.928l5.589-5.588Z" fill="currentColor"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 582 B |
@@ -0,0 +1,4 @@
|
|||||||
|
<svg width="1em" height="1em" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M14 15a1 1 0 0 1 1-1h1a1 1 0 0 1 1 1v1a1 1 0 0 1-1 1h-1a1 1 0 0 1-1-1v-1Z" fill="currentColor"/>
|
||||||
|
<path d="M2 4a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v16a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V4Zm2 0v2h16V4H4Zm16 4H4v12h16V8Z" fill="currentColor"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 348 B |
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="1em" height="1em" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M2 5a1 1 0 0 1 1-1h18a1 1 0 1 1 0 2H3a1 1 0 0 1-1-1Zm0 7a1 1 0 0 1 1-1h11a1 1 0 1 1 0 2H3a1 1 0 0 1-1-1Zm1 6a1 1 0 1 0 0 2h11a1 1 0 1 0 0-2H3Z" fill="currentColor"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 281 B |
@@ -0,0 +1,8 @@
|
|||||||
|
<svg width="1em" height="1em" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<circle cx="9" cy="6" r="1.5" fill="currentColor"/>
|
||||||
|
<circle cx="15" cy="6" r="1.5" fill="currentColor"/>
|
||||||
|
<circle cx="9" cy="12" r="1.5" fill="currentColor"/>
|
||||||
|
<circle cx="15" cy="12" r="1.5" fill="currentColor"/>
|
||||||
|
<circle cx="9" cy="18" r="1.5" fill="currentColor"/>
|
||||||
|
<circle cx="15" cy="18" r="1.5" fill="currentColor"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 435 B |
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="1em" height="1em" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M15 6.5a3 3 0 1 0-6 0 3 3 0 0 0 6 0Zm2 0a5 5 0 1 1-10 0 5 5 0 0 1 10 0ZM4 19v2h16v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4Zm-2 0a6 6 0 0 1 6-6h8a6 6 0 0 1 6 6v2a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2v-2Z" fill="currentColor"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 325 B |
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="1em" height="1em" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M8.25 6.5a1.75 1.75 0 1 0 0-3.5 1.75 1.75 0 0 0 0 3.5Zm0 7.25a1.75 1.75 0 1 0 0-3.5 1.75 1.75 0 0 0 0 3.5Zm1.75 5.5a1.75 1.75 0 1 1-3.5 0 1.75 1.75 0 0 1 3.5 0ZM14.753 6.5a1.75 1.75 0 1 0 0-3.5 1.75 1.75 0 0 0 0 3.5ZM16.5 12a1.75 1.75 0 1 1-3.5 0 1.75 1.75 0 0 1 3.5 0Zm-1.747 9a1.75 1.75 0 1 0 0-3.5 1.75 1.75 0 0 0 0 3.5Z" fill="currentColor"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 462 B |
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M3 4.5A2.5 2.5 0 0 1 5.5 2h6.879a2.5 2.5 0 0 1 1.767.732l5.122 5.122a2.5 2.5 0 0 1 .732 1.767V19.5a2.5 2.5 0 0 1-2.5 2.5h-13A2.5 2.5 0 0 1 2 19.5v-15ZM5.5 3.5a1 1 0 0 0-1 1v15a1 1 0 0 0 1 1h13a1 1 0 0 0 1-1V9.621a1 1 0 0 0-.293-.707l-5.122-5.121A1 1 0 0 0 13.38 3.5H5.5Z" fill="currentColor"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 407 B |
@@ -0,0 +1,4 @@
|
|||||||
|
<svg width="1em" height="1em" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M7.755 11.658a1 1 0 0 1 1.416-1.415L12 13.07l2.828-2.829a1 1 0 0 1 1.416 1.416c-1.181 1.189-2.356 2.386-3.553 3.56a.987.987 0 0 1-1.383 0c-1.196-1.175-2.371-2.371-3.553-3.56Z" fill="currentColor"/>
|
||||||
|
<path d="M12 23C5.925 23 1 18.075 1 12S5.925 1 12 1s11 4.925 11 11-4.925 11-11 11Zm0-2a9 9 0 1 0 0-18 9 9 0 0 0 0 18Z" fill="currentColor"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 456 B |
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="1em" height="1em" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M12 1a1 1 0 0 1 1 1v1.143A9.009 9.009 0 0 1 20.857 11H22a1 1 0 1 1 0 2h-1.143A9.009 9.009 0 0 1 13 20.857V22a1 1 0 1 1-2 0v-1.143A9.009 9.009 0 0 1 3.143 13H2a1 1 0 1 1 0-2h1.143A9.009 9.009 0 0 1 11 3.143V2a1 1 0 0 1 1-1Zm0 4a7 7 0 1 0 0 14 7 7 0 0 0 0-14Zm0 3a4 4 0 1 1 0 8 4 4 0 0 1 0-8Z" fill="currentColor"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 429 B |
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="1em" height="1em" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M4 3a1 1 0 0 0-1 1v16a1 1 0 0 0 1 1h16a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1H4Zm1 2h14v14H5V5Zm2 3h10v2H7V8Zm0 4h7v2H7v-2Z" fill="currentColor"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 253 B |
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="1em" height="1em" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M10.868 2.884c-.321-.772-1.415-.772-1.736 0l-1.83 4.401-4.753.381c-.833.067-1.171 1.111-.536 1.607l3.619 3.099-1.106 4.635c-.194.813.691 1.456 1.405 1.02l4.09-2.489 4.09 2.489c.714.436 1.599-.207 1.405-1.02l-1.106-4.635 3.619-3.099c.635-.496.297-1.54-.536-1.607l-4.753-.381-1.83-4.401Z" fill="currentColor"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 424 B |
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="1em" height="1em" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M11.48 3.499a.562.562 0 0 1 1.04 0l2.125 5.111a.563.563 0 0 0 .475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 0 0-.182.557l1.285 5.385a.562.562 0 0 1-.84.61l-4.725-2.885a.562.562 0 0 0-.586 0L6.982 20.54a.562.562 0 0 1-.84-.61l1.285-5.386a.562.562 0 0 0-.182-.557l-4.204-3.602a.562.562 0 0 1 .321-.988l5.518-.442a.563.563 0 0 0 .475-.345L11.48 3.5Z" fill="currentColor"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 506 B |
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="1em" height="1em" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M8.25 6.5a1.75 1.75 0 1 0 0-3.5 1.75 1.75 0 0 0 0 3.5Zm0 7.25a1.75 1.75 0 1 0 0-3.5 1.75 1.75 0 0 0 0 3.5Zm1.75 5.5a1.75 1.75 0 1 1-3.5 0 1.75 1.75 0 0 1 3.5 0ZM14.753 6.5a1.75 1.75 0 1 0 0-3.5 1.75 1.75 0 0 0 0 3.5ZM16.5 12a1.75 1.75 0 1 1-3.5 0 1.75 1.75 0 0 1 3.5 0Zm-1.747 9a1.75 1.75 0 1 0 0-3.5 1.75 1.75 0 0 0 0 3.5Z" fill="currentColor"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 462 B |
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="1em" height="1em" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M17 14a3.001 3.001 0 0 1-2.83-2H7v1a5 5 0 0 0 5 5h2.17a3.001 3.001 0 1 1 0 2H12a7 7 0 0 1-7-7V3a1 1 0 0 1 2 0v7h7.17A3.001 3.001 0 1 1 17 14Zm-1 5a1 1 0 1 0 2 0 1 1 0 0 0-2 0Zm1-7a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z" fill="currentColor"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 347 B |
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="1em" height="1em" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M4 3a1 1 0 0 0-1 1v16a1 1 0 0 0 1 1h16a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1H4Zm1 2h14v14H5V5Zm2 3h10v2H7V8Zm0 4h7v2H7v-2Z" fill="currentColor"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 253 B |
@@ -0,0 +1,4 @@
|
|||||||
|
<svg width="1em" height="1em" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M11 8a1 1 0 1 1 2 0v3h3a1 1 0 1 1 0 2h-4a1 1 0 0 1-1-1V8Z" fill="currentColor"/>
|
||||||
|
<path d="M12 23c6.075 0 11-4.925 11-11S18.075 1 12 1 1 5.925 1 12s4.925 11 11 11Zm0-2a9 9 0 1 1 0-18 9 9 0 0 1 0 18Z" fill="currentColor"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 338 B |
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="1em" height="1em" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M8.437 4.898 5.447 13h6.063L8.437 4.898Zm6.025 15.881L12.269 15h-7.56l-2.131 5.78a1 1 0 1 1-1.873-.703L7.02 2.982c.491-1.31 2.344-1.31 2.835 0l6.48 17.095a1 1 0 1 1-1.872.702ZM15.056 5a1 1 0 1 0 0 2H23a1 1 0 1 0 0-2h-7.944Zm1.055 7a1 1 0 0 1 1-1H23a1 1 0 1 1 0 2h-5.89a1 1 0 0 1-1-1Zm3.056 5a1 1 0 1 0 0 2H23a1 1 0 1 0 0-2h-3.833Z" fill="currentColor"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 469 B |
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="1em" height="1em" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M15 6.5a3 3 0 1 0-6 0 3 3 0 0 0 6 0Zm2 0a5 5 0 1 1-10 0 5 5 0 0 1 10 0ZM4 19v2h16v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4Zm-2 0a6 6 0 0 1 6-6h8a6 6 0 0 1 6 6v2a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2v-2Z" fill="currentColor"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 325 B |
@@ -0,0 +1,155 @@
|
|||||||
|
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',
|
||||||
|
getCurrentUserSecurityLevel = '/tasklist/taskList/getCurrentUserSecurityLevel',
|
||||||
|
}
|
||||||
|
|
||||||
|
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 });
|
||||||
|
|
||||||
|
export const getCurrentUserSecurityLevel = () => defHttp.get({ url: Api.getCurrentUserSecurityLevel });
|
||||||
@@ -0,0 +1,606 @@
|
|||||||
|
import { BasicColumn } from '/@/components/Table';
|
||||||
|
import { FormSchema } from '/@/components/Table';
|
||||||
|
import { JVxeTypes, JVxeColumn } from '/@/components/jeecg/JVxeTable/types';
|
||||||
|
import type { TaskListGroup } from './types';
|
||||||
|
import { getAvailableSecretLevels } 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 function getCreateListFormSchema(userSecurityLevel: number): FormSchema[] {
|
||||||
|
if (!userSecurityLevel) userSecurityLevel = 3;
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
label: '清单名称',
|
||||||
|
field: 'tasklistName',
|
||||||
|
component: 'Input',
|
||||||
|
required: true,
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入清单名称',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '密级',
|
||||||
|
field: 'secretLevel',
|
||||||
|
component: 'Select',
|
||||||
|
required: true,
|
||||||
|
defaultValue: 1,
|
||||||
|
componentProps: {
|
||||||
|
getPopupContainer: () => document.body,
|
||||||
|
options: getAvailableSecretLevels(userSecurityLevel),
|
||||||
|
placeholder: '请选择密级',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
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,506 @@
|
|||||||
|
<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 allMembers" :key="item.permissionId || item.userId" class="member-item" @mouseenter="hoverId = item.permissionId || 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>
|
||||||
|
<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 && item.permissionId">
|
||||||
|
<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="allMembers.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">
|
||||||
|
<SzUserSelect
|
||||||
|
v-model:value="addForm.userIds"
|
||||||
|
:labelKey="'realname'"
|
||||||
|
:rowKey="'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="onConfirmAdd">
|
||||||
|
确认添加
|
||||||
|
</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="onDone">完成</a-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a-modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, reactive, computed } from 'vue';
|
||||||
|
import { message } from 'ant-design-vue';
|
||||||
|
import Icon from '/@/components/Icon/index';
|
||||||
|
import SzUserSelect from '/@/components/semri/userComponent/SzUserSelect.vue';
|
||||||
|
import { getCollaborators, addCollaborator, removeCollaborator, updateCollaboratorPermission } from '../TaskList.api';
|
||||||
|
import { getAvatarColor } from '../types';
|
||||||
|
import { checkUserSecLevel } from '../utils/taskUtils';
|
||||||
|
import { useMessage } from '/@/hooks/web/useMessage';
|
||||||
|
|
||||||
|
const { createMessage } = useMessage();
|
||||||
|
|
||||||
|
interface CollaboratorItem {
|
||||||
|
permissionId: string;
|
||||||
|
userId: string;
|
||||||
|
username: string;
|
||||||
|
permission: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PendingAddItem {
|
||||||
|
userId: string;
|
||||||
|
username: string;
|
||||||
|
permission: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
taskListId: string;
|
||||||
|
isOwner: boolean;
|
||||||
|
secretLevel?: number;
|
||||||
|
secretText?: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
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',
|
||||||
|
});
|
||||||
|
const pendingAddList = ref<PendingAddItem[]>([]);
|
||||||
|
|
||||||
|
const allMembers = computed(() => {
|
||||||
|
const pendingAsCollab: CollaboratorItem[] = pendingAddList.value.map((p) => ({
|
||||||
|
permissionId: '',
|
||||||
|
userId: p.userId,
|
||||||
|
username: p.username,
|
||||||
|
permission: p.permission,
|
||||||
|
}));
|
||||||
|
return [...collaborators.value, ...pendingAsCollab];
|
||||||
|
});
|
||||||
|
|
||||||
|
function getColor(name: string): string {
|
||||||
|
return getAvatarColor(name || '');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function open() {
|
||||||
|
visible.value = true;
|
||||||
|
showAddPanel.value = false;
|
||||||
|
pendingAddList.value = [];
|
||||||
|
addForm.userIds = '';
|
||||||
|
addForm.permission = '2';
|
||||||
|
await loadCollaborators();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onClose() {
|
||||||
|
visible.value = false;
|
||||||
|
showAddPanel.value = false;
|
||||||
|
addForm.userIds = '';
|
||||||
|
addForm.permission = '2';
|
||||||
|
pendingAddList.value = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
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 onConfirmAdd() {
|
||||||
|
if (!addForm.userIds) return;
|
||||||
|
const ids = addForm.userIds.split(',').filter(Boolean);
|
||||||
|
if (ids.length === 0) return;
|
||||||
|
|
||||||
|
const secretLevel = props.secretLevel || 1;
|
||||||
|
const { ok, invalidNames } = await checkUserSecLevel(addForm.userIds, secretLevel);
|
||||||
|
if (!ok) {
|
||||||
|
message.warning(`以下人员的密级不满足当前清单(${props.secretText || '非密'})要求:${invalidNames.join('、')}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nameMap: Record<string, string> = {};
|
||||||
|
try {
|
||||||
|
const { getUserList } = await import('/@/api/common/api');
|
||||||
|
const res = await getUserList({ id: ids.join(','), pageNo: 1, pageSize: ids.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('获取用户名失败', e);
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingIds = new Set(collaborators.value.map((c) => c.userId));
|
||||||
|
const pendingIds = new Set(pendingAddList.value.map((p) => p.userId));
|
||||||
|
|
||||||
|
let addedCount = 0;
|
||||||
|
for (const uid of ids) {
|
||||||
|
if (!existingIds.has(uid) && !pendingIds.has(uid)) {
|
||||||
|
pendingAddList.value.push({
|
||||||
|
userId: uid,
|
||||||
|
username: nameMap[uid] || uid,
|
||||||
|
permission: addForm.permission,
|
||||||
|
});
|
||||||
|
addedCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (addedCount > 0) {
|
||||||
|
createMessage.success(`已添加 ${addedCount} 位协作人`);
|
||||||
|
} else {
|
||||||
|
createMessage.warning('所选用户均已是协作人');
|
||||||
|
}
|
||||||
|
|
||||||
|
addForm.userIds = '';
|
||||||
|
addForm.permission = '2';
|
||||||
|
showAddPanel.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onDone() {
|
||||||
|
if (pendingAddList.value.length > 0) {
|
||||||
|
let successCount = 0;
|
||||||
|
let failCount = 0;
|
||||||
|
for (const item of pendingAddList.value) {
|
||||||
|
try {
|
||||||
|
await addCollaborator({
|
||||||
|
taskListId: props.taskListId,
|
||||||
|
userId: item.userId,
|
||||||
|
permission: item.permission,
|
||||||
|
});
|
||||||
|
successCount++;
|
||||||
|
} catch {
|
||||||
|
failCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (successCount > 0) {
|
||||||
|
createMessage.success(`成功添加 ${successCount} 位协作人${failCount > 0 ? `,${failCount} 位失败` : ''}`);
|
||||||
|
} else {
|
||||||
|
createMessage.warning('添加协作人失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingAddList.value = [];
|
||||||
|
emit('changed');
|
||||||
|
}
|
||||||
|
|
||||||
|
visible.value = false;
|
||||||
|
showAddPanel.value = false;
|
||||||
|
addForm.userIds = '';
|
||||||
|
addForm.permission = '2';
|
||||||
|
}
|
||||||
|
|
||||||
|
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,49 @@
|
|||||||
|
<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 { getCreateListFormSchema } from '../TaskList.data';
|
||||||
|
import { addTaskList } from '../TaskList.api';
|
||||||
|
import { getSecretText } from '../types';
|
||||||
|
|
||||||
|
const emit = defineEmits(['register', 'success']);
|
||||||
|
|
||||||
|
let pendingGroupId = '';
|
||||||
|
let pendingUserSecurityLevel = 3;
|
||||||
|
|
||||||
|
const [registerForm, { resetFields, validate, updateSchema }] = useForm({
|
||||||
|
labelWidth: 100,
|
||||||
|
schemas: getCreateListFormSchema(3),
|
||||||
|
showActionButtonGroup: false,
|
||||||
|
baseColProps: { span: 24 },
|
||||||
|
});
|
||||||
|
|
||||||
|
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
|
||||||
|
await resetFields();
|
||||||
|
pendingGroupId = data?.groupId || '';
|
||||||
|
pendingUserSecurityLevel = data?.userSecurityLevel || 3;
|
||||||
|
updateSchema(getCreateListFormSchema(pendingUserSecurityLevel));
|
||||||
|
});
|
||||||
|
|
||||||
|
async function handleSubmit() {
|
||||||
|
try {
|
||||||
|
setModalProps({ confirmLoading: true });
|
||||||
|
const values = await validate();
|
||||||
|
const result = await addTaskList({
|
||||||
|
tasklistName: values.tasklistName,
|
||||||
|
pid: pendingGroupId || undefined,
|
||||||
|
secretLevel: values.secretLevel,
|
||||||
|
secretText: getSecretText(values.secretLevel),
|
||||||
|
});
|
||||||
|
closeModal();
|
||||||
|
emit('success', result);
|
||||||
|
} finally {
|
||||||
|
setModalProps({ confirmLoading: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
<template>
|
||||||
|
<BasicModal v-bind="$attrs" @register="registerModal" destroy-on-close title="创建任务" :width="600" @ok="handleSubmit">
|
||||||
|
<BasicForm @register="registerForm" />
|
||||||
|
<div class="attachment-row">
|
||||||
|
<span class="attachment-label">附件</span>
|
||||||
|
<span class="attachment-trigger" @click="openFileUploadModal">添加附件</span>
|
||||||
|
<TaskAttachmentUpload
|
||||||
|
ref="fileUploadRef"
|
||||||
|
:bussiness-id="''"
|
||||||
|
:bussiness-secret-level="currentSecretLevel"
|
||||||
|
:disabled="false"
|
||||||
|
style="display: none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</BasicModal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { message } from 'ant-design-vue';
|
||||||
|
import { BasicModal, useModalInner } from '/@/components/Modal';
|
||||||
|
import { BasicForm, useForm } from '/@/components/Form/index';
|
||||||
|
import TaskAttachmentUpload from './TaskAttachmentUpload.vue';
|
||||||
|
import { createTaskFormSchema } from '../TaskList.data';
|
||||||
|
import { addTask } from '../TaskList.api';
|
||||||
|
import { checkUserSecLevel } from '../utils/taskUtils';
|
||||||
|
|
||||||
|
const emit = defineEmits(['register', 'success']);
|
||||||
|
const groupId = ref('');
|
||||||
|
const mainId = ref('');
|
||||||
|
const currentSecretLevel = ref(1);
|
||||||
|
const currentSecretText = ref('非密');
|
||||||
|
const fileUploadRef = ref<InstanceType<typeof TaskAttachmentUpload> | null>(null);
|
||||||
|
|
||||||
|
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 || '';
|
||||||
|
currentSecretLevel.value = data?.secretLevel || 1;
|
||||||
|
currentSecretText.value = data?.secretText || '非密';
|
||||||
|
if (groupId.value) {
|
||||||
|
await setFieldsValue({ groupId: groupId.value });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function handleSubmit() {
|
||||||
|
try {
|
||||||
|
setModalProps({ confirmLoading: true });
|
||||||
|
const values = await validate();
|
||||||
|
|
||||||
|
if (values.assigneeId) {
|
||||||
|
const { ok, invalidNames } = await checkUserSecLevel(values.assigneeId, currentSecretLevel.value);
|
||||||
|
if (!ok) {
|
||||||
|
message.warning(`以下人员的密级不满足当前清单(${currentSecretText.value})要求:${invalidNames.join('、')}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = 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,
|
||||||
|
});
|
||||||
|
if (fileUploadRef.value?.hasPendingFiles()) {
|
||||||
|
await fileUploadRef.value.bindBussinessId(result);
|
||||||
|
}
|
||||||
|
closeModal();
|
||||||
|
emit('success');
|
||||||
|
} finally {
|
||||||
|
setModalProps({ confirmLoading: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function openFileUploadModal() {
|
||||||
|
fileUploadRef.value?.handleOpenUploadModal();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.attachment-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 0 16px;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attachment-label {
|
||||||
|
width: 100px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
text-align: right;
|
||||||
|
font-size: 14px;
|
||||||
|
color: rgba(0, 0, 0, 0.85);
|
||||||
|
}
|
||||||
|
|
||||||
|
.attachment-trigger {
|
||||||
|
color: #8c8c8c;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
&:hover {
|
||||||
|
color: #3370ff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,462 @@
|
|||||||
|
<!-- 文件上传下载删除组件(任务附件专用副本) -->
|
||||||
|
<template>
|
||||||
|
<div class="s-upload-file-container">
|
||||||
|
<div class="upload-btn-wrapper" v-if="!disabled">
|
||||||
|
<a-button type="primary" @click="handleOpenUploadModal">
|
||||||
|
<Icon icon="ant-design:upload-outlined" />
|
||||||
|
选择文件上传
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="file-list-wrapper">
|
||||||
|
<div v-if="displayFileList.length > 0" class="file-list-header">
|
||||||
|
<span class="file-count">共 {{ displayFileList.length }} 个文件</span>
|
||||||
|
<a-button
|
||||||
|
type="link"
|
||||||
|
size="small"
|
||||||
|
:loading="downloadingAll"
|
||||||
|
@click="handleDownloadAll"
|
||||||
|
>
|
||||||
|
<Icon icon="ant-design:download-outlined" />
|
||||||
|
下载全部
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
<a-spin :spinning="loading">
|
||||||
|
<a-empty v-if="!loading && displayFileList.length === 0" description="暂无文件" />
|
||||||
|
<div v-else class="file-list">
|
||||||
|
<div
|
||||||
|
v-for="file in displayFileList"
|
||||||
|
:key="file.fileId || file.id"
|
||||||
|
class="file-item"
|
||||||
|
>
|
||||||
|
<div class="file-info">
|
||||||
|
<Icon icon="ant-design:file-outlined" class="file-icon" />
|
||||||
|
<span class="file-name">{{ file.fileName }}</span>
|
||||||
|
<a-tag :color="getSecretLevelColor(file.secretLevel)" class="secret-tag">
|
||||||
|
{{ getSecretLevelLabel(file.secretLevel) }}
|
||||||
|
</a-tag>
|
||||||
|
<a-tag v-if="file.pending" color="orange" class="secret-tag">
|
||||||
|
待保存
|
||||||
|
</a-tag>
|
||||||
|
<span v-if="file.fileSize" class="file-size">{{ formatFileSize(file.fileSize) }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="file-actions">
|
||||||
|
<a-button
|
||||||
|
type="link"
|
||||||
|
size="small"
|
||||||
|
@click="handleDownload(file)"
|
||||||
|
>
|
||||||
|
<Icon icon="ant-design:download-outlined" />
|
||||||
|
下载
|
||||||
|
</a-button>
|
||||||
|
<a-button
|
||||||
|
v-if="!disabled"
|
||||||
|
type="link"
|
||||||
|
size="small"
|
||||||
|
danger
|
||||||
|
@click="handleDelete(file)"
|
||||||
|
>
|
||||||
|
<Icon icon="ant-design:delete-outlined" />
|
||||||
|
删除
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a-spin>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<TaskAttachmentUploadModal
|
||||||
|
v-model:visible="uploadModalVisible"
|
||||||
|
:secret-level-options="secretLevelOptions"
|
||||||
|
:business-id="bussinessId"
|
||||||
|
:multiple="multiple"
|
||||||
|
@upload-success="handleUploadSuccess"
|
||||||
|
@upload-error="handleUploadError"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, computed, watch } from 'vue';
|
||||||
|
import { Icon } from '/src/components/Icon';
|
||||||
|
import { defHttp } from '/src/utils/http/axios';
|
||||||
|
import { useMessage } from '/src/hooks/web/useMessage';
|
||||||
|
import TaskAttachmentUploadModal from './TaskAttachmentUploadModal.vue';
|
||||||
|
|
||||||
|
const { createMessage, createConfirm } = useMessage();
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
bussinessId: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
bussinessSecretLevel: {
|
||||||
|
type: Number,
|
||||||
|
default: 1,
|
||||||
|
validator: (value: number) => [1, 2, 3, 4].includes(value)
|
||||||
|
},
|
||||||
|
disabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
multiple: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['upload-success', 'delete-success', 'upload-error']);
|
||||||
|
|
||||||
|
const serverFileList = ref<any[]>([]);
|
||||||
|
const pendingFileList = ref<any[]>([]);
|
||||||
|
const loading = ref<boolean>(false);
|
||||||
|
const uploadModalVisible = ref<boolean>(false);
|
||||||
|
const binding = ref<boolean>(false);
|
||||||
|
const downloadingAll = ref<boolean>(false);
|
||||||
|
|
||||||
|
const displayFileList = computed(() => {
|
||||||
|
return [
|
||||||
|
...serverFileList.value.map(f => ({ ...f, pending: false })),
|
||||||
|
...pendingFileList.value.map(f => ({ ...f, pending: true }))
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
const SECRET_LEVEL_CONFIG = {
|
||||||
|
1: { label: '非密', color: 'success' },
|
||||||
|
2: { label: '内部', color: 'processing' },
|
||||||
|
3: { label: '秘密', color: 'warning' },
|
||||||
|
4: { label: '机密', color: 'error' }
|
||||||
|
};
|
||||||
|
|
||||||
|
function getSecretLevelInfo(level: number) {
|
||||||
|
return SECRET_LEVEL_CONFIG[level] || SECRET_LEVEL_CONFIG[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSecretLevelLabel(level: number) {
|
||||||
|
return getSecretLevelInfo(level).label;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSecretLevelColor(level: number) {
|
||||||
|
return getSecretLevelInfo(level).color;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatFileSize(bytes: number): string {
|
||||||
|
if (!bytes || bytes === 0) return '';
|
||||||
|
const k = 1024;
|
||||||
|
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
const secretLevelOptions = computed(() => {
|
||||||
|
const level = props.bussinessSecretLevel;
|
||||||
|
const options = [];
|
||||||
|
options.push({ label: '非密', value: 1 });
|
||||||
|
if (level >= 2) options.push({ label: '内部', value: 2 });
|
||||||
|
if (level >= 3) options.push({ label: '秘密', value: 3 });
|
||||||
|
if (level >= 4) options.push({ label: '机密', value: 4 });
|
||||||
|
return options;
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleOpenUploadModal() {
|
||||||
|
uploadModalVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleUploadSuccess(result: any) {
|
||||||
|
if (result.results && Array.isArray(result.results)) {
|
||||||
|
result.results.forEach((fileInfo: any) => {
|
||||||
|
if (!fileInfo.bound) {
|
||||||
|
pendingFileList.value.push({
|
||||||
|
fileId: fileInfo.fileId,
|
||||||
|
fileName: fileInfo.fileName,
|
||||||
|
filePath: fileInfo.filePath,
|
||||||
|
fileSize: fileInfo.fileSize,
|
||||||
|
secretLevel: fileInfo.secretLevel,
|
||||||
|
fileUploadType: fileInfo.fileUploadType
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (props.bussinessId) {
|
||||||
|
await loadFileList();
|
||||||
|
}
|
||||||
|
emit('upload-success', result);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleUploadError(error: any) {
|
||||||
|
emit('upload-error', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDownload(file: any) {
|
||||||
|
try {
|
||||||
|
const fileUrl = file.filePath;
|
||||||
|
const fileName = file.fileName || fileUrl.substring(fileUrl.lastIndexOf('/') + 1);
|
||||||
|
const domainUrl = import.meta.env.VITE_GLOB_DOMAIN_URL;
|
||||||
|
const data = await defHttp.get(
|
||||||
|
{ url: `${domainUrl}/sys/file/download`, params: { fileUrl }, responseType: 'blob' },
|
||||||
|
{ isTransformResponse: false }
|
||||||
|
);
|
||||||
|
if (!data || data.size === 0) {
|
||||||
|
createMessage.warning('文件下载失败');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (typeof window.navigator.msSaveBlob !== 'undefined') {
|
||||||
|
window.navigator.msSaveBlob(new Blob([data]), fileName);
|
||||||
|
} else {
|
||||||
|
const blobUrl = window.URL.createObjectURL(new Blob([data]));
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.style.display = 'none';
|
||||||
|
link.href = blobUrl;
|
||||||
|
link.setAttribute('download', fileName);
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
window.URL.revokeObjectURL(blobUrl);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('下载失败:', error);
|
||||||
|
createMessage.error('文件下载失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDownloadAll() {
|
||||||
|
const files = displayFileList.value;
|
||||||
|
if (files.length === 0) return;
|
||||||
|
|
||||||
|
downloadingAll.value = true;
|
||||||
|
let successCount = 0;
|
||||||
|
let failCount = 0;
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
try {
|
||||||
|
await handleDownload(file);
|
||||||
|
successCount++;
|
||||||
|
} catch (error) {
|
||||||
|
failCount++;
|
||||||
|
console.error(`下载文件"${file.fileName}"失败:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadingAll.value = false;
|
||||||
|
|
||||||
|
if (successCount > 0) {
|
||||||
|
createMessage.success(`成功下载 ${successCount} 个文件${failCount > 0 ? `,${failCount} 个失败` : ''}`);
|
||||||
|
} else {
|
||||||
|
createMessage.error('所有文件下载失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDelete(file: any) {
|
||||||
|
createConfirm({
|
||||||
|
iconType: 'warning',
|
||||||
|
title: '确认删除',
|
||||||
|
content: `确定要删除文件"${file.fileName}"吗?`,
|
||||||
|
okText: '确认',
|
||||||
|
cancelText: '取消',
|
||||||
|
onOk: async () => {
|
||||||
|
try {
|
||||||
|
loading.value = true;
|
||||||
|
await defHttp.get({
|
||||||
|
url: '/sys/file/delete',
|
||||||
|
params: { id: file.fileId || file.id }
|
||||||
|
});
|
||||||
|
createMessage.success('文件删除成功');
|
||||||
|
if (file.pending) {
|
||||||
|
pendingFileList.value = pendingFileList.value.filter(f => f.fileId !== file.fileId);
|
||||||
|
} else {
|
||||||
|
serverFileList.value = serverFileList.value.filter(f => (f.id || f.fileId) !== (file.id || file.fileId));
|
||||||
|
}
|
||||||
|
emit('delete-success', file.fileId || file.id);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('删除失败:', error);
|
||||||
|
createMessage.error('文件删除失败');
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadFileList() {
|
||||||
|
if (!props.bussinessId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
loading.value = true;
|
||||||
|
const result = await defHttp.get({
|
||||||
|
url: '/sys/file/getFileInfoByBussinessId',
|
||||||
|
params: { bussinessId: props.bussinessId }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (Array.isArray(result)) {
|
||||||
|
serverFileList.value = result;
|
||||||
|
} else {
|
||||||
|
serverFileList.value = [];
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载文件列表失败:', error);
|
||||||
|
serverFileList.value = [];
|
||||||
|
createMessage.error('加载文件列表失败');
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function bindBussinessId(newBussinessId: string) {
|
||||||
|
if (!newBussinessId) {
|
||||||
|
createMessage.warning('bussinessId 不能为空');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pendingFileList.value.length === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileIds = pendingFileList.value.map(f => f.fileId).join(',');
|
||||||
|
|
||||||
|
try {
|
||||||
|
binding.value = true;
|
||||||
|
await defHttp.get({
|
||||||
|
url: '/sys/file/updateFilesInfo',
|
||||||
|
params: {
|
||||||
|
bussinessId: newBussinessId,
|
||||||
|
fileIds: fileIds
|
||||||
|
}
|
||||||
|
});
|
||||||
|
createMessage.success(`成功关联 ${pendingFileList.value.length} 个文件`);
|
||||||
|
pendingFileList.value = [];
|
||||||
|
await loadFileList();
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('关联文件失败:', error);
|
||||||
|
createMessage.error('关联文件失败,请重试');
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
binding.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPendingFileIds(): string[] {
|
||||||
|
return pendingFileList.value.map(f => f.fileId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasPendingFiles(): boolean {
|
||||||
|
return pendingFileList.value.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
bindBussinessId,
|
||||||
|
getPendingFileIds,
|
||||||
|
hasPendingFiles,
|
||||||
|
loadFileList,
|
||||||
|
handleOpenUploadModal
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(() => props.bussinessId, (newVal) => {
|
||||||
|
if (newVal && newVal.length > 0) {
|
||||||
|
loadFileList();
|
||||||
|
} else {
|
||||||
|
serverFileList.value = [];
|
||||||
|
}
|
||||||
|
}, { immediate: true });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.s-upload-file-container {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
|
||||||
|
.upload-btn-wrapper {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-list-wrapper {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 100px;
|
||||||
|
padding: 12px;
|
||||||
|
background: #fafafa;
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
|
.file-list-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
|
||||||
|
.file-count {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #8c8c8c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-list {
|
||||||
|
.file-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #f0f0f0;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
transition: all 0.3s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.file-icon {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #1890ff;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-name {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #262626;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.secret-tag {
|
||||||
|
margin-left: 4px;
|
||||||
|
font-weight: 500;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-size {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #8c8c8c;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,253 @@
|
|||||||
|
<!-- 文件上传弹窗组件(任务附件专用副本) -->
|
||||||
|
<template>
|
||||||
|
<a-modal
|
||||||
|
:visible="visible"
|
||||||
|
title="选择文件密级并上传"
|
||||||
|
:width="600"
|
||||||
|
:confirm-loading="uploading"
|
||||||
|
@ok="handleUpload"
|
||||||
|
@cancel="handleCancel"
|
||||||
|
:destroyOnClose="true"
|
||||||
|
>
|
||||||
|
<a-form layout="vertical" style="padding: 0 20px">
|
||||||
|
<a-form-item label="文件密级" required>
|
||||||
|
<a-radio-group v-model:value="selectedSecretLevel">
|
||||||
|
<a-radio
|
||||||
|
v-for="option in secretLevelOptions"
|
||||||
|
:key="option.value"
|
||||||
|
:value="option.value"
|
||||||
|
>
|
||||||
|
<a-tag :color="getSecretLevelColor(option.value)">
|
||||||
|
{{ option.label }}
|
||||||
|
</a-tag>
|
||||||
|
</a-radio>
|
||||||
|
</a-radio-group>
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item label="选择文件" required>
|
||||||
|
<a-upload
|
||||||
|
:before-upload="beforeUpload"
|
||||||
|
:file-list="tempFileList"
|
||||||
|
@remove="handleRemoveFile"
|
||||||
|
:multiple="multiple"
|
||||||
|
:max-count="multiple ? undefined : 1"
|
||||||
|
>
|
||||||
|
<a-button>
|
||||||
|
<Icon icon="ant-design:file-add-outlined" />
|
||||||
|
{{ multiple ? '选择文件(可多选)' : '选择文件' }}
|
||||||
|
</a-button>
|
||||||
|
</a-upload>
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-alert
|
||||||
|
v-if="selectedFiles.length > 0"
|
||||||
|
:message="`已选择 ${selectedFiles.length} 个文件`"
|
||||||
|
type="info"
|
||||||
|
show-icon
|
||||||
|
>
|
||||||
|
<template #description>
|
||||||
|
<div class="selected-files-summary">
|
||||||
|
<div v-for="(f, idx) in selectedFiles" :key="f.uid" class="summary-item">
|
||||||
|
{{ idx + 1}}. {{ f.name }}({{ formatFileSize(f.size) }})
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</a-alert>
|
||||||
|
</a-form>
|
||||||
|
</a-modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, watch } from 'vue';
|
||||||
|
import { Icon } from '/src/components/Icon';
|
||||||
|
import { useMessage } from '/src/hooks/web/useMessage';
|
||||||
|
import { uploadMyFile } from '/src/api/common/api';
|
||||||
|
|
||||||
|
const { createMessage } = useMessage();
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
visible: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
secretLevelOptions: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
businessId: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
multiple: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:visible', 'upload-success', 'upload-error']);
|
||||||
|
|
||||||
|
const selectedSecretLevel = ref<number>(1);
|
||||||
|
const selectedFiles = ref<any[]>([]);
|
||||||
|
const tempFileList = ref<any[]>([]);
|
||||||
|
const uploading = ref<boolean>(false);
|
||||||
|
|
||||||
|
function getSecretLevelColor(level: number) {
|
||||||
|
const colors = {
|
||||||
|
1: 'success',
|
||||||
|
2: 'processing',
|
||||||
|
3: 'warning',
|
||||||
|
4: 'error'
|
||||||
|
};
|
||||||
|
return colors[level] || 'default';
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatFileSize(bytes: number): string {
|
||||||
|
if (bytes === 0) return '0 B';
|
||||||
|
const k = 1024;
|
||||||
|
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
function beforeUpload(file) {
|
||||||
|
const isValidSize = file.size / 1024 / 1024 < 500;
|
||||||
|
if (!isValidSize) {
|
||||||
|
createMessage.error(`文件"${file.name}"大小超过500MB,已跳过`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!props.multiple) {
|
||||||
|
selectedFiles.value = [file];
|
||||||
|
tempFileList.value = [{
|
||||||
|
uid: file.uid,
|
||||||
|
name: file.name,
|
||||||
|
status: 'done',
|
||||||
|
size: file.size
|
||||||
|
}];
|
||||||
|
} else {
|
||||||
|
selectedFiles.value = [...selectedFiles.value, file];
|
||||||
|
tempFileList.value = [...tempFileList.value, {
|
||||||
|
uid: file.uid,
|
||||||
|
name: file.name,
|
||||||
|
status: 'done',
|
||||||
|
size: file.size
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleRemoveFile(file) {
|
||||||
|
selectedFiles.value = selectedFiles.value.filter(f => f.uid !== file.uid);
|
||||||
|
tempFileList.value = tempFileList.value.filter(f => f.uid !== file.uid);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleUpload() {
|
||||||
|
if (selectedFiles.value.length === 0) {
|
||||||
|
createMessage.warning('请先选择文件');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!selectedSecretLevel.value) {
|
||||||
|
createMessage.warning('请选择文件密级');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
uploading.value = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const uploadPromises = selectedFiles.value.map(file => {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
formData.append('secretLevel', selectedSecretLevel.value.toString());
|
||||||
|
if (props.businessId) {
|
||||||
|
formData.append('business_id', props.businessId);
|
||||||
|
}
|
||||||
|
return uploadMyFile('/sys/file/upload', formData).then(res => ({
|
||||||
|
rawFile: file,
|
||||||
|
response: res
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
const results = await Promise.allSettled(uploadPromises);
|
||||||
|
|
||||||
|
let successCount = 0;
|
||||||
|
let failCount = 0;
|
||||||
|
const successResults: any[] = [];
|
||||||
|
|
||||||
|
results.forEach((r) => {
|
||||||
|
if (r.status === 'fulfilled') {
|
||||||
|
const result = r.value?.response?.data;
|
||||||
|
if (result && result.success) {
|
||||||
|
successCount++;
|
||||||
|
successResults.push({
|
||||||
|
fileId: result.result.fileId,
|
||||||
|
fileName: r.value.rawFile.name,
|
||||||
|
filePath: result.result.savePath,
|
||||||
|
fileSize: r.value.rawFile.size,
|
||||||
|
secretLevel: selectedSecretLevel.value,
|
||||||
|
fileUploadType: result.result.fileUploadType,
|
||||||
|
bound: !!props.businessId
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
failCount++;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
failCount++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (successCount > 0) {
|
||||||
|
createMessage.success(`成功上传 ${successCount} 个文件${failCount > 0 ? `,${failCount} 个失败` : ''}`);
|
||||||
|
emit('upload-success', { success: true, results: successResults });
|
||||||
|
handleCancel();
|
||||||
|
} else {
|
||||||
|
createMessage.error('所有文件上传失败');
|
||||||
|
emit('upload-error', new Error('所有文件上传失败'));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('上传失败:', error);
|
||||||
|
createMessage.error('文件上传失败');
|
||||||
|
emit('upload-error', error);
|
||||||
|
} finally {
|
||||||
|
uploading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCancel() {
|
||||||
|
emit('update:visible', false);
|
||||||
|
setTimeout(() => {
|
||||||
|
selectedSecretLevel.value = 1;
|
||||||
|
selectedFiles.value = [];
|
||||||
|
tempFileList.value = [];
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(() => props.visible, (newVal) => {
|
||||||
|
if (newVal && props.secretLevelOptions.length > 0) {
|
||||||
|
selectedSecretLevel.value = props.secretLevelOptions[0].value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.secret-tip {
|
||||||
|
margin-top: 8px;
|
||||||
|
color: #faad14;
|
||||||
|
font-size: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-files-summary {
|
||||||
|
max-height: 150px;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
.summary-item {
|
||||||
|
padding: 2px 0;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #595959;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -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>
|
||||||
@@ -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,820 @@
|
|||||||
|
<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>
|
||||||
|
<a-dropdown :trigger="['click']">
|
||||||
|
<Icon icon="ant-design:plus-outlined" class="sidebar-group-add" @click.stop />
|
||||||
|
<template #overlay>
|
||||||
|
<a-menu @click="onAddMenuClick">
|
||||||
|
<a-menu-item key="new-list">
|
||||||
|
<Icon icon="ant-design:file-add-outlined" class="mr-1" />
|
||||||
|
新建清单
|
||||||
|
</a-menu-item>
|
||||||
|
<a-menu-item key="new-group">
|
||||||
|
<Icon icon="ant-design:folder-add-outlined" class="mr-1" />
|
||||||
|
新建分组
|
||||||
|
</a-menu-item>
|
||||||
|
</a-menu>
|
||||||
|
</template>
|
||||||
|
</a-dropdown>
|
||||||
|
</div>
|
||||||
|
<div class="sidebar-favorites-items">
|
||||||
|
<draggable :list="localUngroupedTaskLists" group="tasklists" item-key="id" handle=".list-drag-handle" :animation="200" @end="onDragEnd">
|
||||||
|
<template #item="{ element: taskList }">
|
||||||
|
<div
|
||||||
|
v-if="renamingId !== taskList.id"
|
||||||
|
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 ref="renameInputRef" v-model:value="renameValue" size="small" style="flex: 1" @press-enter="confirmInlineRename" @blur="confirmInlineRename" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</draggable>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<draggable :list="localUserGroups" group="tasklistgroups" item-key="id" :animation="200" drag-class="group-drag-ghost" @start="onGroupDragStart" @end="onGroupDragEnd">
|
||||||
|
<template #item="{ element: group }">
|
||||||
|
<div :data-group-id="group.id" :class="{ 'group-being-dragged': draggingGroupId === group.id }">
|
||||||
|
<div
|
||||||
|
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 ref="renameInputRef" v-model:value="renameValue" size="small" style="flex: 1" @press-enter="confirmInlineRename" @blur="confirmInlineRename" />
|
||||||
|
</div>
|
||||||
|
<template v-if="group.delFlag !== 1 && !collapsedGroups[group.id]">
|
||||||
|
<div class="sidebar-group-items" :data-group-id="group.id">
|
||||||
|
<draggable
|
||||||
|
:list="getLocalGroupTaskLists(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 ref="renameInputRef" v-model:value="renameValue" size="small" style="flex: 1" @press-enter="confirmInlineRename" @blur="confirmInlineRename" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</draggable>
|
||||||
|
<div
|
||||||
|
v-if="getLocalGroupTaskLists(group).length === 0"
|
||||||
|
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, watch, nextTick } 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-modal': [groupId: string];
|
||||||
|
'create-group': [name: string];
|
||||||
|
'rename-list': [listId: string, newName: string];
|
||||||
|
'delete-list': [listId: string];
|
||||||
|
'remove-favorite': [listId: string];
|
||||||
|
'rename-group': [groupId: string, newName: string];
|
||||||
|
'delete-group': [groupId: string];
|
||||||
|
'quick-access-tab-click': [tabKey: string];
|
||||||
|
'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 [];
|
||||||
|
});
|
||||||
|
|
||||||
|
const userGroups = computed(() => {
|
||||||
|
return props.taskListGroups.filter((g) => g.id !== '__ungrouped__' && g.delFlag !== 1).sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0));
|
||||||
|
});
|
||||||
|
|
||||||
|
// 本地可变副本,供 draggable :list 模式直接修改,实现乐观更新
|
||||||
|
const localUngroupedTaskLists = ref<any[]>([]);
|
||||||
|
const localUserGroups = ref<any[]>([]);
|
||||||
|
const localGroupTaskListsMap = ref<Record<string, any[]>>({});
|
||||||
|
|
||||||
|
watch(ungroupedTaskLists, (val) => {
|
||||||
|
localUngroupedTaskLists.value = [...val];
|
||||||
|
}, { immediate: true });
|
||||||
|
|
||||||
|
watch(userGroups, (val) => {
|
||||||
|
localUserGroups.value = val.map((g) => ({
|
||||||
|
...g,
|
||||||
|
taskLists: [...(g.taskLists || [])],
|
||||||
|
}));
|
||||||
|
// 同步分组内清单到 map
|
||||||
|
const newMap: Record<string, any[]> = {};
|
||||||
|
for (const g of val) {
|
||||||
|
newMap[g.id] = [...(g.taskLists || [])].filter((l) => l.delFlag !== 1).sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0));
|
||||||
|
}
|
||||||
|
localGroupTaskListsMap.value = newMap;
|
||||||
|
}, { immediate: true });
|
||||||
|
|
||||||
|
function getGroupTaskLists(group: any) {
|
||||||
|
return [...group.taskLists].filter((l) => l.delFlag !== 1).sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLocalGroupTaskLists(group: any) {
|
||||||
|
if (localGroupTaskListsMap.value[group.id]) {
|
||||||
|
return localGroupTaskListsMap.value[group.id];
|
||||||
|
}
|
||||||
|
// 兜底:如果没有本地缓存,从原始数据计算
|
||||||
|
const list = getGroupTaskLists(group);
|
||||||
|
localGroupTaskListsMap.value[group.id] = list;
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hoveredListId = ref('');
|
||||||
|
const collapsedGroups = ref<Record<string, boolean>>({});
|
||||||
|
const showNewGroupInput = ref(false);
|
||||||
|
const newGroupName = ref('');
|
||||||
|
const renamingId = ref('');
|
||||||
|
const renamingType = ref<'list' | 'group'>('list');
|
||||||
|
const renameValue = ref('');
|
||||||
|
const renameInputRef = ref<HTMLInputElement | null>(null);
|
||||||
|
|
||||||
|
watch(renamingId, async (val) => {
|
||||||
|
if (val) {
|
||||||
|
await nextTick();
|
||||||
|
renameInputRef.value?.focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const draggingGroupId = ref('');
|
||||||
|
const dragStartX = ref(0);
|
||||||
|
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;
|
||||||
|
// 拖拽结束后保持分组折叠状态,防止展开跳动
|
||||||
|
collapsedGroups.value[groupId] = true;
|
||||||
|
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 onAddMenuClick({ key }: { key: string }) {
|
||||||
|
if (key === 'new-list') {
|
||||||
|
startNewList('');
|
||||||
|
} else if (key === 'new-group') {
|
||||||
|
showNewGroupInput.value = true;
|
||||||
|
newGroupName.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function startNewList(groupId: string) {
|
||||||
|
emit('create-list-modal', groupId);
|
||||||
|
}
|
||||||
|
|
||||||
|
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('remove-favorite', 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,383 @@
|
|||||||
|
<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">
|
||||||
|
<SzUserSelect
|
||||||
|
v-model:value="addForm.userIds"
|
||||||
|
:labelKey="'realname'"
|
||||||
|
:rowKey="'id'"
|
||||||
|
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 { message } from 'ant-design-vue';
|
||||||
|
import Icon from '/@/components/Icon/index';
|
||||||
|
import SzUserSelect from '/@/components/semri/userComponent/SzUserSelect.vue';
|
||||||
|
import { getAvatarColor } from '../types';
|
||||||
|
import { getUserList } from '/@/api/common/api';
|
||||||
|
import { checkUserSecLevel } from '../utils/taskUtils';
|
||||||
|
|
||||||
|
interface MemberItem {
|
||||||
|
userId: string;
|
||||||
|
username: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
title?: string;
|
||||||
|
userIds: string;
|
||||||
|
userNames: string;
|
||||||
|
multiple?: boolean;
|
||||||
|
secretLevel?: number;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
title: '选择人员',
|
||||||
|
multiple: true,
|
||||||
|
secretLevel: 1,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
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 secretLevel = props.secretLevel || 1;
|
||||||
|
const { ok, invalidNames } = await checkUserSecLevel(addForm.userIds, secretLevel);
|
||||||
|
if (!ok) {
|
||||||
|
const map: Record<number, string> = { 1: '非密', 2: '内部', 3: '秘密', 4: '机密' };
|
||||||
|
message.warning(`以下人员的密级不满足当前清单(${map[secretLevel] || '非密'})要求:${invalidNames.join('、')}`);
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
let addedCount = 0;
|
||||||
|
for (const nid of newIds) {
|
||||||
|
if (!existingIds.has(nid) || !props.multiple) {
|
||||||
|
memberList.value.push({
|
||||||
|
userId: nid,
|
||||||
|
username: nameMap[nid] || nid,
|
||||||
|
});
|
||||||
|
addedCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (addedCount > 0) {
|
||||||
|
message.success(`已添加 ${addedCount} 位人员`);
|
||||||
|
}
|
||||||
|
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,275 @@
|
|||||||
|
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;
|
||||||
|
isDefault?: number;
|
||||||
|
listName: string;
|
||||||
|
mainId: string;
|
||||||
|
myPermission?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TaskGroup {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
taskListId: string;
|
||||||
|
collapsed: boolean;
|
||||||
|
tasks: TaskItem[];
|
||||||
|
isDefault?: number;
|
||||||
|
sortOrder?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
secretLevel?: number;
|
||||||
|
secretText?: 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 =
|
||||||
|
| 'listName'
|
||||||
|
| '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: false },
|
||||||
|
{ key: 'listName', 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SECRET_LEVEL_MAP = [
|
||||||
|
{ value: 1, label: '非密' },
|
||||||
|
{ value: 2, label: '内部' },
|
||||||
|
{ value: 3, label: '秘密' },
|
||||||
|
{ value: 4, label: '机密' },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export const SECRET_LEVEL_COLOR: Record<number, string> = {
|
||||||
|
1: '#8c8c8c',
|
||||||
|
2: '#1677ff',
|
||||||
|
3: '#fa8c16',
|
||||||
|
4: '#f5222d',
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getAvailableSecretLevels(userSecurityLevel: number) {
|
||||||
|
if (!userSecurityLevel) userSecurityLevel = 3;
|
||||||
|
return SECRET_LEVEL_MAP.filter((item) => userSecurityLevel > item.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getSecretText(level?: number | null): string {
|
||||||
|
if (level == null) return '非密';
|
||||||
|
const found = SECRET_LEVEL_MAP.find((item) => item.value === level);
|
||||||
|
return found ? found.label : '非密';
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
export const PRIORITY_OPTIONS = [
|
||||||
|
{ label: '高', value: 'high' },
|
||||||
|
{ label: '中', value: 'medium' },
|
||||||
|
{ label: '低', value: 'low' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const PRIORITY_COLOR_MAP: Record<string, string> = {
|
||||||
|
high: '#ffccc7',
|
||||||
|
medium: '#ffe58f',
|
||||||
|
low: '#91d5ff',
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getPriorityBg(value: string): string {
|
||||||
|
return PRIORITY_COLOR_MAP[value] || '#f5f5f5';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPriorityLabel(value: string): string {
|
||||||
|
const opt = PRIORITY_OPTIONS.find((o) => o.value === value);
|
||||||
|
return opt?.label || value;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isOverdue(task: { endTime?: string; completed?: boolean }): boolean {
|
||||||
|
if (!task.endTime || task.completed) return false;
|
||||||
|
const end = new Date(task.endTime);
|
||||||
|
const now = new Date();
|
||||||
|
now.setHours(0, 0, 0, 0);
|
||||||
|
end.setHours(0, 0, 0, 0);
|
||||||
|
return now > end;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function checkUserSecLevel(
|
||||||
|
userIds: string,
|
||||||
|
secretLevel: number
|
||||||
|
): Promise<{ ok: boolean; invalidNames: string[] }> {
|
||||||
|
if (!userIds || !userIds.trim()) return { ok: true, invalidNames: [] };
|
||||||
|
const { getUserList } = await import('/@/api/common/api');
|
||||||
|
const ids = userIds.split(',').map((id) => id.trim()).filter(Boolean);
|
||||||
|
if (ids.length === 0) return { ok: true, invalidNames: [] };
|
||||||
|
const level = secretLevel || 1;
|
||||||
|
try {
|
||||||
|
const res = await getUserList({ id: ids.join(','), pageNo: 1, pageSize: ids.length });
|
||||||
|
const records: any[] = res?.records || [];
|
||||||
|
const nameMap: Record<string, string> = {};
|
||||||
|
const levelMap: Record<string, number> = {};
|
||||||
|
for (const r of records) {
|
||||||
|
nameMap[r.id] = r.realname || r.id;
|
||||||
|
levelMap[r.id] = r.userSecurityLevel ?? 3;
|
||||||
|
}
|
||||||
|
const invalidNames: string[] = [];
|
||||||
|
for (const id of ids) {
|
||||||
|
const userSecLevel = levelMap[id] ?? 3;
|
||||||
|
if (userSecLevel <= level) {
|
||||||
|
invalidNames.push(nameMap[id] || id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { ok: invalidNames.length === 0, invalidNames };
|
||||||
|
} catch {
|
||||||
|
return { ok: true, invalidNames: [] };
|
||||||
|
}
|
||||||
|
}
|
||||||