From 16a55de6f1873fb46ad8b51dc1730c17aa19f9cf Mon Sep 17 00:00:00 2001 From: kuaifan Date: Mon, 29 Dec 2025 15:43:50 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=BC=BA=E6=90=9C=E7=B4=A2?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=EF=BC=8C=E6=94=AF=E6=8C=81=E9=80=9A=E8=BF=87?= =?UTF-8?q?=20ID=E3=80=81=E5=90=8D=E7=A7=B0=E5=92=8C=E5=85=B6=E4=BB=96?= =?UTF-8?q?=E5=AD=97=E6=AE=B5=E6=90=9C=E7=B4=A2=E4=BB=BB=E5=8A=A1=E3=80=81?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E5=92=8C=E6=8A=A5=E5=91=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/Http/Controllers/Api/FileController.php | 12 +- .../Controllers/Api/ProjectController.php | 25 +- app/Http/Controllers/Api/ReportController.php | 11 +- app/Http/Controllers/Api/UsersController.php | 7 +- ..._29_140720_add_task_list_scope_indexes.php | 34 +++ resources/assets/js/components/SearchBox.vue | 1 + .../manage/components/ChatInput/index.vue | 248 ++++++++++++------ .../sass/pages/components/chat-input.scss | 19 +- 8 files changed, 249 insertions(+), 108 deletions(-) create mode 100644 database/migrations/2025_12_29_140720_add_task_list_scope_indexes.php diff --git a/app/Http/Controllers/Api/FileController.php b/app/Http/Controllers/Api/FileController.php index 12c65d212..bee114847 100755 --- a/app/Http/Controllers/Api/FileController.php +++ b/app/Http/Controllers/Api/FileController.php @@ -152,7 +152,9 @@ class FileController extends AbstractController } if ($key) { if (!$id && Base::isNumber($key)) { - $builder->where("id", $key); + $builder->where(function ($query) use ($key) { + $query->where("id", $key)->orWhere("name", "like", "%{$key}%"); + }); } else { $builder->where("name", "like", "%{$key}%"); } @@ -174,7 +176,13 @@ class FileController extends AbstractController $builder->where("id", $id); } if ($key) { - $builder->where("name", "like", "%{$key}%"); + if (Base::isNumber($key)) { + $builder->where(function ($query) use ($key) { + $query->where("id", $key)->orWhere("name", "like", "%{$key}%"); + }); + } else { + $builder->where("name", "like", "%{$key}%"); + } } $list = $builder->take($take)->get(); if ($list->isNotEmpty()) { diff --git a/app/Http/Controllers/Api/ProjectController.php b/app/Http/Controllers/Api/ProjectController.php index 2145747cf..620e45c4e 100755 --- a/app/Http/Controllers/Api/ProjectController.php +++ b/app/Http/Controllers/Api/ProjectController.php @@ -991,10 +991,12 @@ class ProjectController extends AbstractController * - keys.tag: 标签名称 * - keys.status: 任务状态 (completed: 已完成、uncompleted: 未完成、flow-xx: 流程状态ID) * - * @apiParam {Number} [project_id] 项目ID - * @apiParam {Number} [parent_id] 主任务ID(project_id && parent_id ≤ 0 时 仅查询自己参与的任务) - * - 大于0:指定主任务下的子任务 - * - 等于-1:表示仅主任务 + * @apiParam {Number} [project_id] 项目ID(传入后只查询该项目内任务) + * @apiParam {Number} [parent_id] 主任务ID(查询优先级最高) + * - 大于0:只查该主任务下的子任务(此时 archived 强制 all,忽略 project_id/scope) + * - 等于-1:仅主任务(可与 project_id 组合) + * @apiParam {String} [scope] 查询范围(仅在未指定 project_id 且 parent_id ≤ 0 时生效) + * - all_project:查询“我参与的项目”下的所有任务(仍受可见性限制) * * @apiParam {String} [time] 指定时间范围,如:today, week, month, year, 2020-12-12,2020-12-30 * - today: 今天 @@ -1038,6 +1040,7 @@ class ProjectController extends AbstractController $deleted = Request::input('deleted', 'no'); $keys = Request::input('keys'); $sorts = Request::input('sorts'); + $scope = Request::input('scope'); $keys = is_array($keys) ? $keys : []; $sorts = is_array($sorts) ? $sorts : []; @@ -1045,7 +1048,11 @@ class ProjectController extends AbstractController // if ($keys['name']) { if (Base::isNumber($keys['name'])) { - $builder->where("project_tasks.id", intval($keys['name'])); + $builder->where(function ($query) use ($keys) { + $query->where("project_tasks.id", intval($keys['name'])) + ->orWhere("project_tasks.name", "like", "%{$keys['name']}%") + ->orWhere("project_tasks.desc", "like", "%{$keys['name']}%"); + }); } else { $builder->where(function ($query) use ($keys) { $query->where("project_tasks.name", "like", "%{$keys['name']}%"); @@ -1089,6 +1096,14 @@ class ProjectController extends AbstractController $scopeAll = true; $builder->where('project_tasks.project_id', $project_id); } + if (!$scopeAll && $scope === 'all_project') { + $scopeAll = true; + $builder->whereIn('project_tasks.project_id', function ($query) use ($userid) { + $query->select('project_id') + ->from('project_users') + ->where('userid', $userid); + }); + } if ($scopeAll) { $builder->allData(); } else { diff --git a/app/Http/Controllers/Api/ReportController.php b/app/Http/Controllers/Api/ReportController.php index 437bd0d08..3128f3031 100755 --- a/app/Http/Controllers/Api/ReportController.php +++ b/app/Http/Controllers/Api/ReportController.php @@ -59,6 +59,11 @@ class ReportController extends AbstractController $builder->whereHas('sendUser', function ($q2) use ($keys) { $q2->where("users.email", "LIKE", "%{$keys['key']}%"); }); + } elseif (Base::isNumber($keys['key'])) { + $builder->where(function ($query) use ($keys) { + $query->where("id", intval($keys['key'])) + ->orWhere("title", "LIKE", "%{$keys['key']}%"); + }); } else { $builder->where("title", "LIKE", "%{$keys['key']}%"); } @@ -111,7 +116,11 @@ class ReportController extends AbstractController $q2->where("users.email", "LIKE", "%{$keys['key']}%"); }); } elseif (Base::isNumber($keys['key'])) { - $builder->where("userid", intval($keys['key'])); + $builder->where(function ($query) use ($keys) { + $query->where("userid", intval($keys['key'])) + ->orWhere("id", intval($keys['key'])) + ->orWhere("title", "LIKE", "%{$keys['key']}%"); + }); } else { $builder->where("title", "LIKE", "%{$keys['key']}%"); } diff --git a/app/Http/Controllers/Api/UsersController.php b/app/Http/Controllers/Api/UsersController.php index 29e865422..6792deacd 100755 --- a/app/Http/Controllers/Api/UsersController.php +++ b/app/Http/Controllers/Api/UsersController.php @@ -663,7 +663,12 @@ class UsersController extends AbstractController if (str_contains($keys['key'], "@")) { $builder->where("email", "like", "%{$keys['key']}%"); } elseif (Base::isNumber($keys['key'])) { - $builder->where("userid", intval($keys['key'])); + $builder->where(function ($query) use ($keys) { + $query->where("userid", intval($keys['key'])) + ->orWhere("nickname", "like", "%{$keys['key']}%") + ->orWhere("pinyin", "like", "%{$keys['key']}%") + ->orWhere("profession", "like", "%{$keys['key']}%"); + }); } else { $builder->where(function($query) use ($keys) { $query->where("nickname", "like", "%{$keys['key']}%") diff --git a/database/migrations/2025_12_29_140720_add_task_list_scope_indexes.php b/database/migrations/2025_12_29_140720_add_task_list_scope_indexes.php new file mode 100644 index 000000000..85742974d --- /dev/null +++ b/database/migrations/2025_12_29_140720_add_task_list_scope_indexes.php @@ -0,0 +1,34 @@ +index(['userid', 'project_id']); + }); + + Schema::table('project_tasks', function (Blueprint $table) { + $table->index(['project_id', 'archived_at', 'deleted_at', 'id']); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + // No-op: do not drop indexes automatically. + } +}; diff --git a/resources/assets/js/components/SearchBox.vue b/resources/assets/js/components/SearchBox.vue index f5aa8ad42..428b86226 100755 --- a/resources/assets/js/components/SearchBox.vue +++ b/resources/assets/js/components/SearchBox.vue @@ -339,6 +339,7 @@ export default { data: { keys: {name: key}, archived: 'all', + scope: 'all_project', pagesize: this.action ? 50 : 10, }, }).then(({data}) => { diff --git a/resources/assets/js/pages/manage/components/ChatInput/index.vue b/resources/assets/js/pages/manage/components/ChatInput/index.vue index 6b9eb7bd8..e562f25f2 100755 --- a/resources/assets/js/pages/manage/components/ChatInput/index.vue +++ b/resources/assets/js/pages/manage/components/ChatInput/index.vue @@ -436,8 +436,10 @@ export default { userList: null, userCache: null, taskList: null, + taskSearchList: {}, fileList: {}, reportList: {}, + taskSearchKey: '', showMenu: false, showMore: false, @@ -479,6 +481,7 @@ export default { textTimer: null, fileTimer: null, reportTimer: null, + taskSearchTimer: null, moreTimer: null, selectTimer: null, selectRange: null, @@ -788,6 +791,7 @@ export default { this.userList = null; this.userCache = null; this.taskList = null; + this.taskSearchList = {}; this.fileList = {}; this.reportList = {}; this.loadInputDraft() @@ -798,6 +802,7 @@ export default { this.userList = null; this.userCache = null; this.taskList = null; + this.taskSearchList = {}; this.fileList = {}; this.reportList = {}; this.loadInputDraft() @@ -1266,17 +1271,14 @@ export default { const mentionMap = { '@': 'user-mention', '#': 'task-mention', + '~': 'file-mention', + '%': 'report-mention', '/': 'slash-mention' }; const mentionName = mentionMap[mentionChar] || 'file-mention'; const containers = document.getElementsByClassName("ql-mention-list-container"); for (let i = 0; i < containers.length; i++) { - containers[i].classList.remove( - "user-mention", - "task-mention", - "file-mention", - "slash-mention" - ); + containers[i].classList.remove(...Object.values(mentionMap)); containers[i].classList.add(mentionName); } let mentionSourceCache = null; @@ -2472,87 +2474,159 @@ export default { case "#": // #任务 this.mentionMode = "task-mention"; - if (this.taskList !== null) { - resultCallback(this.taskList) - return; - } - const taskCallback = (list) => { - this.taskList = []; - // 项目任务 - if (list.length > 0) { - list = list.map(item => { - return { - id: item.id, - value: item.name, - tip: item.complete_at ? this.$L('已完成') : null, - } - }).splice(0, 100) - this.taskList.push({ - label: [{id: 0, value: this.$L('项目任务'), disabled: true}], - list, - }) + const searchKey = (searchTerm || '').trim(); + this.taskSearchKey = searchKey; + const buildOtherTasks = (list) => { + const baseLists = Array.isArray(list) ? list : []; + if (!searchKey) { + return baseLists; } - // 待完成任务 - const { overdue, today, todo } = this.$store.getters.dashboardTask; - const combinedTasks = [...overdue, ...today, ...todo]; - let allTask = this.$store.getters.transforTasks(combinedTasks); - if (allTask.length > 0) { - allTask = allTask.sort((a, b) => { - return $A.sortDay(a.end_at || "2099-12-31 23:59:59", b.end_at || "2099-12-31 23:59:59"); - }).splice(0, 100) - this.taskList.push({ - label: [{id: 0, value: this.$L('我的待完成任务'), disabled: true}], - list: allTask.map(item => { - return { - id: item.id, - value: item.name - } - }), - }) + const searchTasks = Array.isArray(this.taskSearchList[searchKey]) ? this.taskSearchList[searchKey] : []; + if (searchTasks.length === 0) { + return baseLists; } - // 我协助的任务 - let assistTask = this.$store.getters.assistTask; - if (assistTask.length > 0) { - assistTask = assistTask.sort((a, b) => { - return $A.sortDay(a.end_at || "2099-12-31 23:59:59", b.end_at || "2099-12-31 23:59:59"); - }).splice(0, 100) - this.taskList.push({ - label: [{id: 0, value: this.$L('我协助的任务'), disabled: true}], - list: assistTask.map(item => { - return { - id: item.id, - value: item.name - } - }), - }) - } - resultCallback(this.taskList) - } - // - const projectId = this.getProjectId(); - if (projectId > 0) { - this.$store.dispatch("getTaskForProject", projectId).then(_ => { - const tasks = this.cacheTasks.filter(task => { - if (task.archived_at) { - return false; - } - return task.project_id == projectId - && task.parent_id === 0 - && !task.archived_at - }).sort((a, b) => { - return $A.sortDay(b.complete_at || "2099-12-31 23:59:59", a.complete_at || "2099-12-31 23:59:59") - }) - if (tasks.length > 0) { - taskCallback(tasks) - } else { - taskCallback([]) + const existingIds = new Set(); + baseLists.forEach(group => { + (group.list || []).forEach(item => existingIds.add(item.id)); + }); + const otherTasks = []; + searchTasks.forEach(task => { + if (!existingIds.has(task.id)) { + existingIds.add(task.id); + otherTasks.push({ + id: task.id, + value: task.name, + tip: task.complete_at ? this.$L('已完成') : null, + }); } - }).catch(_ => { + }); + if (otherTasks.length === 0) { + return baseLists; + } + return [ + ...baseLists, + { + label: [{id: 0, value: this.$L('其他任务'), className: "sticky-top", disabled: true}], + list: otherTasks, + } + ]; + }; + const renderTaskList = (list) => resultCallback(buildOtherTasks(list || [])) + if (this.taskList !== null) { + renderTaskList(this.taskList) + } else { + const taskCallback = (list) => { + this.taskList = []; + // 项目任务 + if (list.length > 0) { + list = list.map(item => { + return { + id: item.id, + value: item.name, + tip: item.complete_at ? this.$L('已完成') : null, + } + }).splice(0, 100) + this.taskList.push({ + label: [{id: 0, value: this.$L('项目任务'), className: "sticky-top", disabled: true}], + list, + }) + } + // 待完成任务 + const { overdue, today, todo } = this.$store.getters.dashboardTask; + const combinedTasks = [...overdue, ...today, ...todo]; + let allTask = this.$store.getters.transforTasks(combinedTasks); + if (allTask.length > 0) { + allTask = allTask.sort((a, b) => { + return $A.sortDay(a.end_at || "2099-12-31 23:59:59", b.end_at || "2099-12-31 23:59:59"); + }).splice(0, 100) + this.taskList.push({ + label: [{id: 0, value: this.$L('我的待完成任务'), className: "sticky-top", disabled: true}], + list: allTask.map(item => { + return { + id: item.id, + value: item.name + } + }), + }) + } + // 我协助的任务 + let assistTask = this.$store.getters.assistTask; + if (assistTask.length > 0) { + assistTask = assistTask.sort((a, b) => { + return $A.sortDay(a.end_at || "2099-12-31 23:59:59", b.end_at || "2099-12-31 23:59:59"); + }).splice(0, 100) + this.taskList.push({ + label: [{id: 0, value: this.$L('我协助的任务'), className: "sticky-top", disabled: true}], + list: assistTask.map(item => { + return { + id: item.id, + value: item.name + } + }), + }) + } + renderTaskList(this.taskList) + } + // + const projectId = this.getProjectId(); + if (projectId > 0) { + this.$store.dispatch("getTaskForProject", projectId).then(_ => { + const tasks = this.cacheTasks.filter(task => { + if (task.archived_at) { + return false; + } + return task.project_id == projectId + && task.parent_id === 0 + && !task.archived_at + }).sort((a, b) => { + return $A.sortDay(b.complete_at || "2099-12-31 23:59:59", a.complete_at || "2099-12-31 23:59:59") + }) + if (tasks.length > 0) { + taskCallback(tasks) + } else { + taskCallback([]) + } + }).catch(_ => { + taskCallback([]) + }) + } else { taskCallback([]) - }) - return; + } + } + if (searchKey) { + if (Array.isArray(this.taskSearchList[searchKey])) { + return; + } + this.taskSearchTimer && clearTimeout(this.taskSearchTimer) + this.taskSearchTimer = setTimeout(async _ => { + if (this.taskSearchKey !== searchKey) { + return; + } + const projectId = this.getProjectId(); + const data = (await this.$store.dispatch("call", { + url: 'project/task/lists', + data: { + keys: { + name: searchKey, + }, + project_id: projectId > 0 ? projectId : undefined, + parent_id: -1, + scope: projectId > 0 ? undefined : 'all_project', + pagesize: 50, + }, + }).catch(_ => {}))?.data; + if (this.taskSearchKey !== searchKey) { + return; + } + const tasks = $A.getObject(data, 'data') || []; + this.taskSearchList[searchKey] = tasks.map(item => ({ + id: item.id, + name: item.name, + complete_at: item.complete_at, + })); + renderTaskList(this.taskList) + }, 300) } - taskCallback([]) break; case "~": // ~文件 @@ -2567,7 +2641,7 @@ export default { const data = (await this.$store.dispatch("searchFiles", searchTerm).catch(_ => {}))?.data; if (data) { lists.push({ - label: [{id: 0, value: this.$L('文件分享查看'), disabled: true}], + label: [{id: 0, value: this.$L('文件分享查看'), className: "sticky-top", disabled: true}], list: data.filter(item => item.type !== "folder").map(item => { return { id: item.id, @@ -2601,7 +2675,7 @@ export default { }).catch(_ => {}))?.data; if (myData) { lists.push({ - label: [{id: 0, value: this.$L('我的报告'), disabled: true}], + label: [{id: 0, value: this.$L('我的报告'), className: "sticky-top", disabled: true}], list: myData.data.map(item => { return { id: item.id, @@ -2621,7 +2695,7 @@ export default { }).catch(_ => {}))?.data; if (receiveData) { lists.push({ - label: [{id: 0, value: this.$L('收到的报告'), disabled: true}], + label: [{id: 0, value: this.$L('收到的报告'), className: "sticky-top", disabled: true}], list: receiveData.data.map(item => { return { id: item.id, @@ -2645,7 +2719,7 @@ export default { const isOwnBot = isBotDialog && this.dialogData.bot == this.userId; const isBotManager = isBotDialog && this.dialogData.email === 'bot-manager@bot.system'; const showBotCommands = allowBotCommands && (isOwnBot || isBotManager); - const baseLabel = showBotCommands ? [{id: 0, value: this.$L('快捷菜单'), disabled: true}] : null; + const baseLabel = showBotCommands ? [{id: 0, value: this.$L('快捷菜单'), className: "sticky-top", disabled: true}] : null; const slashLists = [{ label: baseLabel, list: [ @@ -2740,7 +2814,7 @@ export default { } ); slashLists.push({ - label: [{id: 0, value: this.$L('机器人命令'), disabled: true}], + label: [{id: 0, value: this.$L('机器人命令'), className: "sticky-top", disabled: true}], list: commandList, }); } diff --git a/resources/assets/sass/pages/components/chat-input.scss b/resources/assets/sass/pages/components/chat-input.scss index 3535f5307..bbf47841b 100755 --- a/resources/assets/sass/pages/components/chat-input.scss +++ b/resources/assets/sass/pages/components/chat-input.scss @@ -1132,19 +1132,13 @@ width: auto; overflow: hidden; - &.task-mention { - .ql-mention-list { - > li { - &:first-child { - margin-top: 0; - } - } - } + &.task-mention, + &.file-mention, + &.report-mention, + &.slash-mention { .ql-mention-list-item { - line-height: 36px; - .mention-item-disabled { - padding: 8px 4px 0; - } + line-height: 40px; + padding: 0 4px; } } @@ -1246,6 +1240,7 @@ .mention-item-tip { flex-shrink: 0; + padding-right: 8px; text-align: right; color: #8f8f8e; font-size: 12px;