xto-fronted 0.1.2 → 0.1.3
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 +3 -0
- package/.env.production +3 -0
- package/bin/cli.js +104 -0
- package/index.html +13 -0
- package/package.json +16 -3
- package/public/vite.svg +10 -0
- package/src/App.vue +20 -0
- package/src/api/auth.ts +35 -0
- package/src/api/menu.ts +13 -0
- package/src/api/system.ts +65 -0
- package/src/api/user.ts +12 -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 +335 -0
- package/src/components/Layout/Sidebar.vue +213 -0
- package/src/components/Layout/Tabs.vue +20 -0
- package/src/components/Layout/index.vue +62 -0
- package/src/composables/index.ts +9 -0
- package/src/composables/useApp.ts +170 -0
- package/src/composables/useAuth.ts +70 -0
- package/src/composables/useForm.ts +79 -0
- package/src/composables/useMenu.ts +141 -0
- package/src/composables/useTable.ts +97 -0
- package/src/config/index.ts +19 -0
- package/src/directives/permission.ts +41 -0
- package/src/enums/index.ts +63 -0
- package/src/env.d.ts +17 -0
- package/src/index.ts +44 -0
- package/src/main.ts +34 -0
- package/src/router/dynamicRoutes.ts +163 -0
- package/src/router/index.ts +71 -0
- package/src/router/staticRoutes.ts +43 -0
- package/src/stores/app.ts +145 -0
- package/src/stores/auth.ts +45 -0
- package/src/stores/index.ts +15 -0
- package/src/stores/menu.ts +158 -0
- package/src/stores/user.ts +41 -0
- package/src/types/api.d.ts +103 -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 +86 -0
- package/src/utils/permission.ts +30 -0
- package/src/utils/request.ts +126 -0
- package/src/utils/storage.ts +72 -0
- package/src/views/dashboard/index.vue +32 -0
- package/src/views/error/403.vue +57 -0
- package/src/views/error/404.vue +57 -0
- package/src/views/login/index.vue +141 -0
- package/src/views/system/menu/index.vue +32 -0
- package/src/views/system/role/index.vue +32 -0
- package/src/views/system/user/index.vue +32 -0
- package/tsconfig.json +26 -0
- package/tsconfig.node.json +11 -0
- package/vite.config.ts +139 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 菜单 Hook
|
|
3
|
+
* 封装菜单渲染、搜索、图标映射等逻辑
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { ref, computed } from 'vue'
|
|
7
|
+
import { useRoute, useRouter } from 'vue-router'
|
|
8
|
+
import { useMenuStore } from '@/stores/menu'
|
|
9
|
+
import { useAppStore } from '@/stores/app'
|
|
10
|
+
import type { MenuItem } from '@/types/api'
|
|
11
|
+
|
|
12
|
+
// 默认图标映射
|
|
13
|
+
const defaultIconMap: Record<string, string> = {
|
|
14
|
+
home: '🏠',
|
|
15
|
+
dashboard: '📊',
|
|
16
|
+
system: '⚙️',
|
|
17
|
+
user: '👤',
|
|
18
|
+
role: '👥',
|
|
19
|
+
menu: '📋',
|
|
20
|
+
setting: '🔧'
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function useMenu(iconMap?: Record<string, string>) {
|
|
24
|
+
const route = useRoute()
|
|
25
|
+
const router = useRouter()
|
|
26
|
+
const menuStore = useMenuStore()
|
|
27
|
+
const appStore = useAppStore()
|
|
28
|
+
|
|
29
|
+
// 合并图标映射
|
|
30
|
+
const mergedIconMap = { ...defaultIconMap, ...iconMap }
|
|
31
|
+
|
|
32
|
+
// 搜索关键词
|
|
33
|
+
const searchKeyword = ref('')
|
|
34
|
+
|
|
35
|
+
// 当前激活菜单
|
|
36
|
+
const activeMenu = computed(() => route.path)
|
|
37
|
+
|
|
38
|
+
// 侧边栏是否折叠
|
|
39
|
+
const isCollapsed = computed(() => appStore.isCollapsed)
|
|
40
|
+
|
|
41
|
+
// 菜单列表
|
|
42
|
+
const menuList = computed(() => menuStore.menuList)
|
|
43
|
+
|
|
44
|
+
// 是否有菜单
|
|
45
|
+
const hasMenu = computed(() => menuStore.hasMenu)
|
|
46
|
+
|
|
47
|
+
// ========== 扁平化菜单(用于搜索) ==========
|
|
48
|
+
|
|
49
|
+
const flattenMenus = (
|
|
50
|
+
menus: MenuItem[],
|
|
51
|
+
parentTitle = ''
|
|
52
|
+
): (MenuItem & { parentTitle: string })[] => {
|
|
53
|
+
const result: (MenuItem & { parentTitle: string })[] = []
|
|
54
|
+
menus.forEach(menu => {
|
|
55
|
+
if (menu.children && menu.children.length > 0) {
|
|
56
|
+
result.push(...flattenMenus(menu.children, menu.title))
|
|
57
|
+
} else {
|
|
58
|
+
result.push({ ...menu, parentTitle })
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
return result
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ========== 搜索结果 ==========
|
|
65
|
+
|
|
66
|
+
const searchResults = computed(() => {
|
|
67
|
+
if (!searchKeyword.value.trim()) return []
|
|
68
|
+
const flatMenus = flattenMenus(menuList.value)
|
|
69
|
+
return flatMenus.filter(menu =>
|
|
70
|
+
menu.title.toLowerCase().includes(searchKeyword.value.toLowerCase())
|
|
71
|
+
)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
// ========== 过滤后的菜单列表 ==========
|
|
75
|
+
|
|
76
|
+
const filteredMenuList = computed(() => {
|
|
77
|
+
if (!searchKeyword.value.trim()) return menuList.value
|
|
78
|
+
|
|
79
|
+
return menuList.value.map(menu => {
|
|
80
|
+
if (menu.children && menu.children.length > 0) {
|
|
81
|
+
const filteredChildren = menu.children.filter(child =>
|
|
82
|
+
child.title.toLowerCase().includes(searchKeyword.value.toLowerCase())
|
|
83
|
+
)
|
|
84
|
+
if (filteredChildren.length > 0) {
|
|
85
|
+
return { ...menu, children: filteredChildren }
|
|
86
|
+
}
|
|
87
|
+
return null
|
|
88
|
+
}
|
|
89
|
+
if (menu.title.toLowerCase().includes(searchKeyword.value.toLowerCase())) {
|
|
90
|
+
return menu
|
|
91
|
+
}
|
|
92
|
+
return null
|
|
93
|
+
}).filter(Boolean) as MenuItem[]
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
// ========== 获取菜单图标 ==========
|
|
97
|
+
|
|
98
|
+
const getMenuIcon = (icon?: string): string => {
|
|
99
|
+
return mergedIconMap[icon || ''] || '📄'
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ========== 菜单选择 ==========
|
|
103
|
+
|
|
104
|
+
const handleMenuSelect = (index: string) => {
|
|
105
|
+
if (index && index !== route.path) {
|
|
106
|
+
router.push(index)
|
|
107
|
+
searchKeyword.value = ''
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ========== 搜索结果点击 ==========
|
|
112
|
+
|
|
113
|
+
const handleSearchItemClick = (path: string) => {
|
|
114
|
+
router.push(path)
|
|
115
|
+
searchKeyword.value = ''
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ========== 清除搜索 ==========
|
|
119
|
+
|
|
120
|
+
const clearSearch = () => {
|
|
121
|
+
searchKeyword.value = ''
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
// 状态
|
|
126
|
+
searchKeyword,
|
|
127
|
+
activeMenu,
|
|
128
|
+
isCollapsed,
|
|
129
|
+
menuList,
|
|
130
|
+
hasMenu,
|
|
131
|
+
searchResults,
|
|
132
|
+
filteredMenuList,
|
|
133
|
+
|
|
134
|
+
// 方法
|
|
135
|
+
flattenMenus,
|
|
136
|
+
getMenuIcon,
|
|
137
|
+
handleMenuSelect,
|
|
138
|
+
handleSearchItemClick,
|
|
139
|
+
clearSearch
|
|
140
|
+
}
|
|
141
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 表格组合函数
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { ref, reactive, computed } from 'vue'
|
|
6
|
+
|
|
7
|
+
export interface TableOptions<T = any> {
|
|
8
|
+
fetchData: (params: any) => Promise<{ list: T[]; total: number }>
|
|
9
|
+
defaultPageSize?: number
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function useTable<T = any>(options: TableOptions<T>) {
|
|
13
|
+
const { fetchData, defaultPageSize = 10 } = options
|
|
14
|
+
|
|
15
|
+
const loading = ref(false)
|
|
16
|
+
const data = ref<T[]>([]) as any
|
|
17
|
+
const total = ref(0)
|
|
18
|
+
const currentPage = ref(1)
|
|
19
|
+
const pageSize = ref(defaultPageSize)
|
|
20
|
+
|
|
21
|
+
const searchParams = reactive<Record<string, any>>({})
|
|
22
|
+
|
|
23
|
+
// 获取数据
|
|
24
|
+
const getData = async () => {
|
|
25
|
+
loading.value = true
|
|
26
|
+
try {
|
|
27
|
+
const params = {
|
|
28
|
+
...searchParams,
|
|
29
|
+
page: currentPage.value,
|
|
30
|
+
pageSize: pageSize.value
|
|
31
|
+
}
|
|
32
|
+
const result = await fetchData(params)
|
|
33
|
+
data.value = result.list
|
|
34
|
+
total.value = result.total
|
|
35
|
+
} catch (error) {
|
|
36
|
+
console.error(error)
|
|
37
|
+
} finally {
|
|
38
|
+
loading.value = false
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// 搜索
|
|
43
|
+
const handleSearch = () => {
|
|
44
|
+
currentPage.value = 1
|
|
45
|
+
getData()
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 重置
|
|
49
|
+
const handleReset = () => {
|
|
50
|
+
Object.keys(searchParams).forEach(key => {
|
|
51
|
+
searchParams[key] = undefined
|
|
52
|
+
})
|
|
53
|
+
currentPage.value = 1
|
|
54
|
+
getData()
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// 分页改变
|
|
58
|
+
const handlePageChange = (page: number) => {
|
|
59
|
+
currentPage.value = page
|
|
60
|
+
getData()
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// 每页条数改变
|
|
64
|
+
const handleSizeChange = (size: number) => {
|
|
65
|
+
pageSize.value = size
|
|
66
|
+
currentPage.value = 1
|
|
67
|
+
getData()
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// 刷新
|
|
71
|
+
const refresh = () => {
|
|
72
|
+
getData()
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// 分页配置
|
|
76
|
+
const pagination = computed(() => ({
|
|
77
|
+
current: currentPage.value,
|
|
78
|
+
pageSize: pageSize.value,
|
|
79
|
+
total: total.value
|
|
80
|
+
}))
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
loading,
|
|
84
|
+
data,
|
|
85
|
+
total,
|
|
86
|
+
currentPage,
|
|
87
|
+
pageSize,
|
|
88
|
+
searchParams,
|
|
89
|
+
pagination,
|
|
90
|
+
getData,
|
|
91
|
+
handleSearch,
|
|
92
|
+
handleReset,
|
|
93
|
+
handlePageChange,
|
|
94
|
+
handleSizeChange,
|
|
95
|
+
refresh
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 应用配置
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface AppConfig {
|
|
6
|
+
webTitle: string
|
|
7
|
+
baseUrl: string
|
|
8
|
+
appId: string
|
|
9
|
+
clientId: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const config: AppConfig = {
|
|
13
|
+
webTitle: import.meta.env.VITE_APP_NAME || 'analysis-web',
|
|
14
|
+
baseUrl: 'https://cloud-api-test.tineco.com',
|
|
15
|
+
appId: import.meta.env.VITE_APP_ID || 'DBU-CONTENT-ANALYSIS',
|
|
16
|
+
clientId: import.meta.env.VITE_APP_CLIENT_ID || 'Tineco'
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export default config
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 权限指令
|
|
3
|
+
* v-permission="['user:edit']" 或 v-permission="'user:edit'"
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Directive, DirectiveBinding } from 'vue'
|
|
7
|
+
import { useMenuStore } from '@/stores/menu'
|
|
8
|
+
|
|
9
|
+
const permission: Directive = {
|
|
10
|
+
mounted(el: HTMLElement, binding: DirectiveBinding<string | string[]>) {
|
|
11
|
+
const menuStore = useMenuStore()
|
|
12
|
+
const { value } = binding
|
|
13
|
+
|
|
14
|
+
if (!value) return
|
|
15
|
+
|
|
16
|
+
// 获取当前页面的按钮权限
|
|
17
|
+
const currentPath = window.location.pathname
|
|
18
|
+
const btnList = menuStore.menuBtnListMap[currentPath] || []
|
|
19
|
+
const btnCodes = btnList.map(item => item.code)
|
|
20
|
+
|
|
21
|
+
// 判断是否有权限
|
|
22
|
+
let hasPermission = false
|
|
23
|
+
if (Array.isArray(value)) {
|
|
24
|
+
hasPermission = value.some(p => btnCodes.includes(p))
|
|
25
|
+
} else {
|
|
26
|
+
hasPermission = btnCodes.includes(value)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// 没有权限则移除元素
|
|
30
|
+
if (!hasPermission) {
|
|
31
|
+
el.parentNode?.removeChild(el)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export default permission
|
|
37
|
+
|
|
38
|
+
// 注册指令
|
|
39
|
+
export function setupPermissionDirective(app: any) {
|
|
40
|
+
app.directive('permission', permission)
|
|
41
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 枚举常量
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// 状态
|
|
6
|
+
export enum Status {
|
|
7
|
+
ENABLED = 1,
|
|
8
|
+
DISABLED = 0
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// 性别
|
|
12
|
+
export enum Gender {
|
|
13
|
+
UNKNOWN = 0,
|
|
14
|
+
MALE = 1,
|
|
15
|
+
FEMALE = 2
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// 菜单类型
|
|
19
|
+
export enum MenuType {
|
|
20
|
+
DIRECTORY = 0,
|
|
21
|
+
MENU = 1,
|
|
22
|
+
BUTTON = 2
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// 状态文本映射
|
|
26
|
+
export const StatusText: Record<Status, string> = {
|
|
27
|
+
[Status.ENABLED]: '启用',
|
|
28
|
+
[Status.DISABLED]: '禁用'
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// 性别文本映射
|
|
32
|
+
export const GenderText: Record<Gender, string> = {
|
|
33
|
+
[Gender.UNKNOWN]: '未知',
|
|
34
|
+
[Gender.MALE]: '男',
|
|
35
|
+
[Gender.FEMALE]: '女'
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// 菜单类型文本映射
|
|
39
|
+
export const MenuTypeText: Record<MenuType, string> = {
|
|
40
|
+
[MenuType.DIRECTORY]: '目录',
|
|
41
|
+
[MenuType.MENU]: '菜单',
|
|
42
|
+
[MenuType.BUTTON]: '按钮'
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// 状态选项
|
|
46
|
+
export const StatusOptions = [
|
|
47
|
+
{ label: '启用', value: Status.ENABLED },
|
|
48
|
+
{ label: '禁用', value: Status.DISABLED }
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
// 性别选项
|
|
52
|
+
export const GenderOptions = [
|
|
53
|
+
{ label: '未知', value: Gender.UNKNOWN },
|
|
54
|
+
{ label: '男', value: Gender.MALE },
|
|
55
|
+
{ label: '女', value: Gender.FEMALE }
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
// 菜单类型选项
|
|
59
|
+
export const MenuTypeOptions = [
|
|
60
|
+
{ label: '目录', value: MenuType.DIRECTORY },
|
|
61
|
+
{ label: '菜单', value: MenuType.MENU },
|
|
62
|
+
{ label: '按钮', value: MenuType.BUTTON }
|
|
63
|
+
]
|
package/src/env.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|
|
2
|
+
|
|
3
|
+
declare module '*.vue' {
|
|
4
|
+
import type { DefineComponent } from 'vue'
|
|
5
|
+
const component: DefineComponent<{}, {}, any>
|
|
6
|
+
export default component
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface ImportMetaEnv {
|
|
10
|
+
readonly VITE_APP_TITLE: string
|
|
11
|
+
readonly VITE_API_BASE_URL: string
|
|
12
|
+
readonly VITE_USE_MOCK: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface ImportMeta {
|
|
16
|
+
readonly env: ImportMetaEnv
|
|
17
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// 组件
|
|
2
|
+
export { default as Layout } from './components/Layout/index.vue'
|
|
3
|
+
export { default as Header } from './components/Layout/Header.vue'
|
|
4
|
+
export { default as Sidebar } from './components/Layout/Sidebar.vue'
|
|
5
|
+
export { default as Tabs } from './components/Layout/Tabs.vue'
|
|
6
|
+
export { default as Footer } from './components/Layout/Footer.vue'
|
|
7
|
+
|
|
8
|
+
// 组合式函数
|
|
9
|
+
export { useApp } from './composables/useApp'
|
|
10
|
+
export { useAuth } from './composables/useAuth'
|
|
11
|
+
export { useMenu } from './composables/useMenu'
|
|
12
|
+
export { useForm } from './composables/useForm'
|
|
13
|
+
export { useTable } from './composables/useTable'
|
|
14
|
+
|
|
15
|
+
// 工具函数
|
|
16
|
+
export * from './utils/auth'
|
|
17
|
+
export * from './utils/permission'
|
|
18
|
+
export * from './utils/request'
|
|
19
|
+
export * from './utils/storage'
|
|
20
|
+
|
|
21
|
+
// Store
|
|
22
|
+
export * from './stores/app'
|
|
23
|
+
export * from './stores/auth'
|
|
24
|
+
export * from './stores/menu'
|
|
25
|
+
export * from './stores/user'
|
|
26
|
+
|
|
27
|
+
// 路由
|
|
28
|
+
export { default as router, resetRouter } from './router'
|
|
29
|
+
export * from './router/staticRoutes'
|
|
30
|
+
export * from './router/dynamicRoutes'
|
|
31
|
+
|
|
32
|
+
// API
|
|
33
|
+
export * from './api/auth'
|
|
34
|
+
export * from './api/menu'
|
|
35
|
+
export * from './api/user'
|
|
36
|
+
|
|
37
|
+
// 配置
|
|
38
|
+
export { default as appConfig } from './config'
|
|
39
|
+
|
|
40
|
+
// 枚举
|
|
41
|
+
export * from './enums'
|
|
42
|
+
|
|
43
|
+
// 指令
|
|
44
|
+
export { default as permissionDirective } from './directives/permission'
|
package/src/main.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { createApp } from 'vue'
|
|
2
|
+
import { createPinia } from 'pinia'
|
|
3
|
+
import App from './App.vue'
|
|
4
|
+
import router from './router'
|
|
5
|
+
|
|
6
|
+
// 样式
|
|
7
|
+
import '@/assets/styles/index.scss'
|
|
8
|
+
|
|
9
|
+
// 组件库样式
|
|
10
|
+
import '@xto/base/es/style.css'
|
|
11
|
+
import '@xto/form/es/style.css'
|
|
12
|
+
import '@xto/data/es/style.css'
|
|
13
|
+
import '@xto/feedback/es/style.css'
|
|
14
|
+
import '@xto/navigation/es/style.css'
|
|
15
|
+
import '@xto/layout/es/style.css'
|
|
16
|
+
import '@xto/business/es/style.css'
|
|
17
|
+
|
|
18
|
+
// 全局注册内部组件(解决 resolveComponent 问题)
|
|
19
|
+
import { Loading } from '@xto/base'
|
|
20
|
+
import { TreeNode } from '@xto/data'
|
|
21
|
+
|
|
22
|
+
const app = createApp(App)
|
|
23
|
+
|
|
24
|
+
// 注册内部组件
|
|
25
|
+
app.component('x-loading', Loading)
|
|
26
|
+
app.component('TreeNode', TreeNode)
|
|
27
|
+
|
|
28
|
+
// Pinia
|
|
29
|
+
app.use(createPinia())
|
|
30
|
+
|
|
31
|
+
// Router
|
|
32
|
+
app.use(router)
|
|
33
|
+
|
|
34
|
+
app.mount('#app')
|
|
@@ -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,71 @@
|
|
|
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 { useAppStore } from '@/stores/app'
|
|
10
|
+
import { useApp } from '@/composables'
|
|
11
|
+
|
|
12
|
+
const router = createRouter({
|
|
13
|
+
history: createWebHistory(),
|
|
14
|
+
routes: [...staticRoutes, layoutRoute, errorRoute],
|
|
15
|
+
scrollBehavior: () => ({ left: 0, top: 0 })
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
// 白名单路由
|
|
19
|
+
const whiteList = ['/login', '/404', '/403']
|
|
20
|
+
|
|
21
|
+
// 路由守卫
|
|
22
|
+
router.beforeEach(async (to, _from, next) => {
|
|
23
|
+
const appStore = useAppStore()
|
|
24
|
+
|
|
25
|
+
// 初始化主题
|
|
26
|
+
appStore.initTheme()
|
|
27
|
+
|
|
28
|
+
// 已登录
|
|
29
|
+
if (hasToken()) {
|
|
30
|
+
if (to.path === '/login') {
|
|
31
|
+
// 已登录,跳转到首页
|
|
32
|
+
next({ path: '/' })
|
|
33
|
+
} else {
|
|
34
|
+
// 初始化应用(加载用户信息和菜单)
|
|
35
|
+
const { initApp, isLoggedIn } = useApp()
|
|
36
|
+
|
|
37
|
+
if (!isLoggedIn.value) {
|
|
38
|
+
const success = await initApp()
|
|
39
|
+
if (!success) {
|
|
40
|
+
// 初始化失败,跳转登录页
|
|
41
|
+
next('/login')
|
|
42
|
+
return
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// 添加缓存页面
|
|
47
|
+
if (to.name && to.meta.keepAlive) {
|
|
48
|
+
appStore.addCachedView(to.name as string)
|
|
49
|
+
}
|
|
50
|
+
next()
|
|
51
|
+
}
|
|
52
|
+
} else {
|
|
53
|
+
// 未登录
|
|
54
|
+
if (whiteList.includes(to.path)) {
|
|
55
|
+
next()
|
|
56
|
+
} else {
|
|
57
|
+
// 记录当前路径,登录后跳转
|
|
58
|
+
next({ path: '/login', query: { redirectUrl: encodeURIComponent(to.fullPath) } })
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
export function resetRouter() {
|
|
64
|
+
const newRouter = createRouter({
|
|
65
|
+
history: createWebHistory(),
|
|
66
|
+
routes: [...staticRoutes, layoutRoute, errorRoute]
|
|
67
|
+
})
|
|
68
|
+
;(router as any).matcher = (newRouter as any).matcher
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export default router
|