mirror of
https://github.com/kuaifan/dootask.git
synced 2026-01-01 00:38:14 +00:00
feat: 增强搜索功能,支持通过 ID、名称和其他字段搜索任务、文件和报告
This commit is contained in:
parent
869ac7d316
commit
16a55de6f1
@ -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()) {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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']}%");
|
||||
}
|
||||
|
||||
@ -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']}%")
|
||||
|
||||
@ -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.
|
||||
}
|
||||
};
|
||||
@ -339,6 +339,7 @@ export default {
|
||||
data: {
|
||||
keys: {name: key},
|
||||
archived: 'all',
|
||||
scope: 'all_project',
|
||||
pagesize: this.action ? 50 : 10,
|
||||
},
|
||||
}).then(({data}) => {
|
||||
|
||||
@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user