Skip to content

Commit 189ca4c

Browse files
author
piexlMax(奇淼
committed
feat: 增加侧边常驻模式
1 parent 39ddc9d commit 189ca4c

File tree

4 files changed

+290
-2
lines changed

4 files changed

+290
-2
lines changed

Diff for: web/src/view/layout/aside/index.vue

+6-1
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,26 @@
44
v-if="
55
config.side_mode === 'normal' ||
66
(device === 'mobile' && config.side_mode == 'head') ||
7-
(device === 'mobile' && config.side_mode == 'combination')
7+
(device === 'mobile' && config.side_mode == 'combination') ||
8+
(device === 'mobile' && config.side_mode == 'sidebar')
89
"
910
/>
1011
<head-mode v-if="config.side_mode === 'head' && device !== 'mobile'" />
1112
<combination-mode
1213
v-if="config.side_mode === 'combination' && device !== 'mobile'"
1314
:mode="mode"
1415
/>
16+
<sidebar-mode
17+
v-if="config.side_mode === 'sidebar' && device !== 'mobile'"
18+
/>
1519
</div>
1620
</template>
1721

1822
<script setup>
1923
import NormalMode from './normalMode.vue'
2024
import HeadMode from './headMode.vue'
2125
import CombinationMode from './combinationMode.vue'
26+
import SidebarMode from './sidebarMode.vue'
2227
2328
defineProps({
2429
mode: {

Diff for: web/src/view/layout/aside/sidebarMode.vue

+279
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,279 @@
1+
<template>
2+
<div class="flex h-full">
3+
<!-- 一级菜单常驻侧边栏 -->
4+
<div
5+
class="relative h-full bg-white text-slate-700 dark:text-slate-300 dark:bg-slate-900 border-r shadow dark:shadow-gray-700"
6+
:style="{
7+
width: config.layout_side_collapsed_width + 'px'
8+
}"
9+
>
10+
<el-scrollbar>
11+
<el-menu
12+
:collapse="true"
13+
:collapse-transition="false"
14+
:default-active="topActive"
15+
class="border-r-0 w-full"
16+
unique-opened
17+
@select="selectTopMenuItem"
18+
>
19+
<template v-for="item in routerStore.asyncRouters[0]?.children || []">
20+
<el-menu-item
21+
v-if="!item.hidden && (!item.children || item.children.length === 0)"
22+
:key="item.name"
23+
:index="item.name"
24+
class="dark:text-slate-300 overflow-hidden"
25+
:style="{
26+
height: config.layout_side_item_height + 'px'
27+
}"
28+
>
29+
<el-icon v-if="item.meta.icon">
30+
<component :is="item.meta.icon" />
31+
</el-icon>
32+
<template v-else>
33+
{{ item.meta.title[0] }}
34+
</template>
35+
<template #title>
36+
{{ item.meta.title }}
37+
</template>
38+
</el-menu-item>
39+
<template v-else-if="!item.hidden" >
40+
<el-menu-item
41+
:key="item.name"
42+
:index="item.name"
43+
:class="{'is-active': topActive === item.name}"
44+
class="dark:text-slate-300 overflow-hidden"
45+
:style="{
46+
height: config.layout_side_item_height + 'px'
47+
}"
48+
>
49+
<el-icon v-if="item.meta.icon">
50+
<component :is="item.meta.icon" />
51+
</el-icon>
52+
<template v-else>
53+
{{ item.meta.title[0] }}
54+
</template>
55+
<template #title>
56+
{{ item.meta.title }}
57+
</template>
58+
</el-menu-item>
59+
</template>
60+
61+
</template>
62+
</el-menu>
63+
</el-scrollbar>
64+
</div>
65+
66+
<!-- 二级菜单并列显示 -->
67+
<div
68+
class="relative h-full bg-white text-slate-700 dark:text-slate-300 dark:bg-slate-900 border-r shadow dark:shadow-gray-700 px-2"
69+
:style="{
70+
width: layoutSideWidth + 'px'
71+
}"
72+
>
73+
<el-scrollbar>
74+
<el-menu
75+
:collapse="isCollapse"
76+
:collapse-transition="false"
77+
:default-active="active"
78+
class="border-r-0 w-full"
79+
unique-opened
80+
@select="selectMenuItem"
81+
>
82+
<template v-for="item in secondLevelMenus">
83+
<aside-component
84+
v-if="!item.hidden"
85+
:key="item.name"
86+
:router-info="item"
87+
/>
88+
</template>
89+
</el-menu>
90+
</el-scrollbar>
91+
<div
92+
class="absolute bottom-8 right-2 w-8 h-8 bg-gray-50 dark:bg-slate-800 flex items-center justify-center rounded cursor-pointer"
93+
:class="isCollapse ? 'right-0 left-0 mx-auto' : 'right-2'"
94+
@click="toggleCollapse"
95+
>
96+
<el-icon v-if="!isCollapse">
97+
<DArrowLeft />
98+
</el-icon>
99+
<el-icon v-else>
100+
<DArrowRight />
101+
</el-icon>
102+
</div>
103+
</div>
104+
</div>
105+
</template>
106+
107+
<script setup>
108+
import AsideComponent from '@/view/layout/aside/asideComponent/index.vue'
109+
import { ref, provide, watchEffect, computed } from 'vue'
110+
import { useRoute, useRouter } from 'vue-router'
111+
import { useRouterStore } from '@/pinia/modules/router'
112+
import { useAppStore } from '@/pinia'
113+
import { storeToRefs } from 'pinia'
114+
115+
const appStore = useAppStore()
116+
const { device, config } = storeToRefs(appStore)
117+
118+
defineOptions({
119+
name: 'SidebarMode'
120+
})
121+
122+
const route = useRoute()
123+
const router = useRouter()
124+
const routerStore = useRouterStore()
125+
const isCollapse = ref(false)
126+
const active = ref('')
127+
const topActive = ref('')
128+
const secondLevelMenus = ref([])
129+
130+
const layoutSideWidth = computed(() => {
131+
if (!isCollapse.value) {
132+
return config.value.layout_side_width
133+
} else {
134+
return config.value.layout_side_collapsed_width
135+
}
136+
})
137+
138+
139+
provide('isCollapse', isCollapse)
140+
141+
// 更新二级菜单
142+
const updateSecondLevelMenus = (menuName) => {
143+
const menu = routerStore.asyncRouters[0]?.children.find(item => item.name === menuName)
144+
if (menu && menu.children && menu.children.length > 0) {
145+
secondLevelMenus.value = menu.children
146+
} else {
147+
secondLevelMenus.value = []
148+
}
149+
}
150+
151+
// 选择一级菜单
152+
const selectTopMenuItem = (index) => {
153+
topActive.value = index
154+
updateSecondLevelMenus(index)
155+
156+
// 如果选中的一级菜单有子菜单,则导航到第一个子菜单
157+
const menu = routerStore.asyncRouters[0]?.children.find(item => item.name === index)
158+
if (menu && menu.children && menu.children.length > 0) {
159+
const firstVisibleChild = menu.children.find(child => !child.hidden)
160+
if (firstVisibleChild) {
161+
navigateToMenuItem(firstVisibleChild.name)
162+
}
163+
} else {
164+
// 如果没有子菜单,直接导航到该菜单
165+
navigateToMenuItem(index)
166+
}
167+
}
168+
169+
// 选择二级或更深层级的菜单
170+
const selectMenuItem = (index) => {
171+
navigateToMenuItem(index)
172+
}
173+
174+
// 导航到指定菜单
175+
const navigateToMenuItem = (index) => {
176+
const query = {}
177+
const params = {}
178+
routerStore.routeMap[index]?.parameters &&
179+
routerStore.routeMap[index]?.parameters.forEach((item) => {
180+
if (item.type === 'query') {
181+
query[item.key] = item.value
182+
} else {
183+
params[item.key] = item.value
184+
}
185+
})
186+
if (index === route.name) return
187+
if (index.indexOf('http://') > -1 || index.indexOf('https://') > -1) {
188+
if (index === 'Iframe') {
189+
query.url = decodeURIComponent(index)
190+
router.push({
191+
name: 'Iframe',
192+
query,
193+
params
194+
})
195+
return
196+
} else {
197+
window.open(index, '_blank')
198+
return
199+
}
200+
} else {
201+
router.push({ name: index, query, params })
202+
}
203+
}
204+
205+
const toggleCollapse = () => {
206+
isCollapse.value = !isCollapse.value
207+
}
208+
209+
210+
211+
watchEffect(() => {
212+
if (route.name === 'Iframe') {
213+
active.value = decodeURIComponent(route.query.url)
214+
return
215+
}
216+
active.value = route.meta.activeName || route.name
217+
218+
// 找到当前路由所属的一级菜单
219+
const findParentMenu = () => {
220+
for (const topMenu of routerStore.asyncRouters[0]?.children || []) {
221+
if (topMenu.hidden) continue
222+
223+
// 检查当前路由是否是这个一级菜单的子菜单
224+
if (topMenu.children && topMenu.children.some(child => child.name === route.name)) {
225+
return topMenu.name
226+
}
227+
228+
// 递归检查更深层级
229+
const checkChildren = (items) => {
230+
for (const item of items || []) {
231+
if (item.name === route.name) {
232+
return true
233+
}
234+
if (item.children && checkChildren(item.children)) {
235+
return true
236+
}
237+
}
238+
return false
239+
}
240+
241+
if (topMenu.children && checkChildren(topMenu.children)) {
242+
return topMenu.name
243+
}
244+
}
245+
return null
246+
}
247+
248+
const parentMenu = findParentMenu()
249+
if (parentMenu) {
250+
topActive.value = parentMenu
251+
updateSecondLevelMenus(parentMenu)
252+
} else if (routerStore.asyncRouters[0]?.children?.length > 0) {
253+
// 如果没有找到父菜单,默认选择第一个有子菜单的一级菜单
254+
const firstMenuWithChildren = routerStore.asyncRouters[0].children.find(
255+
item => !item.hidden && item.children && item.children.length > 0
256+
)
257+
if (firstMenuWithChildren) {
258+
topActive.value = firstMenuWithChildren.name
259+
updateSecondLevelMenus(firstMenuWithChildren.name)
260+
}
261+
}
262+
})
263+
264+
watchEffect(() => {
265+
if (device.value === 'mobile') {
266+
isCollapse.value = true
267+
} else {
268+
isCollapse.value = false
269+
}
270+
})
271+
</script>
272+
273+
<style lang="scss" scoped>
274+
:deep(.el-menu--vertical) {
275+
.is-active {
276+
background-color: var(--el-color-primary-light-8) !important;
277+
}
278+
}
279+
</style>

Diff for: web/src/view/layout/index.vue

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
<div class="flex flex-row w-full gva-container pt-16 box-border h-full">
1515
<gva-aside
1616
v-if="
17-
config.side_mode === 'normal' ||
17+
config.side_mode === 'normal' || config.side_mode === 'sidebar' ||
1818
(device === 'mobile' && config.side_mode == 'head') ||
1919
(device === 'mobile' && config.side_mode == 'combination')
2020
"

Diff for: web/src/view/layout/setting/index.vue

+4
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,10 @@
183183
{
184184
label: '组合模式',
185185
value: 'combination'
186+
},
187+
{
188+
label: '侧边栏常驻',
189+
value: 'sidebar'
186190
}
187191
]
188192

0 commit comments

Comments
 (0)