xto-fronted 0.3.5 → 0.3.7

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,9 +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
10
  import { Icon } from '@xto/base'
12
- import type { MenuItem as MenuItemType } from '@/types/api'
13
11
 
14
12
  const route = useRoute()
15
13
  const router = useRouter()
@@ -18,7 +16,6 @@ const userStore = useUserStore()
18
16
  const authStore = useAuthStore()
19
17
  const appStore = useAppStore()
20
18
 
21
- const searchKeyword = ref('')
22
19
  const isCollapsed = computed(() => appStore.isCollapsed)
23
20
  const activeMenu = computed(() => route.path)
24
21
 
@@ -27,63 +24,13 @@ const menuBgColor = computed(() => appStore.isDark ? '#1d1e1f' : '#fff')
27
24
  const menuTextColor = computed(() => appStore.isDark ? '#cfd3dc' : '#303133')
28
25
  const menuActiveTextColor = computed(() => '#409eff')
29
26
 
30
- // 扁平化菜单用于搜索
31
- const flattenMenus = (menus: MenuItemType[], parentTitle = ''): (MenuItemType & { parentTitle: string })[] => {
32
- const result: (MenuItemType & { parentTitle: string })[] = []
33
- menus.forEach(menu => {
34
- if (menu.children && menu.children.length > 0) {
35
- result.push(...flattenMenus(menu.children, menu.title))
36
- } else {
37
- result.push({ ...menu, parentTitle })
38
- }
39
- })
40
- return result
41
- }
42
-
43
- // 搜索结果
44
- const searchResults = computed(() => {
45
- if (!searchKeyword.value.trim()) return []
46
- const flatMenus = flattenMenus(menuStore.menuList)
47
- return flatMenus.filter(menu =>
48
- menu.title.toLowerCase().includes(searchKeyword.value.toLowerCase())
49
- )
50
- })
51
-
52
- // 过滤后的菜单列表
53
- const filteredMenuList = computed(() => {
54
- if (!searchKeyword.value.trim()) return menuStore.menuList
55
-
56
- return menuStore.menuList.map(menu => {
57
- if (menu.children && menu.children.length > 0) {
58
- const filteredChildren = menu.children.filter(child =>
59
- child.title.toLowerCase().includes(searchKeyword.value.toLowerCase())
60
- )
61
- if (filteredChildren.length > 0) {
62
- return { ...menu, children: filteredChildren }
63
- }
64
- return null
65
- }
66
- if (menu.title.toLowerCase().includes(searchKeyword.value.toLowerCase())) {
67
- return menu
68
- }
69
- return null
70
- }).filter(Boolean) as MenuItemType[]
71
- })
72
-
73
27
  // 菜单选择
74
28
  const handleMenuSelect = (index: string) => {
75
29
  if (index && index !== route.path) {
76
30
  router.push(index)
77
- searchKeyword.value = ''
78
31
  }
79
32
  }
80
33
 
81
- // 搜索结果点击
82
- const handleSearchItemClick = (path: string) => {
83
- router.push(path)
84
- searchKeyword.value = ''
85
- }
86
-
87
34
  // 退出登录
88
35
  const handleLogout = () => {
89
36
  authStore.logout()
@@ -114,31 +61,6 @@ const getMenuIcon = (icon?: string): string => {
114
61
  <span v-show="!isCollapsed" class="sidebar__logo-text">{{ appStore.appName }}</span>
115
62
  </div>
116
63
 
117
- <!-- 搜索框 -->
118
- <div v-if="!isCollapsed" class="sidebar__search">
119
- <Input
120
- v-model="searchKeyword"
121
- placeholder="搜索菜单..."
122
- size="small"
123
- clearable
124
- />
125
- <!-- 搜索结果 -->
126
- <div v-if="searchResults.length > 0" class="sidebar__search-results">
127
- <div
128
- v-for="item in searchResults"
129
- :key="item.path"
130
- class="sidebar__search-item"
131
- @click="handleSearchItemClick(item.path)"
132
- >
133
- <Icon :name="getMenuIcon(item.icon)" :size="16" />
134
- <div class="sidebar__search-item-info">
135
- <span class="sidebar__search-item-title">{{ item.title }}</span>
136
- <span v-if="item.parentTitle" class="sidebar__search-item-parent">{{ item.parentTitle }}</span>
137
- </div>
138
- </div>
139
- </div>
140
- </div>
141
-
142
64
  <!-- 菜单 -->
143
65
  <Menu
144
66
  :default-active="activeMenu"
@@ -151,7 +73,7 @@ const getMenuIcon = (icon?: string): string => {
151
73
  class="sidebar__menu"
152
74
  @select="handleMenuSelect"
153
75
  >
154
- <template v-for="menu in filteredMenuList" :key="menu.path">
76
+ <template v-for="menu in menuStore.menuList" :key="menu.path">
155
77
  <!-- 有子菜单 -->
156
78
  <SubMenu v-if="menu.children && menu.children.length > 0" :index="menu.path">
157
79
  <template #title>
@@ -221,55 +143,6 @@ const getMenuIcon = (icon?: string): string => {
221
143
  color: var(--color-primary);
222
144
  }
223
145
 
224
- &__search {
225
- padding: 10px;
226
- border-bottom: 1px solid var(--color-border-lighter);
227
- position: relative;
228
- }
229
-
230
- &__search-results {
231
- position: absolute;
232
- top: 100%;
233
- left: 0;
234
- right: 0;
235
- background-color: var(--bg-color);
236
- border: 1px solid var(--color-border-lighter);
237
- border-radius: var(--border-radius-base);
238
- box-shadow: var(--box-shadow);
239
- max-height: 300px;
240
- overflow-y: auto;
241
- z-index: 100;
242
- }
243
-
244
- &__search-item {
245
- display: flex;
246
- align-items: center;
247
- gap: 10px;
248
- padding: 10px 12px;
249
- cursor: pointer;
250
- transition: background-color 0.2s;
251
-
252
- &:hover {
253
- background-color: var(--color-fill);
254
- }
255
-
256
- &-info {
257
- display: flex;
258
- flex-direction: column;
259
- gap: 2px;
260
- }
261
-
262
- &-title {
263
- font-size: 14px;
264
- color: var(--color-text-primary);
265
- }
266
-
267
- &-parent {
268
- font-size: 12px;
269
- color: var(--color-text-secondary);
270
- }
271
- }
272
-
273
146
  &__menu {
274
147
  flex: 1;
275
148
  border-right: none;
package/src/index.ts CHANGED
@@ -33,6 +33,7 @@ export { default as router, resetRouter } from './router'
33
33
  export * from './router/staticRoutes'
34
34
  export * from './router/dynamicRoutes'
35
35
  export { createLayoutRoute, createRouter } from './router/layoutRoute'
36
+ export { setupRouterGuards } from './router/guards'
36
37
 
37
38
  // API
38
39
  export * from './api/auth'
@@ -0,0 +1,133 @@
1
+ /**
2
+ * 路由守卫设置
3
+ */
4
+
5
+ import type { Router } from 'vue-router'
6
+ import { getToken } from '@/utils/auth'
7
+ import { useUserStore } from '@/stores/user'
8
+ import { useMenuStore } from '@/stores/menu'
9
+ import { useAppStore } from '@/stores/app'
10
+ import { getUserInfo } from '@/api/auth'
11
+ import { getMenuTree } from '@/api/system'
12
+
13
+ // 白名单路由
14
+ const defaultWhiteList = ['/login', '/404', '/403']
15
+
16
+ interface RouterGuardOptions {
17
+ // 白名单路由
18
+ whiteList?: string[]
19
+ // 登录页路径
20
+ loginPath?: string
21
+ // 首页路径
22
+ homePath?: string
23
+ // 获取用户信息的回调(可选,默认调用 API)
24
+ fetchUserInfo?: () => Promise<any>
25
+ // 获取菜单的回调(可选,默认调用 API)
26
+ fetchMenu?: () => Promise<any>
27
+ // 登录成功后的回调
28
+ onLoginSuccess?: () => void
29
+ }
30
+
31
+ /**
32
+ * 设置路由守卫
33
+ * @param router 路由实例
34
+ * @param options 配置选项
35
+ */
36
+ export function setupRouterGuards(router: Router, options: RouterGuardOptions = {}) {
37
+ const whiteList = options.whiteList || defaultWhiteList
38
+ const loginPath = options.loginPath || '/login'
39
+ const homePath = options.homePath || '/'
40
+
41
+ router.beforeEach(async (to, _from, next) => {
42
+ // 启动进度条(可选)
43
+ // NProgress.start()
44
+
45
+ const appStore = useAppStore()
46
+ const userStore = useUserStore()
47
+ const menuStore = useMenuStore()
48
+
49
+ // 初始化主题
50
+ appStore.initTheme()
51
+
52
+ // 检查是否有 token
53
+ const token = getToken()
54
+
55
+ if (token) {
56
+ // 已登录
57
+ if (to.path === loginPath) {
58
+ // 已登录访问登录页,跳转到首页
59
+ next({ path: homePath })
60
+ // NProgress.done()
61
+ } else {
62
+ // 检查是否已获取用户信息
63
+ if (userStore.isLoggedIn) {
64
+ // 已有用户信息,直接放行
65
+ // 添加缓存页面
66
+ if (to.name && to.meta.keepAlive) {
67
+ appStore.addCachedView(to.name as string)
68
+ }
69
+ next()
70
+ } else {
71
+ // 尝试获取用户信息
72
+ try {
73
+ // 获取用户信息
74
+ if (options.fetchUserInfo) {
75
+ const userInfo = await options.fetchUserInfo()
76
+ userStore.setUserInfo(userInfo)
77
+ } else {
78
+ const userInfo = await getUserInfo()
79
+ userStore.setUserInfo(userInfo)
80
+ }
81
+
82
+ // 获取菜单
83
+ if (options.fetchMenu) {
84
+ const menuList = await options.fetchMenu()
85
+ menuStore.setMenuList(menuList)
86
+ } else {
87
+ const menuList = await getMenuTree()
88
+ menuStore.setMenuList(menuList)
89
+ }
90
+
91
+ // 登录成功回调
92
+ if (options.onLoginSuccess) {
93
+ options.onLoginSuccess()
94
+ }
95
+
96
+ // 添加缓存页面
97
+ if (to.name && to.meta.keepAlive) {
98
+ appStore.addCachedView(to.name as string)
99
+ }
100
+
101
+ // 重新导航到目标路由,确保动态路由已添加
102
+ next({ ...to, replace: true })
103
+ } catch (error) {
104
+ // 获取用户信息失败,清除 token 并跳转到登录页
105
+ console.error('获取用户信息失败:', error)
106
+ userStore.clearUserInfo()
107
+ menuStore.clearMenu()
108
+ // 清除 token
109
+ localStorage.removeItem('token')
110
+ localStorage.removeItem('refreshToken')
111
+ next({ path: loginPath, query: { redirect: to.fullPath } })
112
+ // NProgress.done()
113
+ }
114
+ }
115
+ }
116
+ } else {
117
+ // 未登录
118
+ if (whiteList.includes(to.path)) {
119
+ // 在白名单中,直接放行
120
+ next()
121
+ } else {
122
+ // 不在白名单中,跳转到登录页
123
+ next({ path: loginPath, query: { redirect: to.fullPath } })
124
+ // NProgress.done()
125
+ }
126
+ }
127
+ })
128
+
129
+ router.afterEach(() => {
130
+ // 结束进度条
131
+ // NProgress.done()
132
+ })
133
+ }