mirror of
https://github.com/kuaifan/dootask.git
synced 2025-12-10 18:02:55 +00:00
feat: 添加最近访问记录功能
- 在 UsersController 中新增获取和删除最近访问记录的接口 - 在相关控制器中记录用户最近访问的任务、文件和消息文件 - 新增 RecentManagement 组件,展示用户最近访问的记录 - 更新样式和图标以提升用户体验
This commit is contained in:
parent
feeeb26d94
commit
4b106e1f41
@ -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);
|
||||
}
|
||||
|
||||
|
||||
@ -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));
|
||||
}
|
||||
|
||||
@ -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));
|
||||
}
|
||||
|
||||
|
||||
@ -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. 获取用户收藏列表
|
||||
*
|
||||
|
||||
58
app/Models/UserRecentItem.php
Normal file
58
app/Models/UserRecentItem.php
Normal 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(),
|
||||
]
|
||||
));
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -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');
|
||||
}
|
||||
}
|
||||
@ -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':
|
||||
|
||||
@ -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;
|
||||
|
||||
316
resources/assets/js/pages/manage/components/RecentManagement.vue
Normal file
316
resources/assets/js/pages/manage/components/RecentManagement.vue
Normal 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>
|
||||
32
resources/assets/js/store/actions.js
vendored
32
resources/assets/js/store/actions.js
vendored
@ -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, // 静默调用
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
@ -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";
|
||||
|
||||
67
resources/assets/sass/pages/components/recent-management.scss
vendored
Normal file
67
resources/assets/sass/pages/components/recent-management.scss
vendored
Normal 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;
|
||||
}
|
||||
}
|
||||
4
resources/assets/sass/pages/page-apply.scss
vendored
4
resources/assets/sass/pages/page-apply.scss
vendored
@ -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");
|
||||
}
|
||||
|
||||
@ -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 |
Loading…
x
Reference in New Issue
Block a user