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 (!$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()) {

View File

@ -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] 主任务IDproject_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 {

View File

@ -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']}%");
}

View File

@ -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']}%")

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: {
keys: {name: key},
archived: 'all',
scope: 'all_project',
pagesize: this.action ? 50 : 10,
},
}).then(({data}) => {

View File

@ -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,
});
}

View File

@ -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;