xto-fronted 0.4.15 → 0.4.17

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.
@@ -1,253 +0,0 @@
1
- <script setup lang="ts">
2
- import { computed, onMounted } from 'vue'
3
- import { useRoute, useRouter } from 'vue-router'
4
- import { useMenuStore } from '@/stores/menu'
5
- import { useAppStore } from '@/stores/app'
6
- import { Icon } from '@xto/base'
7
- import type { MenuItem } from '@/types/api'
8
-
9
- const route = useRoute()
10
- const router = useRouter()
11
- const menuStore = useMenuStore()
12
- const appStore = useAppStore()
13
-
14
- // 当前选中的一级菜单路径
15
- const activeTopMenu = computed(() => appStore.activeTopMenuPath)
16
-
17
- // 一级菜单列表(只显示顶层菜单)
18
- const topMenuList = computed(() => menuStore.menuList)
19
-
20
- // 已知的图标名称列表
21
- const knownIcons = new Set([
22
- 'arrow-up', 'arrow-down', 'arrow-left', 'arrow-right',
23
- 'caret-down', 'caret-right', 'plus', 'minus', 'close', 'check',
24
- 'edit', 'delete', 'copy', 'download', 'upload', 'refresh', 'search',
25
- 'filter', 'more', 'setting', 'share', 'loading', 'info', 'success',
26
- 'warning', 'error', 'question', 'user', 'user-add', 'user-group',
27
- 'logout', 'login', 'file', 'folder', 'folder-open', 'document',
28
- 'image', 'video', 'music', 'camera', 'mail', 'phone', 'chat',
29
- 'bell', 'message', 'eye', 'eye-off', 'calendar', 'clock', 'history',
30
- 'timer', 'location', 'map', 'globe', 'star', 'heart', 'thumb-up',
31
- 'link', 'external-link', 'lock', 'unlock', 'key', 'home', 'menu',
32
- 'menu-fold', 'menu-unfold', 'sidebar-fold', 'sidebar-expand',
33
- 'sidebar-left', 'dashboard', 'chart', 'chart-pie', 'chart-line',
34
- 'report', 'analytics', 'system', 'permission', 'role', 'user-manage',
35
- 'log', 'notification', 'app', 'list', 'grid', 'fullscreen',
36
- 'fullscreen-exit', 'zoom-in', 'zoom-out', 'print', 'bookmark',
37
- 'tag', 'code', 'terminal', 'database', 'server', 'cloud', 'gift',
38
- 'moon', 'sun', 'theme', 'skin'
39
- ])
40
-
41
- // 获取菜单图标名称
42
- const getMenuIcon = (icon?: string): string => {
43
- if (!icon || icon === '') return ''
44
-
45
- if (icon.startsWith('tineco-icon-')) {
46
- const iconName = icon.replace('tineco-icon-', '')
47
- const tinecoIconMap: Record<string, string> = {
48
- home: 'home',
49
- dashboard: 'dashboard',
50
- system: 'system',
51
- user: 'user',
52
- role: 'role',
53
- menu: 'list',
54
- setting: 'setting',
55
- file: 'file',
56
- folder: 'folder',
57
- chart: 'chart',
58
- report: 'report',
59
- analytics: 'analytics'
60
- }
61
- return tinecoIconMap[iconName] || iconName
62
- }
63
-
64
- const iconMap: Record<string, string> = {
65
- dashboard: 'dashboard',
66
- system: 'system',
67
- user: 'user',
68
- role: 'role',
69
- menu: 'list',
70
- setting: 'setting',
71
- home: 'home',
72
- chart: 'chart',
73
- report: 'report',
74
- analytics: 'analytics',
75
- permission: 'permission',
76
- log: 'log',
77
- notification: 'notification',
78
- app: 'app',
79
- list: 'list',
80
- grid: 'grid'
81
- }
82
-
83
- return iconMap[icon] || icon
84
- }
85
-
86
- const getFirstChar = (name?: string): string => {
87
- if (!name) return ''
88
- return name.charAt(0)
89
- }
90
-
91
- const iconExists = (iconName: string): boolean => {
92
- return knownIcons.has(iconName)
93
- }
94
-
95
- // 点击一级菜单
96
- const handleTopMenuClick = (menu: MenuItem) => {
97
- appStore.setActiveTopMenuPath(menu.menuUrl)
98
-
99
- // 如果一级菜单有直接跳转路径,则跳转
100
- if (menu.menuUrl) {
101
- // 如果有子菜单,跳转到第一个子菜单或默认子菜单
102
- if (menu.children && menu.children.length > 0) {
103
- // 找到默认子菜单或第一个子菜单
104
- const defaultChild = menu.children.find(c => c.isDefault) || menu.children[0]
105
- if (defaultChild && defaultChild.menuUrl !== route.path) {
106
- router.push(defaultChild.menuUrl)
107
- }
108
- } else if (menu.menuUrl !== route.path) {
109
- router.push(menu.menuUrl)
110
- }
111
- }
112
- }
113
-
114
- // 初始化:根据当前路由设置一级菜单选中状态
115
- onMounted(() => {
116
- if (!appStore.activeTopMenuPath && topMenuList.value.length > 0) {
117
- // 找到当前路由所属的一级菜单
118
- for (const menu of topMenuList.value) {
119
- if (menu.menuUrl === route.path) {
120
- appStore.setActiveTopMenuPath(menu.menuUrl)
121
- break
122
- }
123
- if (menu.children && menu.children.length > 0) {
124
- const found = menu.children.some(child =>
125
- child.menuUrl === route.path ||
126
- (child.children && child.children.some(c => c.menuUrl === route.path))
127
- )
128
- if (found) {
129
- appStore.setActiveTopMenuPath(menu.menuUrl)
130
- break
131
- }
132
- }
133
- }
134
- // 如果没找到,默认选中第一个
135
- if (!appStore.activeTopMenuPath) {
136
- appStore.setActiveTopMenuPath(topMenuList.value[0].menuUrl)
137
- }
138
- }
139
- })
140
- </script>
141
-
142
- <template>
143
- <div class="mix-top-menu">
144
- <div class="mix-top-menu__logo">
145
- <span class="mix-top-menu__logo-text">{{ appStore.appName }}</span>
146
- </div>
147
- <div class="mix-top-menu__items">
148
- <div
149
- v-for="menu in topMenuList"
150
- :key="menu.menuUrl"
151
- :class="['mix-top-menu__item', { 'is-active': activeTopMenu === menu.menuUrl }]"
152
- @click="handleTopMenuClick(menu)"
153
- >
154
- <span class="mix-top-menu__item-icon">
155
- <Icon v-if="iconExists(getMenuIcon(menu.icon))" :name="getMenuIcon(menu.icon)" :size="16" />
156
- <span v-else class="mix-top-menu__item-char">{{ getFirstChar(menu.menuName) }}</span>
157
- </span>
158
- <span class="mix-top-menu__item-text">{{ menu.menuName }}</span>
159
- </div>
160
- </div>
161
- <div class="mix-top-menu__actions">
162
- <!-- 这里可以放一些操作按钮 -->
163
- </div>
164
- </div>
165
- </template>
166
-
167
- <style lang="scss" scoped>
168
- .mix-top-menu {
169
- width: 100%;
170
- height: 50px;
171
- display: flex;
172
- align-items: center;
173
- background-color: var(--bg-color);
174
- border-bottom: 1px solid var(--color-border-lighter);
175
- padding: 0 20px;
176
-
177
- &__logo {
178
- display: flex;
179
- align-items: center;
180
- padding-right: 20px;
181
- border-right: 1px solid var(--color-border-lighter);
182
- margin-right: 20px;
183
- }
184
-
185
- &__logo-text {
186
- font-size: 16px;
187
- font-weight: 600;
188
- color: var(--color-primary);
189
- }
190
-
191
- &__items {
192
- flex: 1;
193
- display: flex;
194
- align-items: center;
195
- gap: 4px;
196
- }
197
-
198
- &__item {
199
- display: flex;
200
- align-items: center;
201
- padding: 8px 16px;
202
- cursor: pointer;
203
- border-radius: 4px;
204
- transition: all 0.2s;
205
-
206
- &:hover {
207
- background-color: var(--color-fill);
208
- }
209
-
210
- &.is-active {
211
- background-color: var(--color-primary-light-9);
212
- color: var(--color-primary);
213
- }
214
- }
215
-
216
- &__item-icon {
217
- display: inline-flex;
218
- align-items: center;
219
- justify-content: center;
220
- width: 16px;
221
- height: 16px;
222
- margin-right: 8px;
223
- }
224
-
225
- &__item-char {
226
- display: inline-flex;
227
- align-items: center;
228
- justify-content: center;
229
- width: 16px;
230
- height: 16px;
231
- font-size: 12px;
232
- font-weight: 600;
233
- color: var(--color-primary);
234
- background-color: var(--color-primary-light-8);
235
- border-radius: 4px;
236
- }
237
-
238
- &__item-text {
239
- font-size: 14px;
240
- color: var(--color-text-regular);
241
- }
242
-
243
- &.is-active .mix-top-menu__item-text {
244
- color: var(--color-primary);
245
- }
246
-
247
- &__actions {
248
- display: flex;
249
- align-items: center;
250
- gap: 8px;
251
- }
252
- }
253
- </style>