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.
Files changed (54) hide show
  1. package/.env.development +4 -0
  2. package/.env.production +4 -0
  3. package/index.html +13 -0
  4. package/package.json +18 -10
  5. package/public/vite.svg +10 -0
  6. package/src/App.vue +20 -0
  7. package/src/api/auth.ts +26 -0
  8. package/src/api/system.ts +65 -0
  9. package/src/api/user.ts +46 -0
  10. package/src/assets/styles/_dark.scss +407 -0
  11. package/src/assets/styles/_reset.scss +126 -0
  12. package/src/assets/styles/_root.scss +140 -0
  13. package/src/assets/styles/_transition.scss +119 -0
  14. package/src/assets/styles/_variables.scss +45 -0
  15. package/src/assets/styles/index.scss +187 -0
  16. package/src/components/Layout/Footer.vue +17 -0
  17. package/src/components/Layout/Header.vue +390 -0
  18. package/src/components/Layout/Sidebar.vue +297 -0
  19. package/src/components/Layout/Tabs.vue +134 -0
  20. package/src/components/Layout/index.vue +62 -0
  21. package/src/composables/useAuth.ts +45 -0
  22. package/src/composables/useForm.ts +79 -0
  23. package/src/composables/useTable.ts +97 -0
  24. package/src/directives/permission.ts +38 -0
  25. package/src/enums/index.ts +63 -0
  26. package/src/env.d.ts +17 -0
  27. package/src/index.ts +39 -0
  28. package/src/main.ts +34 -0
  29. package/src/router/dynamicRoutes.ts +163 -0
  30. package/src/router/index.ts +81 -0
  31. package/src/router/staticRoutes.ts +43 -0
  32. package/src/stores/app.ts +145 -0
  33. package/src/stores/auth.ts +32 -0
  34. package/src/stores/index.ts +15 -0
  35. package/src/stores/menu.ts +80 -0
  36. package/src/stores/user.ts +73 -0
  37. package/src/types/api.d.ts +84 -0
  38. package/src/types/global.d.ts +45 -0
  39. package/src/types/router.d.ts +48 -0
  40. package/src/types/xto.d.ts +149 -0
  41. package/src/utils/auth.ts +62 -0
  42. package/src/utils/permission.ts +42 -0
  43. package/src/utils/request.ts +126 -0
  44. package/src/utils/storage.ts +63 -0
  45. package/src/views/dashboard/index.vue +284 -0
  46. package/src/views/error/403.vue +57 -0
  47. package/src/views/error/404.vue +57 -0
  48. package/src/views/login/index.vue +248 -0
  49. package/src/views/system/menu/index.vue +381 -0
  50. package/src/views/system/role/index.vue +304 -0
  51. package/src/views/system/user/index.vue +327 -0
  52. package/tsconfig.json +26 -0
  53. package/tsconfig.node.json +11 -0
  54. 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,15 @@
1
+ /**
2
+ * Pinia Store 入口
3
+ */
4
+
5
+ import { createPinia } from 'pinia'
6
+
7
+ const pinia = createPinia()
8
+
9
+ export default pinia
10
+
11
+ // 导出所有 stores
12
+ export * from './user'
13
+ export * from './auth'
14
+ export * from './menu'
15
+ export * from './app'
@@ -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
+ })