@yyp92-cli/template-vue-pc 1.2.0
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/CHANGELOG.md +13 -0
- package/package.json +15 -0
- package/template/.env.development +2 -0
- package/template/.env.production +2 -0
- package/template/.env.test +2 -0
- package/template/README.md +30 -0
- package/template/auto-imports.d.ts +11 -0
- package/template/components.d.ts +67 -0
- package/template/index.html +16 -0
- package/template/package.json +43 -0
- package/template/pnpm-lock.yaml +2830 -0
- package/template/public/vite.svg +1 -0
- package/template/src/App.vue +26 -0
- package/template/src/assets/img/login-bg.jpg +0 -0
- package/template/src/assets/img/logo.svg +15 -0
- package/template/src/components/layout/header-crumb/index.vue +38 -0
- package/template/src/components/layout/header-info/index.vue +181 -0
- package/template/src/components/layout/index.vue +83 -0
- package/template/src/components/layout/main-header/index.vue +85 -0
- package/template/src/components/layout/main-menu/index.vue +166 -0
- package/template/src/components/layout-horizontal/header-crumb/index.vue +38 -0
- package/template/src/components/layout-horizontal/header-info/index.vue +185 -0
- package/template/src/components/layout-horizontal/index.vue +55 -0
- package/template/src/components/layout-horizontal/main-header/index.vue +86 -0
- package/template/src/components/layout-horizontal/main-menu/index.vue +135 -0
- package/template/src/global/constants.ts +4 -0
- package/template/src/global/register-icons.ts +10 -0
- package/template/src/main.ts +16 -0
- package/template/src/mock/index.ts +8 -0
- package/template/src/mock/login.ts +5 -0
- package/template/src/mock/userDepartmentList.ts +85 -0
- package/template/src/mock/userInfoMock.ts +24 -0
- package/template/src/mock/userList.ts +115 -0
- package/template/src/mock/userListDelete.ts +1 -0
- package/template/src/mock/userMenuList.ts +466 -0
- package/template/src/mock/userMenus.ts +374 -0
- package/template/src/mock/userRoleList.ts +1846 -0
- package/template/src/router/index.ts +41 -0
- package/template/src/router/routes.ts +189 -0
- package/template/src/service/api/index.ts +90 -0
- package/template/src/service/request/index.ts +268 -0
- package/template/src/service/request/type.ts +5 -0
- package/template/src/store/counter.ts +19 -0
- package/template/src/store/index.ts +14 -0
- package/template/src/store/login/index.ts +142 -0
- package/template/src/store/main/index.ts +54 -0
- package/template/src/store/main/system/index.ts +88 -0
- package/template/src/store/main/system/type.ts +19 -0
- package/template/src/styles/global.scss +33 -0
- package/template/src/styles/index.scss +4 -0
- package/template/src/styles/reset.scss +17 -0
- package/template/src/theme/darkTheme.scss +51 -0
- package/template/src/theme/lightTheme.scss +53 -0
- package/template/src/types/index.ts +1 -0
- package/template/src/types/login.ts +4 -0
- package/template/src/utils/cache.ts +44 -0
- package/template/src/utils/download.ts +27 -0
- package/template/src/utils/format.ts +10 -0
- package/template/src/utils/index.ts +14 -0
- package/template/src/utils/map-menus.ts +174 -0
- package/template/src/views/403/index.vue +22 -0
- package/template/src/views/login/component/login-panel.vue +138 -0
- package/template/src/views/login/component/panel-account.vue +138 -0
- package/template/src/views/login/component/panel-phone.vue +56 -0
- package/template/src/views/login/index.vue +28 -0
- package/template/src/views/main/analysis/dashboard/index.vue +19 -0
- package/template/src/views/main/analysis/overview/index.vue +25 -0
- package/template/src/views/main/detail/index.vue +19 -0
- package/template/src/views/main/fullScreen/images/bg.png +0 -0
- package/template/src/views/main/fullScreen/images/contrast-bg.png +0 -0
- package/template/src/views/main/fullScreen/images/dataScreen-alarm.png +0 -0
- package/template/src/views/main/fullScreen/images/dataScreen-header-btn-bg-l.png +0 -0
- package/template/src/views/main/fullScreen/images/dataScreen-header-btn-bg-r.png +0 -0
- package/template/src/views/main/fullScreen/images/dataScreen-header-center-bg.png +0 -0
- package/template/src/views/main/fullScreen/images/dataScreen-header-left-bg.png +0 -0
- package/template/src/views/main/fullScreen/images/dataScreen-header-right-bg.png +0 -0
- package/template/src/views/main/fullScreen/images/dataScreen-header-warn-bg.png +0 -0
- package/template/src/views/main/fullScreen/images/dataScreen-main-cb.png +0 -0
- package/template/src/views/main/fullScreen/images/dataScreen-main-lb.png +0 -0
- package/template/src/views/main/fullScreen/images/dataScreen-main-lc.png +0 -0
- package/template/src/views/main/fullScreen/images/dataScreen-main-lt.png +0 -0
- package/template/src/views/main/fullScreen/images/dataScreen-main-rb.png +0 -0
- package/template/src/views/main/fullScreen/images/dataScreen-main-rc.png +0 -0
- package/template/src/views/main/fullScreen/images/dataScreen-main-rt.png +0 -0
- package/template/src/views/main/fullScreen/images/dataScreen-title.png +0 -0
- package/template/src/views/main/fullScreen/images/dataScreen-warn-bg.png +0 -0
- package/template/src/views/main/fullScreen/images/line-bg.png +0 -0
- package/template/src/views/main/fullScreen/images/man-bg.png +0 -0
- package/template/src/views/main/fullScreen/images/man.png +0 -0
- package/template/src/views/main/fullScreen/images/map-title-bg.png +0 -0
- package/template/src/views/main/fullScreen/images/rankingChart-bg.png +0 -0
- package/template/src/views/main/fullScreen/images/total.png +0 -0
- package/template/src/views/main/fullScreen/images/woman-bg.png +0 -0
- package/template/src/views/main/fullScreen/images/woman.png +0 -0
- package/template/src/views/main/fullScreen/index.vue +33 -0
- package/template/src/views/main/index.vue +24 -0
- package/template/src/views/main/product/category/index.vue +19 -0
- package/template/src/views/main/product/goods/index.vue +19 -0
- package/template/src/views/main/story/chat/index.vue +70 -0
- package/template/src/views/main/story/list/index.vue +19 -0
- package/template/src/views/main/system/department/index.vue +20 -0
- package/template/src/views/main/system/menu/index.vue +19 -0
- package/template/src/views/main/system/role/index.vue +20 -0
- package/template/src/views/main/system/user/index.vue +141 -0
- package/template/src/views/main/system/user/user-comp/UserContent.vue +211 -0
- package/template/src/views/main/system/user/user-comp/UserModal.vue +225 -0
- package/template/src/views/main/system/user/user-comp/UserSearch.vue +147 -0
- package/template/src/views/not-found/index.vue +22 -0
- package/template/src/vite-env.d.ts +9 -0
- package/template/tsconfig.app.json +26 -0
- package/template/tsconfig.json +7 -0
- package/template/tsconfig.node.json +25 -0
- package/template/vite.config.ts +66 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { LOGIN_TOKEN, PERMISSION, USER_MENUS} from '@/global/constants'
|
|
2
|
+
import { localCache } from '@/utils/cache'
|
|
3
|
+
import { createRouter, createWebHistory } from 'vue-router'
|
|
4
|
+
import { getFirstMenu, getMenusRedirect, localRoutes } from '@/utils/map-menus'
|
|
5
|
+
import { routes } from './routes'
|
|
6
|
+
|
|
7
|
+
const router = createRouter({
|
|
8
|
+
history: createWebHistory(),
|
|
9
|
+
routes: routes
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* * 导航守卫
|
|
14
|
+
* 参数: to(跳转到的位置),from(从哪里跳转过来的),next: 路由的放行函数
|
|
15
|
+
* 返回值:返回值决定导航的路径(不返回或者返回 undefined, 默认跳转)
|
|
16
|
+
*/
|
|
17
|
+
router.beforeEach((to, from, next) => {
|
|
18
|
+
const permission = localCache.getCache(PERMISSION) ?? []
|
|
19
|
+
const menus = localCache.getCache(USER_MENUS) ?? []
|
|
20
|
+
// 只有登陆成功(token),才能真正进入到 main 页面
|
|
21
|
+
const token = localCache.getCache(LOGIN_TOKEN)
|
|
22
|
+
|
|
23
|
+
const newUrl = getMenusRedirect(menus, to.path)
|
|
24
|
+
|
|
25
|
+
if (to.path === '/') {
|
|
26
|
+
next({
|
|
27
|
+
path: getFirstMenu(menus)
|
|
28
|
+
})
|
|
29
|
+
}
|
|
30
|
+
else if (newUrl) {
|
|
31
|
+
next(newUrl)
|
|
32
|
+
}
|
|
33
|
+
else if (!['/403', '/login'].includes(to.path) && !permission.includes(to.path) && localRoutes.includes(to.path)) {
|
|
34
|
+
return next('/403')
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
next()
|
|
38
|
+
}
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
export default router
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
export const routes = [
|
|
2
|
+
{
|
|
3
|
+
path: '/',
|
|
4
|
+
name: 'main',
|
|
5
|
+
component: () => import('../views/main/index.vue'),
|
|
6
|
+
children: [
|
|
7
|
+
{
|
|
8
|
+
path: '/analysis',
|
|
9
|
+
name: 'analysis',
|
|
10
|
+
meta: {
|
|
11
|
+
title: '系统总览',
|
|
12
|
+
icon: 'el-icon-monitor',
|
|
13
|
+
hideMenu: false
|
|
14
|
+
},
|
|
15
|
+
children: [
|
|
16
|
+
{
|
|
17
|
+
path: '/overview',
|
|
18
|
+
name: 'overview',
|
|
19
|
+
component: () => import('@/views/main/analysis/overview/index.vue'),
|
|
20
|
+
meta: {
|
|
21
|
+
title: '核心技术',
|
|
22
|
+
hideMenu: false,
|
|
23
|
+
showFullScreen: false
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
path: '/dashboard',
|
|
28
|
+
name: 'dashboard',
|
|
29
|
+
component: () => import('@/views/main/analysis/dashboard/index.vue'),
|
|
30
|
+
meta: {
|
|
31
|
+
title: '商品统计',
|
|
32
|
+
hideMenu: false,
|
|
33
|
+
showFullScreen: false
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
]
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
path: '/system',
|
|
40
|
+
name: 'system',
|
|
41
|
+
meta: {
|
|
42
|
+
title: '系统管理',
|
|
43
|
+
icon: 'el-icon-setting',
|
|
44
|
+
hideMenu: false
|
|
45
|
+
},
|
|
46
|
+
children: [
|
|
47
|
+
{
|
|
48
|
+
path: '/user',
|
|
49
|
+
name: 'user',
|
|
50
|
+
component: () => import('@/views/main/system/user/index.vue'),
|
|
51
|
+
meta: {
|
|
52
|
+
title: '用户管理',
|
|
53
|
+
hideMenu: false,
|
|
54
|
+
showFullScreen: false
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
path: '/department',
|
|
59
|
+
name: 'department',
|
|
60
|
+
component: () => import('@/views/main/system/department/index.vue'),
|
|
61
|
+
meta: {
|
|
62
|
+
title: '部门管理',
|
|
63
|
+
hideMenu: false,
|
|
64
|
+
showFullScreen: false
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
path: '/menu',
|
|
69
|
+
name: 'menu',
|
|
70
|
+
component: () => import('@/views/main/system/menu/index.vue'),
|
|
71
|
+
meta: {
|
|
72
|
+
title: '菜单管理',
|
|
73
|
+
hideMenu: false,
|
|
74
|
+
showFullScreen: false
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
path: '/role',
|
|
79
|
+
name: 'role',
|
|
80
|
+
component: () => import('@/views/main/system/role/index.vue'),
|
|
81
|
+
meta: {
|
|
82
|
+
title: '角色管理',
|
|
83
|
+
hideMenu: false,
|
|
84
|
+
showFullScreen: false
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
]
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
path: '/product',
|
|
91
|
+
name: 'product',
|
|
92
|
+
meta: {
|
|
93
|
+
title: '商品中心',
|
|
94
|
+
icon: 'el-icon-goods',
|
|
95
|
+
hideMenu: false
|
|
96
|
+
},
|
|
97
|
+
children: [
|
|
98
|
+
{
|
|
99
|
+
path: '/category',
|
|
100
|
+
name: 'category',
|
|
101
|
+
component: () => import('@/views/main/product/category/index.vue'),
|
|
102
|
+
meta: {
|
|
103
|
+
title: '商品类别',
|
|
104
|
+
hideMenu: false,
|
|
105
|
+
showFullScreen: false
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
path: '/goods',
|
|
110
|
+
name: 'goods',
|
|
111
|
+
component: () => import('@/views/main/product/goods/index.vue'),
|
|
112
|
+
meta: {
|
|
113
|
+
title: '商品信息',
|
|
114
|
+
hideMenu: false,
|
|
115
|
+
showFullScreen: false
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
]
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
path: '/story',
|
|
122
|
+
name: 'story',
|
|
123
|
+
meta: {
|
|
124
|
+
title: '随便聊聊',
|
|
125
|
+
icon: 'el-icon-setting',
|
|
126
|
+
hideMenu: false
|
|
127
|
+
},
|
|
128
|
+
children: [
|
|
129
|
+
{
|
|
130
|
+
path: '/chat',
|
|
131
|
+
name: 'chat',
|
|
132
|
+
component: () => import('@/views/main/story/chat/index.vue'),
|
|
133
|
+
meta: {
|
|
134
|
+
title: '你的故事',
|
|
135
|
+
hideMenu: false,
|
|
136
|
+
showFullScreen: false
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
path: '/list',
|
|
141
|
+
name: 'list',
|
|
142
|
+
component: () => import('@/views/main/story/list/index.vue'),
|
|
143
|
+
meta: {
|
|
144
|
+
title: '故事列表',
|
|
145
|
+
hideMenu: false,
|
|
146
|
+
showFullScreen: false
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
]
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
path: '/fullScreen',
|
|
153
|
+
name: 'fullScreen',
|
|
154
|
+
component: () => import('@/views/main/fullScreen/index.vue'),
|
|
155
|
+
meta: {
|
|
156
|
+
title: '数据大屏',
|
|
157
|
+
icon: 'el-icon-monitor',
|
|
158
|
+
hideMenu: false,
|
|
159
|
+
showFullScreen: true
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
path: '/detail',
|
|
164
|
+
name: 'detail',
|
|
165
|
+
component: () => import('@/views/main/detail/index.vue'),
|
|
166
|
+
meta: {
|
|
167
|
+
title: '页面详情',
|
|
168
|
+
hideMenu: true,
|
|
169
|
+
showFullScreen: true
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
]
|
|
173
|
+
},
|
|
174
|
+
|
|
175
|
+
{
|
|
176
|
+
path: '/login',
|
|
177
|
+
component: () => import('@/views/login/index.vue')
|
|
178
|
+
},
|
|
179
|
+
|
|
180
|
+
{
|
|
181
|
+
path: '/403',
|
|
182
|
+
component: () => import('@/views/403/index.vue')
|
|
183
|
+
},
|
|
184
|
+
|
|
185
|
+
{
|
|
186
|
+
path: '/:pathMatch(.*)',
|
|
187
|
+
component: () => import('@/views/not-found/index.vue')
|
|
188
|
+
}
|
|
189
|
+
]
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import requestFn from '../request'
|
|
2
|
+
// todo 测试数据,正式使用删除
|
|
3
|
+
import {loginData, userInfoMock, userMenus, userList, userListDelete, userRoleList, userMenuList, userDepartmentList} from '@/mock'
|
|
4
|
+
import type {IAccount} from '@/types'
|
|
5
|
+
|
|
6
|
+
export const accountLoginRequest = (account: IAccount) => {
|
|
7
|
+
// return requestFn.post({
|
|
8
|
+
// url: '/login',
|
|
9
|
+
// data: account
|
|
10
|
+
// })
|
|
11
|
+
|
|
12
|
+
return Promise.resolve({data: loginData})
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const getUserInfoById = (id: number) => {
|
|
16
|
+
// return requestFn.get({
|
|
17
|
+
// url: `/users/${id}`,
|
|
18
|
+
// })
|
|
19
|
+
|
|
20
|
+
return Promise.resolve({data: userInfoMock})
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const getUserMenusByRoleId = (id: number) => {
|
|
24
|
+
// return requestFn.get({
|
|
25
|
+
// url: `/role/${id}/menu`
|
|
26
|
+
// })
|
|
27
|
+
|
|
28
|
+
return Promise.resolve({data: userMenus})
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
// 用户列表
|
|
33
|
+
export const postUserList = (queryInfo: any) => {
|
|
34
|
+
// return requestFn.post({
|
|
35
|
+
// url: 'users/list',
|
|
36
|
+
// data: queryInfo
|
|
37
|
+
// })
|
|
38
|
+
|
|
39
|
+
return Promise.resolve({data: userList})
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function deleteUserById(id: number) {
|
|
43
|
+
// return requestFn.delete({
|
|
44
|
+
// url: `/users/${id}`,
|
|
45
|
+
// })
|
|
46
|
+
|
|
47
|
+
return Promise.resolve({data: userListDelete})
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function getEntireRoles() {
|
|
51
|
+
// return requestFn.post({
|
|
52
|
+
// url: '/role/list'
|
|
53
|
+
// })
|
|
54
|
+
|
|
55
|
+
return Promise.resolve({data: userRoleList})
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function getEntireDepartments() {
|
|
59
|
+
// return requestFn.post({
|
|
60
|
+
// url: '/department/list'
|
|
61
|
+
// })
|
|
62
|
+
|
|
63
|
+
return Promise.resolve({data: userDepartmentList})
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function getEntireMenu() {
|
|
67
|
+
// return requestFn.post({
|
|
68
|
+
// url: '/menu/list'
|
|
69
|
+
// })
|
|
70
|
+
|
|
71
|
+
return Promise.resolve({data: userMenuList})
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function newUserData(userInfo: any) {
|
|
75
|
+
// return requestFn.post({
|
|
76
|
+
// url: '/users',
|
|
77
|
+
// data: userInfo
|
|
78
|
+
// })
|
|
79
|
+
|
|
80
|
+
return Promise.resolve({data: '创建用户成功~'})
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function editUserData(id: number, userInfo: any) {
|
|
84
|
+
// return requestFn.patch({
|
|
85
|
+
// url: `/users/${id}`,
|
|
86
|
+
// data: userInfo
|
|
87
|
+
// })
|
|
88
|
+
|
|
89
|
+
return Promise.resolve({data: '编辑用户成功~'})
|
|
90
|
+
}
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import axios from 'axios'
|
|
2
|
+
import { ElNotification, ElMessage } from 'element-plus'
|
|
3
|
+
import type { AxiosRequestConfig, AxiosError, AxiosResponse } from 'axios'
|
|
4
|
+
import type { MyInternalAxiosRequestConfig } from './type'
|
|
5
|
+
import { localCache } from '@/utils/cache'
|
|
6
|
+
import { LOGIN_TOKEN, USER_INFO } from '@/global/constants'
|
|
7
|
+
import router from '@/router'
|
|
8
|
+
|
|
9
|
+
// * 存储待取消的请求:key 是请求唯一标识,value 是 AbortController 实例
|
|
10
|
+
const pendingRequests = new Map()
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* * 生成请求唯一标识
|
|
14
|
+
* @param config - Axios 请求配置
|
|
15
|
+
* @returns 唯一标识字符串
|
|
16
|
+
*/
|
|
17
|
+
const generateKey = (config: AxiosRequestConfig) => {
|
|
18
|
+
const {
|
|
19
|
+
method,
|
|
20
|
+
url,
|
|
21
|
+
params,
|
|
22
|
+
data
|
|
23
|
+
} = config
|
|
24
|
+
|
|
25
|
+
// 拼接方法、URL、参数(params 是 URL 参数,data 是请求体参数)
|
|
26
|
+
return [
|
|
27
|
+
method?.toUpperCase(),
|
|
28
|
+
url,
|
|
29
|
+
JSON.stringify(params || {}),
|
|
30
|
+
JSON.stringify(data || {})
|
|
31
|
+
].join('&')
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* * 添加请求到待取消列表(若存在重复请求则先取消)
|
|
36
|
+
* @param config - Axios 请求配置
|
|
37
|
+
*/
|
|
38
|
+
const addPendingRequest = (config: AxiosRequestConfig) => {
|
|
39
|
+
const requestKey = generateKey(config)
|
|
40
|
+
|
|
41
|
+
// 若存在重复请求,先取消旧请求
|
|
42
|
+
if (pendingRequests.has(requestKey)) {
|
|
43
|
+
const controller = pendingRequests.get(requestKey)
|
|
44
|
+
controller.abort(`取消重复请求:${requestKey}`)
|
|
45
|
+
pendingRequests.delete(requestKey)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 创建新的 AbortController 并关联到请求
|
|
49
|
+
const controller = new AbortController()
|
|
50
|
+
// 将 signal 绑定到请求配置
|
|
51
|
+
config.signal = controller.signal
|
|
52
|
+
pendingRequests.set(requestKey, controller)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* * 从待取消列表中移除请求(请求完成/失败/取消时调用)
|
|
57
|
+
* @param config - Axios 请求配置
|
|
58
|
+
*/
|
|
59
|
+
const removePendingRequest = (config: AxiosRequestConfig) => {
|
|
60
|
+
const requestKey = generateKey(config)
|
|
61
|
+
|
|
62
|
+
if (pendingRequests.has(requestKey)) {
|
|
63
|
+
pendingRequests.delete(requestKey)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// * Token 过期处理函数
|
|
68
|
+
function handleTokenExpired() {
|
|
69
|
+
// 1. 清除本地存储的 Token(避免死循环)
|
|
70
|
+
localCache.removeCache(LOGIN_TOKEN)
|
|
71
|
+
localCache.removeCache(USER_INFO)
|
|
72
|
+
|
|
73
|
+
// 2. 跳转到登录页,记录当前路径(方便登录后跳转回来)
|
|
74
|
+
const currentPath = router?.currentRoute?.value?.fullPath
|
|
75
|
+
|
|
76
|
+
if (currentPath !== '/login') {
|
|
77
|
+
router.push({
|
|
78
|
+
path: '/login',
|
|
79
|
+
// 携带当前路径作为跳转参数
|
|
80
|
+
query: {
|
|
81
|
+
redirect: currentPath
|
|
82
|
+
}
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// 3. 可选:显示提示信息
|
|
87
|
+
ElMessage.error('登录已过期,请重新登录')
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
// * 创建 Axios 实例
|
|
92
|
+
const instance = axios.create({
|
|
93
|
+
// 从环境变量取 baseURL
|
|
94
|
+
baseURL: import.meta.env.VITE_APP_BASE_API,
|
|
95
|
+
timeout: 5000
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
// * 请求拦截器:添加取消逻辑
|
|
99
|
+
instance.interceptors.request.use(
|
|
100
|
+
(config: MyInternalAxiosRequestConfig) => {
|
|
101
|
+
// 允许通过配置关闭取消功能(例如某些特殊长轮询请求)
|
|
102
|
+
if (config.cancelable !== false) {
|
|
103
|
+
addPendingRequest(config)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const token = localCache.getCache(LOGIN_TOKEN)
|
|
107
|
+
|
|
108
|
+
if (config.headers && token) {
|
|
109
|
+
config.headers.Authorization = `Bearer ${token}`
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return config
|
|
113
|
+
},
|
|
114
|
+
(error) => Promise.reject(error)
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
// * 响应拦截器:清理已完成的请求
|
|
118
|
+
instance.interceptors.response.use(
|
|
119
|
+
(response: AxiosResponse) => {
|
|
120
|
+
const {
|
|
121
|
+
config,
|
|
122
|
+
data: responseData
|
|
123
|
+
} = response ?? {}
|
|
124
|
+
const {
|
|
125
|
+
code,
|
|
126
|
+
message
|
|
127
|
+
} = responseData ?? {}
|
|
128
|
+
|
|
129
|
+
removePendingRequest(config)
|
|
130
|
+
|
|
131
|
+
// * 统一处理接口成功后,返回的 code !== 0 的错误,这样业务代码就不用写错误的逻辑了
|
|
132
|
+
if (code !== 0) {
|
|
133
|
+
// handleTokenExpired()
|
|
134
|
+
ElMessage.error(message)
|
|
135
|
+
return Promise.reject(message)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// 直接返回响应体数据
|
|
139
|
+
return responseData
|
|
140
|
+
},
|
|
141
|
+
(error: AxiosError) => {
|
|
142
|
+
const {
|
|
143
|
+
config,
|
|
144
|
+
response
|
|
145
|
+
} = error ?? {}
|
|
146
|
+
const { status, statusText } = response ?? {}
|
|
147
|
+
|
|
148
|
+
// 移除已失败/取消的请求
|
|
149
|
+
if (error.config) {
|
|
150
|
+
removePendingRequest(error.config)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// 区分取消错误和其他错误
|
|
154
|
+
if (axios.isCancel(error)) {
|
|
155
|
+
// 取消请求不视为错误,返回 null
|
|
156
|
+
return Promise.resolve(null)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// * token 过期, 后端返回 401 状态码
|
|
160
|
+
if (status === 401) {
|
|
161
|
+
handleTokenExpired()
|
|
162
|
+
|
|
163
|
+
return Promise.reject(error)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// 异常处理程序
|
|
167
|
+
if (response && status) {
|
|
168
|
+
ElNotification({
|
|
169
|
+
title: `请求错误 ${status}: ${config?.url}`,
|
|
170
|
+
message: statusText,
|
|
171
|
+
type: 'error',
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
}
|
|
175
|
+
else if (!response) {
|
|
176
|
+
ElNotification({
|
|
177
|
+
title: '网络异常',
|
|
178
|
+
message: '您的网络发生异常,无法连接服务器',
|
|
179
|
+
type: 'error',
|
|
180
|
+
})
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// 其他错误处理(如网络错误、404等)
|
|
184
|
+
return Promise.reject(error)
|
|
185
|
+
}
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
// 扩展实例:添加手动取消方法
|
|
189
|
+
const requestFn = {
|
|
190
|
+
...instance,
|
|
191
|
+
|
|
192
|
+
request<T = any>(config: AxiosRequestConfig) {
|
|
193
|
+
// 返回 promise
|
|
194
|
+
return new Promise<T>((resolve, reject) => {
|
|
195
|
+
instance
|
|
196
|
+
.request<any, T>(config)
|
|
197
|
+
.then((res) => {
|
|
198
|
+
resolve(res)
|
|
199
|
+
})
|
|
200
|
+
.catch((err) => {
|
|
201
|
+
reject(err)
|
|
202
|
+
})
|
|
203
|
+
})
|
|
204
|
+
},
|
|
205
|
+
|
|
206
|
+
get(config: AxiosRequestConfig) {
|
|
207
|
+
return this.request({
|
|
208
|
+
...config,
|
|
209
|
+
method: 'GET'
|
|
210
|
+
})
|
|
211
|
+
},
|
|
212
|
+
|
|
213
|
+
post(config: AxiosRequestConfig) {
|
|
214
|
+
return this.request({
|
|
215
|
+
...config,
|
|
216
|
+
method: 'POST'
|
|
217
|
+
})
|
|
218
|
+
},
|
|
219
|
+
|
|
220
|
+
delete(config: AxiosRequestConfig) {
|
|
221
|
+
return this.request({
|
|
222
|
+
...config,
|
|
223
|
+
method: 'DELETE'
|
|
224
|
+
})
|
|
225
|
+
},
|
|
226
|
+
|
|
227
|
+
patch(config: AxiosRequestConfig) {
|
|
228
|
+
return this.request({
|
|
229
|
+
...config,
|
|
230
|
+
method: 'PATCH'
|
|
231
|
+
})
|
|
232
|
+
},
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* 手动取消单个请求
|
|
236
|
+
* @param config - 请求配置(需包含 method、url,可选 params/data)
|
|
237
|
+
* @param message - 取消原因
|
|
238
|
+
* @returns 是否取消成功
|
|
239
|
+
*/
|
|
240
|
+
cancelOne: (
|
|
241
|
+
config: AxiosRequestConfig,
|
|
242
|
+
message: string = '手动取消请求'
|
|
243
|
+
) => {
|
|
244
|
+
const key = generateKey(config)
|
|
245
|
+
|
|
246
|
+
if (pendingRequests.has(key)) {
|
|
247
|
+
pendingRequests.get(key).abort(message)
|
|
248
|
+
pendingRequests.delete(key)
|
|
249
|
+
|
|
250
|
+
return true
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return false
|
|
254
|
+
},
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* 取消所有待处理的请求
|
|
258
|
+
* @param message - 取消原因
|
|
259
|
+
*/
|
|
260
|
+
cancelAll: (message: string = '取消所有请求') => {
|
|
261
|
+
pendingRequests.forEach((controller, key) => {
|
|
262
|
+
controller.abort(`${message}:${key}`)
|
|
263
|
+
pendingRequests.delete(key)
|
|
264
|
+
})
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export default requestFn
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import {defineStore} from 'pinia'
|
|
2
|
+
|
|
3
|
+
const useCountStore = defineStore('counter', {
|
|
4
|
+
state: () => ({
|
|
5
|
+
counter: 100
|
|
6
|
+
}),
|
|
7
|
+
getters: {
|
|
8
|
+
doubleCounter(state) {
|
|
9
|
+
return state.counter * 2
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
actions: {
|
|
13
|
+
changeCounterAction(newCounter: number) {
|
|
14
|
+
this.counter = newCounter
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
export default useCountStore
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import {createPinia} from 'pinia'
|
|
2
|
+
import useLoginStore from './login'
|
|
3
|
+
import type { App } from 'vue'
|
|
4
|
+
|
|
5
|
+
const pinia = createPinia()
|
|
6
|
+
|
|
7
|
+
function registerStore(app: App<Element>) {
|
|
8
|
+
app.use(pinia)
|
|
9
|
+
|
|
10
|
+
const loginStore = useLoginStore()
|
|
11
|
+
loginStore.loadLocalCacheAction()
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export default registerStore
|