From 845d117f08f11db80069acd1d6e83a51842eb01b Mon Sep 17 00:00:00 2001 From: "Ray.Hao" <1490493387@qq.com> Date: Sun, 21 Jun 2026 14:09:27 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E4=B8=AA=E4=BA=BA=E4=B8=AD?= =?UTF-8?q?=E5=BF=83=E9=A1=B5=E9=9D=A2=E9=87=8D=E6=9E=84=E5=92=8C=E5=B8=83?= =?UTF-8?q?=E5=B1=80=E4=BB=A3=E7=A0=81=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mock/menu.mock.ts | 163 +- src/api/system/menu/types.ts | 28 +- src/assets/images/error/401.svg | 398 ----- src/assets/images/error/404.svg | 340 ----- src/components/AppLink/index.vue | 51 +- .../CommandPalette/useCommandPalette.ts | 73 +- src/components/Hamburger/index.vue | 21 +- src/components/NoticeDropdown/useNotice.ts | 66 +- src/constants/index.ts | 25 +- src/enums/business.ts | 9 + src/lang/package/zh-cn.json | 1 + src/layouts/BaseLayout.vue | 10 +- src/layouts/LeftLayout.vue | 132 -- src/layouts/MixLayout.vue | 326 ---- src/layouts/TopLayout.vue | 152 -- src/layouts/components/LayoutLogo.vue | 38 +- src/layouts/components/LayoutMain.vue | 12 +- src/layouts/components/LayoutMenuIcon.vue | 21 + src/layouts/components/LayoutNavbar.vue | 10 +- src/layouts/components/LayoutSettings.vue | 170 ++- src/layouts/components/LayoutSidebar.vue | 86 +- src/layouts/components/LayoutSidebarItem.vue | 219 +-- src/layouts/components/LayoutTagsView.vue | 140 +- src/layouts/components/LayoutToolbar.vue | 161 +- src/layouts/composables/useLayout.ts | 75 + src/layouts/composables/useLayoutDevice.ts | 31 + src/layouts/composables/useMixMenu.ts | 72 +- src/layouts/index.vue | 13 +- src/layouts/{ => modes}/DoubleLayout.vue | 175 ++- src/layouts/modes/LeftLayout.vue | 120 ++ src/layouts/modes/MixLayout.vue | 327 +++++ src/layouts/modes/TopLayout.vue | 194 +++ src/layouts/useLayout.ts | 122 -- src/main.ts | 22 - src/router/guards/permission.ts | 7 +- src/router/index.ts | 2 +- src/stores/permission.ts | 103 +- src/stores/tags-view.ts | 3 +- src/styles/index.scss | 2 +- src/styles/layout.scss | 155 ++ src/styles/menu.scss | 62 - src/styles/page.scss | 91 ++ src/utils/storage.ts | 44 +- src/views/common/iframe.vue | 35 + src/views/common/redirect.vue | 17 + src/views/dashboard/index.vue | 12 - src/views/demo/api/apifox.vue | 25 - src/views/demo/curd-single.vue | 36 +- src/views/demo/internal-doc.vue | 23 - .../children/children/level3-1.vue | 20 - .../children/children/level3-2.vue | 20 - .../multi-level/components/LevelOnePanel.vue | 6 + .../LevelTwoPanel.vue} | 4 +- .../detail.ts => components/ToDetail.ts} | 0 .../level-two/level-three-a/index.vue | 20 + .../level-two/level-three-b/index.vue | 20 + src/views/demo/multi-level/level1.vue | 6 - src/views/error/401.vue | 41 +- src/views/error/404.vue | 87 +- src/views/error/components/ErrorPage.vue | 311 ++++ src/views/profile/index.vue | 1306 ++++++++++++----- src/views/redirect/index.vue | 15 - src/views/system/config/index.vue | 14 +- src/views/system/menu/index.vue | 820 ++++++++--- .../system/user/components/UserDeptTree.vue | 17 +- src/views/system/user/index.vue | 291 ++-- types/router.d.ts | 3 + vite.config.ts | 23 +- 68 files changed, 4026 insertions(+), 3418 deletions(-) delete mode 100644 src/assets/images/error/401.svg delete mode 100644 src/assets/images/error/404.svg delete mode 100644 src/layouts/LeftLayout.vue delete mode 100644 src/layouts/MixLayout.vue delete mode 100644 src/layouts/TopLayout.vue create mode 100644 src/layouts/components/LayoutMenuIcon.vue create mode 100644 src/layouts/composables/useLayout.ts create mode 100644 src/layouts/composables/useLayoutDevice.ts rename src/layouts/{ => modes}/DoubleLayout.vue (62%) create mode 100644 src/layouts/modes/LeftLayout.vue create mode 100644 src/layouts/modes/MixLayout.vue create mode 100644 src/layouts/modes/TopLayout.vue delete mode 100644 src/layouts/useLayout.ts create mode 100644 src/styles/layout.scss delete mode 100644 src/styles/menu.scss create mode 100644 src/views/common/iframe.vue create mode 100644 src/views/common/redirect.vue delete mode 100644 src/views/demo/api/apifox.vue delete mode 100644 src/views/demo/internal-doc.vue delete mode 100644 src/views/demo/multi-level/children/children/level3-1.vue delete mode 100644 src/views/demo/multi-level/children/children/level3-2.vue create mode 100644 src/views/demo/multi-level/components/LevelOnePanel.vue rename src/views/demo/multi-level/{children/level2.vue => components/LevelTwoPanel.vue} (53%) rename src/views/demo/multi-level/{children/children/detail.ts => components/ToDetail.ts} (100%) create mode 100644 src/views/demo/multi-level/level-one/level-two/level-three-a/index.vue create mode 100644 src/views/demo/multi-level/level-one/level-two/level-three-b/index.vue delete mode 100644 src/views/demo/multi-level/level1.vue create mode 100644 src/views/error/components/ErrorPage.vue delete mode 100644 src/views/redirect/index.vue diff --git a/mock/menu.mock.ts b/mock/menu.mock.ts index 47801d57..ec59c02f 100644 --- a/mock/menu.mock.ts +++ b/mock/menu.mock.ts @@ -179,14 +179,18 @@ export default defineMock([ children: [ { path: "apifox", - component: "demo/api/apifox", + component: null, name: "Apifox", meta: { title: "Apifox", + type: "E", icon: "api", hidden: false, keepAlive: true, alwaysShow: false, + externalUrl: + "https://www.apifox.cn/apidoc/shared-195e783f-4d85-4235-a038-eec696de4ea5", + openMode: 2, params: null, }, }, @@ -195,7 +199,7 @@ export default defineMock([ { path: "/doc", component: "Layout", - redirect: "https://juejin.cn/post/7228990409909108793", + redirect: "/doc/internal-doc", name: "/doc", meta: { title: "平台文档", @@ -207,24 +211,31 @@ export default defineMock([ children: [ { path: "internal-doc", - component: "demo/internal-doc", + component: null, name: "InternalDoc", meta: { - title: "document", + title: "平台文档(内嵌)", + type: "E", icon: "document", hidden: false, + keepAlive: true, alwaysShow: false, + externalUrl: "https://juejin.cn/post/7228990409909108793", + openMode: 2, params: null, }, }, { path: "https://juejin.cn/post/7228990409909108793", - name: "Https://juejin.cn/post/7228990409909108793", + name: "ext-30", meta: { title: "平台文档(外链)", + type: "E", icon: "link", hidden: false, alwaysShow: false, + externalUrl: "https://juejin.cn/post/7228990409909108793", + openMode: 1, params: null, }, }, @@ -243,11 +254,11 @@ export default defineMock([ }, children: [ { - path: "multi-level1", + path: "level-one", component: "Layout", - name: "MultiLevel1", + name: "MultiLevelLevelOne", meta: { - title: "菜单一级", + title: "一级菜单", icon: "", hidden: false, alwaysShow: true, @@ -255,11 +266,11 @@ export default defineMock([ }, children: [ { - path: "multi-level2", + path: "level-two", component: "Layout", - name: "MultiLevel2", + name: "MultiLevelLevelTwo", meta: { - title: "菜单二级", + title: "二级菜单", icon: "", hidden: false, alwaysShow: false, @@ -267,11 +278,11 @@ export default defineMock([ }, children: [ { - path: "multi-level3-1", - component: "demo/multi-level/children/children/level3-1", - name: "MultiLevel31", + path: "level-three-a", + component: "demo/multi-level/level-one/level-two/level-three-a/index", + name: "MultiLevelLevelThreeA", meta: { - title: "菜单三级-1", + title: "三级菜单 A", icon: "", hidden: false, keepAlive: true, @@ -280,11 +291,11 @@ export default defineMock([ }, }, { - path: "multi-level3-2", - component: "demo/multi-level/children/children/level3-2", - name: "MultiLevel32", + path: "level-three-b", + component: "demo/multi-level/level-one/level-two/level-three-b/index", + name: "MultiLevelLevelThreeB", meta: { - title: "菜单三级-2", + title: "三级菜单 B", icon: "", hidden: false, keepAlive: true, @@ -1241,15 +1252,20 @@ export default defineMock([ id: "41", parentId: "40", name: "Apifox", - type: "M", - routeName: null, + type: "E", + routeName: "Apifox", routePath: "apifox", - component: "demo/api/apifox", + component: null, + externalUrl: + "https://www.apifox.cn/apidoc/shared-195e783f-4d85-4235-a038-eec696de4ea5", + openMode: 2, sort: 1, visible: 1, icon: "api", redirect: "", perm: null, + keepAlive: 1, + params: [], children: [], }, ], @@ -1272,25 +1288,31 @@ export default defineMock([ id: "102", parentId: "26", name: "平台文档(内嵌)", - type: "EXTLINK", - routeName: null, + type: "E", + routeName: "InternalDoc", routePath: "internal-doc", - component: "demo/internal-doc", + component: null, + externalUrl: "https://juejin.cn/post/7228990409909108793", + openMode: 2, sort: 1, visible: 1, icon: "document", redirect: "", perm: null, + keepAlive: 1, + params: [], children: [], }, { id: "30", parentId: "26", name: "平台文档(外链)", - type: "EXTLINK", + type: "E", routeName: null, - routePath: "https://juejin.cn/post/7228990409909108793", - component: "", + routePath: "", + component: null, + externalUrl: "https://juejin.cn/post/7228990409909108793", + openMode: 1, sort: 2, visible: 1, icon: "link", @@ -1317,10 +1339,10 @@ export default defineMock([ { id: "21", parentId: "20", - name: "菜单一级", + name: "一级菜单", type: "C", routeName: null, - routePath: "multi-level1", + routePath: "level-one", component: "Layout", sort: 1, visible: 1, @@ -1331,10 +1353,10 @@ export default defineMock([ { id: "22", parentId: "21", - name: "菜单二级", + name: "二级菜单", type: "C", routeName: null, - routePath: "multi-level2", + routePath: "level-two", component: "Layout", sort: 1, visible: 1, @@ -1345,11 +1367,11 @@ export default defineMock([ { id: "23", parentId: "22", - name: "菜单三级-1", + name: "三级菜单 A", type: "M", routeName: null, - routePath: "multi-level3-1", - component: "demo/multi-level/children/children/level3-1", + routePath: "level-three-a", + component: "demo/multi-level/level-one/level-two/level-three-a/index", sort: 1, visible: 1, icon: "", @@ -1360,11 +1382,11 @@ export default defineMock([ { id: "24", parentId: "22", - name: "菜单三级-2", + name: "三级菜单 B", type: "M", routeName: null, - routePath: "multi-level3-2", - component: "demo/multi-level/children/children/level3-2", + routePath: "level-three-b", + component: "demo/multi-level/level-one/level-two/level-three-b/index", sort: 2, visible: 1, icon: "", @@ -1868,19 +1890,19 @@ export default defineMock([ children: [ { value: "21", - label: "菜单一级", + label: "一级菜单", children: [ { value: "22", - label: "菜单二级", + label: "二级菜单", children: [ { value: "23", - label: "菜单三级-1", + label: "三级菜单 A", }, { value: "24", - label: "菜单三级-2", + label: "三级菜单 B", }, ], }, @@ -2217,4 +2239,61 @@ const menuMap: Record = { alwaysShow: null, params: null, }, + 41: { + id: "41", + parentId: "40", + name: "Apifox", + type: "E", + routeName: "Apifox", + routePath: "apifox", + component: null, + externalUrl: "https://www.apifox.cn/apidoc/shared-195e783f-4d85-4235-a038-eec696de4ea5", + openMode: 2, + perm: null, + visible: 1, + sort: 1, + icon: "api", + redirect: "", + keepAlive: 1, + alwaysShow: null, + params: [], + }, + 102: { + id: "102", + parentId: "26", + name: "平台文档(内嵌)", + type: "E", + routeName: "InternalDoc", + routePath: "internal-doc", + component: null, + externalUrl: "https://juejin.cn/post/7228990409909108793", + openMode: 2, + perm: null, + visible: 1, + sort: 1, + icon: "document", + redirect: "", + keepAlive: 1, + alwaysShow: null, + params: [], + }, + 30: { + id: "30", + parentId: "26", + name: "平台文档(外链)", + type: "E", + routeName: null, + routePath: "", + component: null, + externalUrl: "https://juejin.cn/post/7228990409909108793", + openMode: 1, + perm: null, + visible: 1, + sort: 2, + icon: "link", + redirect: "", + keepAlive: null, + alwaysShow: null, + params: [], + }, }; diff --git a/src/api/system/menu/types.ts b/src/api/system/menu/types.ts index 1c3b7afa..f502a233 100644 --- a/src/api/system/menu/types.ts +++ b/src/api/system/menu/types.ts @@ -14,6 +14,8 @@ export interface MenuItem { children?: MenuItem[]; /** 组件路径 */ component?: string; + /** 外链地址 */ + externalUrl?: string; /** ICON */ icon?: string; /** 菜单ID */ @@ -22,15 +24,27 @@ export interface MenuItem { name?: string; /** 父菜单ID */ parentId?: string; + /** 路由名称 */ + routeName?: string; + /** 路由路径 */ + routePath?: string; /** 路由路径 */ path?: string; /** 按钮权限标识 */ perm?: string; + /** 外链打开方式(1-新标签页 2-系统内嵌) */ + openMode?: number; /** 跳转路径 */ redirect?: string; + /** 是否始终显示 */ + alwaysShow?: number | boolean; + /** 是否缓存 */ + keepAlive?: number | boolean; + /** 路由参数 */ + params?: { key?: string; value?: string }[]; /** 菜单排序(数字越小排名越靠前) */ sort?: number; - /** 菜单类型(C-目录 M-菜单 B-按钮) */ + /** 菜单类型(C-目录 M-菜单 E-外链 B-按钮) */ type?: string; /** 菜单是否可见(1:显示;0:隐藏) */ visible?: number; @@ -46,7 +60,7 @@ export interface MenuForm { parentId?: string; /** 菜单名称 */ name?: string; - /** 菜单类型(C-目录 M-菜单 B-按钮) */ + /** 菜单类型(C-目录 M-菜单 E-外链 B-按钮) */ type?: string; /** 路由路径 */ path?: string; @@ -58,6 +72,8 @@ export interface MenuForm { redirect?: string; /** 组件路径 */ component?: string; + /** 外链地址 */ + externalUrl?: string; /** ICON */ icon?: string; /** 排序 */ @@ -68,6 +84,8 @@ export interface MenuForm { scope?: number; /** 按钮权限标识 */ perm?: string; + /** 外链打开方式(1-新标签页 2-系统内嵌) */ + openMode?: number; /** 路由参数(用于表单编辑 params) */ params?: { key?: string; value?: string }[]; /** 是否始终显示(仅对目录生效) */ @@ -108,8 +126,14 @@ export interface Meta { icon?: string; /** 【菜单】是否开启页面缓存 */ keepAlive?: boolean; + /** 菜单类型 */ + type?: string; /** 路由参数 */ params?: Record; + /** 外链地址 */ + externalUrl?: string; + /** 外链打开方式(1-新标签页 2-系统内嵌) */ + openMode?: number; /** 角色集合 */ roles?: string[]; /** 路由title */ diff --git a/src/assets/images/error/401.svg b/src/assets/images/error/401.svg deleted file mode 100644 index 6096d41a..00000000 --- a/src/assets/images/error/401.svg +++ /dev/null @@ -1,398 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/assets/images/error/404.svg b/src/assets/images/error/404.svg deleted file mode 100644 index b9bf23cf..00000000 --- a/src/assets/images/error/404.svg +++ /dev/null @@ -1,340 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/components/AppLink/index.vue b/src/components/AppLink/index.vue index b3b8f172..6ddecfb4 100644 --- a/src/components/AppLink/index.vue +++ b/src/components/AppLink/index.vue @@ -1,38 +1,65 @@ diff --git a/src/components/CommandPalette/useCommandPalette.ts b/src/components/CommandPalette/useCommandPalette.ts index 5bdcffd2..33f1bee0 100644 --- a/src/components/CommandPalette/useCommandPalette.ts +++ b/src/components/CommandPalette/useCommandPalette.ts @@ -1,13 +1,10 @@ -/** - * 菜单搜索逻辑 - */ -import { ref, onMounted, onBeforeUnmount, toRaw } from "vue"; -import { RouteRecordRaw, LocationQueryRaw } from "vue-router"; +import { onBeforeUnmount, onMounted, ref, toRaw } from "vue"; +import type { LocationQueryRaw, RouteRecordRaw } from "vue-router"; import router from "@/router"; import { usePermissionStore } from "@/stores"; import { isExternal } from "@/utils"; -/** 搜索项类型 */ +/** 命令面板中的可搜索菜单项。 */ interface SearchItem { title: string; path: string; @@ -19,26 +16,22 @@ interface SearchItem { const STORAGE_KEY = "menu_search_history"; const MAX_HISTORY = 5; +const EXCLUDED_PATHS = ["/redirect", "/login", "/401", "/404"]; export function useCommandPalette() { const permissionStore = usePermissionStore(); - // 状态 + // 面板状态 const visible = ref(false); const keyword = ref(""); const activeIndex = ref(-1); const inputRef = ref(); + + // 菜单数据 const menuItems = ref([]); const results = ref([]); const history = ref([]); - // 排除的路由 - const excludedPaths = ["/redirect", "/login", "/401", "/404"]; - - // ============================================ - // 弹窗控制 - // ============================================ - function open() { keyword.value = ""; results.value = []; @@ -51,24 +44,28 @@ export function useCommandPalette() { visible.value = false; } - // ============================================ - // 搜索逻辑 - // ============================================ - + /** + * 搜索仅匹配菜单标题,避免路径命中过多造成结果噪音。 + */ function onSearch() { activeIndex.value = -1; if (!keyword.value.trim()) { results.value = []; return; } - const kw = keyword.value.toLowerCase(); - results.value = menuItems.value.filter((item) => item.title.toLowerCase().includes(kw)); + const keywordText = keyword.value.toLowerCase(); + results.value = menuItems.value.filter((item) => + item.title.toLowerCase().includes(keywordText) + ); } - function getDisplayList() { + function getDisplayList(): SearchItem[] { return results.value.length ? results.value : history.value; } + /** + * 键盘选择当前展示列表,搜索为空时回退历史记录。 + */ function onSelect() { const list = getDisplayList(); if (list.length === 0) return; @@ -100,10 +97,6 @@ export function useCommandPalette() { } } - // ============================================ - // 历史记录 - // ============================================ - function loadHistory() { try { const data = localStorage.getItem(STORAGE_KEY); @@ -117,15 +110,15 @@ export function useCommandPalette() { localStorage.setItem(STORAGE_KEY, JSON.stringify(history.value)); } + /** + * 历史记录按最近使用排序,并限制本地缓存数量。 + */ function addHistory(item: SearchItem) { - // 去重 - const idx = history.value.findIndex((i) => i.path === item.path); - if (idx !== -1) history.value.splice(idx, 1); + const index = history.value.findIndex((historyItem) => historyItem.path === item.path); + if (index !== -1) history.value.splice(index, 1); - // 添加到开头 history.value.unshift(item); - // 限制数量 if (history.value.length > MAX_HISTORY) { history.value = history.value.slice(0, MAX_HISTORY); } @@ -143,17 +136,16 @@ export function useCommandPalette() { localStorage.removeItem(STORAGE_KEY); } - // ============================================ - // 路由解析 - // ============================================ - + /** + * 将权限路由拍平成命令面板可搜索的菜单项。 + */ function loadRoutes(routes: RouteRecordRaw[], parentPath = "") { routes.forEach((route) => { const path = route.path.startsWith("/") ? route.path : `${parentPath}${parentPath.endsWith("/") ? "" : "/"}${route.path}`; - if (excludedPaths.includes(route.path) || isExternal(route.path)) return; + if (EXCLUDED_PATHS.includes(route.path) || isExternal(route.path)) return; if (route.children) { loadRoutes(route.children, path); @@ -172,10 +164,9 @@ export function useCommandPalette() { }); } - // ============================================ - // 快捷键 - // ============================================ - + /** + * Ctrl/Cmd + K 打开命令面板,并阻止浏览器默认搜索。 + */ function handleKeydown(e: KeyboardEvent) { if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "k") { e.preventDefault(); @@ -183,10 +174,6 @@ export function useCommandPalette() { } } - // ============================================ - // 生命周期 - // ============================================ - onMounted(() => { loadRoutes(permissionStore.routes); loadHistory(); diff --git a/src/components/Hamburger/index.vue b/src/components/Hamburger/index.vue index 832b054a..5aca9ee2 100644 --- a/src/components/Hamburger/index.vue +++ b/src/components/Hamburger/index.vue @@ -18,12 +18,10 @@ const settingsStore = useSettingsStore(); const layout = computed(() => settingsStore.layout); const hamburgerClass = computed(() => { - // 如果暗黑主题 if (settingsStore.resolvedTheme === ThemeMode.DARK) { return "hamburger--white"; } - // 如果是混合布局 && 侧边栏配色方案是经典蓝 if ( layout.value === LayoutMode.MIX && settingsStore.sidebarColorScheme === SidebarColor.CLASSIC_BLUE @@ -31,7 +29,6 @@ const hamburgerClass = computed(() => { return "hamburger--white"; } - // 默认返回空字符串 return ""; }); @@ -45,13 +42,23 @@ function toggleClick() { display: flex; align-items: center; justify-content: center; - padding: 0 15px; + width: 48px; + height: 100%; + padding: 0; + color: var(--el-text-color-regular); cursor: pointer; .hamburger { + width: 16px; + height: 16px; + font-size: 16px; vertical-align: middle; + color: currentcolor; + background-color: currentcolor; transform: scaleX(-1); - transition: transform 0.3s ease; + transition: + color 0.16s, + transform 0.3s ease; &--white { color: #fff; @@ -61,5 +68,9 @@ function toggleClick() { transform: scaleX(1); } } + + &:hover { + color: var(--el-color-primary); + } } diff --git a/src/components/NoticeDropdown/useNotice.ts b/src/components/NoticeDropdown/useNotice.ts index 723b8a87..8d7f4aff 100644 --- a/src/components/NoticeDropdown/useNotice.ts +++ b/src/components/NoticeDropdown/useNotice.ts @@ -1,21 +1,29 @@ -/** - * 通知中心逻辑 - */ -import { computed, ref, onMounted, onBeforeUnmount } from "vue"; -import type { NoticeItem, NoticeDetail, NoticeQueryParams } from "@/api/system/notice"; +import { computed, onBeforeUnmount, onMounted, ref } from "vue"; +import type { NoticeDetail, NoticeItem, NoticeQueryParams } from "@/api/system/notice"; import NoticeAPI from "@/api/system/notice"; import { useSse } from "@/composables"; import router from "@/router"; const PAGE_SIZE = 5; - const NOTICE_EVENT = "notice"; +const NOTICE_REVOKE_EVENT = "notice-revoke"; + type NoticeStatus = 0 | 1; +interface NoticeMessage { + id: string; + title: string; + type: number; + publishTime?: Date; +} + +interface NoticeRevokeMessage { + id: string; +} + export function useNotice() { const { on } = useSse(); - // 状态 const list = ref([]); const unreadTotal = ref(0); const activeStatus = ref(0); @@ -23,11 +31,7 @@ export function useNotice() { const dialogVisible = ref(false); const emptyText = computed(() => (activeStatus.value === 0 ? "暂无未读消息" : "暂无已读消息")); - let unsubscribe: (() => void) | null = null; - - // ============================================ - // 数据获取 - // ============================================ + let stopSubscriptions: (() => void) | null = null; async function fetchList(params?: Partial) { const query: NoticeQueryParams = { @@ -98,14 +102,10 @@ export function useNotice() { router.push({ name: "MyNotice" }); } - // ============================================ - // SSE 订阅 - // ============================================ - function setupSubscription() { - if (unsubscribe) return; + if (stopSubscriptions) return; - unsubscribe = on(NOTICE_EVENT, (data: any) => { + const stopNotice = on(NOTICE_EVENT, (data) => { try { if (!data.id) return; @@ -116,10 +116,13 @@ export function useNotice() { list.value.unshift({ id: data.id, title: data.title, + content: "", type: data.type, + level: "", + publishStatus: 1, publishTime: data.publishTime, isRead: 0, - } as NoticeItem); + }); if (list.value.length > PAGE_SIZE) { list.value.length = PAGE_SIZE; @@ -136,25 +139,26 @@ export function useNotice() { } }); - on("notice-revoke", (data: any) => { + const stopRevoke = on(NOTICE_REVOKE_EVENT, (data) => { try { if (!data.id) return; - const idx = list.value.findIndex((item: NoticeItem) => item.id === data.id); - if (idx >= 0) { - const wasUnread = list.value[idx].isRead !== 1; - list.value.splice(idx, 1); + const index = list.value.findIndex((item: NoticeItem) => item.id === data.id); + if (index >= 0) { + const wasUnread = list.value[index].isRead !== 1; + list.value.splice(index, 1); if (wasUnread && unreadTotal.value > 0) unreadTotal.value -= 1; } } catch (e) { console.error("处理撤回通知失败", e); } }); - } - // ============================================ - // 生命周期 - // ============================================ + stopSubscriptions = () => { + stopNotice(); + stopRevoke(); + }; + } onMounted(() => { refresh(); @@ -162,9 +166,9 @@ export function useNotice() { }); onBeforeUnmount(() => { - if (unsubscribe) { - unsubscribe(); - unsubscribe = null; + if (stopSubscriptions) { + stopSubscriptions(); + stopSubscriptions = null; } }); diff --git a/src/constants/index.ts b/src/constants/index.ts index 526a239a..a39c3659 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -1,13 +1,4 @@ -/** - * 应用常量定义 - * - * @description - * 包含应用中所有的常量定义,包括角色、存储键名等 - */ - -/** - * 应用存储前缀 - */ +/** 应用存储前缀 */ export const APP_PREFIX = "vea"; /** @@ -34,19 +25,19 @@ export const PLATFORM_TENANT_ID = 0; * 命名规则:{APP_PREFIX}:{分类}:{具体名称} */ export const STORAGE_KEYS = { - // ===== 认证相关 ===== + // 认证 ACCESS_TOKEN: `${APP_PREFIX}:auth:access_token`, REFRESH_TOKEN: `${APP_PREFIX}:auth:refresh_token`, REMEMBER_ME: `${APP_PREFIX}:auth:remember_me`, - // ===== 租户相关 ===== + // 租户 TENANT_ID: `${APP_PREFIX}:tenant:id`, TENANT_INFO: `${APP_PREFIX}:tenant:info`, - // ===== 系统相关 ===== + // 系统 DICT_CACHE: `${APP_PREFIX}:system:dict_cache`, - // ===== UI 设置 ===== + // UI SHOW_TAGS_VIEW: `${APP_PREFIX}:ui:show_tags_view`, TAGS_VIEW_STYLE: `${APP_PREFIX}:ui:tags_view_style`, SHOW_APP_LOGO: `${APP_PREFIX}:ui:show_app_logo`, @@ -60,7 +51,7 @@ export const STORAGE_KEYS = { GRAY_MODE: `${APP_PREFIX}:ui:gray_mode`, COLOR_WEAK: `${APP_PREFIX}:ui:color_weak`, - // ===== 应用状态 ===== + // 应用 DEVICE: `${APP_PREFIX}:app:device`, SIZE: `${APP_PREFIX}:app:size`, LANGUAGE: `${APP_PREFIX}:app:language`, @@ -68,7 +59,5 @@ export const STORAGE_KEYS = { ACTIVE_TOP_MENU_PATH: `${APP_PREFIX}:app:active_top_menu_path`, } as const; -/** - * 存储键名类型 - */ +/** 存储键名类型 */ export type StorageKey = (typeof STORAGE_KEYS)[keyof typeof STORAGE_KEYS]; diff --git a/src/enums/business.ts b/src/enums/business.ts index b97a1a78..23d03746 100644 --- a/src/enums/business.ts +++ b/src/enums/business.ts @@ -11,9 +11,18 @@ export enum MenuTypeEnum { CATALOG = "C", // 目录 MENU = "M", // 菜单 + EXTERNAL = "E", // 外链 BUTTON = "B", // 按钮 } +/** + * 外链打开方式枚举 + */ +export enum ExternalOpenModeEnum { + NEW_TAB = 1, // 新标签页 + EMBEDDED = 2, // 系统内嵌 +} + /** * 菜单范围枚举 */ diff --git a/src/lang/package/zh-cn.json b/src/lang/package/zh-cn.json index 6b978574..3db77907 100644 --- a/src/lang/package/zh-cn.json +++ b/src/lang/package/zh-cn.json @@ -67,6 +67,7 @@ "theme": "主题设置", "interface": "界面设置", "navigation": "导航设置", + "assist": "辅助设置", "themePalette": "配色体系", "customColors": "自定义颜色", "customPalette": "自定义", diff --git a/src/layouts/BaseLayout.vue b/src/layouts/BaseLayout.vue index 9edd6450..df862e6c 100644 --- a/src/layouts/BaseLayout.vue +++ b/src/layouts/BaseLayout.vue @@ -1,21 +1,19 @@ diff --git a/src/layouts/MixLayout.vue b/src/layouts/MixLayout.vue deleted file mode 100644 index fe3aca3f..00000000 --- a/src/layouts/MixLayout.vue +++ /dev/null @@ -1,326 +0,0 @@ - - - - - diff --git a/src/layouts/TopLayout.vue b/src/layouts/TopLayout.vue deleted file mode 100644 index 38122b3e..00000000 --- a/src/layouts/TopLayout.vue +++ /dev/null @@ -1,152 +0,0 @@ - - - - - diff --git a/src/layouts/components/LayoutLogo.vue b/src/layouts/components/LayoutLogo.vue index 9d7fd382..f3303492 100644 --- a/src/layouts/components/LayoutLogo.vue +++ b/src/layouts/components/LayoutLogo.vue @@ -1,9 +1,9 @@