feat: 增强搜索功能,支持通过 ID、名称和其他字段搜索任务、文件和报告

This commit is contained in:
kuaifan 2025-12-29 15:43:50 +00:00
parent 869ac7d316
commit 16a55de6f1
8 changed files with 249 additions and 108 deletions

View File

@ -152,7 +152,9 @@ class FileController extends AbstractController
} }
if ($key) { if ($key) {
if (!$id && Base::isNumber($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 { } else {
$builder->where("name", "like", "%{$key}%"); $builder->where("name", "like", "%{$key}%");
} }
@ -174,7 +176,13 @@ class FileController extends AbstractController
$builder->where("id", $id); $builder->where("id", $id);
} }
if ($key) { 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(); $list = $builder->take($take)->get();
if ($list->isNotEmpty()) { if ($list->isNotEmpty()) {

View File

@ -991,10 +991,12 @@ class ProjectController extends AbstractController
* - keys.tag: 标签名称 * - keys.tag: 标签名称
* - keys.status: 任务状态 (completed: 已完成、uncompleted: 未完成、flow-xx: 流程状态ID) * - keys.status: 任务状态 (completed: 已完成、uncompleted: 未完成、flow-xx: 流程状态ID)
* *
* @apiParam {Number} [project_id] 项目ID * @apiParam {Number} [project_id] 项目ID传入后只查询该项目内任务
* @apiParam {Number} [parent_id] 主任务IDproject_id && parent_id 0 仅查询自己参与的任务) * @apiParam {Number} [parent_id] 主任务ID查询优先级最高
* - 大于0指定主任务下的子任务 * - 大于0只查该主任务下的子任务此时 archived 强制 all忽略 project_id/scope
* - 等于-1:表示仅主任务 * - 等于-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 * @apiParam {String} [time] 指定时间范围today, week, month, year, 2020-12-12,2020-12-30
* - today: 今天 * - today: 今天
@ -1038,6 +1040,7 @@ class ProjectController extends AbstractController
$deleted = Request::input('deleted', 'no'); $deleted = Request::input('deleted', 'no');
$keys = Request::input('keys'); $keys = Request::input('keys');
$sorts = Request::input('sorts'); $sorts = Request::input('sorts');
$scope = Request::input('scope');
$keys = is_array($keys) ? $keys : []; $keys = is_array($keys) ? $keys : [];
$sorts = is_array($sorts) ? $sorts : []; $sorts = is_array($sorts) ? $sorts : [];
@ -1045,7 +1048,11 @@ class ProjectController extends AbstractController
// //
if ($keys['name']) { if ($keys['name']) {
if (Base::isNumber($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 { } else {
$builder->where(function ($query) use ($keys) { $builder->where(function ($query) use ($keys) {
$query->where("project_tasks.name", "like", "%{$keys['name']}%"); $query->where("project_tasks.name", "like", "%{$keys['name']}%");
@ -1089,6 +1096,14 @@ class ProjectController extends AbstractController
$scopeAll = true; $scopeAll = true;
$builder->where('project_tasks.project_id', $project_id); $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) { if ($scopeAll) {
$builder->allData(); $builder->allData();
} else { } else {

View File

@ -59,6 +59,11 @@ class ReportController extends AbstractController
$builder->whereHas('sendUser', function ($q2) use ($keys) { $builder->whereHas('sendUser', function ($q2) use ($keys) {
$q2->where("users.email", "LIKE", "%{$keys['key']}%"); $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 { } else {
$builder->where("title", "LIKE", "%{$keys['key']}%"); $builder->where("title", "LIKE", "%{$keys['key']}%");
} }
@ -111,7 +116,11 @@ class ReportController extends AbstractController
$q2->where("users.email", "LIKE", "%{$keys['key']}%"); $q2->where("users.email", "LIKE", "%{$keys['key']}%");
}); });
} elseif (Base::isNumber($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 { } else {
$builder->where("title", "LIKE", "%{$keys['key']}%"); $builder->where("title", "LIKE", "%{$keys['key']}%");
} }

View File

@ -663,7 +663,12 @@ class UsersController extends AbstractController
if (str_contains($keys['key'], "@")) { if (str_contains($keys['key'], "@")) {
$builder->where("email", "like", "%{$keys['key']}%"); $builder->where("email", "like", "%{$keys['key']}%");
} elseif (Base::isNumber($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 { } else {
$builder->where(function($query) use ($keys) { $builder->where(function($query) use ($keys) {
$query->where("nickname", "like", "%{$keys['key']}%") $query->where("nickname", "like", "%{$keys['key']}%")

View File

@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('project_users', function (Blueprint $table) {
$table->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.
}
};

View File

@ -339,6 +339,7 @@ export default {
data: { data: {
keys: {name: key}, keys: {name: key},
archived: 'all', archived: 'all',
scope: 'all_project',
pagesize: this.action ? 50 : 10, pagesize: this.action ? 50 : 10,
}, },
}).then(({data}) => { }).then(({data}) => {

View File

@ -436,8 +436,10 @@ export default {
userList: null, userList: null,
userCache: null, userCache: null,
taskList: null, taskList: null,
taskSearchList: {},
fileList: {}, fileList: {},
reportList: {}, reportList: {},
taskSearchKey: '',
showMenu: false, showMenu: false,
showMore: false, showMore: false,
@ -479,6 +481,7 @@ export default {
textTimer: null, textTimer: null,
fileTimer: null, fileTimer: null,
reportTimer: null, reportTimer: null,
taskSearchTimer: null,
moreTimer: null, moreTimer: null,
selectTimer: null, selectTimer: null,
selectRange: null, selectRange: null,
@ -788,6 +791,7 @@ export default {
this.userList = null; this.userList = null;
this.userCache = null; this.userCache = null;
this.taskList = null; this.taskList = null;
this.taskSearchList = {};
this.fileList = {}; this.fileList = {};
this.reportList = {}; this.reportList = {};
this.loadInputDraft() this.loadInputDraft()
@ -798,6 +802,7 @@ export default {
this.userList = null; this.userList = null;
this.userCache = null; this.userCache = null;
this.taskList = null; this.taskList = null;
this.taskSearchList = {};
this.fileList = {}; this.fileList = {};
this.reportList = {}; this.reportList = {};
this.loadInputDraft() this.loadInputDraft()
@ -1266,17 +1271,14 @@ export default {
const mentionMap = { const mentionMap = {
'@': 'user-mention', '@': 'user-mention',
'#': 'task-mention', '#': 'task-mention',
'~': 'file-mention',
'%': 'report-mention',
'/': 'slash-mention' '/': 'slash-mention'
}; };
const mentionName = mentionMap[mentionChar] || 'file-mention'; const mentionName = mentionMap[mentionChar] || 'file-mention';
const containers = document.getElementsByClassName("ql-mention-list-container"); const containers = document.getElementsByClassName("ql-mention-list-container");
for (let i = 0; i < containers.length; i++) { for (let i = 0; i < containers.length; i++) {
containers[i].classList.remove( containers[i].classList.remove(...Object.values(mentionMap));
"user-mention",
"task-mention",
"file-mention",
"slash-mention"
);
containers[i].classList.add(mentionName); containers[i].classList.add(mentionName);
} }
let mentionSourceCache = null; let mentionSourceCache = null;
@ -2472,87 +2474,159 @@ export default {
case "#": // # case "#": // #
this.mentionMode = "task-mention"; this.mentionMode = "task-mention";
if (this.taskList !== null) { const searchKey = (searchTerm || '').trim();
resultCallback(this.taskList) this.taskSearchKey = searchKey;
return; const buildOtherTasks = (list) => {
} const baseLists = Array.isArray(list) ? list : [];
const taskCallback = (list) => { if (!searchKey) {
this.taskList = []; return baseLists;
//
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 searchTasks = Array.isArray(this.taskSearchList[searchKey]) ? this.taskSearchList[searchKey] : [];
const { overdue, today, todo } = this.$store.getters.dashboardTask; if (searchTasks.length === 0) {
const combinedTasks = [...overdue, ...today, ...todo]; return baseLists;
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 existingIds = new Set();
let assistTask = this.$store.getters.assistTask; baseLists.forEach(group => {
if (assistTask.length > 0) { (group.list || []).forEach(item => existingIds.add(item.id));
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"); const otherTasks = [];
}).splice(0, 100) searchTasks.forEach(task => {
this.taskList.push({ if (!existingIds.has(task.id)) {
label: [{id: 0, value: this.$L('我协助的任务'), disabled: true}], existingIds.add(task.id);
list: assistTask.map(item => { otherTasks.push({
return { id: task.id,
id: item.id, value: task.name,
value: item.name tip: task.complete_at ? this.$L('已完成') : null,
} });
}),
})
}
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([])
} }
}).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([]) 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; break;
case "~": // ~ case "~": // ~
@ -2567,7 +2641,7 @@ export default {
const data = (await this.$store.dispatch("searchFiles", searchTerm).catch(_ => {}))?.data; const data = (await this.$store.dispatch("searchFiles", searchTerm).catch(_ => {}))?.data;
if (data) { if (data) {
lists.push({ 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 => { list: data.filter(item => item.type !== "folder").map(item => {
return { return {
id: item.id, id: item.id,
@ -2601,7 +2675,7 @@ export default {
}).catch(_ => {}))?.data; }).catch(_ => {}))?.data;
if (myData) { if (myData) {
lists.push({ 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 => { list: myData.data.map(item => {
return { return {
id: item.id, id: item.id,
@ -2621,7 +2695,7 @@ export default {
}).catch(_ => {}))?.data; }).catch(_ => {}))?.data;
if (receiveData) { if (receiveData) {
lists.push({ 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 => { list: receiveData.data.map(item => {
return { return {
id: item.id, id: item.id,
@ -2645,7 +2719,7 @@ export default {
const isOwnBot = isBotDialog && this.dialogData.bot == this.userId; const isOwnBot = isBotDialog && this.dialogData.bot == this.userId;
const isBotManager = isBotDialog && this.dialogData.email === 'bot-manager@bot.system'; const isBotManager = isBotDialog && this.dialogData.email === 'bot-manager@bot.system';
const showBotCommands = allowBotCommands && (isOwnBot || isBotManager); 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 = [{ const slashLists = [{
label: baseLabel, label: baseLabel,
list: [ list: [
@ -2740,7 +2814,7 @@ export default {
} }
); );
slashLists.push({ slashLists.push({
label: [{id: 0, value: this.$L('机器人命令'), disabled: true}], label: [{id: 0, value: this.$L('机器人命令'), className: "sticky-top", disabled: true}],
list: commandList, list: commandList,
}); });
} }

View File

@ -1132,19 +1132,13 @@
width: auto; width: auto;
overflow: hidden; overflow: hidden;
&.task-mention { &.task-mention,
.ql-mention-list { &.file-mention,
> li { &.report-mention,
&:first-child { &.slash-mention {
margin-top: 0;
}
}
}
.ql-mention-list-item { .ql-mention-list-item {
line-height: 36px; line-height: 40px;
.mention-item-disabled { padding: 0 4px;
padding: 8px 4px 0;
}
} }
} }
@ -1246,6 +1240,7 @@
.mention-item-tip { .mention-item-tip {
flex-shrink: 0; flex-shrink: 0;
padding-right: 8px;
text-align: right; text-align: right;
color: #8f8f8e; color: #8f8f8e;
font-size: 12px; font-size: 12px;