From ecdc22969d7a1d275f9772369ba07ca01bebbb24 Mon Sep 17 00:00:00 2001 From: "Ray.Hao" <1490493387@qq.com> Date: Tue, 24 Feb 2026 21:32:14 +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=8F=9C=E5=8D=95=E5=8A=9F=E8=83=BD=E5=B9=B6?= =?UTF-8?q?=E7=A7=BB=E9=99=A4Websocket=E8=8F=9C=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mock/menu.mock.ts | 28 -- src/composables/index.ts | 6 +- src/composables/useRecentMenus.ts | 99 +++++++ .../{table => }/useTableSelection.ts | 0 src/router/guards/permission.ts | 9 +- src/views/dashboard/index.vue | 144 ++++------- src/views/demo/websocket.vue | 242 ------------------ src/views/login/index.vue | 4 + 8 files changed, 170 insertions(+), 362 deletions(-) create mode 100644 src/composables/useRecentMenus.ts rename src/composables/{table => }/useTableSelection.ts (100%) delete mode 100644 src/views/demo/websocket.vue diff --git a/mock/menu.mock.ts b/mock/menu.mock.ts index 7b4a955d..d2187778 100644 --- a/mock/menu.mock.ts +++ b/mock/menu.mock.ts @@ -498,19 +498,6 @@ export default defineMock([ params: null, }, }, - { - path: "/function/websocket", - component: "demo/websocket", - name: "/function/websocket", - meta: { - title: "Websocket", - icon: "", - hidden: false, - keepAlive: true, - alwaysShow: false, - params: null, - }, - }, { path: "/function/ai-command", component: "demo/ai-command", @@ -1571,21 +1558,6 @@ export default defineMock([ perm: null, children: [], }, - { - id: 90, - parentId: 89, - name: "Websocket", - type: "MENU", - routeName: null, - routePath: "/function/websocket", - component: "demo/websocket", - sort: 3, - visible: 1, - icon: "", - redirect: "", - perm: null, - children: [], - }, { id: 91, parentId: 89, diff --git a/src/composables/index.ts b/src/composables/index.ts index 086f473a..1cb27b9e 100644 --- a/src/composables/index.ts +++ b/src/composables/index.ts @@ -4,4 +4,8 @@ export { useStomp, useDictSync, useOnlineCount } from "./websocket"; export type { DictMessage, DictChangeMessage, DictChangeCallback } from "./websocket"; // 表格相关 -export { useTableSelection } from "./table/useTableSelection"; +export { useTableSelection } from "./useTableSelection"; + +// 最近访问菜单 +export { useRecentMenus } from "./useRecentMenus"; +export type { RecentMenuItem } from "./useRecentMenus"; diff --git a/src/composables/useRecentMenus.ts b/src/composables/useRecentMenus.ts new file mode 100644 index 00000000..f614107c --- /dev/null +++ b/src/composables/useRecentMenus.ts @@ -0,0 +1,99 @@ +import { ref } from "vue"; + +/** + * 最近访问菜单项 + */ +export interface RecentMenuItem { + path: string; + title: string; + icon?: string; + visitedAt: number; +} + +const STORAGE_KEY = "recent_menus"; +const MAX_COUNT = 8; + +// 全局状态 +const recentMenus = ref([]); + +/** + * 从 localStorage 加载数据 + */ +function loadFromStorage(): RecentMenuItem[] { + try { + const data = localStorage.getItem(STORAGE_KEY); + return data ? JSON.parse(data) : []; + } catch { + return []; + } +} + +/** + * 保存到 localStorage + */ +function saveToStorage(menus: RecentMenuItem[]) { + localStorage.setItem(STORAGE_KEY, JSON.stringify(menus)); +} + +// 初始化 +recentMenus.value = loadFromStorage(); + +/** + * 最近访问菜单 composable + */ +export function useRecentMenus() { + /** + * 清空所有记录 + */ + function clearRecentMenus() { + recentMenus.value = []; + localStorage.removeItem(STORAGE_KEY); + } + + /** + * 格式化访问时间 + */ + function formatVisitTime(timestamp: number): string { + const now = Date.now(); + const diff = now - timestamp; + + if (diff < 60000) return "刚刚"; + if (diff < 3600000) return `${Math.floor(diff / 60000)}分钟前`; + if (diff < 86400000) return `${Math.floor(diff / 3600000)}小时前`; + if (diff < 604800000) return `${Math.floor(diff / 86400000)}天前`; + + const date = new Date(timestamp); + return `${date.getMonth() + 1}-${date.getDate()}`; + } + + return { + recentMenus, + clearRecentMenus, + formatVisitTime, + }; +} + +/** + * 添加最近访问记录(全局方法,供路由守卫调用) + */ +export function addRecentMenu(path: string, title: string, icon?: string) { + if (!path || !title) return; + + // 过滤掉不需要记录的路径 + const excludePaths = ["/dashboard", "/redirect", "/404", "/401", "/login", "/"]; + if (excludePaths.some((p) => path === p || path.startsWith(p + "/"))) return; + + // 移除已存在的相同路径 + const filtered = recentMenus.value.filter((item) => item.path !== path); + + // 添加到开头 + const newItem: RecentMenuItem = { + path, + title, + icon, + visitedAt: Date.now(), + }; + + recentMenus.value = [newItem, ...filtered].slice(0, MAX_COUNT); + saveToStorage(recentMenus.value); +} diff --git a/src/composables/table/useTableSelection.ts b/src/composables/useTableSelection.ts similarity index 100% rename from src/composables/table/useTableSelection.ts rename to src/composables/useTableSelection.ts diff --git a/src/router/guards/permission.ts b/src/router/guards/permission.ts index 86ad540f..376862ce 100644 --- a/src/router/guards/permission.ts +++ b/src/router/guards/permission.ts @@ -4,6 +4,7 @@ import router from "@/router"; import { usePermissionStore, useUserStore } from "@/store"; import { useTenantStoreHook } from "@/store/modules/tenant"; import { isTenantEnabled } from "@/utils/tenant"; +import { addRecentMenu } from "@/composables/useRecentMenus"; /** * 路由权限守卫 @@ -78,8 +79,14 @@ export function setupPermissionGuard() { } }); - router.afterEach(() => { + router.afterEach((to) => { NProgress.done(); + + // 记录最近访问 + if (to.meta?.title && to.path) { + const icon = typeof to.meta.icon === "string" ? to.meta.icon : undefined; + addRecentMenu(to.path, to.meta.title as string, icon); + } }); } diff --git a/src/views/dashboard/index.vue b/src/views/dashboard/index.vue index 7c234001..b9948d5d 100644 --- a/src/views/dashboard/index.vue +++ b/src/views/dashboard/index.vue @@ -319,68 +319,63 @@ - + - - - + +
+
-
-
- {{ item.title }} - - {{ item.tag }} - -
- - - {{ item.content }} - - -
- - 详情 - - -
+ +
+ + + +
+
- - - + + + {{ item.title }} + +
+
+ + +
+ + + +

暂无访问记录

+

访问的页面会自动记录在这里

+
+
@@ -395,17 +390,23 @@ defineOptions({ import { dayjs } from "element-plus"; import { ref } from "vue"; +import { useRouter } from "vue-router"; import StatisticsAPI from "@/api/system/statistics"; import type { VisitStatsDetail, VisitTrendDetail } from "@/types/api"; import { useUserStore } from "@/store/modules/user"; import { formatGrowthRate } from "@/utils"; import { useTransition, useDateFormat } from "@vueuse/core"; -import { CircleCheck, CircleClose, Loading } from "@element-plus/icons-vue"; -import { useOnlineCount } from "@/composables"; +import { CircleCheck, CircleClose, Loading, Clock, Menu } from "@element-plus/icons-vue"; +import { useOnlineCount, useRecentMenus } from "@/composables"; + +const router = useRouter(); // 在线用户数量组件相关 const { onlineUserCount, lastUpdateTime, isConnected, connectionState } = useOnlineCount(); +// 最近访问菜单 +const { recentMenus, clearRecentMenus } = useRecentMenus(); + // 格式化时间戳 const formattedTime = computed(() => { if (!lastUpdateTime.value) return "--"; @@ -429,45 +430,8 @@ const wsStatusClass = computed(() => { : "text-[--el-color-danger] bg-[--el-color-danger-light-9] border-[--el-color-danger-light-7]"; }); -interface VersionItem { - id: string; - title: string; // 版本标题(如:v2.4.0) - date: string; // 发布时间 - content: string; // 版本描述 - link: string; // 详情链接 - tag?: string; // 版本标签(可选) -} - const userStore = useUserStore(); -// 当前通知公告列表 -const vesionList = ref([ - { - id: "1", - title: "v2.4.0", - date: "2021-09-01 00:00:00", - content: "实现基础框架搭建,包含权限管理、路由系统等核心功能。", - link: "https://gitee.com/youlaiorg/vue3-element-admin/releases", - tag: "里程碑", - }, - { - id: "2", - title: "v2.4.0", - date: "2021-09-01 00:00:00", - content: "实现基础框架搭建,包含权限管理、路由系统等核心功能。", - link: "https://gitee.com/youlaiorg/vue3-element-admin/releases", - tag: "里程碑", - }, - { - id: "3", - title: "v2.4.0", - date: "2021-09-01 00:00:00", - content: "实现基础框架搭建,包含权限管理、路由系统等核心功能。", - link: "https://gitee.com/youlaiorg/vue3-element-admin/releases", - tag: "里程碑", - }, -]); - // 当前时间(用于计算问候语) const currentDate = new Date(); diff --git a/src/views/demo/websocket.vue b/src/views/demo/websocket.vue deleted file mode 100644 index 76ae716a..00000000 --- a/src/views/demo/websocket.vue +++ /dev/null @@ -1,242 +0,0 @@ - - - - - diff --git a/src/views/login/index.vue b/src/views/login/index.vue index 14f6424f..a234acd4 100644 --- a/src/views/login/index.vue +++ b/src/views/login/index.vue @@ -203,6 +203,10 @@ const formComponents = { animation: featureFade 0.8s ease-out; } +.dark .auth-feature { + color: rgba(240, 245, 255, 0.92); +} + @media (max-width: 768px) { .auth-view__wrapper { display: block;