xto-fronted 0.4.6 → 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.
Files changed (160) hide show
  1. package/.env.development +7 -7
  2. package/.env.production +7 -7
  3. package/dist/assets/403-AFBQifUI.js +1 -0
  4. package/dist/assets/403-BHEXXbt2.css +1 -0
  5. package/dist/assets/404-Ct_A1n7S.css +1 -0
  6. package/dist/assets/404-WFvpcD2_.js +1 -0
  7. package/dist/assets/_plugin-vue_export-helper-DlAUqK2U.js +1 -0
  8. package/dist/assets/index-1juADvYN.js +2 -0
  9. package/dist/assets/index-B-sX4Ru0.js +1 -0
  10. package/dist/assets/index-BMcziU5a.css +1 -0
  11. package/dist/assets/index-BRR97dc6.js +1 -0
  12. package/dist/assets/index-BZA0ksjx.css +1 -0
  13. package/dist/assets/index-BpV_8nl0.js +1 -0
  14. package/dist/assets/index-BvzhR4zp.js +1 -0
  15. package/dist/assets/index-CUh_s55Z.css +1 -0
  16. package/dist/assets/index-CVjdnIgR.css +1 -0
  17. package/dist/assets/index-CYq57-zj.js +1 -0
  18. package/dist/assets/index-CkL3sVAQ.js +2 -0
  19. package/dist/assets/index-CtrKVYJb.css +1 -0
  20. package/dist/assets/index-Cz2P_bsS.js +1 -0
  21. package/dist/assets/index-D9wlAuR_.js +1 -0
  22. package/dist/assets/index-DawJb02s.css +1 -0
  23. package/dist/assets/index-DfFR6NLf.js +1 -0
  24. package/dist/assets/index-DwVgMO8e.js +1 -0
  25. package/dist/assets/index-GDP-IkXE.css +1 -0
  26. package/dist/assets/index-Iaz1ZzPC.js +2 -0
  27. package/dist/assets/index-PfV8pzQz.css +1 -0
  28. package/dist/assets/index-Swfu6yvD.css +1 -0
  29. package/dist/assets/index-Te8_PRgJ.js +1 -0
  30. package/dist/assets/index-WyZ91RLx.css +1 -0
  31. package/dist/assets/index-tFYRoFdE.js +1 -0
  32. package/dist/assets/vendor-42ANG6Sg.js +6 -0
  33. package/dist/assets/vite-Dw-pgLOX.js +1 -0
  34. package/dist/assets/vue-vendor-Br-l7wbK.js +29 -0
  35. package/dist/assets/xto-base-C-IBqjVs.js +1 -0
  36. package/dist/assets/xto-base-C6eqMPdO.css +1 -0
  37. package/dist/assets/xto-business--V1F5Gwb.css +1 -0
  38. package/dist/assets/xto-core-DZK7Cyg0.js +1 -0
  39. package/dist/assets/xto-data-BFpiDgJi.js +1 -0
  40. package/dist/assets/xto-data-CnAQAQH2.css +1 -0
  41. package/dist/assets/xto-feedback-B7ipsTfz.js +1 -0
  42. package/dist/assets/xto-feedback-DBwJzoTj.css +1 -0
  43. package/dist/assets/xto-form-CrsyAjyr.css +1 -0
  44. package/dist/assets/xto-form-NRjKKNcY.js +1 -0
  45. package/dist/assets/xto-layout-BqU8RuWL.css +1 -0
  46. package/dist/assets/xto-navigation-BiSaXPfr.js +1 -0
  47. package/dist/assets/xto-navigation-C1cnSL2E.css +1 -0
  48. package/dist/assets/xto-navigation-CBPg4dCc.css +1 -0
  49. package/dist/assets/xto-navigation-CKabFu9d.js +1 -0
  50. package/dist/index-54irhCHL.js +1830 -0
  51. package/dist/{index-15Bu0M8D.js → index-BzRf1eoJ.js} +1 -1
  52. package/dist/{index-BO2Zf9u6.js → index-DH4aoCZb.js} +1 -1
  53. package/dist/{index-BBqvHkzE.js → index-Kqa7iZ9E.js} +1 -1
  54. package/dist/{index-BQqo0ZIb.js → index-pxkZlvBw.js} +1 -1
  55. package/dist/index.es.js +1 -1
  56. package/dist/index.html +28 -0
  57. package/dist/index.umd.js +1 -1
  58. package/dist/style.css +1 -1
  59. package/package.json +85 -86
  60. package/src/api/auth.ts +25 -25
  61. package/src/api/system.ts +66 -66
  62. package/src/assets/styles/_dark.scss +406 -406
  63. package/src/components/Layout/Header.vue +973 -973
  64. package/src/components/Layout/Sidebar.vue +273 -212
  65. package/src/components/Layout/index.vue +443 -63
  66. package/src/composables/useApp.ts +61 -61
  67. package/src/composables/useAuth.ts +16 -16
  68. package/src/directives/permission.ts +27 -27
  69. package/src/env.d.ts +18 -18
  70. package/src/index.ts +47 -47
  71. package/src/router/dynamicRoutes.ts +162 -162
  72. package/src/router/guards.ts +128 -128
  73. package/src/router/index.ts +79 -79
  74. package/src/stores/auth.ts +65 -65
  75. package/src/stores/menu.ts +48 -48
  76. package/src/stores/user.ts +50 -50
  77. package/src/types/api.d.ts +80 -80
  78. package/src/utils/auth.ts +99 -99
  79. package/src/utils/config.ts +80 -80
  80. package/src/utils/permission.ts +32 -32
  81. package/src/utils/request.ts +124 -124
  82. package/src/views/login/index.vue +194 -188
  83. package/vite.config.ts +135 -135
  84. package/dist/index-B3PLzNB0.js +0 -345
  85. package/dist/index-B6DTsC6l.js +0 -1715
  86. package/dist/index-B7etKk33.js +0 -372
  87. package/dist/index-B7mpL6Zf.js +0 -475
  88. package/dist/index-BC2PGkkJ.js +0 -1644
  89. package/dist/index-BGgbfcmf.js +0 -475
  90. package/dist/index-BGmUwemj.js +0 -372
  91. package/dist/index-BQFfQj5Q.js +0 -142
  92. package/dist/index-BkRneTya.js +0 -142
  93. package/dist/index-BlRrngsc.js +0 -475
  94. package/dist/index-BmVvM7sm.js +0 -345
  95. package/dist/index-Bn4ThpX9.js +0 -142
  96. package/dist/index-BwfjwDKr.js +0 -1477
  97. package/dist/index-BxIL2hrt.js +0 -475
  98. package/dist/index-C-3fhbN2.js +0 -1644
  99. package/dist/index-C0VN9nFF.js +0 -142
  100. package/dist/index-C0xyGOsz.js +0 -475
  101. package/dist/index-C3c8NAZq.js +0 -1477
  102. package/dist/index-C42VtP71.js +0 -142
  103. package/dist/index-C6Nm0r9k.js +0 -475
  104. package/dist/index-C6w0-8xN.js +0 -1648
  105. package/dist/index-CD364XjV.js +0 -142
  106. package/dist/index-CHww99-i.js +0 -345
  107. package/dist/index-CIgWYERJ.js +0 -1644
  108. package/dist/index-CTs6DTuQ.js +0 -345
  109. package/dist/index-CWRs4WMN.js +0 -372
  110. package/dist/index-Cb-SxHJp.js +0 -345
  111. package/dist/index-CeCysOnl.js +0 -345
  112. package/dist/index-Cg1UpC8D.js +0 -1644
  113. package/dist/index-Cgkqpyx2.js +0 -345
  114. package/dist/index-CiuDEfo-.js +0 -142
  115. package/dist/index-CmQfZC8r.js +0 -372
  116. package/dist/index-CmkjhpX_.js +0 -475
  117. package/dist/index-CpxpXTQX.js +0 -1462
  118. package/dist/index-CqXFk_ET.js +0 -345
  119. package/dist/index-Cqix1YLE.js +0 -1697
  120. package/dist/index-CtvB5J9E.js +0 -372
  121. package/dist/index-Cu3Z2-PY.js +0 -345
  122. package/dist/index-CvDxK7Ab.js +0 -372
  123. package/dist/index-D-FER0vJ.js +0 -372
  124. package/dist/index-D2fQ8TK8.js +0 -475
  125. package/dist/index-D3xVcFvg.js +0 -372
  126. package/dist/index-D4crnrO6.js +0 -142
  127. package/dist/index-D7EzwTM5.js +0 -372
  128. package/dist/index-D7TZamyY.js +0 -1664
  129. package/dist/index-D88fiqXR.js +0 -475
  130. package/dist/index-DEbpF-M4.js +0 -1457
  131. package/dist/index-DFXuyPge.js +0 -1627
  132. package/dist/index-DLgimJYb.js +0 -1667
  133. package/dist/index-DPEVEyik.js +0 -475
  134. package/dist/index-DWy_UGhI.js +0 -345
  135. package/dist/index-DYVtddfw.js +0 -142
  136. package/dist/index-DYnXaqYf.js +0 -142
  137. package/dist/index-DcvRPHuy.js +0 -372
  138. package/dist/index-DdC1uV2v.js +0 -1700
  139. package/dist/index-Dga14ZN7.js +0 -1774
  140. package/dist/index-Dk2V44uP.js +0 -372
  141. package/dist/index-DnJ481u1.js +0 -475
  142. package/dist/index-Do1CBqg8.js +0 -345
  143. package/dist/index-DqQRSPeF.js +0 -345
  144. package/dist/index-Jb4VMHIS.js +0 -142
  145. package/dist/index-MC3wWjNt.js +0 -475
  146. package/dist/index-MG0JePmx.js +0 -142
  147. package/dist/index-PRFGBLWt.js +0 -475
  148. package/dist/index-QgkT42dc.js +0 -372
  149. package/dist/index-TrLCW5xL.js +0 -372
  150. package/dist/index-YDlNLFVk.js +0 -142
  151. package/dist/index-ZAJgA3XD.js +0 -475
  152. package/dist/index-a_ilWAvi.js +0 -345
  153. package/dist/index-bi1TMGid.js +0 -372
  154. package/dist/index-fyarVCog.js +0 -475
  155. package/dist/index-mnTZtPFa.js +0 -345
  156. package/dist/index-orZCyV6I.js +0 -345
  157. package/dist/index-p3TbK44c.js +0 -142
  158. package/dist/index-sRwZYbZ4.js +0 -372
  159. package/dist/index-wATqKEcF.js +0 -142
  160. package/dist/setup.d.ts +0 -17
@@ -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,189 +1,195 @@
1
- <script setup lang="ts">
2
- import { ref, reactive } from 'vue'
3
- import { useRouter, useRoute } from 'vue-router'
4
- import { Button } 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
- prefix-icon="👤"
88
- size="large"
89
- />
90
- </FormItem>
91
-
92
- <FormItem prop="password">
93
- <Input
94
- v-model="formData.password"
95
- type="password"
96
- placeholder="密码"
97
- prefix-icon="🔒"
98
- size="large"
99
- show-password
100
- @keyup.enter="handleLogin"
101
- />
102
- </FormItem>
103
-
104
- <FormItem>
105
- <Checkbox v-model="rememberMe">记住我</Checkbox>
106
- </FormItem>
107
-
108
- <FormItem>
109
- <Button
110
- type="primary"
111
- size="large"
112
- :loading="loading"
113
- class="login__submit"
114
- @click="handleLogin"
115
- >
116
- 登录
117
- </Button>
118
- </FormItem>
119
- </Form>
120
-
121
- <div class="login__footer">
122
- <p>请输入您的用户名和密码</p>
123
- </div>
124
- </div>
125
- </div>
126
- </template>
127
-
128
- <style lang="scss" scoped>
129
- .login {
130
- width: 100%;
131
- min-height: 100vh;
132
- display: flex;
133
- align-items: center;
134
- justify-content: flex-end;
135
- padding-right: 15%;
136
- background: linear-gradient(135deg, var(--color-primary-light-9) 0%, var(--color-primary-light-7) 100%);
137
-
138
- &__container {
139
- width: 400px;
140
- padding: 40px;
141
- background-color: var(--bg-color);
142
- border-radius: var(--border-radius-large);
143
- box-shadow: var(--box-shadow-dark);
144
- }
145
-
146
- &__header {
147
- text-align: center;
148
- margin-bottom: 30px;
149
- }
150
-
151
- &__logo {
152
- width: 60px;
153
- height: 60px;
154
- }
155
-
156
- &__title {
157
- font-size: 28px;
158
- font-weight: 600;
159
- color: var(--color-primary);
160
- margin: 15px 0 5px;
161
- }
162
-
163
- &__subtitle {
164
- font-size: 14px;
165
- color: var(--color-text-secondary);
166
- }
167
-
168
- &__form {
169
- :deep(.x-form-item) {
170
- margin-bottom: 20px;
171
- }
172
-
173
- :deep(.x-input__prefix) {
174
- margin-right: 8px;
175
- }
176
- }
177
-
178
- &__submit {
179
- width: 100%;
180
- }
181
-
182
- &__footer {
183
- text-align: center;
184
- margin-top: 20px;
185
- font-size: 12px;
186
- color: var(--color-text-placeholder);
187
- }
188
- }
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
+ }
189
195
  </style>