feat: 添加任务浏览历史功能

- 在 UsersController 中新增获取、记录和清理任务浏览历史的 API 接口
- 创建 UserTaskBrowse 模型以管理用户的任务浏览记录
- 更新前端 Vue 组件以支持任务浏览历史的加载和显示
- 移除不再使用的本地缓存逻辑,直接通过 API 进行数据交互
This commit is contained in:
kuaifan 2025-09-22 07:10:12 +08:00
parent 6148b996d8
commit 0401b8a6e6
6 changed files with 334 additions and 23 deletions

View File

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

View File

@ -0,0 +1,128 @@
<?php
namespace App\Models;
use Carbon\Carbon;
/**
* App\Models\UserTaskBrowse
*
* @property int $id
* @property int $userid 用户ID
* @property int $task_id 任务ID
* @property \Illuminate\Support\Carbon|null $browsed_at 浏览时间
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read \App\Models\ProjectTask|null $task
* @property-read \App\Models\User|null $user
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelAppend()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelHidden()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel change($array)
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel getKeyValue()
* @method static \Illuminate\Database\Eloquent\Builder|UserTaskBrowse newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|UserTaskBrowse newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|UserTaskBrowse query()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
* @method static \Illuminate\Database\Eloquent\Builder|UserTaskBrowse whereBrowsedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserTaskBrowse whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserTaskBrowse whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserTaskBrowse whereTaskId($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserTaskBrowse whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserTaskBrowse whereUserid($value)
* @mixin \Eloquent
*/
class UserTaskBrowse extends AbstractModel
{
protected $fillable = [
'userid',
'task_id',
'browsed_at',
];
protected $dates = [
'browsed_at',
];
/**
* 关联用户
*/
public function user()
{
return $this->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();
}
}

View File

@ -0,0 +1,42 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateUserTaskBrowsesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
if (Schema::hasTable('user_task_browses'))
return;
Schema::create('user_task_browses', function (Blueprint $table) {
$table->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');
}
}

View File

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

View File

@ -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, // 静默调用
});
},
/**

View File

@ -102,7 +102,6 @@ export default {
cacheColumns: [],
cacheTasks: [],
cacheProjectParameter: [],
cacheTaskBrowse: [],
// Emoji
cacheEmojis: [],