Skip to content

Commit e5c9d2f

Browse files
zk020106Charles7c
authored andcommitted
feat: 优化布局样式(同步gi-demo)
Co-authored-by: kiki1373639299<zkai0106@163.com> # message auto-generated for no-merge-commit merge: !14 merge type-fix into dev feat: 优化布局样式(同步gi-demo) Created-by: kiki1373639299 Commit-by: kiki1373639299 Merged-by: Charles_7c Description: <!-- 非常感谢您的 PR!在提交之前,请务必确保您 PR 的代码经过了完整测试,并且通过了代码规范检查。 --> <!-- 在 [] 中输入 x 来勾选) --> ## PR 类型 <!-- 您的 PR 引入了哪种类型的变更? --> <!-- 只支持选择一种类型,如果有多种类型,可以在更新日志中增加 “类型” 列。 --> - [X] 新 feature - [ ] Bug 修复 - [ ] 功能增强 - [ ] 文档变更 - [ ] 代码样式变更 - [ ] 重构 - [ ] 性能改进 - [ ] 单元测试 - [ ] CI/CD - [ ] 其他 ## PR 目的 <!-- 描述一下您的 PR 解决了什么问题。如果可以,请链接到相关 issues。 --> ## 解决方案 <!-- 详细描述您是如何解决的问题 --> ## PR 测试 <!-- 如果可以,请为您的 PR 添加或更新单元测试。 --> <!-- 请描述一下您是如何测试 PR 的。例如:创建/更新单元测试或添加相关的截图。 --> ## Changelog | 模块 | Changelog | Related issues | |-----|-----------| -------------- | | | | | <!-- 如果有多种类型的变更,可以在变更日志表中增加 “类型” 列,该列的值与上方 “PR 类型” 相同。 --> <!-- Related issues 格式为 Closes #<issue号>,或者 Fixes #<issue号>,或者 Resolves #<issue号>。 --> ## 其他信息 <!-- 请描述一下还有哪些注意事项。例如:如果引入了一个不向下兼容的变更,请描述其影响。 --> ## 提交前确认 - [X] PR 代码经过了完整测试,并且通过了代码规范检查 - [ ] 已经完整填写 Changelog,并链接到了相关 issues - [X] PR 代码将要提交到 dev 分支 See merge request: continew/continew-admin-ui!14
1 parent 069175b commit e5c9d2f

File tree

7 files changed

+327
-266
lines changed

7 files changed

+327
-266
lines changed

src/hooks/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ export * from './modules/useBreakpoint'
88
export * from './modules/useDownload'
99
export * from './modules/useResetReactive'
1010
export * from './modules/useMultipartUploader'
11+
export * from './modules/useRouteListener'
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
/**
2+
* 路由变化监听 Hook
3+
* @description 使用发布订阅模式管理路由变化,避免重复监听导致的性能问题
4+
*/
5+
6+
import type { Handler } from 'mitt'
7+
import type { RouteLocationNormalized } from 'vue-router'
8+
import mitt from 'mitt'
9+
import { onUnmounted, ref } from 'vue'
10+
11+
// 事件类型定义
12+
interface RouteChangeEvent {
13+
to: RouteLocationNormalized
14+
from?: RouteLocationNormalized
15+
}
16+
17+
// 事件键定义
18+
const ROUTE_CHANGE_KEY = Symbol('ROUTE_CHANGE')
19+
20+
// 事件发射器
21+
const emitter = mitt<{
22+
[ROUTE_CHANGE_KEY]: RouteChangeEvent
23+
}>()
24+
25+
// 最新路由状态
26+
const latestRoute = ref<RouteLocationNormalized | null>(null)
27+
28+
/**
29+
* 设置路由变化事件
30+
* @param to 目标路由
31+
* @param from 来源路由
32+
*/
33+
export function setRouteEmitter(to: RouteLocationNormalized, from?: RouteLocationNormalized) {
34+
const event: RouteChangeEvent = { to, from }
35+
emitter.emit(ROUTE_CHANGE_KEY, event)
36+
latestRoute.value = to
37+
}
38+
39+
/**
40+
* 路由监听 Hook
41+
* @returns 路由监听相关方法
42+
*/
43+
export function useRouteListener() {
44+
// 监听器列表
45+
const listeners = new Set<Handler<RouteChangeEvent>>()
46+
47+
/**
48+
* 监听路由变化
49+
* @param handler 处理函数
50+
* @param immediate 是否立即执行
51+
*/
52+
function listenerRouteChange(
53+
handler: (event: RouteChangeEvent) => void,
54+
immediate = true,
55+
) {
56+
emitter.on(ROUTE_CHANGE_KEY, handler)
57+
listeners.add(handler)
58+
59+
if (immediate && latestRoute.value) {
60+
handler({ to: latestRoute.value })
61+
}
62+
63+
return handler
64+
}
65+
66+
/**
67+
* 移除路由监听器
68+
* @param handler 要移除的处理函数
69+
*/
70+
function removeRouteListener(handler?: Handler<RouteChangeEvent>) {
71+
if (handler) {
72+
emitter.off(ROUTE_CHANGE_KEY, handler)
73+
listeners.delete(handler)
74+
} else {
75+
// 移除所有监听器
76+
listeners.forEach((listener) => {
77+
emitter.off(ROUTE_CHANGE_KEY, listener)
78+
})
79+
listeners.clear()
80+
}
81+
}
82+
83+
// 组件卸载时清理
84+
onUnmounted(() => {
85+
removeRouteListener()
86+
})
87+
88+
return {
89+
listenerRouteChange,
90+
removeRouteListener,
91+
}
92+
}

src/layout/LayoutColumns.vue

Lines changed: 27 additions & 134 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,16 @@
11
<template>
22
<div class="layout-columns">
33
<div v-show="isDesktop" class="layout-columns__left">
4-
<div class="layout-columns__menu-wrapper">
5-
<!-- 左侧一级菜单区域 -->
6-
<OneLevelMenu :menus="oneLevelMenus" @menu-click="handleMenuClick"></OneLevelMenu>
7-
<div class="layout-columns__right-menu" :class="{ collapsed: appStore.menuCollapse }">
8-
<!-- 系统标题 -->
9-
<div class="layout-columns__title" @click="toHome">
10-
<span v-show="!appStore.menuCollapse" class="system-name gi_line_1">{{ title }}</span>
11-
</div>
12-
<!-- 左侧二级菜单区域 -->
13-
<Menu
14-
v-if="twoLevelMenus.length > 1" class="layout-columns__menu" :menus="twoLevelMenus"
15-
:menu-style="menuStyle"
16-
/>
17-
</div>
4+
<!-- 左侧一级菜单区域 -->
5+
<OneLevelMenu :menus="oneLevelMenus" @menu-click="handleMenuItemClickByItem"></OneLevelMenu>
6+
7+
<!-- 左侧二级菜单区域 -->
8+
<div class="layout-columns__right-menu">
9+
<div class="layout-columns__system-name gi_line_1">{{ appStore.getTitle() }}</div>
10+
<Menu
11+
v-if="twoLevelMenus.length > 1 || oneLevelMenuActiveRoute?.meta?.alwaysShow === true"
12+
class="layout-columns__menu" :menus="twoLevelMenus" :menu-style="{ width: '180px' }"
13+
/>
1814
</div>
1915
</div>
2016

@@ -28,111 +24,47 @@
2824
</template>
2925

3026
<script setup lang="ts">
31-
import type { RouteRecordRaw } from 'vue-router'
32-
import { findTree, mapTree } from 'xe-utils'
3327
import Header from './components/Header/index.vue'
3428
import Main from './components/Main.vue'
3529
import Menu from './components/Menu/index.vue'
3630
import OneLevelMenu from './components/OneLevelMenu/index.vue'
3731
import Tabs from './components/Tabs/index.vue'
38-
import { filterTree } from '@/utils'
39-
import { useAppStore, useRouteStore } from '@/stores'
32+
import { useAppStore } from '@/stores'
33+
import { useLevelMenu } from '@/layout/hooks/useLevelMenu'
4034
import { useDevice } from '@/hooks'
4135
4236
defineOptions({ name: 'LayoutColumns' })
4337
44-
const route = useRoute()
45-
const router = useRouter()
4638
const appStore = useAppStore()
47-
const routeStore = useRouteStore()
4839
const { isDesktop } = useDevice()
4940
50-
// 系统标题和Logo
51-
const title = computed(() => appStore.getTitle())
52-
53-
// 菜单样式 - 根据折叠状态动态调整宽度
54-
const menuStyle = computed(() => {
55-
return {
56-
width: appStore.menuCollapse ? '48px' : '200px',
57-
}
58-
})
59-
60-
// 跳转首页
61-
const toHome = () => {
62-
router.push('/')
63-
}
64-
65-
// 处理菜单路由数据
66-
const cloneRoutes = JSON.parse(JSON.stringify(routeStore.routes)) as RouteRecordRaw[]
67-
const showMenuList = filterTree(cloneRoutes, (i) => i.meta?.hidden === false) as RouteRecordRaw[]
68-
69-
// 一级菜单
70-
const oneLevelMenus = ref<RouteRecordRaw[]>([])
71-
function getOneLevelMenus() {
72-
const cloneList = JSON.parse(JSON.stringify(routeStore.routes)) as RouteRecordRaw[]
73-
const formatList = mapTree(cloneList, (i) => {
74-
if (i?.children?.length === 1 && i?.meta?.alwaysShow !== true) {
75-
return i.children?.[0]
76-
}
77-
return i
78-
})
79-
const arr = formatList.filter((i) => i.meta?.hidden === false)
80-
return arr
81-
}
82-
oneLevelMenus.value = getOneLevelMenus()
83-
84-
// 二级菜单
85-
const twoLevelMenus = computed(() => {
86-
const obj = findTree(showMenuList, (i) => i.path === route.path)
87-
return showMenuList?.[Number(obj.path[0])]?.children || []
88-
})
89-
90-
function handleMenuClick(item: RouteRecordRaw) {
91-
if (item.redirect === 'noRedirect') {
92-
router.replace({ path: item.children?.[0]?.path })
93-
return
94-
}
95-
router.replace({ path: (item.redirect as string) || item.path })
96-
}
41+
const { oneLevelMenus, twoLevelMenus, oneLevelMenuActiveRoute, getOneLevelMenus, handleMenuItemClickByItem } = useLevelMenu()
42+
getOneLevelMenus()
9743
</script>
9844

9945
<style lang="scss" scoped>
10046
.layout-columns {
47+
display: flex;
10148
width: 100%;
10249
height: 100%;
103-
display: flex;
10450
overflow: hidden;
10551
10652
&__left {
107-
height: 100%;
108-
background-color: var(--color-bg-1);
109-
overflow: hidden;
110-
display: flex;
111-
flex-direction: column;
112-
position: relative;
113-
}
114-
115-
&__menu-wrapper {
116-
flex: 1;
11753
display: flex;
54+
height: 100%;
11855
overflow: hidden;
56+
background-color: var(--color-bg-1);
11957
}
12058
12159
&__right-menu {
122-
flex-shrink: 0;
12360
display: flex;
12461
flex-direction: column;
62+
width: 180px;
12563
overflow: hidden;
126-
border-right: 1px solid var(--color-border-2);
127-
transition: width 0.2s;
128-
width: 200px;
129-
130-
&.collapsed {
131-
width: 48px;
132-
}
64+
background-color: var(--color-bg-1);
13365
}
13466
135-
&__title {
67+
&__system-name {
13668
height: 56px;
13769
padding: 0 12px;
13870
color: var(--color-text-1);
@@ -146,61 +78,22 @@ function handleMenuClick(item: RouteRecordRaw) {
14678
border-bottom: 1px solid var(--color-border);
14779
cursor: pointer;
14880
user-select: none;
149-
transition: padding 0.2s;
81+
transition: padding .2s;
15082
151-
.layout-columns__right-menu.collapsed & {
152-
padding: 0;
153-
}
154-
155-
.logo {
156-
width: 32px;
157-
height: 32px;
158-
border-radius: 6px;
159-
transition: all 0.2s;
160-
overflow: hidden;
161-
flex-shrink: 0;
162-
}
163-
164-
.system-name {
165-
padding-left: 6px;
166-
white-space: nowrap;
167-
transition: color 0.3s;
168-
line-height: 1.5;
169-
display: inline-flex;
170-
align-items: center;
171-
172-
&:hover {
173-
color: $color-theme !important;
174-
cursor: pointer;
175-
}
83+
&:hover {
84+
color: $color-theme !important;
85+
cursor: pointer;
17686
}
17787
}
178-
17988
&__menu {
18089
flex: 1;
181-
overflow: hidden;
182-
}
183-
184-
// 折叠状态下的菜单样式
185-
:deep(.arco-menu.arco-menu-vertical.arco-menu-collapsed) {
186-
.arco-menu-icon {
187-
margin-right: 0;
188-
padding: 10px 0;
189-
}
190-
191-
.arco-menu-has-icon {
192-
padding: 0;
193-
justify-content: center;
194-
}
195-
196-
.arco-menu-title {
197-
display: none;
198-
}
90+
overflow: auto;
91+
border-right: 1px solid var(--color-border-2);
19992
}
20093
20194
&__content {
202-
flex: 1;
20395
display: flex;
96+
flex: 1;
20497
flex-direction: column;
20598
overflow: hidden;
20699
}

0 commit comments

Comments
 (0)