diff --git a/app/Http/Controllers/Api/UsersController.php b/app/Http/Controllers/Api/UsersController.php index a62990b38..4dfb44282 100755 --- a/app/Http/Controllers/Api/UsersController.php +++ b/app/Http/Controllers/Api/UsersController.php @@ -15,6 +15,7 @@ use App\Module\Timer; use App\Ldap\LdapUser; use App\Models\Meeting; use App\Models\Project; +use App\Models\ProjectTask; use App\Models\UserBot; use App\Models\WebSocket; use App\Models\UmengAlias; @@ -28,6 +29,7 @@ use App\Models\UserDepartment; use App\Models\WebSocketDialog; use App\Models\UserCheckinRecord; use App\Models\WebSocketDialogMsg; +use App\Models\UserTaskBrowse; use Illuminate\Support\Facades\DB; use App\Models\UserEmailVerification; use App\Module\AgoraIO\AgoraTokenGenerator; @@ -2718,4 +2720,109 @@ class UsersController extends AbstractController // return Base::retSuccess('保存成功'); } + + /** + * @api {get} api/users/task/browse 43. 获取任务浏览历史 + * + * @apiDescription 需要token身份 + * @apiVersion 1.0.0 + * @apiGroup users + * @apiName task__browse + * + * @apiParam {Number} [limit=20] 获取数量限制,最大50 + * + * @apiSuccess {Number} ret 返回状态码(1正确、0错误) + * @apiSuccess {String} msg 返回信息(错误描述) + * @apiSuccess {Object} data 返回数据 + */ + public function task__browse() + { + $user = User::auth(); + // + $limit = min(intval(Request::input('limit', 20)), 50); + // + $browseHistory = UserTaskBrowse::getUserBrowseHistory($user->userid, $limit); + + $data = []; + foreach ($browseHistory as $browse) { + if ($browse->task) { + // 解析 flow_item_name 字段(格式:status|name|color) + $flowItemParts = explode('|', $browse->task->flow_item_name ?: ''); + $flowItemStatus = $flowItemParts[0] ?? ''; + $flowItemName = $flowItemParts[1] ?? $browse->task->flow_item_name; + $flowItemColor = $flowItemParts[2] ?? ''; + + $data[] = [ + 'id' => $browse->task->id, + 'name' => $browse->task->name, + 'project_id' => $browse->task->project_id, + 'column_id' => $browse->task->column_id, + 'parent_id' => $browse->task->parent_id, + 'flow_item_id' => $browse->task->flow_item_id, + 'flow_item_name' => $flowItemName, + 'flow_item_status' => $flowItemStatus, + 'flow_item_color' => $flowItemColor, + 'complete_at' => $browse->task->complete_at, + 'browsed_at' => $browse->browsed_at, + ]; + } + } + // + return Base::retSuccess('success', $data); + } + + /** + * @api {post} api/users/task/browse_save 44. 记录任务浏览历史 + * + * @apiDescription 需要token身份 + * @apiVersion 1.0.0 + * @apiGroup users + * @apiName task__browse_save + * + * @apiParam {Number} task_id 任务ID + * + * @apiSuccess {Number} ret 返回状态码(1正确、0错误) + * @apiSuccess {String} msg 返回信息(错误描述) + * @apiSuccess {Object} data 返回数据 + */ + public function task__browse_save() + { + $user = User::auth(); + // + $task_id = intval(Request::input('task_id')); + if ($task_id <= 0) { + return Base::retError('参数错误'); + } + // + ProjectTask::userTask($task_id, null, null); + // + UserTaskBrowse::recordBrowse($user->userid, $task_id); + // + return Base::retSuccess('记录成功'); + } + + /** + * @api {post} api/users/task/browse_clean 45. 清理任务浏览历史 + * + * @apiDescription 需要token身份 + * @apiVersion 1.0.0 + * @apiGroup users + * @apiName task__browse_clean + * + * @apiParam {Number} [keep_count=100] 保留记录数量,0表示全部清理 + * + * @apiSuccess {Number} ret 返回状态码(1正确、0错误) + * @apiSuccess {String} msg 返回信息(错误描述) + * @apiSuccess {Object} data 返回数据 + */ + public function task__browse_clean() + { + $user = User::auth(); + // + $keepCount = intval(Request::input('keep_count', 100)); + // + $deletedCount = UserTaskBrowse::cleanUserBrowseHistory($user->userid, $keepCount); + // + return Base::retSuccess('清理完成', ['deleted_count' => $deletedCount]); + } } diff --git a/app/Models/UserTaskBrowse.php b/app/Models/UserTaskBrowse.php new file mode 100644 index 000000000..efb2aecb9 --- /dev/null +++ b/app/Models/UserTaskBrowse.php @@ -0,0 +1,128 @@ +belongsTo(User::class, 'userid', 'userid'); + } + + /** + * 关联任务 + */ + public function task() + { + return $this->belongsTo(ProjectTask::class, 'task_id', 'id'); + } + + /** + * 记录用户浏览任务 + * @param int $userid 用户ID + * @param int $task_id 任务ID + * @return UserTaskBrowse + */ + public static function recordBrowse($userid, $task_id) + { + return self::updateOrCreate( + [ + 'userid' => $userid, + 'task_id' => $task_id, + ], + [ + 'browsed_at' => Carbon::now(), + ] + ); + } + + /** + * 获取用户浏览历史 + * @param int $userid 用户ID + * @param int $limit 获取数量 + * @return \Illuminate\Database\Eloquent\Collection + */ + public static function getUserBrowseHistory($userid, $limit = 20) + { + return self::with(['task' => function ($query) { + $query->select([ + 'id', 'name', 'project_id', 'column_id', 'parent_id', + 'flow_item_id', 'flow_item_name', + 'complete_at', 'archived_at' + ]); + }]) + ->whereUserid($userid) + ->whereHas('task', function ($query) { + // 只获取存在且未被删除的任务 + $query->whereNull('archived_at'); + }) + ->orderByDesc('browsed_at') + ->limit($limit) + ->get(); + } + + /** + * 清理用户浏览历史 + * @param int $userid 用户ID + * @param int $keepCount 保留数量,0表示全部删除 + * @return int 删除的记录数 + */ + public static function cleanUserBrowseHistory($userid, $keepCount = 100) + { + if ($keepCount === 0) { + return self::whereUserid($userid)->delete(); + } + + $keepIds = self::whereUserid($userid) + ->orderByDesc('browsed_at') + ->limit($keepCount) + ->pluck('id'); + + return self::whereUserid($userid) + ->whereNotIn('id', $keepIds) + ->delete(); + } +} diff --git a/database/migrations/2025_09_22_101500_create_user_task_browses_table.php b/database/migrations/2025_09_22_101500_create_user_task_browses_table.php new file mode 100644 index 000000000..c91bd8860 --- /dev/null +++ b/database/migrations/2025_09_22_101500_create_user_task_browses_table.php @@ -0,0 +1,42 @@ +bigIncrements('id'); + $table->bigInteger('userid')->index()->nullable()->default(0)->comment('用户ID'); + $table->bigInteger('task_id')->index()->nullable()->default(0)->comment('任务ID'); + $table->timestamp('browsed_at')->index()->nullable()->comment('浏览时间'); + $table->timestamps(); + + // 复合索引:用户ID + 浏览时间(用于按时间排序获取用户浏览历史) + $table->index(['userid', 'browsed_at']); + // 唯一索引:用户ID + 任务ID(防止重复记录,相同任务会更新浏览时间) + $table->unique(['userid', 'task_id']); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('user_task_browses'); + } +} diff --git a/resources/assets/js/pages/manage.vue b/resources/assets/js/pages/manage.vue index 283c4f7ca..2607bf777 100644 --- a/resources/assets/js/pages/manage.vue +++ b/resources/assets/js/pages/manage.vue @@ -487,6 +487,9 @@ export default { approveShow: false, approveDetails: {id: 0}, approveDetailsShow: false, + + taskBrowseLoading: false, + taskBrowseHistory: [], // 存储任务浏览历史 } }, @@ -537,7 +540,6 @@ export default { 'columnTemplate', 'clientNewVersion', - 'cacheTaskBrowse', 'reportUnreadNumber', 'approveUnreadNumber', @@ -708,10 +710,8 @@ export default { }, taskBrowseLists() { - const {cacheTasks, cacheTaskBrowse, userId} = this; - return cacheTaskBrowse.filter(({userid}) => userid === userId).map(({id}) => { - return cacheTasks.find(task => task.id === id) || {} - }); + // 直接使用组件内的响应式数据 + return this.taskBrowseHistory.slice(0, 10); // 只显示前10个 }, }, @@ -912,6 +912,10 @@ export default { menuVisibleChange(visible) { this.visibleMenu = visible + // 当菜单展开时,获取最新的浏览历史 + if (visible && !this.taskBrowseLoading) { + this.loadTaskBrowseHistory() + } }, classNameRoute(path) { @@ -1387,6 +1391,24 @@ export default { }); } }, + + /** + * 加载任务浏览历史 + */ + loadTaskBrowseHistory() { + if (this.taskBrowseLoading) return + + this.taskBrowseLoading = true + this.$store.dispatch("getTaskBrowseHistory", 20).then(({data}) => { + // 更新组件内的浏览历史数据 + this.taskBrowseHistory = data || [] + }).catch(error => { + console.warn('获取任务浏览历史失败:', error) + // 失败时保持当前数据不变 + }).finally(() => { + this.taskBrowseLoading = false + }) + }, } } diff --git a/resources/assets/js/store/actions.js b/resources/assets/js/store/actions.js index b707e5de2..a2dc8cd57 100644 --- a/resources/assets/js/store/actions.js +++ b/resources/assets/js/store/actions.js @@ -1076,7 +1076,6 @@ export default { cacheProjectParameter: await $A.IDBArray("cacheProjectParameter"), cacheLoginEmail: await $A.IDBString("cacheLoginEmail"), cacheFileSort: await $A.IDBJson("cacheFileSort"), - cacheTaskBrowse: await $A.IDBArray("cacheTaskBrowse"), cacheTranslationLanguage: await $A.IDBString("cacheTranslationLanguage"), cacheTranscriptionLanguage: await $A.IDBString("cacheTranscriptionLanguage"), cacheTranslations: await $A.IDBArray("cacheTranslations"), @@ -1124,7 +1123,6 @@ export default { 'cacheColumns', 'cacheTasks', 'cacheProjectParameter', - 'cacheTaskBrowse', 'cacheTranslations', 'dialogMsgs', 'dialogDrafts', @@ -2746,23 +2744,38 @@ export default { /** * 保存任务浏览记录 - * @param state + * @param dispatch * @param task_id */ - saveTaskBrowse({state}, task_id) { - const index = state.cacheTaskBrowse.findIndex(({id}) => id == task_id) - if (index > -1) { - state.cacheTaskBrowse.splice(index, 1) - } - state.cacheTaskBrowse.unshift({ - id: task_id, - userid: state.userId - }) - if (state.cacheTaskBrowse.length > 200) { - state.cacheTaskBrowse.splice(200); - } - // - $A.IDBSave("cacheTaskBrowse", state.cacheTaskBrowse); + saveTaskBrowse({dispatch}, task_id) { + // 直接调用API保存到远程,不维护本地缓存 + dispatch('call', { + url: 'users/task/browse_save', + data: { + task_id: task_id + }, + method: 'post', + spinner: 0, // 静默调用,不显示loading + }).catch(error => { + console.warn('保存任务浏览历史失败:', error); + // API失败时不影响用户体验,只记录错误 + }); + }, + + /** + * 获取任务浏览历史 + * @param dispatch + * @param limit + */ + getTaskBrowseHistory({dispatch}, limit = 20) { + return dispatch('call', { + url: 'users/task/browse', + data: { + limit: limit + }, + method: 'get', + spinner: 0, // 静默调用 + }); }, /** diff --git a/resources/assets/js/store/state.js b/resources/assets/js/store/state.js index 4a4cf68d5..0484ebe94 100644 --- a/resources/assets/js/store/state.js +++ b/resources/assets/js/store/state.js @@ -102,7 +102,6 @@ export default { cacheColumns: [], cacheTasks: [], cacheProjectParameter: [], - cacheTaskBrowse: [], // Emoji cacheEmojis: [],