xto-fronted 0.4.7 → 0.4.8
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 +7 -7
- package/.env.production +7 -7
- package/dist/assets/403-AFBQifUI.js +1 -0
- package/dist/assets/403-BHEXXbt2.css +1 -0
- package/dist/assets/404-Ct_A1n7S.css +1 -0
- package/dist/assets/404-WFvpcD2_.js +1 -0
- package/dist/assets/_plugin-vue_export-helper-DlAUqK2U.js +1 -0
- package/dist/assets/index-1juADvYN.js +2 -0
- package/dist/assets/index-B-sX4Ru0.js +1 -0
- package/dist/assets/index-BMcziU5a.css +1 -0
- package/dist/assets/index-BRR97dc6.js +1 -0
- package/dist/assets/index-BZA0ksjx.css +1 -0
- package/dist/assets/index-BpV_8nl0.js +1 -0
- package/dist/assets/index-BvzhR4zp.js +1 -0
- package/dist/assets/index-CUh_s55Z.css +1 -0
- package/dist/assets/index-CVjdnIgR.css +1 -0
- package/dist/assets/index-CYq57-zj.js +1 -0
- package/dist/assets/index-CkL3sVAQ.js +2 -0
- package/dist/assets/index-CtrKVYJb.css +1 -0
- package/dist/assets/index-Cz2P_bsS.js +1 -0
- package/dist/assets/index-D9wlAuR_.js +1 -0
- package/dist/assets/index-DawJb02s.css +1 -0
- package/dist/assets/index-DfFR6NLf.js +1 -0
- package/dist/assets/index-DwVgMO8e.js +1 -0
- package/dist/assets/index-GDP-IkXE.css +1 -0
- package/dist/assets/index-Iaz1ZzPC.js +2 -0
- package/dist/assets/index-PfV8pzQz.css +1 -0
- package/dist/assets/index-Swfu6yvD.css +1 -0
- package/dist/assets/index-Te8_PRgJ.js +1 -0
- package/dist/assets/index-WyZ91RLx.css +1 -0
- package/dist/assets/index-tFYRoFdE.js +1 -0
- package/dist/assets/vendor-42ANG6Sg.js +6 -0
- package/dist/assets/vite-Dw-pgLOX.js +1 -0
- package/dist/assets/vue-vendor-Br-l7wbK.js +29 -0
- package/dist/assets/xto-base-C-IBqjVs.js +1 -0
- package/dist/assets/xto-base-C6eqMPdO.css +1 -0
- package/dist/assets/xto-business--V1F5Gwb.css +1 -0
- package/dist/assets/xto-core-DZK7Cyg0.js +1 -0
- package/dist/assets/xto-data-BFpiDgJi.js +1 -0
- package/dist/assets/xto-data-CnAQAQH2.css +1 -0
- package/dist/assets/xto-feedback-B7ipsTfz.js +1 -0
- package/dist/assets/xto-feedback-DBwJzoTj.css +1 -0
- package/dist/assets/xto-form-CrsyAjyr.css +1 -0
- package/dist/assets/xto-form-NRjKKNcY.js +1 -0
- package/dist/assets/xto-layout-BqU8RuWL.css +1 -0
- package/dist/assets/xto-navigation-BiSaXPfr.js +1 -0
- package/dist/assets/xto-navigation-C1cnSL2E.css +1 -0
- package/dist/assets/xto-navigation-CBPg4dCc.css +1 -0
- package/dist/assets/xto-navigation-CKabFu9d.js +1 -0
- package/dist/index.html +28 -0
- package/package.json +85 -85
- package/src/api/auth.ts +25 -25
- package/src/api/system.ts +66 -66
- package/src/assets/styles/_dark.scss +406 -406
- package/src/components/Layout/Header.vue +973 -973
- package/src/components/Layout/Sidebar.vue +273 -273
- package/src/components/Layout/index.vue +443 -63
- package/src/composables/useApp.ts +61 -61
- package/src/composables/useAuth.ts +16 -16
- package/src/directives/permission.ts +27 -27
- package/src/env.d.ts +18 -18
- package/src/index.ts +47 -47
- package/src/router/dynamicRoutes.ts +162 -162
- package/src/router/guards.ts +128 -128
- package/src/router/index.ts +79 -79
- package/src/stores/auth.ts +65 -65
- package/src/stores/menu.ts +48 -48
- package/src/stores/user.ts +50 -50
- package/src/types/api.d.ts +80 -80
- package/src/utils/auth.ts +99 -99
- package/src/utils/config.ts +80 -80
- package/src/utils/permission.ts +32 -32
- package/src/utils/request.ts +124 -124
- package/src/views/login/index.vue +194 -194
- package/vite.config.ts +135 -135
package/src/utils/request.ts
CHANGED
|
@@ -1,125 +1,125 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Axios 请求封装
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig } from 'axios'
|
|
6
|
-
import { getToken, getTokenType, clearToken } from './auth'
|
|
7
|
-
import { Message } from '@xto/feedback'
|
|
8
|
-
|
|
9
|
-
// 响应数据接口(Euler 框架 Result 格式)
|
|
10
|
-
export interface ApiResponse<T = unknown> {
|
|
11
|
-
code: number
|
|
12
|
-
data: T
|
|
13
|
-
message: string
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
// 分页参数(Euler 框架格式)
|
|
17
|
-
export interface PageParams {
|
|
18
|
-
pageNo: number
|
|
19
|
-
pageSize: number
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
// 分页响应(Euler 框架格式)
|
|
23
|
-
export interface PageResponse<T> {
|
|
24
|
-
records: T[]
|
|
25
|
-
total: number
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
// 创建 axios 实例
|
|
29
|
-
const createRequest = (): AxiosInstance => {
|
|
30
|
-
const instance = axios.create({
|
|
31
|
-
baseURL: import.meta.env.VITE_API_BASE_URL,
|
|
32
|
-
timeout: 30000,
|
|
33
|
-
headers: {
|
|
34
|
-
'Content-Type': 'application/json'
|
|
35
|
-
}
|
|
36
|
-
})
|
|
37
|
-
|
|
38
|
-
// 请求拦截器
|
|
39
|
-
instance.interceptors.request.use(
|
|
40
|
-
(config: InternalAxiosRequestConfig) => {
|
|
41
|
-
const token = getToken()
|
|
42
|
-
const tokenType = getTokenType() || 'Bearer'
|
|
43
|
-
if (token) {
|
|
44
|
-
config.headers.Authorization = `${tokenType} ${token}`
|
|
45
|
-
}
|
|
46
|
-
return config
|
|
47
|
-
},
|
|
48
|
-
(error) => {
|
|
49
|
-
return Promise.reject(error)
|
|
50
|
-
}
|
|
51
|
-
)
|
|
52
|
-
|
|
53
|
-
// 响应拦截器
|
|
54
|
-
instance.interceptors.response.use(
|
|
55
|
-
(response: AxiosResponse<ApiResponse>) => {
|
|
56
|
-
const { data } = response
|
|
57
|
-
|
|
58
|
-
// Euler 框架返回 Result 包装格式,成功时直接返回 data.data 中的实际数据
|
|
59
|
-
if (data.code === 200 || data.code === 0) {
|
|
60
|
-
return data.data as any
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// 业务错误
|
|
64
|
-
Message.error(data.message || '请求失败')
|
|
65
|
-
return Promise.reject(new Error(data.message || '请求失败'))
|
|
66
|
-
},
|
|
67
|
-
(error) => {
|
|
68
|
-
const { response } = error
|
|
69
|
-
|
|
70
|
-
if (response) {
|
|
71
|
-
switch (response.status) {
|
|
72
|
-
case 401:
|
|
73
|
-
Message.error('登录已过期,请重新登录')
|
|
74
|
-
clearToken()
|
|
75
|
-
window.location.href = '/login'
|
|
76
|
-
break
|
|
77
|
-
case 403:
|
|
78
|
-
Message.error('没有权限访问')
|
|
79
|
-
break
|
|
80
|
-
case 404:
|
|
81
|
-
Message.error('请求资源不存在')
|
|
82
|
-
break
|
|
83
|
-
case 500:
|
|
84
|
-
Message.error('服务器错误')
|
|
85
|
-
break
|
|
86
|
-
default:
|
|
87
|
-
Message.error(response.data?.message || '请求失败')
|
|
88
|
-
}
|
|
89
|
-
} else {
|
|
90
|
-
Message.error('网络连接失败')
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
return Promise.reject(error)
|
|
94
|
-
}
|
|
95
|
-
)
|
|
96
|
-
|
|
97
|
-
return instance
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const request = createRequest()
|
|
101
|
-
|
|
102
|
-
// 请求方法 - 直接返回实际数据,无需手动解包
|
|
103
|
-
export const http = {
|
|
104
|
-
get<T = unknown>(url: string, config?: AxiosRequestConfig): Promise<T> {
|
|
105
|
-
return request.get(url, config)
|
|
106
|
-
},
|
|
107
|
-
|
|
108
|
-
post<T = unknown>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<T> {
|
|
109
|
-
return request.post(url, data, config)
|
|
110
|
-
},
|
|
111
|
-
|
|
112
|
-
put<T = unknown>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<T> {
|
|
113
|
-
return request.put(url, data, config)
|
|
114
|
-
},
|
|
115
|
-
|
|
116
|
-
patch<T = unknown>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<T> {
|
|
117
|
-
return request.patch(url, data, config)
|
|
118
|
-
},
|
|
119
|
-
|
|
120
|
-
delete<T = unknown>(url: string, config?: AxiosRequestConfig): Promise<T> {
|
|
121
|
-
return request.delete(url, config)
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Axios 请求封装
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig } from 'axios'
|
|
6
|
+
import { getToken, getTokenType, clearToken } from './auth'
|
|
7
|
+
import { Message } from '@xto/feedback'
|
|
8
|
+
|
|
9
|
+
// 响应数据接口(Euler 框架 Result 格式)
|
|
10
|
+
export interface ApiResponse<T = unknown> {
|
|
11
|
+
code: number
|
|
12
|
+
data: T
|
|
13
|
+
message: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// 分页参数(Euler 框架格式)
|
|
17
|
+
export interface PageParams {
|
|
18
|
+
pageNo: number
|
|
19
|
+
pageSize: number
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// 分页响应(Euler 框架格式)
|
|
23
|
+
export interface PageResponse<T> {
|
|
24
|
+
records: T[]
|
|
25
|
+
total: number
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// 创建 axios 实例
|
|
29
|
+
const createRequest = (): AxiosInstance => {
|
|
30
|
+
const instance = axios.create({
|
|
31
|
+
baseURL: import.meta.env.VITE_API_BASE_URL,
|
|
32
|
+
timeout: 30000,
|
|
33
|
+
headers: {
|
|
34
|
+
'Content-Type': 'application/json'
|
|
35
|
+
}
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
// 请求拦截器
|
|
39
|
+
instance.interceptors.request.use(
|
|
40
|
+
(config: InternalAxiosRequestConfig) => {
|
|
41
|
+
const token = getToken()
|
|
42
|
+
const tokenType = getTokenType() || 'Bearer'
|
|
43
|
+
if (token) {
|
|
44
|
+
config.headers.Authorization = `${tokenType} ${token}`
|
|
45
|
+
}
|
|
46
|
+
return config
|
|
47
|
+
},
|
|
48
|
+
(error) => {
|
|
49
|
+
return Promise.reject(error)
|
|
50
|
+
}
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
// 响应拦截器
|
|
54
|
+
instance.interceptors.response.use(
|
|
55
|
+
(response: AxiosResponse<ApiResponse>) => {
|
|
56
|
+
const { data } = response
|
|
57
|
+
|
|
58
|
+
// Euler 框架返回 Result 包装格式,成功时直接返回 data.data 中的实际数据
|
|
59
|
+
if (data.code === 200 || data.code === 0) {
|
|
60
|
+
return data.data as any
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// 业务错误
|
|
64
|
+
Message.error(data.message || '请求失败')
|
|
65
|
+
return Promise.reject(new Error(data.message || '请求失败'))
|
|
66
|
+
},
|
|
67
|
+
(error) => {
|
|
68
|
+
const { response } = error
|
|
69
|
+
|
|
70
|
+
if (response) {
|
|
71
|
+
switch (response.status) {
|
|
72
|
+
case 401:
|
|
73
|
+
Message.error('登录已过期,请重新登录')
|
|
74
|
+
clearToken()
|
|
75
|
+
window.location.href = '/login'
|
|
76
|
+
break
|
|
77
|
+
case 403:
|
|
78
|
+
Message.error('没有权限访问')
|
|
79
|
+
break
|
|
80
|
+
case 404:
|
|
81
|
+
Message.error('请求资源不存在')
|
|
82
|
+
break
|
|
83
|
+
case 500:
|
|
84
|
+
Message.error('服务器错误')
|
|
85
|
+
break
|
|
86
|
+
default:
|
|
87
|
+
Message.error(response.data?.message || '请求失败')
|
|
88
|
+
}
|
|
89
|
+
} else {
|
|
90
|
+
Message.error('网络连接失败')
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return Promise.reject(error)
|
|
94
|
+
}
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
return instance
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const request = createRequest()
|
|
101
|
+
|
|
102
|
+
// 请求方法 - 直接返回实际数据,无需手动解包
|
|
103
|
+
export const http = {
|
|
104
|
+
get<T = unknown>(url: string, config?: AxiosRequestConfig): Promise<T> {
|
|
105
|
+
return request.get(url, config)
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
post<T = unknown>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<T> {
|
|
109
|
+
return request.post(url, data, config)
|
|
110
|
+
},
|
|
111
|
+
|
|
112
|
+
put<T = unknown>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<T> {
|
|
113
|
+
return request.put(url, data, config)
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
patch<T = unknown>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<T> {
|
|
117
|
+
return request.patch(url, data, config)
|
|
118
|
+
},
|
|
119
|
+
|
|
120
|
+
delete<T = unknown>(url: string, config?: AxiosRequestConfig): Promise<T> {
|
|
121
|
+
return request.delete(url, config)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
125
|
export default request
|
|
@@ -1,195 +1,195 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
import { ref, reactive } from 'vue'
|
|
3
|
-
import { useRouter, useRoute } from 'vue-router'
|
|
4
|
-
import { Button, Icon } from '@xto/base'
|
|
5
|
-
import { Form, FormItem, Input, Checkbox } from '@xto/form'
|
|
6
|
-
import { Message } from '@xto/feedback'
|
|
7
|
-
import { login } from '@/api/auth'
|
|
8
|
-
import { setTokenInfo } from '@/utils/auth'
|
|
9
|
-
import { getAppId, getClientId } from '@/utils/config'
|
|
10
|
-
|
|
11
|
-
const router = useRouter()
|
|
12
|
-
const route = useRoute()
|
|
13
|
-
|
|
14
|
-
const loading = ref(false)
|
|
15
|
-
const rememberMe = ref(false)
|
|
16
|
-
|
|
17
|
-
const formData = reactive({
|
|
18
|
-
uid: '',
|
|
19
|
-
password: ''
|
|
20
|
-
})
|
|
21
|
-
|
|
22
|
-
const rules: Record<string, any[]> = {
|
|
23
|
-
uid: [
|
|
24
|
-
{ required: true, message: '请输入用户名', trigger: 'blur' }
|
|
25
|
-
],
|
|
26
|
-
password: [
|
|
27
|
-
{ required: true, message: '请输入密码', trigger: 'blur' },
|
|
28
|
-
{ min: 6, message: '密码长度至少6位', trigger: 'blur' }
|
|
29
|
-
]
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const formRef = ref()
|
|
33
|
-
|
|
34
|
-
// 登录
|
|
35
|
-
const handleLogin = async () => {
|
|
36
|
-
try {
|
|
37
|
-
await formRef.value?.validate()
|
|
38
|
-
loading.value = true
|
|
39
|
-
|
|
40
|
-
// 调用登录 API
|
|
41
|
-
const result = await login({
|
|
42
|
-
appId: getAppId(),
|
|
43
|
-
clientId: getClientId(),
|
|
44
|
-
uid: formData.uid,
|
|
45
|
-
password: formData.password,
|
|
46
|
-
code: true
|
|
47
|
-
})
|
|
48
|
-
|
|
49
|
-
// 保存 token
|
|
50
|
-
setTokenInfo(result)
|
|
51
|
-
|
|
52
|
-
Message.success('登录成功')
|
|
53
|
-
|
|
54
|
-
// 获取重定向地址
|
|
55
|
-
const redirect = route.query.redirect as string || '/'
|
|
56
|
-
|
|
57
|
-
// 跳转到目标页面(路由守卫会自动获取用户信息和菜单)
|
|
58
|
-
router.push(redirect)
|
|
59
|
-
} catch (error) {
|
|
60
|
-
console.error('登录失败:', error)
|
|
61
|
-
} finally {
|
|
62
|
-
loading.value = false
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
</script>
|
|
66
|
-
|
|
67
|
-
<template>
|
|
68
|
-
<div class="login">
|
|
69
|
-
<div class="login__container">
|
|
70
|
-
<div class="login__header">
|
|
71
|
-
<img src="/vite.svg" alt="Logo" class="login__logo" />
|
|
72
|
-
<h1 class="login__title">Xto Demo</h1>
|
|
73
|
-
<p class="login__subtitle">后台管理系统</p>
|
|
74
|
-
</div>
|
|
75
|
-
|
|
76
|
-
<Form
|
|
77
|
-
ref="formRef"
|
|
78
|
-
:model="formData"
|
|
79
|
-
:rules="rules"
|
|
80
|
-
class="login__form"
|
|
81
|
-
label-width="0"
|
|
82
|
-
>
|
|
83
|
-
<FormItem prop="uid">
|
|
84
|
-
<Input
|
|
85
|
-
v-model="formData.uid"
|
|
86
|
-
placeholder="用户名"
|
|
87
|
-
size="large"
|
|
88
|
-
>
|
|
89
|
-
<template #prefix>
|
|
90
|
-
<Icon name="user" :size="18" />
|
|
91
|
-
</template>
|
|
92
|
-
</Input>
|
|
93
|
-
</FormItem>
|
|
94
|
-
|
|
95
|
-
<FormItem prop="password">
|
|
96
|
-
<Input
|
|
97
|
-
v-model="formData.password"
|
|
98
|
-
type="password"
|
|
99
|
-
placeholder="密码"
|
|
100
|
-
size="large"
|
|
101
|
-
show-password
|
|
102
|
-
@keyup.enter="handleLogin"
|
|
103
|
-
>
|
|
104
|
-
<template #prefix>
|
|
105
|
-
<Icon name="lock" :size="18" />
|
|
106
|
-
</template>
|
|
107
|
-
</Input>
|
|
108
|
-
</FormItem>
|
|
109
|
-
|
|
110
|
-
<FormItem>
|
|
111
|
-
<Checkbox v-model="rememberMe">记住我</Checkbox>
|
|
112
|
-
</FormItem>
|
|
113
|
-
|
|
114
|
-
<FormItem>
|
|
115
|
-
<Button
|
|
116
|
-
type="primary"
|
|
117
|
-
size="large"
|
|
118
|
-
:loading="loading"
|
|
119
|
-
class="login__submit"
|
|
120
|
-
@click="handleLogin"
|
|
121
|
-
>
|
|
122
|
-
登录
|
|
123
|
-
</Button>
|
|
124
|
-
</FormItem>
|
|
125
|
-
</Form>
|
|
126
|
-
|
|
127
|
-
<div class="login__footer">
|
|
128
|
-
<p>请输入您的用户名和密码</p>
|
|
129
|
-
</div>
|
|
130
|
-
</div>
|
|
131
|
-
</div>
|
|
132
|
-
</template>
|
|
133
|
-
|
|
134
|
-
<style lang="scss" scoped>
|
|
135
|
-
.login {
|
|
136
|
-
width: 100%;
|
|
137
|
-
min-height: 100vh;
|
|
138
|
-
display: flex;
|
|
139
|
-
align-items: center;
|
|
140
|
-
justify-content: flex-end;
|
|
141
|
-
padding-right: 15%;
|
|
142
|
-
background: linear-gradient(135deg, var(--color-primary-light-9) 0%, var(--color-primary-light-7) 100%);
|
|
143
|
-
|
|
144
|
-
&__container {
|
|
145
|
-
width: 400px;
|
|
146
|
-
padding: 40px;
|
|
147
|
-
background-color: var(--bg-color);
|
|
148
|
-
border-radius: var(--border-radius-large);
|
|
149
|
-
box-shadow: var(--box-shadow-dark);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
&__header {
|
|
153
|
-
text-align: center;
|
|
154
|
-
margin-bottom: 30px;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
&__logo {
|
|
158
|
-
width: 60px;
|
|
159
|
-
height: 60px;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
&__title {
|
|
163
|
-
font-size: 28px;
|
|
164
|
-
font-weight: 600;
|
|
165
|
-
color: var(--color-primary);
|
|
166
|
-
margin: 15px 0 5px;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
&__subtitle {
|
|
170
|
-
font-size: 14px;
|
|
171
|
-
color: var(--color-text-secondary);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
&__form {
|
|
175
|
-
:deep(.x-form-item) {
|
|
176
|
-
margin-bottom: 20px;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
:deep(.x-input__prefix) {
|
|
180
|
-
margin-right: 8px;
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
&__submit {
|
|
185
|
-
width: 100%;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
&__footer {
|
|
189
|
-
text-align: center;
|
|
190
|
-
margin-top: 20px;
|
|
191
|
-
font-size: 12px;
|
|
192
|
-
color: var(--color-text-placeholder);
|
|
193
|
-
}
|
|
194
|
-
}
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref, reactive } from 'vue'
|
|
3
|
+
import { useRouter, useRoute } from 'vue-router'
|
|
4
|
+
import { Button, Icon } from '@xto/base'
|
|
5
|
+
import { Form, FormItem, Input, Checkbox } from '@xto/form'
|
|
6
|
+
import { Message } from '@xto/feedback'
|
|
7
|
+
import { login } from '@/api/auth'
|
|
8
|
+
import { setTokenInfo } from '@/utils/auth'
|
|
9
|
+
import { getAppId, getClientId } from '@/utils/config'
|
|
10
|
+
|
|
11
|
+
const router = useRouter()
|
|
12
|
+
const route = useRoute()
|
|
13
|
+
|
|
14
|
+
const loading = ref(false)
|
|
15
|
+
const rememberMe = ref(false)
|
|
16
|
+
|
|
17
|
+
const formData = reactive({
|
|
18
|
+
uid: '',
|
|
19
|
+
password: ''
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
const rules: Record<string, any[]> = {
|
|
23
|
+
uid: [
|
|
24
|
+
{ required: true, message: '请输入用户名', trigger: 'blur' }
|
|
25
|
+
],
|
|
26
|
+
password: [
|
|
27
|
+
{ required: true, message: '请输入密码', trigger: 'blur' },
|
|
28
|
+
{ min: 6, message: '密码长度至少6位', trigger: 'blur' }
|
|
29
|
+
]
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const formRef = ref()
|
|
33
|
+
|
|
34
|
+
// 登录
|
|
35
|
+
const handleLogin = async () => {
|
|
36
|
+
try {
|
|
37
|
+
await formRef.value?.validate()
|
|
38
|
+
loading.value = true
|
|
39
|
+
|
|
40
|
+
// 调用登录 API
|
|
41
|
+
const result = await login({
|
|
42
|
+
appId: getAppId(),
|
|
43
|
+
clientId: getClientId(),
|
|
44
|
+
uid: formData.uid,
|
|
45
|
+
password: formData.password,
|
|
46
|
+
code: true
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
// 保存 token
|
|
50
|
+
setTokenInfo(result)
|
|
51
|
+
|
|
52
|
+
Message.success('登录成功')
|
|
53
|
+
|
|
54
|
+
// 获取重定向地址
|
|
55
|
+
const redirect = route.query.redirect as string || '/'
|
|
56
|
+
|
|
57
|
+
// 跳转到目标页面(路由守卫会自动获取用户信息和菜单)
|
|
58
|
+
router.push(redirect)
|
|
59
|
+
} catch (error) {
|
|
60
|
+
console.error('登录失败:', error)
|
|
61
|
+
} finally {
|
|
62
|
+
loading.value = false
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
</script>
|
|
66
|
+
|
|
67
|
+
<template>
|
|
68
|
+
<div class="login">
|
|
69
|
+
<div class="login__container">
|
|
70
|
+
<div class="login__header">
|
|
71
|
+
<img src="/vite.svg" alt="Logo" class="login__logo" />
|
|
72
|
+
<h1 class="login__title">Xto Demo</h1>
|
|
73
|
+
<p class="login__subtitle">后台管理系统</p>
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
<Form
|
|
77
|
+
ref="formRef"
|
|
78
|
+
:model="formData"
|
|
79
|
+
:rules="rules"
|
|
80
|
+
class="login__form"
|
|
81
|
+
label-width="0"
|
|
82
|
+
>
|
|
83
|
+
<FormItem prop="uid">
|
|
84
|
+
<Input
|
|
85
|
+
v-model="formData.uid"
|
|
86
|
+
placeholder="用户名"
|
|
87
|
+
size="large"
|
|
88
|
+
>
|
|
89
|
+
<template #prefix>
|
|
90
|
+
<Icon name="user" :size="18" />
|
|
91
|
+
</template>
|
|
92
|
+
</Input>
|
|
93
|
+
</FormItem>
|
|
94
|
+
|
|
95
|
+
<FormItem prop="password">
|
|
96
|
+
<Input
|
|
97
|
+
v-model="formData.password"
|
|
98
|
+
type="password"
|
|
99
|
+
placeholder="密码"
|
|
100
|
+
size="large"
|
|
101
|
+
show-password
|
|
102
|
+
@keyup.enter="handleLogin"
|
|
103
|
+
>
|
|
104
|
+
<template #prefix>
|
|
105
|
+
<Icon name="lock" :size="18" />
|
|
106
|
+
</template>
|
|
107
|
+
</Input>
|
|
108
|
+
</FormItem>
|
|
109
|
+
|
|
110
|
+
<FormItem>
|
|
111
|
+
<Checkbox v-model="rememberMe">记住我</Checkbox>
|
|
112
|
+
</FormItem>
|
|
113
|
+
|
|
114
|
+
<FormItem>
|
|
115
|
+
<Button
|
|
116
|
+
type="primary"
|
|
117
|
+
size="large"
|
|
118
|
+
:loading="loading"
|
|
119
|
+
class="login__submit"
|
|
120
|
+
@click="handleLogin"
|
|
121
|
+
>
|
|
122
|
+
登录
|
|
123
|
+
</Button>
|
|
124
|
+
</FormItem>
|
|
125
|
+
</Form>
|
|
126
|
+
|
|
127
|
+
<div class="login__footer">
|
|
128
|
+
<p>请输入您的用户名和密码</p>
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
132
|
+
</template>
|
|
133
|
+
|
|
134
|
+
<style lang="scss" scoped>
|
|
135
|
+
.login {
|
|
136
|
+
width: 100%;
|
|
137
|
+
min-height: 100vh;
|
|
138
|
+
display: flex;
|
|
139
|
+
align-items: center;
|
|
140
|
+
justify-content: flex-end;
|
|
141
|
+
padding-right: 15%;
|
|
142
|
+
background: linear-gradient(135deg, var(--color-primary-light-9) 0%, var(--color-primary-light-7) 100%);
|
|
143
|
+
|
|
144
|
+
&__container {
|
|
145
|
+
width: 400px;
|
|
146
|
+
padding: 40px;
|
|
147
|
+
background-color: var(--bg-color);
|
|
148
|
+
border-radius: var(--border-radius-large);
|
|
149
|
+
box-shadow: var(--box-shadow-dark);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
&__header {
|
|
153
|
+
text-align: center;
|
|
154
|
+
margin-bottom: 30px;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
&__logo {
|
|
158
|
+
width: 60px;
|
|
159
|
+
height: 60px;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
&__title {
|
|
163
|
+
font-size: 28px;
|
|
164
|
+
font-weight: 600;
|
|
165
|
+
color: var(--color-primary);
|
|
166
|
+
margin: 15px 0 5px;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
&__subtitle {
|
|
170
|
+
font-size: 14px;
|
|
171
|
+
color: var(--color-text-secondary);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
&__form {
|
|
175
|
+
:deep(.x-form-item) {
|
|
176
|
+
margin-bottom: 20px;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
:deep(.x-input__prefix) {
|
|
180
|
+
margin-right: 8px;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
&__submit {
|
|
185
|
+
width: 100%;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
&__footer {
|
|
189
|
+
text-align: center;
|
|
190
|
+
margin-top: 20px;
|
|
191
|
+
font-size: 12px;
|
|
192
|
+
color: var(--color-text-placeholder);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
195
|
</style>
|