xto-fronted 0.4.102 → 0.4.103
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{index-3ekBp4iW.js → index-BlN5yXBT.js} +2 -2
- package/dist/{index-BKj-34y6.js → index-BsPaXQak.js} +2 -2
- package/dist/{index-CJSTBnGF.js → index-CNx4xpUG.js} +981 -971
- package/dist/{index-BK4Mut6H.js → index-DEChud2_.js} +2 -2
- package/dist/{index-B5DLfOYb.js → index-DdACCkye.js} +23 -23
- package/dist/index.js +1 -1
- package/dist/stores/app.d.ts +8 -2
- package/dist/stores/auth.d.ts +2 -2
- package/dist/style.css +1 -1
- package/package.json +94 -94
- package/src/App.vue +48 -48
- package/src/api/index.ts +7 -7
- package/src/assets/styles/_dark.scss +639 -639
- package/src/assets/styles/_root.scss +183 -183
- package/src/assets/styles/_variables.scss +69 -69
- package/src/assets/styles/index.scss +460 -460
- package/src/components/Layout/Header.vue +2 -2
- package/src/components/Layout/MixTopMenu.vue +1185 -1185
- package/src/components/Layout/Sidebar.vue +229 -229
- package/src/components/Layout/SidebarMenuItem.vue +163 -163
- package/src/components/Layout/TopMenu.vue +1177 -1177
- package/src/components/Layout/index.vue +199 -199
- package/src/composables/useI18n.ts +43 -43
- package/src/index.ts +114 -100
- package/src/router/layoutRoute.ts +70 -70
- package/src/stores/app.ts +9 -0
- package/src/stores/index.ts +15 -15
- package/src/stores/locale.ts +66 -66
- package/src/stores/user.ts +0 -2
- package/src/types/api.d.ts +0 -1
- package/src/types/json-bigint.d.ts +18 -18
- package/src/types/xto.d.ts +172 -172
- package/src/utils/request.ts +184 -184
- package/src/views/dashboard/index.vue +545 -545
- package/src/views/error/403.vue +251 -251
- package/src/views/error/404.vue +253 -253
- package/src/views/login/index.vue +586 -586
- package/src/views/system/menu/index.vue +690 -690
- package/src/views/system/role/index.vue +583 -583
- package/src/views/system/user/index.vue +655 -655
- package/vite.config.ts +139 -139
- package/dist/assets/404-C9Uh6Uu-.css +0 -1
- package/dist/assets/404-fVB40gfP.js +0 -1
- package/dist/assets/404-zjGLLssH.js +0 -1
- package/dist/assets/_plugin-vue_export-helper-DlAUqK2U.js +0 -1
- package/dist/assets/index-B2Y_ySNp.js +0 -2
- package/dist/assets/index-B5xc4gQB.css +0 -1
- package/dist/assets/index-B75sburk.js +0 -1
- package/dist/assets/index-BBdRdMfs.js +0 -1
- package/dist/assets/index-BDgOY6Rp.js +0 -1
- package/dist/assets/index-BIoRANs0.js +0 -1
- package/dist/assets/index-BRR97dc6.js +0 -1
- package/dist/assets/index-Bz0BgZQ1.js +0 -1
- package/dist/assets/index-CAdztNsv.css +0 -1
- package/dist/assets/index-CCXrcISf.css +0 -1
- package/dist/assets/index-CDPHn9Pd.js +0 -1
- package/dist/assets/index-CfpZmcpk.css +0 -1
- package/dist/assets/index-Cpew6d-v.css +0 -1
- package/dist/assets/index-CwJSA85U.js +0 -1
- package/dist/assets/index-CwRA10ac.js +0 -1
- package/dist/assets/index-D8NDxq9d.js +0 -1
- package/dist/assets/index-DEB6-Iv_.js +0 -2
- package/dist/assets/index-DM4Ezclc.css +0 -1
- package/dist/assets/index-DYv7nImj.css +0 -1
- package/dist/assets/index-Dm3Gq6SY.js +0 -1
- package/dist/assets/index-DxbgF-OR.js +0 -1
- package/dist/assets/index-RUdXk1fA.css +0 -1
- package/dist/assets/index-_xB0udHf.js +0 -1
- package/dist/assets/index-t-2Y0KhA.css +0 -1
- package/dist/assets/vendor-CUVPinTg.js +0 -13
- package/dist/assets/vue-vendor-Bpie-0gH.js +0 -29
- package/dist/assets/vue-vendor-DeJXJVbN.js +0 -29
- package/dist/assets/xto-base-C3XNcx7i.js +0 -1
- package/dist/assets/xto-base-CL2NKZJJ.css +0 -1
- package/dist/assets/xto-base-PwLGsxxb.js +0 -1
- package/dist/assets/xto-business--V1F5Gwb.css +0 -1
- package/dist/assets/xto-core-B1Ho_Ytu.js +0 -1
- package/dist/assets/xto-core-CtL4zKiV.js +0 -1
- package/dist/assets/xto-data-Coeo_ZYH.js +0 -1
- package/dist/assets/xto-data-MxZsiJgi.css +0 -1
- package/dist/assets/xto-data-bCXQa7fT.js +0 -1
- package/dist/assets/xto-feedback-Bxx38c3P.css +0 -1
- package/dist/assets/xto-feedback-CFysasJi.js +0 -1
- package/dist/assets/xto-feedback-CPydp0kn.js +0 -1
- package/dist/assets/xto-form-Cu6q3VLG.css +0 -1
- package/dist/assets/xto-form-DBlhgyXp.js +0 -1
- package/dist/assets/xto-form-bywohdAf.js +0 -1
- package/dist/assets/xto-layout-BDD6sSlM.css +0 -1
- package/dist/assets/xto-navigation-Bbdpine9.js +0 -1
- package/dist/assets/xto-navigation-I2o1CycT.js +0 -1
- package/dist/assets/xto-navigation-XfpyMpEo.css +0 -1
- package/dist/index-58aI1w0v.js +0 -515
- package/dist/index-A_B_Ap_A.js +0 -4240
- package/dist/index-B-lMqzxZ.js +0 -479
- package/dist/index-B6s_uLJE.js +0 -189
- package/dist/index-BAmYUT0G.js +0 -189
- package/dist/index-BJlOXgu5.js +0 -515
- package/dist/index-BMQao91y.js +0 -189
- package/dist/index-BRvi9qW-.js +0 -515
- package/dist/index-BVGW4DDQ.js +0 -189
- package/dist/index-BXg94yA2.js +0 -515
- package/dist/index-BYAkZ2gD.js +0 -641
- package/dist/index-BfXnrw05.js +0 -515
- package/dist/index-Bmb0rt9C.js +0 -641
- package/dist/index-Bmf0YbVq.js +0 -189
- package/dist/index-C1BnOFy7.js +0 -3145
- package/dist/index-C1j4f3mM.js +0 -479
- package/dist/index-C2-a5KSQ.js +0 -4233
- package/dist/index-C3K89jzC.js +0 -515
- package/dist/index-C92NkXAn.js +0 -479
- package/dist/index-CAHSv7LK.js +0 -4285
- package/dist/index-CVH7bDsl.js +0 -4285
- package/dist/index-Ccp6zfq-.js +0 -4290
- package/dist/index-CeZ0CSSs.js +0 -641
- package/dist/index-Cf8E7FM1.js +0 -4270
- package/dist/index-CgyQqbdx.js +0 -189
- package/dist/index-ChowNrlU.js +0 -641
- package/dist/index-CvQgEgUM.js +0 -641
- package/dist/index-D25KzR0I.js +0 -479
- package/dist/index-D4LWXVnG.js +0 -515
- package/dist/index-DCApv1oX.js +0 -641
- package/dist/index-DCBIjLHy.js +0 -515
- package/dist/index-DEYOivza.js +0 -641
- package/dist/index-DHH8Os_2.js +0 -189
- package/dist/index-DReodgBw.js +0 -4233
- package/dist/index-DTRJONCd.js +0 -515
- package/dist/index-DgffG7KK.js +0 -641
- package/dist/index-DjERNRXX.js +0 -515
- package/dist/index-DjXyzwL0.js +0 -479
- package/dist/index-DkOqM4e2.js +0 -3147
- package/dist/index-Ds8IV04t.js +0 -189
- package/dist/index-LSdsO2Ox.js +0 -479
- package/dist/index-UJixTdep.js +0 -479
- package/dist/index-WPRGF_GX.js +0 -189
- package/dist/index-WPWzllES.js +0 -641
- package/dist/index-Wl2Qg26t.js +0 -3147
- package/dist/index-dk0diNwi.js +0 -479
- package/dist/index-gBlRG4kk.js +0 -479
- package/dist/index-mVol7F2K.js +0 -479
- package/dist/index-xWU3J3OH.js +0 -641
- package/dist/index-zKJLxthI.js +0 -189
- package/dist/index.es.js +0 -95
- package/dist/index.html +0 -28
- package/dist/index.umd.js +0 -8
|
@@ -1,230 +1,230 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
import { computed, ref, watch } from 'vue'
|
|
3
|
-
import { useRoute, useRouter } from 'vue-router'
|
|
4
|
-
import { useMenuStore } from '@/stores/menu'
|
|
5
|
-
import { useUserStore } from '@/stores/user'
|
|
6
|
-
import { useAuthStore } from '@/stores/auth'
|
|
7
|
-
import { useAppStore } from '@/stores/app'
|
|
8
|
-
import { Menu } from '@xto/navigation'
|
|
9
|
-
import { Button } from '@xto/base'
|
|
10
|
-
import { local } from '@/utils/storage'
|
|
11
|
-
import SidebarMenuItem from './SidebarMenuItem.vue'
|
|
12
|
-
|
|
13
|
-
// Props
|
|
14
|
-
const props = withDefaults(defineProps<{
|
|
15
|
-
menuList?: any[]
|
|
16
|
-
showLogo?: boolean
|
|
17
|
-
showUser?: boolean
|
|
18
|
-
logoSrc?: string
|
|
19
|
-
}>(), {
|
|
20
|
-
menuList: () => [],
|
|
21
|
-
showLogo: true,
|
|
22
|
-
showUser: true,
|
|
23
|
-
logoSrc: '/vite.svg'
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
const route = useRoute()
|
|
27
|
-
const router = useRouter()
|
|
28
|
-
const menuStore = useMenuStore()
|
|
29
|
-
const userStore = useUserStore()
|
|
30
|
-
const authStore = useAuthStore()
|
|
31
|
-
const appStore = useAppStore()
|
|
32
|
-
|
|
33
|
-
// 使用传入的菜单列表,如果没有传入则使用 store 中的
|
|
34
|
-
const displayMenuList = computed(() => props.menuList.length > 0 ? props.menuList : menuStore.menuList)
|
|
35
|
-
|
|
36
|
-
const isCollapsed = computed(() => appStore.isCollapsed)
|
|
37
|
-
const activeMenu = computed(() => route.path)
|
|
38
|
-
|
|
39
|
-
// 获取菜单的唯一标识(优先使用 menuId,其次 menuCode,最后 menuUrl)
|
|
40
|
-
const getMenuKey = (menu: any): string => {
|
|
41
|
-
return menu.menuId || menu.menuCode || menu.menuUrl || ''
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// 菜单展开状态持久化(基于当前菜单列表的标识)
|
|
45
|
-
const OPENED_MENUS_KEY = 'sidebar_opened_menus'
|
|
46
|
-
const MENU_LIST_KEY = 'sidebar_menu_list_key'
|
|
47
|
-
|
|
48
|
-
// 获取当前菜单列表的唯一标识
|
|
49
|
-
const getCurrentMenuListKey = (): string => {
|
|
50
|
-
return displayMenuList.value.map(getMenuKey).join(',')
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// 初始化展开状态:只有当菜单列表相同时才恢复之前的展开状态
|
|
54
|
-
const savedMenuListKey = local.get<string>(MENU_LIST_KEY) || ''
|
|
55
|
-
const currentKey = getCurrentMenuListKey()
|
|
56
|
-
const initialOpenedMenus = savedMenuListKey === currentKey
|
|
57
|
-
? (local.get<string[]>(OPENED_MENUS_KEY) || [])
|
|
58
|
-
: []
|
|
59
|
-
const openedMenus = ref<string[]>(initialOpenedMenus)
|
|
60
|
-
|
|
61
|
-
// 监听变化并持久化
|
|
62
|
-
watch(openedMenus, (val) => {
|
|
63
|
-
local.set(OPENED_MENUS_KEY, val)
|
|
64
|
-
local.set(MENU_LIST_KEY, getCurrentMenuListKey())
|
|
65
|
-
}, { deep: true })
|
|
66
|
-
|
|
67
|
-
// 递归查找当前路由对应的父菜单路径(返回 menuId)
|
|
68
|
-
const findMenuPath = (menus: any[], path: string, parentIds: string[] = []): string[] | null => {
|
|
69
|
-
for (const menu of menus) {
|
|
70
|
-
// 当前菜单匹配(使用 menuUrl 匹配路由)
|
|
71
|
-
if (menu.menuUrl && (path === menu.menuUrl || path.startsWith(menu.menuUrl + '/'))) {
|
|
72
|
-
return [...parentIds, getMenuKey(menu)]
|
|
73
|
-
}
|
|
74
|
-
// 递归查找子菜单
|
|
75
|
-
if (menu.children?.length) {
|
|
76
|
-
const result = findMenuPath(menu.children, path, [...parentIds, getMenuKey(menu)])
|
|
77
|
-
if (result) return result
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
return null
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// 监听路由变化,确保当前路由的父菜单展开
|
|
84
|
-
watch([() => route.path, displayMenuList], ([path, menus]) => {
|
|
85
|
-
if (menus.length > 0) {
|
|
86
|
-
const menuPath = findMenuPath(menus, path)
|
|
87
|
-
if (menuPath) {
|
|
88
|
-
// 获取需要展开的父菜单(不包括当前页面本身)
|
|
89
|
-
const parentsToOpen = menuPath.slice(0, -1)
|
|
90
|
-
// 合并已展开的菜单,确保父菜单保持展开状态
|
|
91
|
-
const newOpenedMenus = new Set([...openedMenus.value, ...parentsToOpen])
|
|
92
|
-
openedMenus.value = Array.from(newOpenedMenus)
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
}, { immediate: true })
|
|
96
|
-
|
|
97
|
-
// 监听菜单列表变化,清空展开状态(用于 mix 模式切换一级菜单时)
|
|
98
|
-
watch(displayMenuList, (newList, oldList) => {
|
|
99
|
-
// 当菜单列表完全变化时(如 mix 模式切换一级菜单),清空展开状态
|
|
100
|
-
if (newList !== oldList) {
|
|
101
|
-
openedMenus.value = []
|
|
102
|
-
local.set(OPENED_MENUS_KEY, [])
|
|
103
|
-
local.set(MENU_LIST_KEY, getCurrentMenuListKey())
|
|
104
|
-
}
|
|
105
|
-
}, { immediate: false })
|
|
106
|
-
|
|
107
|
-
// 菜单主题相关
|
|
108
|
-
const menuBgColor = computed(() => appStore.isDark ? '#1d1e1f' : '#fff')
|
|
109
|
-
const menuTextColor = computed(() => appStore.isDark ? '#cfd3dc' : '#303133')
|
|
110
|
-
const menuActiveTextColor = computed(() => '#409eff')
|
|
111
|
-
|
|
112
|
-
// 菜单选择
|
|
113
|
-
const handleMenuSelect = (index: string) => {
|
|
114
|
-
if (index && index !== route.path) {
|
|
115
|
-
router.push(index)
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// 退出登录
|
|
120
|
-
const handleLogout = () => {
|
|
121
|
-
authStore.logout()
|
|
122
|
-
userStore.clearUserInfo()
|
|
123
|
-
menuStore.clearMenu()
|
|
124
|
-
router.push(
|
|
125
|
-
}
|
|
126
|
-
</script>
|
|
127
|
-
|
|
128
|
-
<template>
|
|
129
|
-
<div class="sidebar" :class="{ 'sidebar--collapsed': isCollapsed }">
|
|
130
|
-
<!-- Logo -->
|
|
131
|
-
<div v-if="props.showLogo" class="sidebar__logo">
|
|
132
|
-
<img :src="props.logoSrc" alt="Logo" class="sidebar__logo-img" />
|
|
133
|
-
<span v-show="!isCollapsed" class="sidebar__logo-text">{{ appStore.appName }}</span>
|
|
134
|
-
</div>
|
|
135
|
-
|
|
136
|
-
<!-- 菜单 -->
|
|
137
|
-
<Menu
|
|
138
|
-
v-model="activeMenu"
|
|
139
|
-
v-model:openeds="openedMenus"
|
|
140
|
-
mode="vertical"
|
|
141
|
-
:collapse="isCollapsed"
|
|
142
|
-
:collapse-transition="false"
|
|
143
|
-
:background-color="menuBgColor"
|
|
144
|
-
:text-color="menuTextColor"
|
|
145
|
-
:active-text-color="menuActiveTextColor"
|
|
146
|
-
class="sidebar__menu"
|
|
147
|
-
@select="handleMenuSelect"
|
|
148
|
-
>
|
|
149
|
-
<template v-for="menu in displayMenuList" :key="getMenuKey(menu)">
|
|
150
|
-
<SidebarMenuItem :menu="menu" />
|
|
151
|
-
</template>
|
|
152
|
-
</Menu>
|
|
153
|
-
|
|
154
|
-
<!-- 用户信息 -->
|
|
155
|
-
<div v-if="props.showUser && !isCollapsed" class="sidebar__user">
|
|
156
|
-
<div class="sidebar__user-info">
|
|
157
|
-
<span class="sidebar__user-name">{{ userStore.userName }}</span>
|
|
158
|
-
<span class="sidebar__user-role">{{ userStore.departmentName }}</span>
|
|
159
|
-
</div>
|
|
160
|
-
<Button type="text" size="small" @click="handleLogout">退出</Button>
|
|
161
|
-
</div>
|
|
162
|
-
</div>
|
|
163
|
-
</template>
|
|
164
|
-
|
|
165
|
-
<style lang="scss" scoped>
|
|
166
|
-
.sidebar {
|
|
167
|
-
height: 100%;
|
|
168
|
-
display: flex;
|
|
169
|
-
flex-direction: column;
|
|
170
|
-
background-color: var(--bg-color);
|
|
171
|
-
border-right: 1px solid var(--color-border-lighter);
|
|
172
|
-
|
|
173
|
-
&--collapsed {
|
|
174
|
-
.sidebar__logo {
|
|
175
|
-
justify-content: center;
|
|
176
|
-
padding: 0;
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
&__logo {
|
|
181
|
-
height: 50px;
|
|
182
|
-
display: flex;
|
|
183
|
-
align-items: center;
|
|
184
|
-
padding: 0 20px;
|
|
185
|
-
gap: 10px;
|
|
186
|
-
border-bottom: 1px solid var(--color-border-lighter);
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
&__logo-img {
|
|
190
|
-
width: 32px;
|
|
191
|
-
height: 32px;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
&__logo-text {
|
|
195
|
-
font-size: 16px;
|
|
196
|
-
font-weight: 600;
|
|
197
|
-
color: var(--color-primary);
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
&__menu {
|
|
201
|
-
flex: 1;
|
|
202
|
-
border-right: none;
|
|
203
|
-
overflow-y: auto;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
&__user {
|
|
207
|
-
padding: 10px;
|
|
208
|
-
border-top: 1px solid var(--color-border-lighter);
|
|
209
|
-
display: flex;
|
|
210
|
-
align-items: center;
|
|
211
|
-
justify-content: space-between;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
&__user-info {
|
|
215
|
-
display: flex;
|
|
216
|
-
flex-direction: column;
|
|
217
|
-
gap: 2px;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
&__user-name {
|
|
221
|
-
font-size: 14px;
|
|
222
|
-
font-weight: 500;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
&__user-role {
|
|
226
|
-
font-size: 12px;
|
|
227
|
-
color: var(--color-text-secondary);
|
|
228
|
-
}
|
|
229
|
-
}
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, ref, watch } from 'vue'
|
|
3
|
+
import { useRoute, useRouter } from 'vue-router'
|
|
4
|
+
import { useMenuStore } from '@/stores/menu'
|
|
5
|
+
import { useUserStore } from '@/stores/user'
|
|
6
|
+
import { useAuthStore } from '@/stores/auth'
|
|
7
|
+
import { useAppStore } from '@/stores/app'
|
|
8
|
+
import { Menu } from '@xto/navigation'
|
|
9
|
+
import { Button } from '@xto/base'
|
|
10
|
+
import { local } from '@/utils/storage'
|
|
11
|
+
import SidebarMenuItem from './SidebarMenuItem.vue'
|
|
12
|
+
|
|
13
|
+
// Props
|
|
14
|
+
const props = withDefaults(defineProps<{
|
|
15
|
+
menuList?: any[]
|
|
16
|
+
showLogo?: boolean
|
|
17
|
+
showUser?: boolean
|
|
18
|
+
logoSrc?: string
|
|
19
|
+
}>(), {
|
|
20
|
+
menuList: () => [],
|
|
21
|
+
showLogo: true,
|
|
22
|
+
showUser: true,
|
|
23
|
+
logoSrc: '/vite.svg'
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
const route = useRoute()
|
|
27
|
+
const router = useRouter()
|
|
28
|
+
const menuStore = useMenuStore()
|
|
29
|
+
const userStore = useUserStore()
|
|
30
|
+
const authStore = useAuthStore()
|
|
31
|
+
const appStore = useAppStore()
|
|
32
|
+
|
|
33
|
+
// 使用传入的菜单列表,如果没有传入则使用 store 中的
|
|
34
|
+
const displayMenuList = computed(() => props.menuList.length > 0 ? props.menuList : menuStore.menuList)
|
|
35
|
+
|
|
36
|
+
const isCollapsed = computed(() => appStore.isCollapsed)
|
|
37
|
+
const activeMenu = computed(() => route.path)
|
|
38
|
+
|
|
39
|
+
// 获取菜单的唯一标识(优先使用 menuId,其次 menuCode,最后 menuUrl)
|
|
40
|
+
const getMenuKey = (menu: any): string => {
|
|
41
|
+
return menu.menuId || menu.menuCode || menu.menuUrl || ''
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// 菜单展开状态持久化(基于当前菜单列表的标识)
|
|
45
|
+
const OPENED_MENUS_KEY = 'sidebar_opened_menus'
|
|
46
|
+
const MENU_LIST_KEY = 'sidebar_menu_list_key'
|
|
47
|
+
|
|
48
|
+
// 获取当前菜单列表的唯一标识
|
|
49
|
+
const getCurrentMenuListKey = (): string => {
|
|
50
|
+
return displayMenuList.value.map(getMenuKey).join(',')
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 初始化展开状态:只有当菜单列表相同时才恢复之前的展开状态
|
|
54
|
+
const savedMenuListKey = local.get<string>(MENU_LIST_KEY) || ''
|
|
55
|
+
const currentKey = getCurrentMenuListKey()
|
|
56
|
+
const initialOpenedMenus = savedMenuListKey === currentKey
|
|
57
|
+
? (local.get<string[]>(OPENED_MENUS_KEY) || [])
|
|
58
|
+
: []
|
|
59
|
+
const openedMenus = ref<string[]>(initialOpenedMenus)
|
|
60
|
+
|
|
61
|
+
// 监听变化并持久化
|
|
62
|
+
watch(openedMenus, (val) => {
|
|
63
|
+
local.set(OPENED_MENUS_KEY, val)
|
|
64
|
+
local.set(MENU_LIST_KEY, getCurrentMenuListKey())
|
|
65
|
+
}, { deep: true })
|
|
66
|
+
|
|
67
|
+
// 递归查找当前路由对应的父菜单路径(返回 menuId)
|
|
68
|
+
const findMenuPath = (menus: any[], path: string, parentIds: string[] = []): string[] | null => {
|
|
69
|
+
for (const menu of menus) {
|
|
70
|
+
// 当前菜单匹配(使用 menuUrl 匹配路由)
|
|
71
|
+
if (menu.menuUrl && (path === menu.menuUrl || path.startsWith(menu.menuUrl + '/'))) {
|
|
72
|
+
return [...parentIds, getMenuKey(menu)]
|
|
73
|
+
}
|
|
74
|
+
// 递归查找子菜单
|
|
75
|
+
if (menu.children?.length) {
|
|
76
|
+
const result = findMenuPath(menu.children, path, [...parentIds, getMenuKey(menu)])
|
|
77
|
+
if (result) return result
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return null
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// 监听路由变化,确保当前路由的父菜单展开
|
|
84
|
+
watch([() => route.path, displayMenuList], ([path, menus]) => {
|
|
85
|
+
if (menus.length > 0) {
|
|
86
|
+
const menuPath = findMenuPath(menus, path)
|
|
87
|
+
if (menuPath) {
|
|
88
|
+
// 获取需要展开的父菜单(不包括当前页面本身)
|
|
89
|
+
const parentsToOpen = menuPath.slice(0, -1)
|
|
90
|
+
// 合并已展开的菜单,确保父菜单保持展开状态
|
|
91
|
+
const newOpenedMenus = new Set([...openedMenus.value, ...parentsToOpen])
|
|
92
|
+
openedMenus.value = Array.from(newOpenedMenus)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}, { immediate: true })
|
|
96
|
+
|
|
97
|
+
// 监听菜单列表变化,清空展开状态(用于 mix 模式切换一级菜单时)
|
|
98
|
+
watch(displayMenuList, (newList, oldList) => {
|
|
99
|
+
// 当菜单列表完全变化时(如 mix 模式切换一级菜单),清空展开状态
|
|
100
|
+
if (newList !== oldList) {
|
|
101
|
+
openedMenus.value = []
|
|
102
|
+
local.set(OPENED_MENUS_KEY, [])
|
|
103
|
+
local.set(MENU_LIST_KEY, getCurrentMenuListKey())
|
|
104
|
+
}
|
|
105
|
+
}, { immediate: false })
|
|
106
|
+
|
|
107
|
+
// 菜单主题相关
|
|
108
|
+
const menuBgColor = computed(() => appStore.isDark ? '#1d1e1f' : '#fff')
|
|
109
|
+
const menuTextColor = computed(() => appStore.isDark ? '#cfd3dc' : '#303133')
|
|
110
|
+
const menuActiveTextColor = computed(() => '#409eff')
|
|
111
|
+
|
|
112
|
+
// 菜单选择
|
|
113
|
+
const handleMenuSelect = (index: string) => {
|
|
114
|
+
if (index && index !== route.path) {
|
|
115
|
+
router.push(index)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// 退出登录
|
|
120
|
+
const handleLogout = () => {
|
|
121
|
+
authStore.logout()
|
|
122
|
+
userStore.clearUserInfo()
|
|
123
|
+
menuStore.clearMenu()
|
|
124
|
+
router.push(appStore.loginPath)
|
|
125
|
+
}
|
|
126
|
+
</script>
|
|
127
|
+
|
|
128
|
+
<template>
|
|
129
|
+
<div class="sidebar" :class="{ 'sidebar--collapsed': isCollapsed }">
|
|
130
|
+
<!-- Logo -->
|
|
131
|
+
<div v-if="props.showLogo" class="sidebar__logo">
|
|
132
|
+
<img :src="props.logoSrc" alt="Logo" class="sidebar__logo-img" />
|
|
133
|
+
<span v-show="!isCollapsed" class="sidebar__logo-text">{{ appStore.appName }}</span>
|
|
134
|
+
</div>
|
|
135
|
+
|
|
136
|
+
<!-- 菜单 -->
|
|
137
|
+
<Menu
|
|
138
|
+
v-model="activeMenu"
|
|
139
|
+
v-model:openeds="openedMenus"
|
|
140
|
+
mode="vertical"
|
|
141
|
+
:collapse="isCollapsed"
|
|
142
|
+
:collapse-transition="false"
|
|
143
|
+
:background-color="menuBgColor"
|
|
144
|
+
:text-color="menuTextColor"
|
|
145
|
+
:active-text-color="menuActiveTextColor"
|
|
146
|
+
class="sidebar__menu"
|
|
147
|
+
@select="handleMenuSelect"
|
|
148
|
+
>
|
|
149
|
+
<template v-for="menu in displayMenuList" :key="getMenuKey(menu)">
|
|
150
|
+
<SidebarMenuItem :menu="menu" />
|
|
151
|
+
</template>
|
|
152
|
+
</Menu>
|
|
153
|
+
|
|
154
|
+
<!-- 用户信息 -->
|
|
155
|
+
<div v-if="props.showUser && !isCollapsed" class="sidebar__user">
|
|
156
|
+
<div class="sidebar__user-info">
|
|
157
|
+
<span class="sidebar__user-name">{{ userStore.userName }}</span>
|
|
158
|
+
<span class="sidebar__user-role">{{ userStore.departmentName }}</span>
|
|
159
|
+
</div>
|
|
160
|
+
<Button type="text" size="small" @click="handleLogout">退出</Button>
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
163
|
+
</template>
|
|
164
|
+
|
|
165
|
+
<style lang="scss" scoped>
|
|
166
|
+
.sidebar {
|
|
167
|
+
height: 100%;
|
|
168
|
+
display: flex;
|
|
169
|
+
flex-direction: column;
|
|
170
|
+
background-color: var(--bg-color);
|
|
171
|
+
border-right: 1px solid var(--color-border-lighter);
|
|
172
|
+
|
|
173
|
+
&--collapsed {
|
|
174
|
+
.sidebar__logo {
|
|
175
|
+
justify-content: center;
|
|
176
|
+
padding: 0;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
&__logo {
|
|
181
|
+
height: 50px;
|
|
182
|
+
display: flex;
|
|
183
|
+
align-items: center;
|
|
184
|
+
padding: 0 20px;
|
|
185
|
+
gap: 10px;
|
|
186
|
+
border-bottom: 1px solid var(--color-border-lighter);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
&__logo-img {
|
|
190
|
+
width: 32px;
|
|
191
|
+
height: 32px;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
&__logo-text {
|
|
195
|
+
font-size: 16px;
|
|
196
|
+
font-weight: 600;
|
|
197
|
+
color: var(--color-primary);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
&__menu {
|
|
201
|
+
flex: 1;
|
|
202
|
+
border-right: none;
|
|
203
|
+
overflow-y: auto;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
&__user {
|
|
207
|
+
padding: 10px;
|
|
208
|
+
border-top: 1px solid var(--color-border-lighter);
|
|
209
|
+
display: flex;
|
|
210
|
+
align-items: center;
|
|
211
|
+
justify-content: space-between;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
&__user-info {
|
|
215
|
+
display: flex;
|
|
216
|
+
flex-direction: column;
|
|
217
|
+
gap: 2px;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
&__user-name {
|
|
221
|
+
font-size: 14px;
|
|
222
|
+
font-weight: 500;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
&__user-role {
|
|
226
|
+
font-size: 12px;
|
|
227
|
+
color: var(--color-text-secondary);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
230
|
</style>
|