xto-fronted 0.3.4 → 0.3.6

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,5 +1,5 @@
1
1
  <script setup lang="ts">
2
- import { ref, computed } from 'vue'
2
+ import { computed } from 'vue'
3
3
  import { useRoute, useRouter } from 'vue-router'
4
4
  import { useMenuStore } from '@/stores/menu'
5
5
  import { useUserStore } from '@/stores/user'
@@ -7,19 +7,7 @@ import { useAuthStore } from '@/stores/auth'
7
7
  import { useAppStore } from '@/stores/app'
8
8
  import { Menu, MenuItem, SubMenu } from '@xto/navigation'
9
9
  import { Button } from '@xto/base'
10
- import { Input } from '@xto/form'
11
- import type { MenuItem as MenuItemType } from '@/types/api'
12
-
13
- const props = withDefaults(
14
- defineProps<{
15
- mode?: 'vertical' | 'horizontal'
16
- showLogo?: boolean
17
- }>(),
18
- {
19
- mode: 'vertical',
20
- showLogo: true
21
- }
22
- )
10
+ import { Icon } from '@xto/base'
23
11
 
24
12
  const route = useRoute()
25
13
  const router = useRouter()
@@ -28,7 +16,6 @@ const userStore = useUserStore()
28
16
  const authStore = useAuthStore()
29
17
  const appStore = useAppStore()
30
18
 
31
- const searchKeyword = ref('')
32
19
  const isCollapsed = computed(() => appStore.isCollapsed)
33
20
  const activeMenu = computed(() => route.path)
34
21
 
@@ -37,63 +24,13 @@ const menuBgColor = computed(() => appStore.isDark ? '#1d1e1f' : '#fff')
37
24
  const menuTextColor = computed(() => appStore.isDark ? '#cfd3dc' : '#303133')
38
25
  const menuActiveTextColor = computed(() => '#409eff')
39
26
 
40
- // 扁平化菜单用于搜索
41
- const flattenMenus = (menus: MenuItemType[], parentTitle = ''): (MenuItemType & { parentTitle: string })[] => {
42
- const result: (MenuItemType & { parentTitle: string })[] = []
43
- menus.forEach(menu => {
44
- if (menu.children && menu.children.length > 0) {
45
- result.push(...flattenMenus(menu.children, menu.title))
46
- } else {
47
- result.push({ ...menu, parentTitle })
48
- }
49
- })
50
- return result
51
- }
52
-
53
- // 搜索结果
54
- const searchResults = computed(() => {
55
- if (!searchKeyword.value.trim()) return []
56
- const flatMenus = flattenMenus(menuStore.menuList)
57
- return flatMenus.filter(menu =>
58
- menu.title.toLowerCase().includes(searchKeyword.value.toLowerCase())
59
- )
60
- })
61
-
62
- // 过滤后的菜单列表
63
- const filteredMenuList = computed(() => {
64
- if (!searchKeyword.value.trim()) return menuStore.menuList
65
-
66
- return menuStore.menuList.map(menu => {
67
- if (menu.children && menu.children.length > 0) {
68
- const filteredChildren = menu.children.filter(child =>
69
- child.title.toLowerCase().includes(searchKeyword.value.toLowerCase())
70
- )
71
- if (filteredChildren.length > 0) {
72
- return { ...menu, children: filteredChildren }
73
- }
74
- return null
75
- }
76
- if (menu.title.toLowerCase().includes(searchKeyword.value.toLowerCase())) {
77
- return menu
78
- }
79
- return null
80
- }).filter(Boolean) as MenuItemType[]
81
- })
82
-
83
27
  // 菜单选择
84
28
  const handleMenuSelect = (index: string) => {
85
29
  if (index && index !== route.path) {
86
30
  router.push(index)
87
- searchKeyword.value = ''
88
31
  }
89
32
  }
90
33
 
91
- // 搜索结果点击
92
- const handleSearchItemClick = (path: string) => {
93
- router.push(path)
94
- searchKeyword.value = ''
95
- }
96
-
97
34
  // 退出登录
98
35
  const handleLogout = () => {
99
36
  authStore.logout()
@@ -102,65 +39,33 @@ const handleLogout = () => {
102
39
  router.push('/login')
103
40
  }
104
41
 
105
- // 菜单图标
106
- const getMenuIcon = (icon?: string) => {
42
+ // 获取菜单图标名称
43
+ const getMenuIcon = (icon?: string): string => {
107
44
  const iconMap: Record<string, string> = {
108
- dashboard: '📊',
109
- system: '⚙️',
110
- user: '👤',
111
- role: '👥',
112
- menu: '📋',
113
- setting: '🔧'
114
- }
115
- return iconMap[icon || ''] || '📄'
45
+ dashboard: 'dashboard',
46
+ system: 'system',
47
+ user: 'user',
48
+ role: 'role',
49
+ menu: 'list',
50
+ setting: 'setting'
51
+ }
52
+ return iconMap[icon || ''] || 'file'
116
53
  }
117
-
118
- const isVertical = computed(() => props.mode === 'vertical')
119
- const isHorizontal = computed(() => props.mode === 'horizontal')
120
54
  </script>
121
55
 
122
56
  <template>
123
- <div class="sidebar" :class="[
124
- `sidebar--${mode}`,
125
- { 'sidebar--collapsed': isCollapsed && isVertical },
126
- { 'sidebar--horizontal': isHorizontal }
127
- ]">
128
- <!-- Logo(仅垂直模式显示) -->
129
- <div v-if="showLogo && isVertical" class="sidebar__logo">
57
+ <div class="sidebar" :class="{ 'sidebar--collapsed': isCollapsed }">
58
+ <!-- Logo -->
59
+ <div class="sidebar__logo">
130
60
  <img src="/vite.svg" alt="Logo" class="sidebar__logo-img" />
131
61
  <span v-show="!isCollapsed" class="sidebar__logo-text">{{ appStore.appName }}</span>
132
62
  </div>
133
63
 
134
- <!-- 搜索框(仅垂直模式显示) -->
135
- <div v-if="isVertical && !isCollapsed" class="sidebar__search">
136
- <Input
137
- v-model="searchKeyword"
138
- placeholder="搜索菜单..."
139
- size="small"
140
- clearable
141
- />
142
- <!-- 搜索结果 -->
143
- <div v-if="searchResults.length > 0" class="sidebar__search-results">
144
- <div
145
- v-for="item in searchResults"
146
- :key="item.path"
147
- class="sidebar__search-item"
148
- @click="handleSearchItemClick(item.path)"
149
- >
150
- <span class="menu-icon">{{ getMenuIcon(item.icon) }}</span>
151
- <div class="sidebar__search-item-info">
152
- <span class="sidebar__search-item-title">{{ item.title }}</span>
153
- <span v-if="item.parentTitle" class="sidebar__search-item-parent">{{ item.parentTitle }}</span>
154
- </div>
155
- </div>
156
- </div>
157
- </div>
158
-
159
64
  <!-- 菜单 -->
160
65
  <Menu
161
66
  :default-active="activeMenu"
162
- :mode="mode"
163
- :collapse="isCollapsed && isVertical"
67
+ mode="vertical"
68
+ :collapse="isCollapsed"
164
69
  :collapse-transition="false"
165
70
  :background-color="menuBgColor"
166
71
  :text-color="menuTextColor"
@@ -168,11 +73,11 @@ const isHorizontal = computed(() => props.mode === 'horizontal')
168
73
  class="sidebar__menu"
169
74
  @select="handleMenuSelect"
170
75
  >
171
- <template v-for="menu in filteredMenuList" :key="menu.path">
76
+ <template v-for="menu in menuStore.menuList" :key="menu.path">
172
77
  <!-- 有子菜单 -->
173
78
  <SubMenu v-if="menu.children && menu.children.length > 0" :index="menu.path">
174
79
  <template #title>
175
- <span class="menu-icon">{{ getMenuIcon(menu.icon) }}</span>
80
+ <Icon :name="getMenuIcon(menu.icon)" :size="16" />
176
81
  <span>{{ menu.title }}</span>
177
82
  </template>
178
83
  <MenuItem
@@ -180,20 +85,20 @@ const isHorizontal = computed(() => props.mode === 'horizontal')
180
85
  :key="child.path"
181
86
  :index="child.path"
182
87
  >
183
- <span class="menu-icon">{{ getMenuIcon(child.icon) }}</span>
88
+ <Icon :name="getMenuIcon(child.icon)" :size="16" />
184
89
  <span>{{ child.title }}</span>
185
90
  </MenuItem>
186
91
  </SubMenu>
187
92
  <!-- 无子菜单 -->
188
93
  <MenuItem v-else :index="menu.path">
189
- <span class="menu-icon">{{ getMenuIcon(menu.icon) }}</span>
94
+ <Icon :name="getMenuIcon(menu.icon)" :size="16" />
190
95
  <span>{{ menu.title }}</span>
191
96
  </MenuItem>
192
97
  </template>
193
98
  </Menu>
194
99
 
195
- <!-- 用户信息(仅垂直模式显示) -->
196
- <div v-if="isVertical && !isCollapsed" class="sidebar__user">
100
+ <!-- 用户信息 -->
101
+ <div v-if="!isCollapsed" class="sidebar__user">
197
102
  <div class="sidebar__user-info">
198
103
  <span class="sidebar__user-name">{{ userStore.nickname }}</span>
199
104
  <span class="sidebar__user-role">{{ userStore.roles?.join(', ') }}</span>
@@ -209,26 +114,9 @@ const isHorizontal = computed(() => props.mode === 'horizontal')
209
114
  display: flex;
210
115
  flex-direction: column;
211
116
  background-color: var(--bg-color);
117
+ border-right: 1px solid var(--color-border-lighter);
212
118
 
213
- // 垂直模式
214
- &--vertical {
215
- border-right: 1px solid var(--color-border-lighter);
216
- }
217
-
218
- // 水平模式
219
- &--horizontal {
220
- flex-direction: row;
221
- border: none;
222
- background-color: transparent;
223
-
224
- .sidebar__menu {
225
- border: none;
226
- background-color: transparent;
227
- }
228
- }
229
-
230
- // 折叠状态
231
- &--collapsed.sidebar--vertical {
119
+ &--collapsed {
232
120
  .sidebar__logo {
233
121
  justify-content: center;
234
122
  padding: 0;
@@ -255,55 +143,6 @@ const isHorizontal = computed(() => props.mode === 'horizontal')
255
143
  color: var(--color-primary);
256
144
  }
257
145
 
258
- &__search {
259
- padding: 10px;
260
- border-bottom: 1px solid var(--color-border-lighter);
261
- position: relative;
262
- }
263
-
264
- &__search-results {
265
- position: absolute;
266
- top: 100%;
267
- left: 0;
268
- right: 0;
269
- background-color: var(--bg-color);
270
- border: 1px solid var(--color-border-lighter);
271
- border-radius: var(--border-radius-base);
272
- box-shadow: var(--box-shadow);
273
- max-height: 300px;
274
- overflow-y: auto;
275
- z-index: 100;
276
- }
277
-
278
- &__search-item {
279
- display: flex;
280
- align-items: center;
281
- gap: 10px;
282
- padding: 10px 12px;
283
- cursor: pointer;
284
- transition: background-color 0.2s;
285
-
286
- &:hover {
287
- background-color: var(--color-fill);
288
- }
289
-
290
- &-info {
291
- display: flex;
292
- flex-direction: column;
293
- gap: 2px;
294
- }
295
-
296
- &-title {
297
- font-size: 14px;
298
- color: var(--color-text-primary);
299
- }
300
-
301
- &-parent {
302
- font-size: 12px;
303
- color: var(--color-text-secondary);
304
- }
305
- }
306
-
307
146
  &__menu {
308
147
  flex: 1;
309
148
  border-right: none;
@@ -334,8 +173,4 @@ const isHorizontal = computed(() => props.mode === 'horizontal')
334
173
  color: var(--color-text-secondary);
335
174
  }
336
175
  }
337
-
338
- .menu-icon {
339
- margin-right: 8px;
340
- }
341
176
  </style>
@@ -9,73 +9,21 @@ const appStore = useAppStore()
9
9
  const sidebarWidth = computed(() =>
10
10
  appStore.isCollapsed ? '64px' : '210px'
11
11
  )
12
-
13
- const layoutMode = computed(() => appStore.layout)
14
-
15
- const isSidebarMode = computed(() => layoutMode.value === 'sidebar')
16
- const isTopMode = computed(() => layoutMode.value === 'top')
17
- const isMixMode = computed(() => layoutMode.value === 'mix')
18
12
  </script>
19
13
 
20
14
  <template>
21
- <div class="layout" :class="`layout--${layoutMode}`">
22
- <!-- 顶部模式:顶部导航 + 内容 -->
23
- <template v-if="isTopMode">
24
- <header class="layout__header-top">
25
- <div class="layout__header-logo">
26
- <img src="/vite.svg" alt="Logo" class="layout__logo-img" />
27
- <span class="layout__logo-text">{{ appStore.appName }}</span>
28
- </div>
29
- <Sidebar class="layout__top-menu" mode="horizontal" />
30
- <div class="layout__header-right">
31
- <Header :show-breadcrumb="false" :show-collapse="false" />
32
- </div>
15
+ <div class="layout">
16
+ <aside class="layout__aside" :style="{ width: sidebarWidth }">
17
+ <Sidebar />
18
+ </aside>
19
+ <div class="layout__main">
20
+ <header class="layout__header">
21
+ <Header />
33
22
  </header>
34
23
  <main class="layout__content">
35
24
  <router-view />
36
25
  </main>
37
- </template>
38
-
39
- <!-- 混合模式:顶部导航 + 左侧菜单 -->
40
- <template v-else-if="isMixMode">
41
- <header class="layout__header-mix">
42
- <div class="layout__header-left">
43
- <div class="layout__collapse" @click="appStore.toggleCollapse">
44
- <svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor">
45
- <path v-if="appStore.isCollapsed" d="M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z"/>
46
- <path v-else d="M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z"/>
47
- </svg>
48
- </div>
49
- <Sidebar class="layout__top-menu" mode="horizontal" />
50
- </div>
51
- <div class="layout__header-right">
52
- <Header :show-breadcrumb="false" :show-collapse="false" />
53
- </div>
54
- </header>
55
- <div class="layout__body">
56
- <aside class="layout__aside" :style="{ width: sidebarWidth }">
57
- <Sidebar mode="vertical" :show-logo="false" />
58
- </aside>
59
- <main class="layout__content">
60
- <router-view />
61
- </main>
62
- </div>
63
- </template>
64
-
65
- <!-- 侧边栏模式:左侧菜单 + 顶部 -->
66
- <template v-else-if="isSidebarMode">
67
- <aside class="layout__aside" :style="{ width: sidebarWidth }">
68
- <Sidebar />
69
- </aside>
70
- <div class="layout__main">
71
- <header class="layout__header">
72
- <Header />
73
- </header>
74
- <main class="layout__content">
75
- <router-view />
76
- </main>
77
- </div>
78
- </template>
26
+ </div>
79
27
  </div>
80
28
  </template>
81
29
 
@@ -85,126 +33,6 @@ const isMixMode = computed(() => layoutMode.value === 'mix')
85
33
  width: 100%;
86
34
  height: 100%;
87
35
 
88
- // 侧边栏模式
89
- &--sidebar {
90
- flex-direction: row;
91
- }
92
-
93
- // 顶部模式
94
- &--top {
95
- flex-direction: column;
96
-
97
- .layout__header-top {
98
- height: 50px;
99
- background-color: var(--bg-color);
100
- border-bottom: 1px solid var(--color-border-lighter);
101
- display: flex;
102
- align-items: center;
103
- padding: 0 20px;
104
- gap: 20px;
105
- }
106
-
107
- .layout__header-logo {
108
- display: flex;
109
- align-items: center;
110
- gap: 10px;
111
- }
112
-
113
- .layout__logo-img {
114
- width: 32px;
115
- height: 32px;
116
- }
117
-
118
- .layout__logo-text {
119
- font-size: 16px;
120
- font-weight: 600;
121
- color: var(--color-primary);
122
- }
123
-
124
- .layout__top-menu {
125
- flex: 1;
126
- border: none;
127
- }
128
-
129
- .layout__header-right {
130
- display: flex;
131
- align-items: center;
132
- }
133
-
134
- .layout__content {
135
- flex: 1;
136
- overflow: auto;
137
- background-color: var(--bg-color-page);
138
- }
139
- }
140
-
141
- // 混合模式
142
- &--mix {
143
- flex-direction: column;
144
-
145
- .layout__header-mix {
146
- height: 50px;
147
- background-color: var(--bg-color);
148
- border-bottom: 1px solid var(--color-border-lighter);
149
- display: flex;
150
- align-items: center;
151
- justify-content: space-between;
152
- padding: 0 20px;
153
- }
154
-
155
- .layout__header-left {
156
- display: flex;
157
- align-items: center;
158
- gap: 15px;
159
- }
160
-
161
- .layout__collapse {
162
- width: 24px;
163
- height: 24px;
164
- display: flex;
165
- align-items: center;
166
- justify-content: center;
167
- cursor: pointer;
168
- color: var(--color-text-regular);
169
- transition: color 0.2s;
170
-
171
- &:hover {
172
- color: var(--color-primary);
173
- }
174
- }
175
-
176
- .layout__top-menu {
177
- border: none;
178
- }
179
-
180
- .layout__header-right {
181
- display: flex;
182
- align-items: center;
183
- }
184
-
185
- .layout__body {
186
- flex: 1;
187
- display: flex;
188
- overflow: hidden;
189
- }
190
-
191
- .layout__aside {
192
- transition: width 0.3s;
193
- overflow: hidden;
194
- flex-shrink: 0;
195
- height: 100%;
196
- background-color: var(--bg-color);
197
- border-right: 1px solid var(--color-border-lighter);
198
- }
199
-
200
- .layout__content {
201
- flex: 1;
202
- overflow: auto;
203
- background-color: var(--bg-color-page);
204
- }
205
- }
206
-
207
- // 通用样式
208
36
  &__aside {
209
37
  transition: width 0.3s;
210
38
  overflow: hidden;