diff --git a/app/Http/Controllers/Api/ProjectController.php b/app/Http/Controllers/Api/ProjectController.php index 262cd862a..e35639780 100755 --- a/app/Http/Controllers/Api/ProjectController.php +++ b/app/Http/Controllers/Api/ProjectController.php @@ -2537,6 +2537,20 @@ class ProjectController extends AbstractController $task->pushMsg('add', $data); $task->taskPush(null, 0); + + // 应用任务模板使用统计(不影响主流程;非成员或模板已删除时静默忽略) + $templateId = intval(Request::input('template_id', 0)); + if ($templateId > 0) { + $tpl = ProjectTaskTemplate::find($templateId); + if ($tpl) { + $isMember = ProjectUser::where('project_id', $tpl->project_id) + ->where('userid', $user->userid)->exists(); + if ($isMember) { + $tpl->incrementUsage(); + } + } + } + return Base::retSuccess('添加成功', $data); } @@ -3640,6 +3654,117 @@ class ProjectController extends AbstractController return Base::retSuccess('success', $templates); } + /** + * @api {get} api/project/task/template_visible 02. 当前用户跨项目可见的全部任务模板 + * + * @apiDescription 返回当前用户加入的所有项目下的任务模板。当前项目的模板优先排序。 + * @apiVersion 1.0.0 + * @apiGroup project + * @apiName task__template_visible + * + * @apiParam {Number} [current_project_id] 当前项目 ID(用于排序优先;可空) + * + * @apiSuccess {Number} ret 返回状态码(1 正确、0 错误) + * @apiSuccess {String} msg 返回信息 + * @apiSuccess {Object[]} data 模板列表,每条包含 project_id, project_name, name, title, content, sort, is_default, userid, use_count, last_used_at + */ + public function task__template_visible() + { + $user = User::auth(); + $currentProjectId = intval(Request::input('current_project_id', 0)); + + $projectIds = ProjectUser::where('userid', $user->userid)->pluck('project_id'); + + $rows = ProjectTaskTemplate::with(['project:id,name']) + ->whereIn('project_id', $projectIds) + ->orderByRaw('project_id = ? DESC', [$currentProjectId]) + ->orderBy('sort') + ->orderBy('id') + ->get() + ->map(function ($tpl) { + return [ + 'id' => $tpl->id, + 'project_id' => $tpl->project_id, + 'project_name' => $tpl->project->name ?? '', + 'name' => $tpl->name, + 'title' => $tpl->title, + 'content' => $tpl->content, + 'sort' => $tpl->sort, + 'is_default' => $tpl->is_default, + 'userid' => $tpl->userid, + 'use_count' => $tpl->use_count, + 'last_used_at' => $tpl->last_used_at, + ]; + }); + + return Base::retSuccess('success', $rows); + } + + /** + * @api {get} api/project/task/template_search 03. 跨项目模板搜索分页 + * + * @apiDescription "更多"弹层用。返回当前用户跨项目可见模板,支持关键字 + 分页。 + * @apiVersion 1.0.0 + * @apiGroup project + * @apiName task__template_search + * + * @apiParam {String} [keyword] 关键字(在 name/title/content 上模糊匹配) + * @apiParam {Number} [page=1] 页码 + * @apiParam {Number} [page_size=20] 每页条数(最大 50) + * + * @apiSuccess {Number} ret 返回状态码 + * @apiSuccess {Object} data 含 total / page / page_size / items + */ + public function task__template_search() + { + $user = User::auth(); + $keyword = trim((string) Request::input('keyword', '')); + $page = max(1, intval(Request::input('page', 1))); + $pageSize = min(50, max(1, intval(Request::input('page_size', 20)))); + + $projectIds = ProjectUser::where('userid', $user->userid)->pluck('project_id'); + + $q = ProjectTaskTemplate::with(['project:id,name', 'user:userid,nickname']) + ->whereIn('project_id', $projectIds); + + if ($keyword !== '') { + $like = '%' . $keyword . '%'; + $q->where(function ($qq) use ($like) { + $qq->where('name', 'like', $like) + ->orWhere('title', 'like', $like) + ->orWhere('content', 'like', $like); + }); + } + + $total = (clone $q)->count(); + $items = $q->orderByDesc('use_count') + ->orderByDesc('last_used_at') + ->orderByDesc('created_at') + ->forPage($page, $pageSize) + ->get() + ->map(function ($tpl) { + return [ + 'id' => $tpl->id, + 'project_id' => $tpl->project_id, + 'project_name' => $tpl->project->name ?? '', + 'name' => $tpl->name, + 'title' => $tpl->title, + 'content' => $tpl->content, + 'use_count' => $tpl->use_count, + 'userid' => $tpl->userid, + 'user_name' => $tpl->user->nickname ?? '', + 'last_used_at' => $tpl->last_used_at, + ]; + }); + + return Base::retSuccess('success', [ + 'total' => $total, + 'page' => $page, + 'page_size' => $pageSize, + 'items' => $items, + ]); + } + /** * @api {post} api/project/task/template_save 保存任务模板 * diff --git a/app/Models/ProjectTaskTemplate.php b/app/Models/ProjectTaskTemplate.php index 7c3de6920..43d63619c 100644 --- a/app/Models/ProjectTaskTemplate.php +++ b/app/Models/ProjectTaskTemplate.php @@ -13,6 +13,8 @@ namespace App\Models; * @property int $sort 排序 * @property int $is_default 是否默认模板 * @property int $userid 创建人 + * @property int $use_count 累计使用次数 + * @property \Illuminate\Support\Carbon|null $last_used_at 最近一次使用时间 * @property \Illuminate\Support\Carbon|null $created_at * @property \Illuminate\Support\Carbon|null $updated_at * @property-read \App\Models\Project $project @@ -52,7 +54,18 @@ class ProjectTaskTemplate extends AbstractModel 'content', 'sort', 'is_default', - 'userid' + 'userid', + 'use_count', + 'last_used_at' + ]; + + /** + * The attributes that should be cast. + * + * @var array + */ + protected $casts = [ + 'last_used_at' => 'datetime', ]; /** @@ -74,4 +87,17 @@ class ProjectTaskTemplate extends AbstractModel { return $this->belongsTo(User::class, 'userid'); } + + /** + * 原子递增使用次数并刷新最近使用时间。 + */ + public function incrementUsage(): void + { + $this->newQuery() + ->where('id', $this->id) + ->update([ + 'use_count' => \DB::raw('use_count + 1'), + 'last_used_at' => now(), + ]); + } } diff --git a/database/migrations/2026_05_10_100000_add_use_count_to_project_task_templates.php b/database/migrations/2026_05_10_100000_add_use_count_to_project_task_templates.php new file mode 100644 index 000000000..ad7d2558f --- /dev/null +++ b/database/migrations/2026_05_10_100000_add_use_count_to_project_task_templates.php @@ -0,0 +1,25 @@ +unsignedInteger('use_count')->default(0)->after('is_default')->comment('累计使用次数'); + $table->timestamp('last_used_at')->nullable()->after('use_count')->comment('最近一次使用时间'); + $table->index(['use_count', 'last_used_at'], 'idx_template_usage'); + }); + } + + public function down() + { + Schema::table('project_task_templates', function (Blueprint $table) { + $table->dropIndex('idx_template_usage'); + $table->dropColumn(['use_count', 'last_used_at']); + }); + } +} diff --git a/language/original-api.txt b/language/original-api.txt index e31cfeeb2..508a80fb3 100644 --- a/language/original-api.txt +++ b/language/original-api.txt @@ -992,3 +992,4 @@ LDAP 用户缺少邮箱属性,请联系管理员配置 不能将负责人任命为项目管理员 不能将部门负责人任命为部门管理员 该用户不存在 +无权操作此模板 diff --git a/language/original-web.txt b/language/original-web.txt index 6372ca108..c5683268c 100644 --- a/language/original-web.txt +++ b/language/original-web.txt @@ -2384,3 +2384,7 @@ AI任务分析 即将罢免项目管理员 请确认以下操作,注意此操作不可逆! 移除成员负责的任务将变成无负责人。 +搜索模板 +来自(*) +暂无可用模板 +加载中 diff --git a/resources/assets/js/pages/manage/components/TaskAdd.vue b/resources/assets/js/pages/manage/components/TaskAdd.vue index cf6e9a09b..4c9300b3f 100644 --- a/resources/assets/js/pages/manage/components/TaskAdd.vue +++ b/resources/assets/js/pages/manage/components/TaskAdd.vue @@ -12,14 +12,21 @@ @on-visible-change="cascaderShow=!cascaderShow" filterable/> -