xto-fronted 0.1.0 → 0.1.1
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.
- package/.env.development +4 -0
- package/.env.production +4 -0
- package/index.html +13 -0
- package/package.json +18 -10
- package/public/vite.svg +10 -0
- package/src/App.vue +20 -0
- package/src/api/auth.ts +26 -0
- package/src/api/system.ts +65 -0
- package/src/api/user.ts +46 -0
- package/src/assets/styles/_dark.scss +407 -0
- package/src/assets/styles/_reset.scss +126 -0
- package/src/assets/styles/_root.scss +140 -0
- package/src/assets/styles/_transition.scss +119 -0
- package/src/assets/styles/_variables.scss +45 -0
- package/src/assets/styles/index.scss +187 -0
- package/src/components/Layout/Footer.vue +17 -0
- package/src/components/Layout/Header.vue +390 -0
- package/src/components/Layout/Sidebar.vue +297 -0
- package/src/components/Layout/Tabs.vue +134 -0
- package/src/components/Layout/index.vue +62 -0
- package/src/composables/useAuth.ts +45 -0
- package/src/composables/useForm.ts +79 -0
- package/src/composables/useTable.ts +97 -0
- package/src/directives/permission.ts +38 -0
- package/src/enums/index.ts +63 -0
- package/src/env.d.ts +17 -0
- package/src/index.ts +39 -0
- package/src/main.ts +34 -0
- package/src/router/dynamicRoutes.ts +163 -0
- package/src/router/index.ts +81 -0
- package/src/router/staticRoutes.ts +43 -0
- package/src/stores/app.ts +145 -0
- package/src/stores/auth.ts +32 -0
- package/src/stores/index.ts +15 -0
- package/src/stores/menu.ts +80 -0
- package/src/stores/user.ts +73 -0
- package/src/types/api.d.ts +84 -0
- package/src/types/global.d.ts +45 -0
- package/src/types/router.d.ts +48 -0
- package/src/types/xto.d.ts +149 -0
- package/src/utils/auth.ts +62 -0
- package/src/utils/permission.ts +42 -0
- package/src/utils/request.ts +126 -0
- package/src/utils/storage.ts +63 -0
- package/src/views/dashboard/index.vue +284 -0
- package/src/views/error/403.vue +57 -0
- package/src/views/error/404.vue +57 -0
- package/src/views/login/index.vue +248 -0
- package/src/views/system/menu/index.vue +381 -0
- package/src/views/system/role/index.vue +304 -0
- package/src/views/system/user/index.vue +327 -0
- package/tsconfig.json +26 -0
- package/tsconfig.node.json +11 -0
- package/vite.config.ts +139 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 动态路由模板
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { RouteRecordRaw } from 'vue-router'
|
|
6
|
+
|
|
7
|
+
// 布局路由(包含默认子路由)
|
|
8
|
+
export const layoutRoute: RouteRecordRaw = {
|
|
9
|
+
path: '/',
|
|
10
|
+
name: 'Layout',
|
|
11
|
+
component: () => import('@/components/Layout/index.vue'),
|
|
12
|
+
redirect: '/dashboard',
|
|
13
|
+
children: [
|
|
14
|
+
{
|
|
15
|
+
path: '/dashboard',
|
|
16
|
+
name: 'Dashboard',
|
|
17
|
+
component: () => import('@/views/dashboard/index.vue'),
|
|
18
|
+
meta: {
|
|
19
|
+
title: '仪表盘',
|
|
20
|
+
icon: 'dashboard',
|
|
21
|
+
keepAlive: true,
|
|
22
|
+
affix: true
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
path: '/system/user',
|
|
27
|
+
name: 'SystemUser',
|
|
28
|
+
component: () => import('@/views/system/user/index.vue'),
|
|
29
|
+
meta: {
|
|
30
|
+
title: '用户管理',
|
|
31
|
+
icon: 'user',
|
|
32
|
+
keepAlive: true
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
path: '/system/role',
|
|
37
|
+
name: 'SystemRole',
|
|
38
|
+
component: () => import('@/views/system/role/index.vue'),
|
|
39
|
+
meta: {
|
|
40
|
+
title: '角色管理',
|
|
41
|
+
icon: 'role',
|
|
42
|
+
keepAlive: true
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
path: '/system/menu',
|
|
47
|
+
name: 'SystemMenu',
|
|
48
|
+
component: () => import('@/views/system/menu/index.vue'),
|
|
49
|
+
meta: {
|
|
50
|
+
title: '菜单管理',
|
|
51
|
+
icon: 'menu',
|
|
52
|
+
keepAlive: true
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
]
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// 默认路由(用于开发,后续由后端返回)
|
|
59
|
+
export const defaultDynamicRoutes: RouteRecordRaw[] = [
|
|
60
|
+
{
|
|
61
|
+
path: '/dashboard',
|
|
62
|
+
name: 'Dashboard',
|
|
63
|
+
component: () => import('@/views/dashboard/index.vue'),
|
|
64
|
+
meta: {
|
|
65
|
+
title: '仪表盘',
|
|
66
|
+
icon: 'dashboard',
|
|
67
|
+
keepAlive: true,
|
|
68
|
+
affix: true
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
path: '/system',
|
|
73
|
+
name: 'System',
|
|
74
|
+
redirect: '/system/user',
|
|
75
|
+
meta: {
|
|
76
|
+
title: '系统管理',
|
|
77
|
+
icon: 'setting'
|
|
78
|
+
},
|
|
79
|
+
children: [
|
|
80
|
+
{
|
|
81
|
+
path: 'user',
|
|
82
|
+
name: 'SystemUser',
|
|
83
|
+
component: () => import('@/views/system/user/index.vue'),
|
|
84
|
+
meta: {
|
|
85
|
+
title: '用户管理',
|
|
86
|
+
icon: 'user',
|
|
87
|
+
keepAlive: true
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
path: 'role',
|
|
92
|
+
name: 'SystemRole',
|
|
93
|
+
component: () => import('@/views/system/role/index.vue'),
|
|
94
|
+
meta: {
|
|
95
|
+
title: '角色管理',
|
|
96
|
+
icon: 'role',
|
|
97
|
+
keepAlive: true
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
path: 'menu',
|
|
102
|
+
name: 'SystemMenu',
|
|
103
|
+
component: () => import('@/views/system/menu/index.vue'),
|
|
104
|
+
meta: {
|
|
105
|
+
title: '菜单管理',
|
|
106
|
+
icon: 'menu',
|
|
107
|
+
keepAlive: true
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
]
|
|
111
|
+
}
|
|
112
|
+
]
|
|
113
|
+
|
|
114
|
+
// Mock 菜单数据
|
|
115
|
+
export const mockMenuData = [
|
|
116
|
+
{
|
|
117
|
+
id: 1,
|
|
118
|
+
name: 'Dashboard',
|
|
119
|
+
path: '/dashboard',
|
|
120
|
+
component: 'dashboard/index',
|
|
121
|
+
icon: 'dashboard',
|
|
122
|
+
title: '仪表盘',
|
|
123
|
+
keepAlive: true,
|
|
124
|
+
affix: true
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
id: 2,
|
|
128
|
+
name: 'System',
|
|
129
|
+
path: '/system',
|
|
130
|
+
redirect: '/system/user',
|
|
131
|
+
icon: 'setting',
|
|
132
|
+
title: '系统管理',
|
|
133
|
+
children: [
|
|
134
|
+
{
|
|
135
|
+
id: 21,
|
|
136
|
+
name: 'SystemUser',
|
|
137
|
+
path: '/system/user',
|
|
138
|
+
component: 'system/user/index',
|
|
139
|
+
icon: 'user',
|
|
140
|
+
title: '用户管理',
|
|
141
|
+
keepAlive: true
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
id: 22,
|
|
145
|
+
name: 'SystemRole',
|
|
146
|
+
path: '/system/role',
|
|
147
|
+
component: 'system/role/index',
|
|
148
|
+
icon: 'role',
|
|
149
|
+
title: '角色管理',
|
|
150
|
+
keepAlive: true
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
id: 23,
|
|
154
|
+
name: 'SystemMenu',
|
|
155
|
+
path: '/system/menu',
|
|
156
|
+
component: 'system/menu/index',
|
|
157
|
+
icon: 'menu',
|
|
158
|
+
title: '菜单管理',
|
|
159
|
+
keepAlive: true
|
|
160
|
+
}
|
|
161
|
+
]
|
|
162
|
+
}
|
|
163
|
+
]
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 路由配置
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { createRouter, createWebHistory } from 'vue-router'
|
|
6
|
+
import { staticRoutes, errorRoute } from './staticRoutes'
|
|
7
|
+
import { layoutRoute } from './dynamicRoutes'
|
|
8
|
+
import { hasToken } from '@/utils/auth'
|
|
9
|
+
import { useUserStore } from '@/stores/user'
|
|
10
|
+
import { useMenuStore } from '@/stores/menu'
|
|
11
|
+
import { useAppStore } from '@/stores/app'
|
|
12
|
+
import { mockMenuData } from './dynamicRoutes'
|
|
13
|
+
|
|
14
|
+
const router = createRouter({
|
|
15
|
+
history: createWebHistory(),
|
|
16
|
+
routes: [...staticRoutes, layoutRoute, errorRoute],
|
|
17
|
+
scrollBehavior: () => ({ left: 0, top: 0 })
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
// 白名单路由
|
|
21
|
+
const whiteList = ['/login', '/404', '/403']
|
|
22
|
+
|
|
23
|
+
// 路由守卫
|
|
24
|
+
router.beforeEach(async (to, _from, next) => {
|
|
25
|
+
const appStore = useAppStore()
|
|
26
|
+
|
|
27
|
+
// 初始化主题
|
|
28
|
+
appStore.initTheme()
|
|
29
|
+
|
|
30
|
+
// 已登录
|
|
31
|
+
if (hasToken()) {
|
|
32
|
+
if (to.path === '/login') {
|
|
33
|
+
// 已登录,跳转到首页
|
|
34
|
+
next({ path: '/' })
|
|
35
|
+
} else {
|
|
36
|
+
// 设置用户信息(如果未设置)
|
|
37
|
+
const userStore = useUserStore()
|
|
38
|
+
if (!userStore.isLoggedIn) {
|
|
39
|
+
userStore.setUserInfo({
|
|
40
|
+
id: 1,
|
|
41
|
+
username: 'admin',
|
|
42
|
+
nickname: '管理员',
|
|
43
|
+
avatar: '',
|
|
44
|
+
email: 'admin@example.com',
|
|
45
|
+
phone: '13800138000',
|
|
46
|
+
status: 1,
|
|
47
|
+
roles: ['admin'],
|
|
48
|
+
permissions: ['*'],
|
|
49
|
+
createTime: new Date().toISOString()
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
// 设置菜单
|
|
53
|
+
const menuStore = useMenuStore()
|
|
54
|
+
menuStore.setMenuList(mockMenuData)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// 添加缓存页面
|
|
58
|
+
if (to.name && to.meta.keepAlive) {
|
|
59
|
+
appStore.addCachedView(to.name as string)
|
|
60
|
+
}
|
|
61
|
+
next()
|
|
62
|
+
}
|
|
63
|
+
} else {
|
|
64
|
+
// 未登录
|
|
65
|
+
if (whiteList.includes(to.path)) {
|
|
66
|
+
next()
|
|
67
|
+
} else {
|
|
68
|
+
next('/login')
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
export function resetRouter() {
|
|
74
|
+
const newRouter = createRouter({
|
|
75
|
+
history: createWebHistory(),
|
|
76
|
+
routes: [...staticRoutes, layoutRoute, errorRoute]
|
|
77
|
+
})
|
|
78
|
+
;(router as any).matcher = (newRouter as any).matcher
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export default router
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 静态路由
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { RouteRecordRaw } from 'vue-router'
|
|
6
|
+
|
|
7
|
+
export const staticRoutes: RouteRecordRaw[] = [
|
|
8
|
+
{
|
|
9
|
+
path: '/login',
|
|
10
|
+
name: 'Login',
|
|
11
|
+
component: () => import('@/views/login/index.vue'),
|
|
12
|
+
meta: {
|
|
13
|
+
title: '登录',
|
|
14
|
+
hidden: true
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
path: '/404',
|
|
19
|
+
name: 'NotFound',
|
|
20
|
+
component: () => import('@/views/error/404.vue'),
|
|
21
|
+
meta: {
|
|
22
|
+
title: '404',
|
|
23
|
+
hidden: true
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
path: '/403',
|
|
28
|
+
name: 'Forbidden',
|
|
29
|
+
component: () => import('@/views/error/403.vue'),
|
|
30
|
+
meta: {
|
|
31
|
+
title: '403',
|
|
32
|
+
hidden: true
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
export const errorRoute: RouteRecordRaw = {
|
|
38
|
+
path: '/:pathMatch(.*)*',
|
|
39
|
+
redirect: '/404',
|
|
40
|
+
meta: {
|
|
41
|
+
hidden: true
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 应用状态
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { defineStore } from 'pinia'
|
|
6
|
+
import { ref, computed, watch } from 'vue'
|
|
7
|
+
import { local } from '@/utils/storage'
|
|
8
|
+
|
|
9
|
+
export type ThemeMode = 'light' | 'dark'
|
|
10
|
+
export type LayoutMode = 'sidebar' | 'top' | 'mix'
|
|
11
|
+
|
|
12
|
+
export const useAppStore = defineStore('app', () => {
|
|
13
|
+
// 状态
|
|
14
|
+
const isDark = ref<boolean>(local.get<boolean>('isDark') || false)
|
|
15
|
+
const theme = ref<ThemeMode>(local.get<ThemeMode>('theme') || 'light')
|
|
16
|
+
const layout = ref<LayoutMode>(local.get<LayoutMode>('layout') || 'sidebar')
|
|
17
|
+
const isCollapsed = ref<boolean>(local.get<boolean>('isCollapsed') || false)
|
|
18
|
+
const showTabs = ref<boolean>(local.get<boolean>('showTabs') ?? true)
|
|
19
|
+
const showFooter = ref<boolean>(local.get<boolean>('showFooter') ?? true)
|
|
20
|
+
const showBreadcrumb = ref<boolean>(local.get<boolean>('showBreadcrumb') ?? true)
|
|
21
|
+
const primaryColor = ref<string>(local.get<string>('primaryColor') || '#409eff')
|
|
22
|
+
const cachedViews = ref<string[]>([])
|
|
23
|
+
|
|
24
|
+
// 计算属性
|
|
25
|
+
const themeClass = computed(() => (isDark.value ? 'dark' : 'light'))
|
|
26
|
+
|
|
27
|
+
// 切换主题
|
|
28
|
+
const toggleTheme = () => {
|
|
29
|
+
isDark.value = !isDark.value
|
|
30
|
+
theme.value = isDark.value ? 'dark' : 'light'
|
|
31
|
+
updateTheme()
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// 设置主题
|
|
35
|
+
const setTheme = (mode: ThemeMode) => {
|
|
36
|
+
theme.value = mode
|
|
37
|
+
isDark.value = mode === 'dark'
|
|
38
|
+
updateTheme()
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// 更新主题
|
|
42
|
+
const updateTheme = () => {
|
|
43
|
+
const html = document.documentElement
|
|
44
|
+
if (isDark.value) {
|
|
45
|
+
html.classList.add('dark')
|
|
46
|
+
} else {
|
|
47
|
+
html.classList.remove('dark')
|
|
48
|
+
}
|
|
49
|
+
local.set('isDark', isDark.value)
|
|
50
|
+
local.set('theme', theme.value)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 切换菜单折叠
|
|
54
|
+
const toggleCollapse = () => {
|
|
55
|
+
isCollapsed.value = !isCollapsed.value
|
|
56
|
+
local.set('isCollapsed', isCollapsed.value)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// 设置布局
|
|
60
|
+
const setLayout = (mode: LayoutMode) => {
|
|
61
|
+
layout.value = mode
|
|
62
|
+
local.set('layout', mode)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// 切换标签页
|
|
66
|
+
const toggleTabs = () => {
|
|
67
|
+
showTabs.value = !showTabs.value
|
|
68
|
+
local.set('showTabs', showTabs.value)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 切换底部
|
|
72
|
+
const toggleFooter = () => {
|
|
73
|
+
showFooter.value = !showFooter.value
|
|
74
|
+
local.set('showFooter', showFooter.value)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// 切换面包屑
|
|
78
|
+
const toggleBreadcrumb = () => {
|
|
79
|
+
showBreadcrumb.value = !showBreadcrumb.value
|
|
80
|
+
local.set('showBreadcrumb', showBreadcrumb.value)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// 设置主题色
|
|
84
|
+
const setPrimaryColor = (color: string) => {
|
|
85
|
+
primaryColor.value = color
|
|
86
|
+
document.documentElement.style.setProperty('--color-primary', color)
|
|
87
|
+
local.set('primaryColor', color)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// 添加缓存页面
|
|
91
|
+
const addCachedView = (name: string) => {
|
|
92
|
+
if (!cachedViews.value.includes(name)) {
|
|
93
|
+
cachedViews.value.push(name)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// 移除缓存页面
|
|
98
|
+
const removeCachedView = (name: string) => {
|
|
99
|
+
const index = cachedViews.value.indexOf(name)
|
|
100
|
+
if (index > -1) {
|
|
101
|
+
cachedViews.value.splice(index, 1)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// 清除缓存页面
|
|
106
|
+
const clearCachedViews = () => {
|
|
107
|
+
cachedViews.value = []
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// 初始化主题
|
|
111
|
+
const initTheme = () => {
|
|
112
|
+
updateTheme()
|
|
113
|
+
if (primaryColor.value !== '#409eff') {
|
|
114
|
+
document.documentElement.style.setProperty('--color-primary', primaryColor.value)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// 监听主题变化
|
|
119
|
+
watch(isDark, updateTheme)
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
isDark,
|
|
123
|
+
theme,
|
|
124
|
+
layout,
|
|
125
|
+
isCollapsed,
|
|
126
|
+
showTabs,
|
|
127
|
+
showFooter,
|
|
128
|
+
showBreadcrumb,
|
|
129
|
+
primaryColor,
|
|
130
|
+
cachedViews,
|
|
131
|
+
themeClass,
|
|
132
|
+
toggleTheme,
|
|
133
|
+
toggleCollapse,
|
|
134
|
+
setTheme,
|
|
135
|
+
setLayout,
|
|
136
|
+
toggleTabs,
|
|
137
|
+
toggleFooter,
|
|
138
|
+
toggleBreadcrumb,
|
|
139
|
+
setPrimaryColor,
|
|
140
|
+
addCachedView,
|
|
141
|
+
removeCachedView,
|
|
142
|
+
clearCachedViews,
|
|
143
|
+
initTheme
|
|
144
|
+
}
|
|
145
|
+
})
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 认证状态
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { defineStore } from 'pinia'
|
|
6
|
+
import { ref, computed } from 'vue'
|
|
7
|
+
import { getToken, clearToken, setTokenInfo, hasToken } from '@/utils/auth'
|
|
8
|
+
|
|
9
|
+
export const useAuthStore = defineStore('auth', () => {
|
|
10
|
+
// 状态
|
|
11
|
+
const token = ref<string | null>(getToken())
|
|
12
|
+
const isLoggedIn = computed(() => hasToken())
|
|
13
|
+
|
|
14
|
+
// 设置 token
|
|
15
|
+
const login = (tokenInfo: { token: string; refreshToken: string; expireTime: number }) => {
|
|
16
|
+
token.value = tokenInfo.token
|
|
17
|
+
setTokenInfo(tokenInfo)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// 登出
|
|
21
|
+
const logout = () => {
|
|
22
|
+
token.value = null
|
|
23
|
+
clearToken()
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
token,
|
|
28
|
+
isLoggedIn,
|
|
29
|
+
login,
|
|
30
|
+
logout
|
|
31
|
+
}
|
|
32
|
+
})
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 菜单状态
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { defineStore } from 'pinia'
|
|
6
|
+
import { ref, computed } from 'vue'
|
|
7
|
+
import type { AppRoute } from '@/types/router'
|
|
8
|
+
import type { MenuItem } from '@/types/api'
|
|
9
|
+
import { local } from '@/utils/storage'
|
|
10
|
+
import router from '@/router'
|
|
11
|
+
|
|
12
|
+
export const useMenuStore = defineStore('menu', () => {
|
|
13
|
+
// 状态
|
|
14
|
+
const menuList = ref<MenuItem[]>(local.get<MenuItem[]>('menuList') || [])
|
|
15
|
+
|
|
16
|
+
// 计算属性
|
|
17
|
+
const hasMenu = computed(() => menuList.value.length > 0)
|
|
18
|
+
|
|
19
|
+
// 设置菜单
|
|
20
|
+
const setMenuList = (menus: MenuItem[]) => {
|
|
21
|
+
menuList.value = menus
|
|
22
|
+
local.set('menuList', menus)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// 清除菜单
|
|
26
|
+
const clearMenu = () => {
|
|
27
|
+
menuList.value = []
|
|
28
|
+
local.remove('menuList')
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// 生成路由
|
|
32
|
+
const generateRoutes = (menus: MenuItem[]): AppRoute[] => {
|
|
33
|
+
return menus
|
|
34
|
+
.filter(menu => !menu.hidden)
|
|
35
|
+
.map(menu => {
|
|
36
|
+
const route: AppRoute = {
|
|
37
|
+
path: menu.path,
|
|
38
|
+
name: menu.name,
|
|
39
|
+
meta: {
|
|
40
|
+
title: menu.title,
|
|
41
|
+
icon: menu.icon,
|
|
42
|
+
keepAlive: menu.keepAlive,
|
|
43
|
+
hidden: menu.hidden
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (menu.redirect) {
|
|
48
|
+
route.redirect = menu.redirect
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (menu.component) {
|
|
52
|
+
// 动态导入组件
|
|
53
|
+
route.component = () => import(`@/views/${menu.component}.vue`)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (menu.children && menu.children.length > 0) {
|
|
57
|
+
route.children = generateRoutes(menu.children)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return route
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// 添加路由
|
|
65
|
+
const addRoutes = (menus: MenuItem[]) => {
|
|
66
|
+
const routes = generateRoutes(menus)
|
|
67
|
+
routes.forEach(route => {
|
|
68
|
+
router.addRoute('Layout', route as any)
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
menuList,
|
|
74
|
+
hasMenu,
|
|
75
|
+
setMenuList,
|
|
76
|
+
clearMenu,
|
|
77
|
+
generateRoutes,
|
|
78
|
+
addRoutes
|
|
79
|
+
}
|
|
80
|
+
})
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 用户状态
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { defineStore } from 'pinia'
|
|
6
|
+
import { ref, computed } from 'vue'
|
|
7
|
+
import type { UserInfo } from '@/types/api'
|
|
8
|
+
import { local } from '@/utils/storage'
|
|
9
|
+
|
|
10
|
+
export const useUserStore = defineStore('user', () => {
|
|
11
|
+
// 状态
|
|
12
|
+
const userInfo = ref<UserInfo | null>(local.get<UserInfo>('userInfo'))
|
|
13
|
+
const roles = ref<string[]>(local.get<string[]>('roles') || [])
|
|
14
|
+
const permissions = ref<string[]>(local.get<string[]>('permissions') || [])
|
|
15
|
+
|
|
16
|
+
// 计算属性
|
|
17
|
+
const isLoggedIn = computed(() => !!userInfo.value)
|
|
18
|
+
const username = computed(() => userInfo.value?.username || '')
|
|
19
|
+
const nickname = computed(() => userInfo.value?.nickname || '')
|
|
20
|
+
const avatar = computed(() => userInfo.value?.avatar || '')
|
|
21
|
+
const userId = computed(() => userInfo.value?.id)
|
|
22
|
+
|
|
23
|
+
// 设置用户信息
|
|
24
|
+
const setUserInfo = (info: UserInfo) => {
|
|
25
|
+
userInfo.value = info
|
|
26
|
+
roles.value = info.roles || []
|
|
27
|
+
permissions.value = info.permissions || []
|
|
28
|
+
local.set('userInfo', info)
|
|
29
|
+
local.set('roles', info.roles || [])
|
|
30
|
+
local.set('permissions', info.permissions || [])
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// 清除用户信息
|
|
34
|
+
const clearUserInfo = () => {
|
|
35
|
+
userInfo.value = null
|
|
36
|
+
roles.value = []
|
|
37
|
+
permissions.value = []
|
|
38
|
+
local.remove('userInfo')
|
|
39
|
+
local.remove('roles')
|
|
40
|
+
local.remove('permissions')
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// 检查权限
|
|
44
|
+
const hasPermission = (permission: string | string[]): boolean => {
|
|
45
|
+
if (Array.isArray(permission)) {
|
|
46
|
+
return permission.some(p => permissions.value.includes(p))
|
|
47
|
+
}
|
|
48
|
+
return permissions.value.includes(permission)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// 检查角色
|
|
52
|
+
const hasRole = (role: string | string[]): boolean => {
|
|
53
|
+
if (Array.isArray(role)) {
|
|
54
|
+
return role.some(r => roles.value.includes(r))
|
|
55
|
+
}
|
|
56
|
+
return roles.value.includes(role)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
userInfo,
|
|
61
|
+
roles,
|
|
62
|
+
permissions,
|
|
63
|
+
isLoggedIn,
|
|
64
|
+
username,
|
|
65
|
+
nickname,
|
|
66
|
+
avatar,
|
|
67
|
+
userId,
|
|
68
|
+
setUserInfo,
|
|
69
|
+
clearUserInfo,
|
|
70
|
+
hasPermission,
|
|
71
|
+
hasRole
|
|
72
|
+
}
|
|
73
|
+
})
|