feat: 添加最近访问记录功能

- 在 UsersController 中新增获取和删除最近访问记录的接口
- 在相关控制器中记录用户最近访问的任务、文件和消息文件
- 新增 RecentManagement 组件,展示用户最近访问的记录
- 更新样式和图标以提升用户体验
This commit is contained in:
kuaifan 2025-09-24 09:51:13 +08:00
parent feeeb26d94
commit 4b106e1f41
15 changed files with 826 additions and 7 deletions

View File

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

View File

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

View File

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

View File

@ -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. 获取用户收藏列表
*

View File

@ -0,0 +1,58 @@
<?php
namespace App\Models;
use Carbon\Carbon;
/**
* @property int $id
* @property int $userid
* @property string $target_type
* @property int $target_id
* @property string $source_type
* @property int $source_id
* @property Carbon|null $browsed_at
* @property Carbon|null $created_at
* @property Carbon|null $updated_at
*/
class UserRecentItem extends AbstractModel
{
public const TYPE_TASK = 'task';
public const TYPE_FILE = 'file';
public const TYPE_TASK_FILE = 'task_file';
public const TYPE_MESSAGE_FILE = 'message_file';
public const SOURCE_PROJECT = 'project';
public const SOURCE_FILESYSTEM = 'filesystem';
public const SOURCE_PROJECT_TASK = 'project_task';
public const SOURCE_DIALOG = 'dialog';
protected $fillable = [
'userid',
'target_type',
'target_id',
'source_type',
'source_id',
'browsed_at',
];
protected $dates = [
'browsed_at',
];
public static function record(int $userid, string $targetType, int $targetId, string $sourceType = '', int $sourceId = 0): self
{
return tap(self::updateOrCreate(
[
'userid' => $userid,
'target_type' => $targetType,
'target_id' => $targetId,
'source_type' => $sourceType,
'source_id' => $sourceId,
],
[
'browsed_at' => Carbon::now(),
]
));
}
}

View File

@ -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;
}
/**

View File

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

View File

@ -344,6 +344,14 @@
<FavoriteManagement v-if="favoriteShow" @on-close="favoriteShow = false"/>
</DrawerOverlay>
<!--最近打开-->
<DrawerOverlay
v-model="recentShow"
placement="right"
:size="1200">
<RecentManagement v-if="recentShow" @on-close="recentShow = false"/>
</DrawerOverlay>
<!--团队成员管理-->
<DrawerOverlay
v-model="allUserShow"
@ -401,6 +409,7 @@ import { mapState, mapGetters } from 'vuex'
import ProjectArchived from "./manage/components/ProjectArchived";
import TeamManagement from "./manage/components/TeamManagement";
import FavoriteManagement from "./manage/components/FavoriteManagement";
import RecentManagement from "./manage/components/RecentManagement";
import ProjectManagement from "./manage/components/ProjectManagement";
import DrawerOverlay from "../components/DrawerOverlay";
import MobileTabbar from "../components/Mobile/Tabbar";
@ -445,6 +454,7 @@ export default {
ProjectManagement,
TeamManagement,
FavoriteManagement,
RecentManagement,
ProjectArchived,
MicroApps,
ComplaintManagement,
@ -495,6 +505,7 @@ export default {
archivedProjectShow: false,
favoriteShow: false,
recentShow: false,
natificationReady: false,
notificationManage: null,
@ -526,6 +537,7 @@ export default {
emitter.on('approveDetails', this.openApproveDetails);
emitter.on('openReport', this.openReport);
emitter.on('openFavorite', this.openFavorite);
emitter.on('openRecent', this.openRecent);
emitter.on('openManageExport', this.openManageExport);
//
document.addEventListener('keydown', this.shortcutEvent);
@ -545,6 +557,7 @@ export default {
emitter.off('approveDetails', this.openApproveDetails);
emitter.off('openReport', this.openReport);
emitter.off('openFavorite', this.openFavorite);
emitter.off('openRecent', this.openRecent);
emitter.off('openManageExport', this.openManageExport);
//
document.removeEventListener('keydown', this.shortcutEvent);
@ -1319,6 +1332,10 @@ export default {
this.favoriteShow = true;
},
openRecent() {
this.recentShow = true;
},
openManageExport(type) {
switch (type) {
case 'task':

View File

@ -416,6 +416,7 @@ export default {
const list = [
{value: "approve", label: "审批中心", sort: 30, show: this.microAppsIds.includes('approve')},
{value: "favorite", label: "我的收藏", sort: 45},
{value: "recent", label: "最近打开", sort: 47},
{value: "report", label: "工作报告", sort: 50},
{value: "mybot", label: "我的机器人", sort: 55},
{value: "robot", label: "AI 机器人", sort: 60, show: this.microAppsIds.includes('ai')},
@ -485,6 +486,9 @@ export default {
case 'favorite':
emitter.emit('openFavorite');
break;
case 'recent':
emitter.emit('openRecent');
break;
case 'mybot':
this.getMybot();
this.mybotShow = true;

View File

@ -0,0 +1,316 @@
<template>
<div class="recent-management">
<div class="management-title">
{{$L('最近打开')}}
<div class="title-icon">
<Loading v-if="loading > 0"/>
</div>
</div>
<div class="search-container lr">
<ul>
<li>
<div class="search-label">{{$L('类型')}}</div>
<div class="search-content">
<Select v-model="filters.type" clearable :placeholder="$L('全部类型')" @on-change="handleTypeChange">
<Option v-for="item in typeOptions" :key="item.value" :value="item.value">{{$L(item.label)}}</Option>
</Select>
</div>
</li>
<li class="search-button">
<Button type="primary" :loading="loading > 0" @click="refreshList">{{$L('刷新')}}</Button>
</li>
</ul>
</div>
<div class="table-page-box">
<Table
:columns="columns"
:data="records"
:loading="loading > 0"
:no-data-text="$L(noDataText)"
stripe/>
<Page
:total="total"
:current="page"
:page-size="pageSize"
:page-size-opts="[10,20,30,50,100]"
:simple="windowPortrait"
:disabled="loading > 0"
show-elevator
show-sizer
show-total
@on-change="setPage"
@on-page-size-change="setPageSize"/>
</div>
</div>
</template>
<script>
import {mapState} from "vuex";
export default {
name: "RecentManagement",
data() {
return {
loading: 0,
records: [],
total: 0,
page: 1,
pageSize: 20,
filters: {
type: ''
},
noDataText: '暂无打开记录'
}
},
computed: {
...mapState(['windowPortrait']),
typeMap() {
return {
task: {label: '任务', color: 'success'},
file: {label: '文件库', color: 'warning'},
task_file: {label: '任务文件', color: 'primary'},
message_file: {label: '聊天文件', color: 'magenta'}
}
},
typeOptions() {
return [
{value: '', label: '全部类型'},
{value: 'task', label: this.typeMap.task.label},
{value: 'file', label: this.typeMap.file.label},
{value: 'task_file', label: this.typeMap.task_file.label},
{value: 'message_file', label: this.typeMap.message_file.label},
]
},
columns() {
return [
{
title: this.$L('类型'),
key: 'type',
width: 120,
render: (h, {row}) => {
const info = this.getTypeInfo(row.type);
return h('Tag', {
class: 'recent-type-tag',
props: {
color: info.color || 'primary'
}
}, this.$L(info.label || row.type));
}
},
{
title: this.$L('名称'),
key: 'name',
minWidth: 200,
render: (h, {row}) => {
const text = row.name || this.$L('未命名');
return h('div', {
class: 'recent-name',
on: {
click: () => this.openItem(row)
}
}, [
h('AutoTip', text)
]);
}
},
{
title: this.$L('来源'),
minWidth: 220,
render: (h, {row}) => {
return h('AutoTip', this.getSourceText(row));
}
},
{
title: this.$L('最近访问时间'),
key: 'browsed_at',
width: 168,
},
{
title: this.$L('操作'),
align: 'center',
width: 120,
render: (h, params) => {
const actions = [
h('Poptip', {
props: {
title: this.$L(`确定要删除记录"${params.row.name || this.$L('未命名')}"吗?`),
confirm: true,
transfer: true,
placement: 'left',
okText: this.$L('确定'),
cancelText: this.$L('取消'),
},
style: {
fontSize: '13px',
cursor: 'pointer',
color: '#f00',
},
on: {
'on-ok': () => this.removeItem(params.row)
},
}, this.$L('删除'))
];
return h('TableAction', {
props: {
column: params.column
}
}, actions);
}
}
]
}
},
mounted() {
this.getLists();
},
methods: {
getTypeInfo(type) {
return this.typeMap[type] || {label: type, color: 'default'};
},
getSourceText(row) {
switch (row.type) {
case 'task': {
const project = row.project_name ? `${this.$L('项目')}: ${row.project_name}` : this.$L('项目');
const status = this.getTaskStatus(row);
return status ? `${project} | ${status}` : project;
}
case 'file':
return this.$L('文件库');
case 'task_file': {
const parts = [];
if (row.project_name) {
parts.push(`${this.$L('项目')}: ${row.project_name}`);
}
if (row.task_name) {
parts.push(`${this.$L('任务')}: ${row.task_name}`);
}
return parts.length > 0 ? parts.join(' | ') : this.$L('任务文件');
}
case 'message_file':
if (row.dialog_name) {
return `${this.$L('聊天')}: ${row.dialog_name}`;
}
return this.$L('聊天文件');
}
return this.$L('未知');
},
getTaskStatus(row) {
if (row.flow_item_name) {
return row.flow_item_name;
}
if (row.complete_at) {
return this.$L('已完成');
}
return this.$L('进行中');
},
getLists(page = this.page) {
this.loading++;
const params = {
page,
page_size: this.pageSize,
};
if (this.filters.type) {
params.type = this.filters.type;
}
this.$store.dispatch('getRecentBrowseHistory', params).then(({data}) => {
if ($A.isJson(data)) {
this.records = data.list || [];
this.total = data.total || 0;
this.page = data.page || page;
this.pageSize = data.page_size || this.pageSize;
} else {
this.records = [];
this.total = 0;
}
}).catch(({msg}) => {
if (msg) {
$A.modalError(msg);
}
}).finally(() => {
this.loading--;
});
},
refreshList() {
this.getLists(1);
},
handleTypeChange() {
this.page = 1;
this.getLists(1);
},
setPage(page) {
this.page = page;
this.getLists(page);
},
setPageSize(size) {
this.pageSize = size;
this.getLists(1);
},
openItem(row) {
switch (row.type) {
case 'task':
this.$store.dispatch('openTask', row);
break;
case 'file':
this.openWindow(`/single/file/${row.id}`, row.name, `file-${row.id}`, row.size);
break;
case 'task_file':
this.openWindow(`/single/file/task/${row.id}`, row.name, `file-task-${row.id}`, row.size);
break;
case 'message_file':
this.openWindow(`/single/file/msg/${row.id}`, row.name, `file-msg-${row.id}`, row.size);
break;
}
},
openWindow(path, title, name, size) {
const text = title || this.$L('查看');
const finalTitle = size ? `${text} (${$A.bytesToSize(size)})` : text;
if (this.$Electron) {
this.$store.dispatch('openChildWindow', {
name,
path,
userAgent: "/hideenOfficeTitle/",
force: false,
config: {
title: finalTitle,
titleFixed: true,
parent: null,
width: Math.min(window.screen.availWidth, 1440),
height: Math.min(window.screen.availHeight, 900),
},
});
} else if (this.$isEEUIApp) {
this.$store.dispatch('openAppChildPage', {
pageType: 'app',
pageTitle: finalTitle,
url: 'web.js',
params: {
titleFixed: true,
url: $A.urlReplaceHash(path)
},
});
} else {
window.open($A.mainUrl(path.substring(1)));
}
},
removeItem(row) {
if (!row.record_id) {
return;
}
const targetPage = this.records.length === 1 && this.page > 1 ? this.page - 1 : this.page;
this.loading++;
this.$store.dispatch('removeRecentBrowseRecord', row.record_id).then(({msg}) => {
$A.messageSuccess(msg || this.$L('删除成功'));
this.page = targetPage;
this.getLists(targetPage);
}).catch(({msg}) => {
if (msg) {
$A.modalError(msg);
}
}).finally(() => {
this.loading--;
});
}
}
}
</script>

View File

@ -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<unknown>}
*/
getRecentBrowseHistory({dispatch}, params = {}) {
return dispatch('call', {
url: 'users/recent/browse',
data: params,
method: 'get',
});
},
/**
* 删除最近浏览记录
* @param dispatch
* @param id
* @returns {Promise<unknown>}
*/
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, // 静默调用
});
},

View File

@ -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";

View File

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

View File

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

View File

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 48 48">
<rect width="48" height="48" rx="12" fill="#5DADEC"/>
<path fill="#FFFFFF" fill-rule="evenodd" d="M24 13c-6.627 0-12 5.373-12 12s5.373 12 12 12 12-5.373 12-12S30.627 13 24 13Zm0 22c-5.523 0-10-4.477-10-10s4.477-10 10-10 10 4.477 10 10-4.477 10-10 10Z" clip-rule="evenodd"/>
<path fill="#FFFFFF" d="M24 16a1 1 0 0 1 1 1v7.586l4.121 4.121a1 1 0 1 1-1.414 1.414l-4.414-4.414A1 1 0 0 1 23 24V17a1 1 0 0 1 1-1Z"/>
</svg>

After

Width:  |  Height:  |  Size: 495 B