xto-fronted 0.1.2 → 0.1.4

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 (60) hide show
  1. package/.env.development +3 -0
  2. package/.env.production +3 -0
  3. package/bin/cli.js +103 -0
  4. package/index.html +13 -0
  5. package/package.json +16 -3
  6. package/public/vite.svg +10 -0
  7. package/src/App.vue +20 -0
  8. package/src/api/auth.ts +35 -0
  9. package/src/api/menu.ts +13 -0
  10. package/src/api/system.ts +65 -0
  11. package/src/api/user.ts +12 -0
  12. package/src/assets/styles/_dark.scss +407 -0
  13. package/src/assets/styles/_reset.scss +126 -0
  14. package/src/assets/styles/_root.scss +140 -0
  15. package/src/assets/styles/_transition.scss +119 -0
  16. package/src/assets/styles/_variables.scss +45 -0
  17. package/src/assets/styles/index.scss +187 -0
  18. package/src/components/Layout/Footer.vue +17 -0
  19. package/src/components/Layout/Header.vue +335 -0
  20. package/src/components/Layout/Sidebar.vue +213 -0
  21. package/src/components/Layout/Tabs.vue +20 -0
  22. package/src/components/Layout/index.vue +62 -0
  23. package/src/composables/index.ts +9 -0
  24. package/src/composables/useApp.ts +170 -0
  25. package/src/composables/useAuth.ts +70 -0
  26. package/src/composables/useForm.ts +79 -0
  27. package/src/composables/useMenu.ts +141 -0
  28. package/src/composables/useTable.ts +97 -0
  29. package/src/config/index.ts +19 -0
  30. package/src/directives/permission.ts +41 -0
  31. package/src/enums/index.ts +63 -0
  32. package/src/env.d.ts +17 -0
  33. package/src/index.ts +44 -0
  34. package/src/main.ts +34 -0
  35. package/src/router/dynamicRoutes.ts +163 -0
  36. package/src/router/index.ts +71 -0
  37. package/src/router/staticRoutes.ts +43 -0
  38. package/src/stores/app.ts +145 -0
  39. package/src/stores/auth.ts +45 -0
  40. package/src/stores/index.ts +15 -0
  41. package/src/stores/menu.ts +158 -0
  42. package/src/stores/user.ts +41 -0
  43. package/src/types/api.d.ts +103 -0
  44. package/src/types/global.d.ts +45 -0
  45. package/src/types/router.d.ts +48 -0
  46. package/src/types/xto.d.ts +149 -0
  47. package/src/utils/auth.ts +86 -0
  48. package/src/utils/permission.ts +30 -0
  49. package/src/utils/request.ts +126 -0
  50. package/src/utils/storage.ts +72 -0
  51. package/src/views/dashboard/index.vue +32 -0
  52. package/src/views/error/403.vue +57 -0
  53. package/src/views/error/404.vue +57 -0
  54. package/src/views/login/index.vue +141 -0
  55. package/src/views/system/menu/index.vue +32 -0
  56. package/src/views/system/role/index.vue +32 -0
  57. package/src/views/system/user/index.vue +32 -0
  58. package/tsconfig.json +26 -0
  59. package/tsconfig.node.json +11 -0
  60. package/vite.config.ts +139 -0
@@ -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,45 @@
1
+ /**
2
+ * 认证状态
3
+ */
4
+
5
+ import { defineStore } from 'pinia'
6
+ import { ref, computed } from 'vue'
7
+ import { getLoginInfo, setLoginInfo, clearToken, hasToken } from '@/utils/auth'
8
+ import type { LoginInfo } from '@/utils/auth'
9
+
10
+ export const useAuthStore = defineStore('auth', () => {
11
+ // 状态
12
+ const loginInfo = ref<LoginInfo | null>(getLoginInfo())
13
+ const isLoggedIn = computed(() => hasToken())
14
+
15
+ // 设置登录信息
16
+ const login = (data: Record<string, unknown>) => {
17
+ setLoginInfo(data)
18
+ loginInfo.value = getLoginInfo()
19
+ }
20
+
21
+ // 登出
22
+ const logout = () => {
23
+ loginInfo.value = null
24
+ clearToken()
25
+ }
26
+
27
+ // 获取 access token
28
+ const accessToken = computed(() => loginInfo.value?.accessToken || null)
29
+
30
+ // 获取 token type
31
+ const tokenType = computed(() => loginInfo.value?.tokenType || 'Bearer')
32
+
33
+ // 获取 code
34
+ const code = computed(() => loginInfo.value?.code || null)
35
+
36
+ return {
37
+ loginInfo,
38
+ isLoggedIn,
39
+ accessToken,
40
+ tokenType,
41
+ code,
42
+ login,
43
+ logout
44
+ }
45
+ })
@@ -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,158 @@
1
+ /**
2
+ * 菜单状态
3
+ */
4
+
5
+ import { defineStore } from 'pinia'
6
+ import { ref, computed } from 'vue'
7
+ import type { MenuItem, RemoteMenuItem } from '@/types/api'
8
+ import { local } from '@/utils/storage'
9
+
10
+ export const useMenuStore = defineStore('menu', () => {
11
+ // 存储 key
12
+ const MENU_LIST_KEY = 'menu_list'
13
+ const MENU_BTN_LIST_KEY = 'menu_btn_list'
14
+
15
+ // 状态
16
+ const menuList = ref<MenuItem[]>(local.get<MenuItem[]>(MENU_LIST_KEY) || [])
17
+ const menuBtnListMap = ref<Record<string, MenuItem[]>>(local.get<Record<string, MenuItem[]>>(MENU_BTN_LIST_KEY) || {})
18
+
19
+ // 首页菜单信息
20
+ const indexMenu: MenuItem = {
21
+ code: 'home',
22
+ name: '首页',
23
+ icon: 'home',
24
+ closable: false,
25
+ default: false,
26
+ out: false,
27
+ path: '/dashboard',
28
+ title: '首页'
29
+ }
30
+
31
+ // 计算属性
32
+ const hasMenu = computed(() => menuList.value.length > 0)
33
+
34
+ // 获取首页地址
35
+ const index = computed(() => {
36
+ function recursion(menuList: MenuItem[]): string | null {
37
+ if (menuList && menuList.length > 0) {
38
+ for (let i = 0; i < menuList.length; i++) {
39
+ const menu = menuList[i]
40
+ if (menu.default) {
41
+ return menu.path
42
+ } else {
43
+ const url = recursion(menu.children || [])
44
+ if (url) {
45
+ return url
46
+ }
47
+ }
48
+ }
49
+ }
50
+ return null
51
+ }
52
+ const indexUrl = recursion(menuList.value)
53
+ return indexUrl || indexMenu.path
54
+ })
55
+
56
+ // 解析后端菜单数据
57
+ const parseMenuData = (remoteMenuList: RemoteMenuItem[], parentMenuUrl?: string) => {
58
+ const localMenuList: MenuItem[] = []
59
+
60
+ if (!remoteMenuList || remoteMenuList.length <= 0) {
61
+ return localMenuList
62
+ }
63
+
64
+ remoteMenuList.forEach(remoteMenu => {
65
+ if (remoteMenu.type === 1) {
66
+ // 按钮权限
67
+ const btn: MenuItem = {
68
+ code: remoteMenu.menuCode,
69
+ name: remoteMenu.menuName,
70
+ path: '',
71
+ title: remoteMenu.menuName
72
+ }
73
+ const parentUrl = parentMenuUrl || ''
74
+ let btnList = menuBtnListMap.value[parentUrl] || []
75
+ btnList.push(btn)
76
+ menuBtnListMap.value[parentUrl] = btnList
77
+ } else {
78
+ // 菜单权限
79
+ const children: MenuItem[] = []
80
+ let menuPath = remoteMenu.menuUrl
81
+
82
+ // 处理外链
83
+ let isOut = remoteMenu.isOut || false
84
+ if (!isOut && remoteMenu.menuUrl) {
85
+ if (remoteMenu.menuUrl.startsWith('http')) {
86
+ menuPath = '/iframe/' + encodeURIComponent(menuPath)
87
+ } else if (remoteMenu.menuUrl.startsWith('keep-alive:')) {
88
+ menuPath = '/iframe/keep-alive/' + encodeURIComponent(menuPath.split('keep-alive:')[1])
89
+ }
90
+ }
91
+
92
+ const menu: MenuItem = {
93
+ code: remoteMenu.menuCode,
94
+ name: remoteMenu.menuName,
95
+ path: menuPath,
96
+ icon: remoteMenu.icon,
97
+ closable: remoteMenu.closable,
98
+ default: remoteMenu.isDefault,
99
+ out: isOut,
100
+ children: children,
101
+ title: remoteMenu.menuName
102
+ }
103
+
104
+ localMenuList.push(menu)
105
+
106
+ // 递归处理子菜单
107
+ if (remoteMenu.children && remoteMenu.children.length > 0) {
108
+ const childMenus = parseMenuData(remoteMenu.children, remoteMenu.menuUrl)
109
+ menu.children = childMenus
110
+ }
111
+ }
112
+ })
113
+
114
+ return localMenuList
115
+ }
116
+
117
+ // 设置菜单
118
+ const setMenuList = (menus: MenuItem[]) => {
119
+ menuList.value = menus
120
+ local.set(MENU_LIST_KEY, menus)
121
+ }
122
+
123
+ // 设置菜单(从后端数据)
124
+ const setMenuFromRemote = (remoteMenus: RemoteMenuItem[]) => {
125
+ // 清除旧数据
126
+ clearMenu()
127
+
128
+ // 添加首页
129
+ menuList.value.push(indexMenu)
130
+
131
+ // 解析后端菜单
132
+ const parsedMenus = parseMenuData(remoteMenus)
133
+ menuList.value.push(...parsedMenus)
134
+
135
+ // 保存到本地存储
136
+ local.set(MENU_LIST_KEY, menuList.value)
137
+ local.set(MENU_BTN_LIST_KEY, menuBtnListMap.value)
138
+ }
139
+
140
+ // 清除菜单
141
+ const clearMenu = () => {
142
+ menuList.value = []
143
+ menuBtnListMap.value = {}
144
+ local.remove(MENU_LIST_KEY)
145
+ local.remove(MENU_BTN_LIST_KEY)
146
+ }
147
+
148
+ return {
149
+ menuList,
150
+ menuBtnListMap,
151
+ hasMenu,
152
+ index,
153
+ indexMenu,
154
+ setMenuList,
155
+ setMenuFromRemote,
156
+ clearMenu
157
+ }
158
+ })
@@ -0,0 +1,41 @@
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>('user_info'))
13
+
14
+ // 计算属性
15
+ const isLoggedIn = computed(() => !!userInfo.value)
16
+ const userName = computed(() => userInfo.value?.userName || '')
17
+ const avatar = computed(() => userInfo.value?.avatar || '')
18
+ const userId = computed(() => userInfo.value?.userId)
19
+
20
+ // 设置用户信息
21
+ const setUserInfo = (info: UserInfo) => {
22
+ userInfo.value = info
23
+ local.set('user_info', info)
24
+ }
25
+
26
+ // 清除用户信息
27
+ const clearUserInfo = () => {
28
+ userInfo.value = null
29
+ local.remove('user_info')
30
+ }
31
+
32
+ return {
33
+ userInfo,
34
+ isLoggedIn,
35
+ userName,
36
+ avatar,
37
+ userId,
38
+ setUserInfo,
39
+ clearUserInfo
40
+ }
41
+ })
@@ -0,0 +1,103 @@
1
+ /**
2
+ * API 通用类型定义
3
+ */
4
+
5
+ // 基础响应
6
+ export interface ApiResponse<T = unknown> {
7
+ code: number
8
+ data: T
9
+ message: string
10
+ }
11
+
12
+ // 分页请求参数
13
+ export interface PageParams {
14
+ page: number
15
+ pageSize: number
16
+ [key: string]: unknown
17
+ }
18
+
19
+ // 分页响应
20
+ export interface PageResponse<T> {
21
+ list: T[]
22
+ total: number
23
+ page: number
24
+ pageSize: number
25
+ }
26
+
27
+ // 用户信息(来自后端)
28
+ export interface UserInfo {
29
+ uId: string
30
+ appId: string
31
+ userId: string
32
+ userName: string
33
+ departmentName?: string
34
+ email?: string
35
+ mobilePhone?: string
36
+ positionName?: string
37
+ avatar?: string
38
+ workNo?: string
39
+ }
40
+
41
+ // 登录请求
42
+ export interface LoginParams {
43
+ uid: string
44
+ password: string
45
+ }
46
+
47
+ // 登录响应
48
+ export interface LoginResult {
49
+ access_token: string
50
+ refresh_token: string
51
+ expires_time: string
52
+ refresh_time: string
53
+ token_type: string
54
+ code: string
55
+ }
56
+
57
+ // 后端菜单项
58
+ export interface RemoteMenuItem {
59
+ menuCode: string
60
+ menuName: string
61
+ menuUrl: string
62
+ icon?: string
63
+ closable?: boolean
64
+ isDefault?: boolean
65
+ isOut?: boolean
66
+ type: number // 0: 菜单, 1: 按钮
67
+ children?: RemoteMenuItem[]
68
+ }
69
+
70
+ // 前端菜单项
71
+ export interface MenuItem {
72
+ code: string
73
+ name: string
74
+ path: string
75
+ component?: string
76
+ redirect?: string
77
+ icon?: string
78
+ title: string
79
+ hidden?: boolean
80
+ keepAlive?: boolean
81
+ affix?: boolean
82
+ default?: boolean
83
+ out?: boolean
84
+ closable?: boolean
85
+ children?: MenuItem[]
86
+ }
87
+
88
+ // 按钮权限
89
+ export interface ButtonPermission {
90
+ code: string
91
+ }
92
+
93
+ // 角色信息
94
+ export interface RoleInfo {
95
+ id: number | string
96
+ name: string
97
+ code: string
98
+ description?: string
99
+ status: number
100
+ permissions: string[]
101
+ createTime?: string
102
+ updateTime?: string
103
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * 全局类型定义
3
+ */
4
+
5
+ // 通用对象类型
6
+ export type AnyObject = Record<string, unknown>
7
+
8
+ // 通用函数类型
9
+ export type AnyFunction = (...args: unknown[]) => unknown
10
+
11
+ // 值类型
12
+ export type ValueOf<T> = T[keyof T]
13
+
14
+ // 可空类型
15
+ export type Nullable<T> = T | null
16
+
17
+ // 可选类型
18
+ export type Optional<T> = T | undefined
19
+
20
+ // 深度可选
21
+ export type DeepPartial<T> = {
22
+ [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P]
23
+ }
24
+
25
+ // 深度必需
26
+ export type DeepRequired<T> = {
27
+ [P in keyof T]-?: T[P] extends object ? DeepRequired<T[P]> : T[P]
28
+ }
29
+
30
+ // 深度只读
31
+ export type DeepReadonly<T> = {
32
+ readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P]
33
+ }
34
+
35
+ // 提取数组元素类型
36
+ export type ArrayElement<T> = T extends readonly (infer E)[] ? E : never
37
+
38
+ // 提取 Promise 值类型
39
+ export type Awaited<T> = T extends Promise<infer U> ? U : T
40
+
41
+ // 排除 null 和 undefined
42
+ export type NonNullable<T> = T extends null | undefined ? never : T
43
+
44
+ // 枚举值类型
45
+ export type EnumValue<T extends Record<string, string | number>> = T[keyof T]
@@ -0,0 +1,48 @@
1
+ /**
2
+ * 路由类型定义
3
+ */
4
+
5
+ import 'vue-router'
6
+
7
+ declare module 'vue-router' {
8
+ interface RouteMeta {
9
+ // 页面标题
10
+ title?: string
11
+ // 图标
12
+ icon?: string
13
+ // 是否隐藏菜单
14
+ hidden?: boolean
15
+ // 是否缓存页面
16
+ keepAlive?: boolean
17
+ // 是否固定在 tabs 中
18
+ affix?: boolean
19
+ // 权限标识
20
+ permissions?: string[]
21
+ // 角色标识
22
+ roles?: string[]
23
+ // 面包屑
24
+ breadcrumb?: boolean
25
+ // 当前激活菜单
26
+ activeMenu?: string
27
+ }
28
+ }
29
+
30
+ // 路由项
31
+ export interface AppRoute {
32
+ path: string
33
+ name?: string
34
+ redirect?: string
35
+ component?: () => Promise<unknown>
36
+ meta?: RouteMeta
37
+ children?: AppRoute[]
38
+ }
39
+
40
+ // 动态路由
41
+ export interface DynamicRoute {
42
+ path: string
43
+ name: string
44
+ component: string
45
+ redirect?: string
46
+ meta?: RouteMeta
47
+ children?: DynamicRoute[]
48
+ }