From 4b106e1f410b8ad5af039e61c8103401c9bb7ecc Mon Sep 17 00:00:00 2001 From: kuaifan Date: Wed, 24 Sep 2025 09:51:13 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E6=9C=80=E8=BF=91?= =?UTF-8?q?=E8=AE=BF=E9=97=AE=E8=AE=B0=E5=BD=95=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 UsersController 中新增获取和删除最近访问记录的接口 - 在相关控制器中记录用户最近访问的任务、文件和消息文件 - 新增 RecentManagement 组件,展示用户最近访问的记录 - 更新样式和图标以提升用户体验 --- app/Http/Controllers/Api/DialogController.php | 13 +- app/Http/Controllers/Api/FileController.php | 11 + .../Controllers/Api/ProjectController.php | 11 +- app/Http/Controllers/Api/UsersController.php | 238 +++++++++++++ app/Models/UserRecentItem.php | 58 ++++ app/Models/UserTaskBrowse.php | 12 +- ..._101600_create_user_recent_items_table.php | 44 +++ resources/assets/js/pages/manage.vue | 17 + .../assets/js/pages/manage/application.vue | 4 + .../manage/components/RecentManagement.vue | 316 ++++++++++++++++++ resources/assets/js/store/actions.js | 32 +- resources/assets/sass/pages/components/_.scss | 1 + .../pages/components/recent-management.scss | 67 ++++ resources/assets/sass/pages/page-apply.scss | 4 + .../public/images/application/recent.svg | 5 + 15 files changed, 826 insertions(+), 7 deletions(-) create mode 100644 app/Models/UserRecentItem.php create mode 100644 database/migrations/2025_09_22_101600_create_user_recent_items_table.php create mode 100644 resources/assets/js/pages/manage/components/RecentManagement.vue create mode 100644 resources/assets/sass/pages/components/recent-management.scss create mode 100644 resources/assets/statics/public/images/application/recent.svg diff --git a/app/Http/Controllers/Api/DialogController.php b/app/Http/Controllers/Api/DialogController.php index e2c96fe49..bc01443dd 100755 --- a/app/Http/Controllers/Api/DialogController.php +++ b/app/Http/Controllers/Api/DialogController.php @@ -28,6 +28,7 @@ use App\Models\WebSocketDialogMsgRead; use App\Models\WebSocketDialogMsgTodo; use App\Models\WebSocketDialogMsgTranslate; use App\Models\WebSocketDialogSession; +use App\Models\UserRecentItem; use App\Module\Table\OnlineData; use App\Module\ZincSearch\ZincSearchDialogMsg; use App\Tasks\BotReceiveMsgTask; @@ -1886,7 +1887,7 @@ class DialogController extends AbstractController */ public function msg__detail() { - User::auth(); + $user =User::auth(); // $msg_id = intval(Request::input('msg_id')); $only_update_at = Request::input('only_update_at', 'no'); @@ -1924,6 +1925,16 @@ class DialogController extends AbstractController } } // + if ($dialogMsg->type === 'file') { + UserRecentItem::record( + $user->userid, + UserRecentItem::TYPE_MESSAGE_FILE, + $dialogMsg->id, + UserRecentItem::SOURCE_DIALOG, + $dialogMsg->dialog_id + ); + } + return Base::retSuccess('success', $data); } diff --git a/app/Http/Controllers/Api/FileController.php b/app/Http/Controllers/Api/FileController.php index 384be537c..54ab4f682 100755 --- a/app/Http/Controllers/Api/FileController.php +++ b/app/Http/Controllers/Api/FileController.php @@ -11,6 +11,7 @@ use App\Models\FileContent; use App\Models\FileLink; use App\Models\FileUser; use App\Models\User; +use App\Models\UserRecentItem; use App\Module\Base; use App\Module\Down; use App\Module\Timer; @@ -560,6 +561,16 @@ class FileController extends AbstractController $builder->whereId($history_id); } $content = $builder->orderByDesc('id')->first(); + if (isset($user)) { + UserRecentItem::record( + $user->userid, + UserRecentItem::TYPE_FILE, + $file->id, + UserRecentItem::SOURCE_FILESYSTEM, + intval($file->pid) + ); + } + if ($down === 'preview') { return Redirect::to(FileContent::formatPreview($file, $content?->content)); } diff --git a/app/Http/Controllers/Api/ProjectController.php b/app/Http/Controllers/Api/ProjectController.php index 9d93dd041..a82268353 100755 --- a/app/Http/Controllers/Api/ProjectController.php +++ b/app/Http/Controllers/Api/ProjectController.php @@ -38,6 +38,7 @@ use App\Models\ProjectPermission; use App\Models\ProjectTaskContent; use App\Models\WebSocketDialogMsg; use App\Module\BillMultipleExport; +use App\Models\UserRecentItem; use Illuminate\Support\Facades\DB; use App\Models\ProjectTaskFlowChange; use App\Models\ProjectTaskVisibilityUser; @@ -1908,7 +1909,7 @@ class ProjectController extends AbstractController */ public function task__filedetail() { - User::auth(); + $user = User::auth(); // $file_id = intval(Request::input('file_id')); $only_update_at = Request::input('only_update_at', 'no'); @@ -1931,6 +1932,14 @@ class ProjectController extends AbstractController // ProjectTask::userTask($file->task_id, null); // + UserRecentItem::record( + $user->userid, + UserRecentItem::TYPE_TASK_FILE, + $file->id, + UserRecentItem::SOURCE_PROJECT_TASK, + $file->task_id + ); + return Base::retSuccess('success', File::formatFileData($data)); } diff --git a/app/Http/Controllers/Api/UsersController.php b/app/Http/Controllers/Api/UsersController.php index 93fca0d64..63031664b 100755 --- a/app/Http/Controllers/Api/UsersController.php +++ b/app/Http/Controllers/Api/UsersController.php @@ -16,6 +16,7 @@ use App\Ldap\LdapUser; use App\Models\Meeting; use App\Models\Project; use App\Models\ProjectTask; +use App\Models\ProjectTaskFile; use App\Models\UserBot; use App\Models\WebSocket; use App\Models\UmengAlias; @@ -32,6 +33,7 @@ use App\Models\WebSocketDialogMsg; use App\Models\WebSocketDialogUser; use App\Models\UserTaskBrowse; use App\Models\UserFavorite; +use App\Models\UserRecentItem; use Illuminate\Support\Facades\DB; use App\Models\UserEmailVerification; use App\Module\AgoraIO\AgoraTokenGenerator; @@ -2849,6 +2851,242 @@ class UsersController extends AbstractController return Base::retSuccess('清理完成', ['deleted_count' => $deletedCount]); } + /** + * @api {get} api/users/recent/browse 45. 获取最近访问记录 + * + * @apiDescription 需要token身份 + * @apiVersion 1.0.0 + * @apiGroup users + * @apiName recent__browse + * + * @apiParam {String} [type] 类型过滤 (task/file/task_file/message_file) + * @apiParam {Number} [page=1] 页码 + * @apiParam {Number} [page_size=20] 每页数量,最大100 + * + * @apiSuccess {Number} ret 返回状态码(1正确、0错误) + * @apiSuccess {String} msg 返回信息(错误描述) + * @apiSuccess {Object} data 返回数据 + */ + public function recent__browse() + { + $user = User::auth(); + + $type = trim(Request::input('type')); + $page = max(1, intval(Request::input('page', 1))); + $pageSize = intval(Request::input('page_size', 20)); + $pageSize = max(1, min(100, $pageSize)); + + $query = UserRecentItem::whereUserid($user->userid); + if ($type !== '') { + $query->where('target_type', $type); + } + + $total = (clone $query)->count(); + $items = $query->orderByDesc('browsed_at') + ->skip(($page - 1) * $pageSize) + ->take($pageSize) + ->get(); + + $taskIds = []; + $fileIds = []; + $taskFileIds = []; + $messageIds = []; + + foreach ($items as $item) { + switch ($item->target_type) { + case UserRecentItem::TYPE_TASK: + $taskIds[] = $item->target_id; + break; + case UserRecentItem::TYPE_FILE: + $fileIds[] = $item->target_id; + break; + case UserRecentItem::TYPE_TASK_FILE: + $taskFileIds[] = $item->target_id; + break; + case UserRecentItem::TYPE_MESSAGE_FILE: + $messageIds[] = $item->target_id; + break; + } + } + + $tasks = empty($taskIds) ? collect() : ProjectTask::with(['project']) + ->whereIn('id', array_unique($taskIds)) + ->whereNull('archived_at') + ->get() + ->keyBy('id'); + + $files = empty($fileIds) ? collect() : File::whereIn('id', array_unique($fileIds)) + ->get() + ->keyBy('id'); + + $taskFiles = empty($taskFileIds) ? collect() : ProjectTaskFile::whereIn('id', array_unique($taskFileIds)) + ->get() + ->keyBy('id'); + + $taskFileTaskIds = $taskFiles->pluck('task_id')->filter()->unique()->all(); + $taskFileTasks = empty($taskFileTaskIds) ? collect() : ProjectTask::whereIn('id', $taskFileTaskIds) + ->get() + ->keyBy('id'); + + $projectIds = $tasks->pluck('project_id') + ->merge($taskFiles->pluck('project_id')) + ->filter() + ->unique() + ->all(); + + $projects = empty($projectIds) ? collect() : Project::whereIn('id', $projectIds) + ->get() + ->keyBy('id'); + + $messages = empty($messageIds) ? collect() : WebSocketDialogMsg::whereIn('id', array_unique($messageIds)) + ->get() + ->keyBy('id'); + + $dialogIds = $messages->pluck('dialog_id')->filter()->unique()->all(); + $dialogs = empty($dialogIds) ? collect() : WebSocketDialog::whereIn('id', $dialogIds) + ->get() + ->keyBy('id'); + + $result = []; + foreach ($items as $item) { + $timestamp = $item->browsed_at ?: $item->updated_at; + if ($timestamp instanceof Carbon) { + $browsedAt = $timestamp->toDateTimeString(); + } elseif ($timestamp) { + $browsedAt = Carbon::parse($timestamp)->toDateTimeString(); + } else { + $browsedAt = Carbon::now()->toDateTimeString(); + } + + $baseData = [ + 'record_id' => $item->id, + 'source_type' => $item->source_type, + 'source_id' => $item->source_id, + 'browsed_at' => $browsedAt, + ]; + + switch ($item->target_type) { + case UserRecentItem::TYPE_TASK: + $task = $tasks->get($item->target_id); + if (!$task) { + continue 2; + } + $flowItemParts = explode('|', $task->flow_item_name ?: ''); + $flowItemName = $flowItemParts[1] ?? $task->flow_item_name; + $flowItemStatus = $flowItemParts[0] ?? ''; + $flowItemColor = $flowItemParts[2] ?? ''; + $result[] = array_merge($baseData, [ + 'type' => UserRecentItem::TYPE_TASK, + 'id' => $task->id, + 'name' => $task->name, + 'project_id' => $task->project_id, + 'project_name' => $task->project->name ?? '', + 'column_id' => $task->column_id, + 'flow_item_id' => $task->flow_item_id, + 'flow_item_name' => $flowItemName, + 'flow_item_status' => $flowItemStatus, + 'flow_item_color' => $flowItemColor, + 'complete_at' => $task->complete_at, + ]); + break; + + case UserRecentItem::TYPE_FILE: + $file = $files->get($item->target_id); + if (!$file) { + continue 2; + } + $result[] = array_merge($baseData, [ + 'type' => UserRecentItem::TYPE_FILE, + 'id' => $file->id, + 'name' => $file->name, + 'ext' => $file->ext, + 'size' => (int) $file->size, + 'file_type' => $file->type, + 'folder_id' => (int) $file->pid, + ]); + break; + + case UserRecentItem::TYPE_TASK_FILE: + $taskFile = $taskFiles->get($item->target_id); + if (!$taskFile) { + continue 2; + } + $project = $projects->get($taskFile->project_id); + $taskInfo = $taskFileTasks->get($taskFile->task_id); + $result[] = array_merge($baseData, [ + 'type' => UserRecentItem::TYPE_TASK_FILE, + 'id' => $taskFile->id, + 'name' => $taskFile->name, + 'ext' => $taskFile->ext, + 'size' => (int) $taskFile->size, + 'task_id' => $taskFile->task_id, + 'task_name' => $taskInfo->name ?? '', + 'project_id' => $taskFile->project_id, + 'project_name' => $project->name ?? '', + ]); + break; + + case UserRecentItem::TYPE_MESSAGE_FILE: + $message = $messages->get($item->target_id); + if (!$message || $message->type !== 'file') { + continue 2; + } + $msgData = Base::json2array($message->getRawOriginal('msg')); + $dialog = $dialogs->get($message->dialog_id); + $result[] = array_merge($baseData, [ + 'type' => UserRecentItem::TYPE_MESSAGE_FILE, + 'id' => $message->id, + 'name' => $msgData['name'] ?? '', + 'ext' => $msgData['ext'] ?? '', + 'size' => isset($msgData['size']) ? (int) $msgData['size'] : 0, + 'dialog_id' => $message->dialog_id, + 'dialog_name' => $dialog->name ?? '', + ]); + break; + } + } + + return Base::retSuccess('success', [ + 'list' => $result, + 'page' => $page, + 'page_size' => $pageSize, + 'total' => $total, + ]); + } + + /** + * @api {post} api/users/recent/delete 45.1 删除最近访问记录 + * + * @apiDescription 需要token身份 + * @apiVersion 1.0.0 + * @apiGroup users + * @apiName recent__delete + * + * @apiParam {Number} id 记录ID + * + * @apiSuccess {Number} ret 返回状态码(1正确、0错误) + * @apiSuccess {String} msg 返回信息(错误描述) + * @apiSuccess {Object} data 返回数据 + */ + public function recent__delete() + { + $user = User::auth(); + + $id = intval(Request::input('id')); + if ($id <= 0) { + return Base::retError('参数错误'); + } + + $record = UserRecentItem::whereUserid($user->userid)->whereId($id)->first(); + if (!$record) { + return Base::retError('记录不存在'); + } + + $record->delete(); + + return Base::retSuccess('删除成功'); + } + /** * @api {get} api/users/favorites 46. 获取用户收藏列表 * diff --git a/app/Models/UserRecentItem.php b/app/Models/UserRecentItem.php new file mode 100644 index 000000000..9a92414dc --- /dev/null +++ b/app/Models/UserRecentItem.php @@ -0,0 +1,58 @@ + $userid, + 'target_type' => $targetType, + 'target_id' => $targetId, + 'source_type' => $sourceType, + 'source_id' => $sourceId, + ], + [ + 'browsed_at' => Carbon::now(), + ] + )); + } +} diff --git a/app/Models/UserTaskBrowse.php b/app/Models/UserTaskBrowse.php index efb2aecb9..7b79b8112 100644 --- a/app/Models/UserTaskBrowse.php +++ b/app/Models/UserTaskBrowse.php @@ -68,7 +68,7 @@ class UserTaskBrowse extends AbstractModel */ public static function recordBrowse($userid, $task_id) { - return self::updateOrCreate( + $record = self::updateOrCreate( [ 'userid' => $userid, 'task_id' => $task_id, @@ -77,6 +77,16 @@ class UserTaskBrowse extends AbstractModel 'browsed_at' => Carbon::now(), ] ); + + UserRecentItem::record( + $userid, + UserRecentItem::TYPE_TASK, + $task_id, + UserRecentItem::SOURCE_PROJECT, + 0 + ); + + return $record; } /** diff --git a/database/migrations/2025_09_22_101600_create_user_recent_items_table.php b/database/migrations/2025_09_22_101600_create_user_recent_items_table.php new file mode 100644 index 000000000..817e131bc --- /dev/null +++ b/database/migrations/2025_09_22_101600_create_user_recent_items_table.php @@ -0,0 +1,44 @@ +bigIncrements('id'); + $table->bigInteger('userid')->index()->default(0)->comment('用户ID'); + $table->string('target_type', 50)->default('')->comment('目标类型(task/file/task_file/message_file 等)'); + $table->bigInteger('target_id')->default(0)->comment('目标ID'); + $table->string('source_type', 50)->default('')->comment('来源类型(project/filesystem/project_task/dialog 等)'); + $table->bigInteger('source_id')->default(0)->comment('来源ID'); + $table->timestamp('browsed_at')->nullable()->index()->comment('浏览时间'); + $table->timestamps(); + + $table->index(['userid', 'browsed_at']); + $table->unique(['userid', 'target_type', 'target_id', 'source_type', 'source_id'], 'recent_unique'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('user_recent_items'); + } +} diff --git a/resources/assets/js/pages/manage.vue b/resources/assets/js/pages/manage.vue index 8c61bc1a8..f0ccfc9d0 100644 --- a/resources/assets/js/pages/manage.vue +++ b/resources/assets/js/pages/manage.vue @@ -344,6 +344,14 @@ + + + + + +
+
+ {{$L('最近打开')}} +
+ +
+
+
+
    +
  • +
    {{$L('类型')}}
    +
    + +
    +
  • +
  • + +
  • +
+
+
+ + + + + + + diff --git a/resources/assets/js/store/actions.js b/resources/assets/js/store/actions.js index 1e23921d2..db5b27925 100644 --- a/resources/assets/js/store/actions.js +++ b/resources/assets/js/store/actions.js @@ -2758,10 +2758,8 @@ export default { task_id: task_id }, method: 'post', - spinner: 0, // 静默调用,不显示loading }).catch(error => { console.warn('保存任务浏览历史失败:', error); - // API失败时不影响用户体验,只记录错误 }); }, @@ -2777,7 +2775,34 @@ export default { limit: limit }, method: 'get', - spinner: 0, // 静默调用 + }); + }, + + /** + * 获取最近浏览历史 + * @param dispatch + * @param params + * @returns {Promise} + */ + getRecentBrowseHistory({dispatch}, params = {}) { + return dispatch('call', { + url: 'users/recent/browse', + data: params, + method: 'get', + }); + }, + + /** + * 删除最近浏览记录 + * @param dispatch + * @param id + * @returns {Promise} + */ + removeRecentBrowseRecord({dispatch}, id) { + return dispatch('call', { + url: 'users/recent/delete', + data: {id}, + method: 'post', }); }, @@ -2864,7 +2889,6 @@ export default { id: id }, method: 'get', - spinner: 0, // 静默调用 }); }, diff --git a/resources/assets/sass/pages/components/_.scss b/resources/assets/sass/pages/components/_.scss index 3255ae5cd..d13d96ac1 100755 --- a/resources/assets/sass/pages/components/_.scss +++ b/resources/assets/sass/pages/components/_.scss @@ -6,6 +6,7 @@ @import "dialog-session-history"; @import "dialog-wrapper"; @import "favorite-management"; +@import "recent-management"; @import "file-content"; @import "forwarder"; @import "general-operation"; diff --git a/resources/assets/sass/pages/components/recent-management.scss b/resources/assets/sass/pages/components/recent-management.scss new file mode 100644 index 000000000..57bfe1553 --- /dev/null +++ b/resources/assets/sass/pages/components/recent-management.scss @@ -0,0 +1,67 @@ +.recent-management { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + padding: 20px; + display: flex; + flex-direction: column; + + .management-title { + color: $primary-title-color; + font-size: 20px; + font-weight: 500; + line-height: 1; + margin-bottom: 24px; + display: flex; + align-items: center; + + .title-icon { + display: flex; + align-items: center; + width: 14px; + height: 14px; + margin-left: 4px; + margin-top: 2px; + } + } + + .recent-name { + display: flex; + align-items: center; + cursor: pointer; + color: #2d8cf0; + + &:hover { + text-decoration: underline; + } + + .ivu-tag { + height: 18px; + line-height: 18px; + padding: 0 4px; + transform: scale(0.8); + transform-origin: right center; + flex-shrink: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + } + + .recent-type-tag { + min-width: 60px; + text-align: center; + border-radius: 4px; + font-size: 12px; + line-height: 18px; + height: 20px; + padding: 0 8px; + } + + .table-page-box { + flex: 1; + height: 0; + } +} diff --git a/resources/assets/sass/pages/page-apply.scss b/resources/assets/sass/pages/page-apply.scss index 891597a15..36b34816c 100644 --- a/resources/assets/sass/pages/page-apply.scss +++ b/resources/assets/sass/pages/page-apply.scss @@ -208,6 +208,10 @@ background-image: url("../images/application/favorite.svg"); } + &.recent { + background-image: url("../images/application/recent.svg"); + } + &.export-manage { background-image: url("../images/application/export.svg"); } diff --git a/resources/assets/statics/public/images/application/recent.svg b/resources/assets/statics/public/images/application/recent.svg new file mode 100644 index 000000000..c7f770ded --- /dev/null +++ b/resources/assets/statics/public/images/application/recent.svg @@ -0,0 +1,5 @@ + + + + +