xto-fronted 0.4.6 → 0.4.8

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.
Files changed (160) hide show
  1. package/.env.development +7 -7
  2. package/.env.production +7 -7
  3. package/dist/assets/403-AFBQifUI.js +1 -0
  4. package/dist/assets/403-BHEXXbt2.css +1 -0
  5. package/dist/assets/404-Ct_A1n7S.css +1 -0
  6. package/dist/assets/404-WFvpcD2_.js +1 -0
  7. package/dist/assets/_plugin-vue_export-helper-DlAUqK2U.js +1 -0
  8. package/dist/assets/index-1juADvYN.js +2 -0
  9. package/dist/assets/index-B-sX4Ru0.js +1 -0
  10. package/dist/assets/index-BMcziU5a.css +1 -0
  11. package/dist/assets/index-BRR97dc6.js +1 -0
  12. package/dist/assets/index-BZA0ksjx.css +1 -0
  13. package/dist/assets/index-BpV_8nl0.js +1 -0
  14. package/dist/assets/index-BvzhR4zp.js +1 -0
  15. package/dist/assets/index-CUh_s55Z.css +1 -0
  16. package/dist/assets/index-CVjdnIgR.css +1 -0
  17. package/dist/assets/index-CYq57-zj.js +1 -0
  18. package/dist/assets/index-CkL3sVAQ.js +2 -0
  19. package/dist/assets/index-CtrKVYJb.css +1 -0
  20. package/dist/assets/index-Cz2P_bsS.js +1 -0
  21. package/dist/assets/index-D9wlAuR_.js +1 -0
  22. package/dist/assets/index-DawJb02s.css +1 -0
  23. package/dist/assets/index-DfFR6NLf.js +1 -0
  24. package/dist/assets/index-DwVgMO8e.js +1 -0
  25. package/dist/assets/index-GDP-IkXE.css +1 -0
  26. package/dist/assets/index-Iaz1ZzPC.js +2 -0
  27. package/dist/assets/index-PfV8pzQz.css +1 -0
  28. package/dist/assets/index-Swfu6yvD.css +1 -0
  29. package/dist/assets/index-Te8_PRgJ.js +1 -0
  30. package/dist/assets/index-WyZ91RLx.css +1 -0
  31. package/dist/assets/index-tFYRoFdE.js +1 -0
  32. package/dist/assets/vendor-42ANG6Sg.js +6 -0
  33. package/dist/assets/vite-Dw-pgLOX.js +1 -0
  34. package/dist/assets/vue-vendor-Br-l7wbK.js +29 -0
  35. package/dist/assets/xto-base-C-IBqjVs.js +1 -0
  36. package/dist/assets/xto-base-C6eqMPdO.css +1 -0
  37. package/dist/assets/xto-business--V1F5Gwb.css +1 -0
  38. package/dist/assets/xto-core-DZK7Cyg0.js +1 -0
  39. package/dist/assets/xto-data-BFpiDgJi.js +1 -0
  40. package/dist/assets/xto-data-CnAQAQH2.css +1 -0
  41. package/dist/assets/xto-feedback-B7ipsTfz.js +1 -0
  42. package/dist/assets/xto-feedback-DBwJzoTj.css +1 -0
  43. package/dist/assets/xto-form-CrsyAjyr.css +1 -0
  44. package/dist/assets/xto-form-NRjKKNcY.js +1 -0
  45. package/dist/assets/xto-layout-BqU8RuWL.css +1 -0
  46. package/dist/assets/xto-navigation-BiSaXPfr.js +1 -0
  47. package/dist/assets/xto-navigation-C1cnSL2E.css +1 -0
  48. package/dist/assets/xto-navigation-CBPg4dCc.css +1 -0
  49. package/dist/assets/xto-navigation-CKabFu9d.js +1 -0
  50. package/dist/index-54irhCHL.js +1830 -0
  51. package/dist/{index-15Bu0M8D.js → index-BzRf1eoJ.js} +1 -1
  52. package/dist/{index-BO2Zf9u6.js → index-DH4aoCZb.js} +1 -1
  53. package/dist/{index-BBqvHkzE.js → index-Kqa7iZ9E.js} +1 -1
  54. package/dist/{index-BQqo0ZIb.js → index-pxkZlvBw.js} +1 -1
  55. package/dist/index.es.js +1 -1
  56. package/dist/index.html +28 -0
  57. package/dist/index.umd.js +1 -1
  58. package/dist/style.css +1 -1
  59. package/package.json +85 -86
  60. package/src/api/auth.ts +25 -25
  61. package/src/api/system.ts +66 -66
  62. package/src/assets/styles/_dark.scss +406 -406
  63. package/src/components/Layout/Header.vue +973 -973
  64. package/src/components/Layout/Sidebar.vue +273 -212
  65. package/src/components/Layout/index.vue +443 -63
  66. package/src/composables/useApp.ts +61 -61
  67. package/src/composables/useAuth.ts +16 -16
  68. package/src/directives/permission.ts +27 -27
  69. package/src/env.d.ts +18 -18
  70. package/src/index.ts +47 -47
  71. package/src/router/dynamicRoutes.ts +162 -162
  72. package/src/router/guards.ts +128 -128
  73. package/src/router/index.ts +79 -79
  74. package/src/stores/auth.ts +65 -65
  75. package/src/stores/menu.ts +48 -48
  76. package/src/stores/user.ts +50 -50
  77. package/src/types/api.d.ts +80 -80
  78. package/src/utils/auth.ts +99 -99
  79. package/src/utils/config.ts +80 -80
  80. package/src/utils/permission.ts +32 -32
  81. package/src/utils/request.ts +124 -124
  82. package/src/views/login/index.vue +194 -188
  83. package/vite.config.ts +135 -135
  84. package/dist/index-B3PLzNB0.js +0 -345
  85. package/dist/index-B6DTsC6l.js +0 -1715
  86. package/dist/index-B7etKk33.js +0 -372
  87. package/dist/index-B7mpL6Zf.js +0 -475
  88. package/dist/index-BC2PGkkJ.js +0 -1644
  89. package/dist/index-BGgbfcmf.js +0 -475
  90. package/dist/index-BGmUwemj.js +0 -372
  91. package/dist/index-BQFfQj5Q.js +0 -142
  92. package/dist/index-BkRneTya.js +0 -142
  93. package/dist/index-BlRrngsc.js +0 -475
  94. package/dist/index-BmVvM7sm.js +0 -345
  95. package/dist/index-Bn4ThpX9.js +0 -142
  96. package/dist/index-BwfjwDKr.js +0 -1477
  97. package/dist/index-BxIL2hrt.js +0 -475
  98. package/dist/index-C-3fhbN2.js +0 -1644
  99. package/dist/index-C0VN9nFF.js +0 -142
  100. package/dist/index-C0xyGOsz.js +0 -475
  101. package/dist/index-C3c8NAZq.js +0 -1477
  102. package/dist/index-C42VtP71.js +0 -142
  103. package/dist/index-C6Nm0r9k.js +0 -475
  104. package/dist/index-C6w0-8xN.js +0 -1648
  105. package/dist/index-CD364XjV.js +0 -142
  106. package/dist/index-CHww99-i.js +0 -345
  107. package/dist/index-CIgWYERJ.js +0 -1644
  108. package/dist/index-CTs6DTuQ.js +0 -345
  109. package/dist/index-CWRs4WMN.js +0 -372
  110. package/dist/index-Cb-SxHJp.js +0 -345
  111. package/dist/index-CeCysOnl.js +0 -345
  112. package/dist/index-Cg1UpC8D.js +0 -1644
  113. package/dist/index-Cgkqpyx2.js +0 -345
  114. package/dist/index-CiuDEfo-.js +0 -142
  115. package/dist/index-CmQfZC8r.js +0 -372
  116. package/dist/index-CmkjhpX_.js +0 -475
  117. package/dist/index-CpxpXTQX.js +0 -1462
  118. package/dist/index-CqXFk_ET.js +0 -345
  119. package/dist/index-Cqix1YLE.js +0 -1697
  120. package/dist/index-CtvB5J9E.js +0 -372
  121. package/dist/index-Cu3Z2-PY.js +0 -345
  122. package/dist/index-CvDxK7Ab.js +0 -372
  123. package/dist/index-D-FER0vJ.js +0 -372
  124. package/dist/index-D2fQ8TK8.js +0 -475
  125. package/dist/index-D3xVcFvg.js +0 -372
  126. package/dist/index-D4crnrO6.js +0 -142
  127. package/dist/index-D7EzwTM5.js +0 -372
  128. package/dist/index-D7TZamyY.js +0 -1664
  129. package/dist/index-D88fiqXR.js +0 -475
  130. package/dist/index-DEbpF-M4.js +0 -1457
  131. package/dist/index-DFXuyPge.js +0 -1627
  132. package/dist/index-DLgimJYb.js +0 -1667
  133. package/dist/index-DPEVEyik.js +0 -475
  134. package/dist/index-DWy_UGhI.js +0 -345
  135. package/dist/index-DYVtddfw.js +0 -142
  136. package/dist/index-DYnXaqYf.js +0 -142
  137. package/dist/index-DcvRPHuy.js +0 -372
  138. package/dist/index-DdC1uV2v.js +0 -1700
  139. package/dist/index-Dga14ZN7.js +0 -1774
  140. package/dist/index-Dk2V44uP.js +0 -372
  141. package/dist/index-DnJ481u1.js +0 -475
  142. package/dist/index-Do1CBqg8.js +0 -345
  143. package/dist/index-DqQRSPeF.js +0 -345
  144. package/dist/index-Jb4VMHIS.js +0 -142
  145. package/dist/index-MC3wWjNt.js +0 -475
  146. package/dist/index-MG0JePmx.js +0 -142
  147. package/dist/index-PRFGBLWt.js +0 -475
  148. package/dist/index-QgkT42dc.js +0 -372
  149. package/dist/index-TrLCW5xL.js +0 -372
  150. package/dist/index-YDlNLFVk.js +0 -142
  151. package/dist/index-ZAJgA3XD.js +0 -475
  152. package/dist/index-a_ilWAvi.js +0 -345
  153. package/dist/index-bi1TMGid.js +0 -372
  154. package/dist/index-fyarVCog.js +0 -475
  155. package/dist/index-mnTZtPFa.js +0 -345
  156. package/dist/index-orZCyV6I.js +0 -345
  157. package/dist/index-p3TbK44c.js +0 -142
  158. package/dist/index-sRwZYbZ4.js +0 -372
  159. package/dist/index-wATqKEcF.js +0 -142
  160. package/dist/setup.d.ts +0 -17
@@ -1,213 +1,274 @@
1
- <script setup lang="ts">
2
- import { computed } 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, MenuItem, SubMenu } from '@xto/navigation'
9
- import { Button } from '@xto/base'
10
- import { Icon } from '@xto/base'
11
-
12
- const route = useRoute()
13
- const router = useRouter()
14
- const menuStore = useMenuStore()
15
- const userStore = useUserStore()
16
- const authStore = useAuthStore()
17
- const appStore = useAppStore()
18
-
19
- const isCollapsed = computed(() => appStore.isCollapsed)
20
- const activeMenu = computed(() => route.path)
21
-
22
- // 菜单主题相关
23
- const menuBgColor = computed(() => appStore.isDark ? '#1d1e1f' : '#fff')
24
- const menuTextColor = computed(() => appStore.isDark ? '#cfd3dc' : '#303133')
25
- const menuActiveTextColor = computed(() => '#409eff')
26
-
27
- // 菜单选择
28
- const handleMenuSelect = (index: string) => {
29
- if (index && index !== route.path) {
30
- router.push(index)
31
- }
32
- }
33
-
34
- // 退出登录
35
- const handleLogout = () => {
36
- authStore.logout()
37
- userStore.clearUserInfo()
38
- menuStore.clearMenu()
39
- router.push('/login')
40
- }
41
-
42
- // 获取菜单图标名称
43
- const getMenuIcon = (icon?: string): string => {
44
- // 无图标时返回默认值
45
- if (!icon || icon === '') return 'folder'
46
-
47
- // 处理 tineco-ui 的图标类名(如 tineco-icon-home)
48
- if (icon.startsWith('tineco-icon-')) {
49
- const iconName = icon.replace('tineco-icon-', '')
50
- // 常见的 tineco icon 映射
51
- const tinecoIconMap: Record<string, string> = {
52
- home: 'home',
53
- dashboard: 'dashboard',
54
- system: 'system',
55
- user: 'user',
56
- role: 'role',
57
- menu: 'list',
58
- setting: 'setting',
59
- file: 'file',
60
- folder: 'folder',
61
- chart: 'chart',
62
- report: 'report',
63
- analytics: 'analytics'
64
- }
65
- return tinecoIconMap[iconName] || iconName
66
- }
67
-
68
- // 常见业务图标映射
69
- const iconMap: Record<string, string> = {
70
- dashboard: 'dashboard',
71
- system: 'system',
72
- user: 'user',
73
- role: 'role',
74
- menu: 'list',
75
- setting: 'setting',
76
- home: 'home',
77
- chart: 'chart',
78
- report: 'report',
79
- analytics: 'analytics',
80
- // 添加更多映射
81
- permission: 'permission',
82
- log: 'log',
83
- notification: 'notification',
84
- app: 'app',
85
- list: 'list',
86
- grid: 'grid'
87
- }
88
-
89
- return iconMap[icon] || icon
90
- }
91
- </script>
92
-
93
- <template>
94
- <div class="sidebar" :class="{ 'sidebar--collapsed': isCollapsed }">
95
- <!-- Logo -->
96
- <div class="sidebar__logo">
97
- <img src="/vite.svg" alt="Logo" class="sidebar__logo-img" />
98
- <span v-show="!isCollapsed" class="sidebar__logo-text">{{ appStore.appName }}</span>
99
- </div>
100
-
101
- <!-- 菜单 -->
102
- <Menu
103
- :default-active="activeMenu"
104
- mode="vertical"
105
- :collapse="isCollapsed"
106
- :collapse-transition="false"
107
- :background-color="menuBgColor"
108
- :text-color="menuTextColor"
109
- :active-text-color="menuActiveTextColor"
110
- class="sidebar__menu"
111
- @select="handleMenuSelect"
112
- >
113
- <template v-for="menu in menuStore.menuList" :key="menu.menuUrl">
114
- <!-- 有子菜单 -->
115
- <SubMenu v-if="menu.children && menu.children.length > 0" :index="menu.menuUrl">
116
- <template #title>
117
- <Icon :name="getMenuIcon(menu.icon)" :size="16" />
118
- <span>{{ menu.menuName }}</span>
119
- </template>
120
- <MenuItem
121
- v-for="child in menu.children"
122
- :key="child.menuUrl"
123
- :index="child.menuUrl"
124
- >
125
- <Icon :name="getMenuIcon(child.icon)" :size="16" />
126
- <span>{{ child.menuName }}</span>
127
- </MenuItem>
128
- </SubMenu>
129
- <!-- 无子菜单 -->
130
- <MenuItem v-else :index="menu.menuUrl">
131
- <Icon :name="getMenuIcon(menu.icon)" :size="16" />
132
- <span>{{ menu.menuName }}</span>
133
- </MenuItem>
134
- </template>
135
- </Menu>
136
-
137
- <!-- 用户信息 -->
138
- <div v-if="!isCollapsed" class="sidebar__user">
139
- <div class="sidebar__user-info">
140
- <span class="sidebar__user-name">{{ userStore.userName }}</span>
141
- <span class="sidebar__user-role">{{ userStore.departmentName }}</span>
142
- </div>
143
- <Button type="text" size="small" @click="handleLogout">退出</Button>
144
- </div>
145
- </div>
146
- </template>
147
-
148
- <style lang="scss" scoped>
149
- .sidebar {
150
- height: 100%;
151
- display: flex;
152
- flex-direction: column;
153
- background-color: var(--bg-color);
154
- border-right: 1px solid var(--color-border-lighter);
155
-
156
- &--collapsed {
157
- .sidebar__logo {
158
- justify-content: center;
159
- padding: 0;
160
- }
161
- }
162
-
163
- &__logo {
164
- height: 50px;
165
- display: flex;
166
- align-items: center;
167
- padding: 0 20px;
168
- gap: 10px;
169
- border-bottom: 1px solid var(--color-border-lighter);
170
- }
171
-
172
- &__logo-img {
173
- width: 32px;
174
- height: 32px;
175
- }
176
-
177
- &__logo-text {
178
- font-size: 16px;
179
- font-weight: 600;
180
- color: var(--color-primary);
181
- }
182
-
183
- &__menu {
184
- flex: 1;
185
- border-right: none;
186
- overflow-y: auto;
187
- }
188
-
189
- &__user {
190
- padding: 10px;
191
- border-top: 1px solid var(--color-border-lighter);
192
- display: flex;
193
- align-items: center;
194
- justify-content: space-between;
195
- }
196
-
197
- &__user-info {
198
- display: flex;
199
- flex-direction: column;
200
- gap: 2px;
201
- }
202
-
203
- &__user-name {
204
- font-size: 14px;
205
- font-weight: 500;
206
- }
207
-
208
- &__user-role {
209
- font-size: 12px;
210
- color: var(--color-text-secondary);
211
- }
212
- }
1
+ <script setup lang="ts">
2
+ import { computed } 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, MenuItem, SubMenu } from '@xto/navigation'
9
+ import { Button, Icon } from '@xto/base'
10
+
11
+ const route = useRoute()
12
+ const router = useRouter()
13
+ const menuStore = useMenuStore()
14
+ const userStore = useUserStore()
15
+ const authStore = useAuthStore()
16
+ const appStore = useAppStore()
17
+
18
+ const isCollapsed = computed(() => appStore.isCollapsed)
19
+ const activeMenu = computed(() => route.path)
20
+
21
+ // 菜单主题相关
22
+ const menuBgColor = computed(() => appStore.isDark ? '#1d1e1f' : '#fff')
23
+ const menuTextColor = computed(() => appStore.isDark ? '#cfd3dc' : '#303133')
24
+ const menuActiveTextColor = computed(() => '#409eff')
25
+
26
+ // 菜单选择
27
+ const handleMenuSelect = (index: string) => {
28
+ if (index && index !== route.path) {
29
+ router.push(index)
30
+ }
31
+ }
32
+
33
+ // 退出登录
34
+ const handleLogout = () => {
35
+ authStore.logout()
36
+ userStore.clearUserInfo()
37
+ menuStore.clearMenu()
38
+ router.push('/login')
39
+ }
40
+
41
+ // 已知的图标名称列表(来自 @xto/base/icons.ts)
42
+ const knownIcons = new Set([
43
+ 'arrow-up', 'arrow-down', 'arrow-left', 'arrow-right',
44
+ 'caret-down', 'caret-right', 'plus', 'minus', 'close', 'check',
45
+ 'edit', 'delete', 'copy', 'download', 'upload', 'refresh', 'search',
46
+ 'filter', 'more', 'setting', 'share', 'loading', 'info', 'success',
47
+ 'warning', 'error', 'question', 'user', 'user-add', 'user-group',
48
+ 'logout', 'login', 'file', 'folder', 'folder-open', 'document',
49
+ 'image', 'video', 'music', 'camera', 'mail', 'phone', 'chat',
50
+ 'bell', 'message', 'eye', 'eye-off', 'calendar', 'clock', 'history',
51
+ 'timer', 'location', 'map', 'globe', 'star', 'heart', 'thumb-up',
52
+ 'link', 'external-link', 'lock', 'unlock', 'key', 'home', 'menu',
53
+ 'menu-fold', 'menu-unfold', 'sidebar-fold', 'sidebar-expand',
54
+ 'sidebar-left', 'dashboard', 'chart', 'chart-pie', 'chart-line',
55
+ 'report', 'analytics', 'system', 'permission', 'role', 'user-manage',
56
+ 'log', 'notification', 'app', 'list', 'grid', 'fullscreen',
57
+ 'fullscreen-exit', 'zoom-in', 'zoom-out', 'print', 'bookmark',
58
+ 'tag', 'code', 'terminal', 'database', 'server', 'cloud', 'gift',
59
+ 'moon', 'sun', 'theme', 'skin'
60
+ ])
61
+
62
+ // 获取菜单图标名称
63
+ const getMenuIcon = (icon?: string): string => {
64
+ // 无图标时返回空
65
+ if (!icon || icon === '') return ''
66
+
67
+ // 处理 tineco-ui 的图标类名(如 tineco-icon-home)
68
+ if (icon.startsWith('tineco-icon-')) {
69
+ const iconName = icon.replace('tineco-icon-', '')
70
+ // 常见的 tineco icon 映射
71
+ const tinecoIconMap: Record<string, string> = {
72
+ home: 'home',
73
+ dashboard: 'dashboard',
74
+ system: 'system',
75
+ user: 'user',
76
+ role: 'role',
77
+ menu: 'list',
78
+ setting: 'setting',
79
+ file: 'file',
80
+ folder: 'folder',
81
+ chart: 'chart',
82
+ report: 'report',
83
+ analytics: 'analytics'
84
+ }
85
+ return tinecoIconMap[iconName] || iconName
86
+ }
87
+
88
+ // 常见业务图标映射
89
+ const iconMap: Record<string, string> = {
90
+ dashboard: 'dashboard',
91
+ system: 'system',
92
+ user: 'user',
93
+ role: 'role',
94
+ menu: 'list',
95
+ setting: 'setting',
96
+ home: 'home',
97
+ chart: 'chart',
98
+ report: 'report',
99
+ analytics: 'analytics',
100
+ permission: 'permission',
101
+ log: 'log',
102
+ notification: 'notification',
103
+ app: 'app',
104
+ list: 'list',
105
+ grid: 'grid'
106
+ }
107
+
108
+ return iconMap[icon] || icon
109
+ }
110
+
111
+ // 获取菜单名称第一个字
112
+ const getFirstChar = (name?: string): string => {
113
+ if (!name) return ''
114
+ return name.charAt(0)
115
+ }
116
+
117
+ // 判断图标是否存在
118
+ const iconExists = (iconName: string): boolean => {
119
+ return knownIcons.has(iconName)
120
+ }
121
+ </script>
122
+
123
+ <template>
124
+ <div class="sidebar" :class="{ 'sidebar--collapsed': isCollapsed }">
125
+ <!-- Logo -->
126
+ <div class="sidebar__logo">
127
+ <img src="/vite.svg" alt="Logo" class="sidebar__logo-img" />
128
+ <span v-show="!isCollapsed" class="sidebar__logo-text">{{ appStore.appName }}</span>
129
+ </div>
130
+
131
+ <!-- 菜单 -->
132
+ <Menu
133
+ :default-active="activeMenu"
134
+ mode="vertical"
135
+ :collapse="isCollapsed"
136
+ :collapse-transition="false"
137
+ :background-color="menuBgColor"
138
+ :text-color="menuTextColor"
139
+ :active-text-color="menuActiveTextColor"
140
+ class="sidebar__menu"
141
+ @select="handleMenuSelect"
142
+ >
143
+ <template v-for="menu in menuStore.menuList" :key="menu.menuUrl">
144
+ <!-- 有子菜单 -->
145
+ <SubMenu v-if="menu.children && menu.children.length > 0" :index="menu.menuUrl">
146
+ <template #title>
147
+ <span class="sidebar__menu-icon">
148
+ <Icon v-if="iconExists(getMenuIcon(menu.icon))" :name="getMenuIcon(menu.icon)" :size="16" />
149
+ <span v-else class="sidebar__menu-char">{{ getFirstChar(menu.menuName) }}</span>
150
+ </span>
151
+ <span>{{ menu.menuName }}</span>
152
+ </template>
153
+ <MenuItem
154
+ v-for="child in menu.children"
155
+ :key="child.menuUrl"
156
+ :index="child.menuUrl"
157
+ >
158
+ <span class="sidebar__menu-icon">
159
+ <Icon v-if="iconExists(getMenuIcon(child.icon))" :name="getMenuIcon(child.icon)" :size="16" />
160
+ <span v-else class="sidebar__menu-char">{{ getFirstChar(child.menuName) }}</span>
161
+ </span>
162
+ <span>{{ child.menuName }}</span>
163
+ </MenuItem>
164
+ </SubMenu>
165
+ <!-- 无子菜单 -->
166
+ <MenuItem v-else :index="menu.menuUrl">
167
+ <span class="sidebar__menu-icon">
168
+ <Icon v-if="iconExists(getMenuIcon(menu.icon))" :name="getMenuIcon(menu.icon)" :size="16" />
169
+ <span v-else class="sidebar__menu-char">{{ getFirstChar(menu.menuName) }}</span>
170
+ </span>
171
+ <span>{{ menu.menuName }}</span>
172
+ </MenuItem>
173
+ </template>
174
+ </Menu>
175
+
176
+ <!-- 用户信息 -->
177
+ <div v-if="!isCollapsed" class="sidebar__user">
178
+ <div class="sidebar__user-info">
179
+ <span class="sidebar__user-name">{{ userStore.userName }}</span>
180
+ <span class="sidebar__user-role">{{ userStore.departmentName }}</span>
181
+ </div>
182
+ <Button type="text" size="small" @click="handleLogout">退出</Button>
183
+ </div>
184
+ </div>
185
+ </template>
186
+
187
+ <style lang="scss" scoped>
188
+ .sidebar {
189
+ height: 100%;
190
+ display: flex;
191
+ flex-direction: column;
192
+ background-color: var(--bg-color);
193
+ border-right: 1px solid var(--color-border-lighter);
194
+
195
+ &--collapsed {
196
+ .sidebar__logo {
197
+ justify-content: center;
198
+ padding: 0;
199
+ }
200
+ }
201
+
202
+ &__logo {
203
+ height: 50px;
204
+ display: flex;
205
+ align-items: center;
206
+ padding: 0 20px;
207
+ gap: 10px;
208
+ border-bottom: 1px solid var(--color-border-lighter);
209
+ }
210
+
211
+ &__logo-img {
212
+ width: 32px;
213
+ height: 32px;
214
+ }
215
+
216
+ &__logo-text {
217
+ font-size: 16px;
218
+ font-weight: 600;
219
+ color: var(--color-primary);
220
+ }
221
+
222
+ &__menu {
223
+ flex: 1;
224
+ border-right: none;
225
+ overflow-y: auto;
226
+ }
227
+
228
+ &__menu-icon {
229
+ display: inline-flex;
230
+ align-items: center;
231
+ justify-content: center;
232
+ width: 16px;
233
+ height: 16px;
234
+ margin-right: 8px;
235
+ }
236
+
237
+ &__menu-char {
238
+ display: inline-flex;
239
+ align-items: center;
240
+ justify-content: center;
241
+ width: 16px;
242
+ height: 16px;
243
+ font-size: 12px;
244
+ font-weight: 600;
245
+ color: var(--color-primary);
246
+ background-color: var(--color-primary-light-8);
247
+ border-radius: 4px;
248
+ }
249
+
250
+ &__user {
251
+ padding: 10px;
252
+ border-top: 1px solid var(--color-border-lighter);
253
+ display: flex;
254
+ align-items: center;
255
+ justify-content: space-between;
256
+ }
257
+
258
+ &__user-info {
259
+ display: flex;
260
+ flex-direction: column;
261
+ gap: 2px;
262
+ }
263
+
264
+ &__user-name {
265
+ font-size: 14px;
266
+ font-weight: 500;
267
+ }
268
+
269
+ &__user-role {
270
+ font-size: 12px;
271
+ color: var(--color-text-secondary);
272
+ }
273
+ }
213
274
  </style>