LoginView.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479
  1. <template>
  2. <div class="auth-view">
  3. <div class="auth-container">
  4. <div class="auth-decoration">
  5. <div class="decoration-circle circle-1"></div>
  6. <div class="decoration-circle circle-2"></div>
  7. <div class="decoration-circle circle-3"></div>
  8. </div>
  9. <el-card class="auth-card" shadow="never">
  10. <el-button class="back-btn" text @click="router.push('/problems')">
  11. <el-icon><ArrowLeft /></el-icon>
  12. 返回主页
  13. </el-button>
  14. <div class="auth-header">
  15. <h1 class="auth-title">欢迎</h1>
  16. <p class="auth-subtitle">请登录或注册您的账号</p>
  17. </div>
  18. <el-tabs v-model="activeTab" class="auth-tabs" stretch>
  19. <el-tab-pane label="登录" name="login">
  20. <el-form
  21. ref="loginFormRef"
  22. :model="loginForm"
  23. :rules="loginRules"
  24. class="auth-form"
  25. @submit.prevent="handleLogin"
  26. >
  27. <el-form-item prop="account">
  28. <el-input
  29. v-model="loginForm.account"
  30. placeholder="请输入账号"
  31. :prefix-icon="User"
  32. size="large"
  33. />
  34. </el-form-item>
  35. <el-form-item prop="password">
  36. <el-input
  37. v-model="loginForm.password"
  38. type="password"
  39. placeholder="请输入密码"
  40. :prefix-icon="Lock"
  41. show-password
  42. size="large"
  43. />
  44. </el-form-item>
  45. <el-form-item>
  46. <el-button
  47. type="primary"
  48. :loading="loginLoading"
  49. size="large"
  50. class="auth-submit"
  51. @click="handleLogin"
  52. >
  53. 登录
  54. </el-button>
  55. </el-form-item>
  56. </el-form>
  57. </el-tab-pane>
  58. <el-tab-pane label="注册" name="register">
  59. <el-form
  60. ref="registerFormRef"
  61. :model="registerForm"
  62. :rules="registerRules"
  63. class="auth-form"
  64. @submit.prevent="handleRegister"
  65. >
  66. <el-form-item prop="account">
  67. <el-input
  68. v-model="registerForm.account"
  69. placeholder="请输入账号"
  70. :prefix-icon="User"
  71. size="large"
  72. />
  73. </el-form-item>
  74. <el-form-item prop="username">
  75. <el-input
  76. v-model="registerForm.username"
  77. placeholder="请输入用户名"
  78. :prefix-icon="User"
  79. size="large"
  80. />
  81. </el-form-item>
  82. <el-form-item prop="password">
  83. <el-input
  84. v-model="registerForm.password"
  85. type="password"
  86. placeholder="请输入密码"
  87. :prefix-icon="Lock"
  88. show-password
  89. size="large"
  90. />
  91. </el-form-item>
  92. <el-form-item prop="confirmPassword">
  93. <el-input
  94. v-model="registerForm.confirmPassword"
  95. type="password"
  96. placeholder="请再次输入密码"
  97. :prefix-icon="Lock"
  98. show-password
  99. size="large"
  100. />
  101. </el-form-item>
  102. <el-form-item>
  103. <el-button
  104. type="primary"
  105. :loading="registerLoading"
  106. size="large"
  107. class="auth-submit"
  108. @click="handleRegister"
  109. >
  110. 注册
  111. </el-button>
  112. </el-form-item>
  113. </el-form>
  114. </el-tab-pane>
  115. </el-tabs>
  116. </el-card>
  117. </div>
  118. </div>
  119. </template>
  120. <script setup lang="ts">
  121. import { reactive, ref } from 'vue'
  122. import { useRoute, useRouter } from 'vue-router'
  123. import { ElMessage } from 'element-plus'
  124. import { User, Lock, ArrowLeft } from '@element-plus/icons-vue'
  125. import type { FormInstance, FormRules } from 'element-plus'
  126. import { getAuthController } from '../api/auth-controller'
  127. import type { LoginDto, RegisterDto } from '../api/models'
  128. import { useLoginUserStore } from '../store'
  129. import type { User as UserType } from '../type'
  130. const route = useRoute()
  131. const router = useRouter()
  132. const loginUserStore = useLoginUserStore()
  133. const activeTab = ref(route.query.tab === 'register' ? 'register' : 'login')
  134. const loginFormRef = ref<FormInstance>()
  135. const registerFormRef = ref<FormInstance>()
  136. const loginLoading = ref(false)
  137. const registerLoading = ref(false)
  138. const loginForm = reactive({
  139. account: '',
  140. password: ''
  141. })
  142. const registerForm = reactive({
  143. account: '',
  144. username: '',
  145. password: '',
  146. confirmPassword: ''
  147. })
  148. const validateConfirmPassword = (_rule: any, value: string, callback: any) => {
  149. if (value !== registerForm.password) {
  150. callback(new Error('两次输入密码不一致'))
  151. } else {
  152. callback()
  153. }
  154. }
  155. const loginRules = reactive<FormRules>({
  156. account: [{ required: true, message: '请输入账号', trigger: 'blur' }],
  157. password: [
  158. { required: true, message: '请输入密码', trigger: 'blur' },
  159. { min: 4, message: '密码长度至少4位', trigger: 'blur' }
  160. ]
  161. })
  162. const registerRules = reactive<FormRules>({
  163. account: [
  164. { required: true, message: '请输入账号', trigger: 'blur' },
  165. { min: 4, max: 20, message: '账号长度4-20个字符', trigger: 'blur' }
  166. ],
  167. username: [
  168. { required: true, message: '请输入用户名', trigger: 'blur' },
  169. { min: 2, max: 32, message: '用户名长度2-32个字符', trigger: 'blur' }
  170. ],
  171. password: [
  172. { required: true, message: '请输入密码', trigger: 'blur' },
  173. { min: 4, max: 20, message: '密码长度4-20个字符', trigger: 'blur' }
  174. ],
  175. confirmPassword: [
  176. { required: true, message: '请再次输入密码', trigger: 'blur' },
  177. { validator: validateConfirmPassword, trigger: 'blur' }
  178. ]
  179. })
  180. const handleLogin = async () => {
  181. const valid = await loginFormRef.value?.validate().catch(() => false)
  182. if (!valid) return
  183. loginLoading.value = true
  184. try {
  185. const authApi = getAuthController()
  186. const res = await authApi.login({
  187. account: loginForm.account,
  188. password: loginForm.password
  189. } as LoginDto)
  190. const tokenData = res.data
  191. if (res.code === 200 && tokenData?.token) {
  192. localStorage.setItem('token', tokenData.token)
  193. loginUserStore.loginSuccess({
  194. id: String(tokenData.userId || ''),
  195. name: tokenData.username || tokenData.account || '用户',
  196. role: (tokenData.role as UserType['role']) || 'user'
  197. })
  198. ElMessage.success('登录成功')
  199. router.push((route.query.redirect as string) || '/')
  200. }
  201. } catch {
  202. } finally {
  203. loginLoading.value = false
  204. }
  205. }
  206. const handleRegister = async () => {
  207. const valid = await registerFormRef.value?.validate().catch(() => false)
  208. if (!valid) return
  209. registerLoading.value = true
  210. try {
  211. const authApi = getAuthController()
  212. const res = await authApi.register1({
  213. account: registerForm.account,
  214. password: registerForm.password,
  215. username: registerForm.username
  216. } as RegisterDto)
  217. if (res.code === 200) {
  218. ElMessage.success('注册成功,请登录')
  219. activeTab.value = 'login'
  220. registerForm.account = ''
  221. registerForm.username = ''
  222. registerForm.password = ''
  223. registerForm.confirmPassword = ''
  224. }
  225. } catch {
  226. } finally {
  227. registerLoading.value = false
  228. }
  229. }
  230. </script>
  231. <style scoped>
  232. .auth-view {
  233. display: flex;
  234. justify-content: center;
  235. align-items: center;
  236. min-height: 100vh;
  237. background: linear-gradient(
  238. 135deg,
  239. var(--el-color-primary-light-9) 0%,
  240. var(--el-color-primary-light-8) 50%,
  241. var(--el-color-primary-light-7) 100%
  242. );
  243. position: relative;
  244. overflow: hidden;
  245. }
  246. .auth-decoration {
  247. position: absolute;
  248. inset: 0;
  249. pointer-events: none;
  250. }
  251. .decoration-circle {
  252. position: absolute;
  253. border-radius: 50%;
  254. opacity: 0.15;
  255. }
  256. .circle-1 {
  257. width: 400px;
  258. height: 400px;
  259. background: var(--el-color-primary);
  260. top: -100px;
  261. right: -100px;
  262. animation: float 20s ease-in-out infinite;
  263. }
  264. .circle-2 {
  265. width: 300px;
  266. height: 300px;
  267. background: var(--el-color-primary-light-3);
  268. bottom: -80px;
  269. left: -80px;
  270. animation: float 15s ease-in-out infinite reverse;
  271. }
  272. .circle-3 {
  273. width: 200px;
  274. height: 200px;
  275. background: var(--el-color-warning);
  276. top: 50%;
  277. left: 60%;
  278. animation: float 18s ease-in-out infinite;
  279. }
  280. @keyframes float {
  281. 0%,
  282. 100% {
  283. transform: translate(0, 0) scale(1);
  284. }
  285. 25% {
  286. transform: translate(20px, -30px) scale(1.05);
  287. }
  288. 50% {
  289. transform: translate(-10px, 20px) scale(0.95);
  290. }
  291. 75% {
  292. transform: translate(15px, 10px) scale(1.02);
  293. }
  294. }
  295. .auth-container {
  296. position: relative;
  297. z-index: 1;
  298. width: 100%;
  299. max-width: 420px;
  300. padding: 20px;
  301. }
  302. .auth-card {
  303. border-radius: 16px;
  304. border: 1px solid color-mix(in srgb, var(--el-color-primary) 10%, transparent);
  305. background: rgba(255, 255, 255, 0.9);
  306. backdrop-filter: blur(10px);
  307. padding: 8px;
  308. }
  309. .back-btn {
  310. position: absolute;
  311. top: 16px;
  312. left: 16px;
  313. color: var(--el-color-primary);
  314. font-size: 14px;
  315. cursor: pointer;
  316. transition: color 0.2s;
  317. }
  318. .back-btn:hover {
  319. color: var(--el-color-primary-dark-2);
  320. }
  321. .back-btn .el-icon {
  322. margin-right: 4px;
  323. }
  324. .auth-header {
  325. text-align: center;
  326. padding: 24px 0 16px;
  327. }
  328. .auth-title {
  329. margin: 0 0 8px;
  330. font-size: 28px;
  331. font-weight: 600;
  332. color: var(--el-text-color-primary);
  333. letter-spacing: 2px;
  334. }
  335. .auth-subtitle {
  336. margin: 0;
  337. font-size: 14px;
  338. color: var(--el-text-color-secondary);
  339. }
  340. .auth-tabs {
  341. padding: 0 8px 8px;
  342. }
  343. :deep(.auth-tabs .el-tabs__header) {
  344. margin-bottom: 24px;
  345. }
  346. :deep(.auth-tabs .el-tabs__item) {
  347. font-size: 16px;
  348. font-weight: 500;
  349. color: var(--el-text-color-regular);
  350. transition: color 0.2s;
  351. }
  352. :deep(.auth-tabs .el-tabs__item.is-active) {
  353. color: var(--el-color-primary);
  354. }
  355. :deep(.auth-tabs .el-tabs__active-bar) {
  356. background-color: var(--el-color-primary);
  357. height: 3px;
  358. border-radius: 3px;
  359. }
  360. :deep(.auth-tabs .el-tabs__nav-wrap::after) {
  361. background-color: color-mix(in srgb, var(--el-color-primary) 15%, transparent);
  362. height: 1px;
  363. }
  364. .auth-form {
  365. padding: 0 8px;
  366. }
  367. :deep(.el-input__wrapper) {
  368. border-radius: 10px;
  369. padding: 8px 12px;
  370. box-shadow: 0 0 0 1px color-mix(in srgb, var(--el-color-primary) 15%, transparent) inset;
  371. transition: box-shadow 0.2s;
  372. }
  373. :deep(.el-input__wrapper:hover) {
  374. box-shadow: 0 0 0 1px color-mix(in srgb, var(--el-color-primary) 30%, transparent) inset;
  375. }
  376. :deep(.el-input__wrapper.is-focus) {
  377. box-shadow: 0 0 0 1px var(--el-color-primary) inset;
  378. }
  379. :deep(.el-input__prefix) {
  380. color: var(--el-color-primary);
  381. margin-right: 8px;
  382. }
  383. .auth-submit {
  384. width: 100%;
  385. border-radius: 10px;
  386. font-size: 16px;
  387. font-weight: 500;
  388. letter-spacing: 4px;
  389. transition: all 0.2s;
  390. }
  391. .auth-submit:hover {
  392. transform: translateY(-1px);
  393. box-shadow: 0 4px 12px color-mix(in srgb, var(--el-color-primary) 30%, transparent);
  394. }
  395. .auth-submit:active {
  396. transform: translateY(0);
  397. }
  398. :deep(.el-form-item) {
  399. margin-bottom: 20px;
  400. }
  401. :deep(.el-form-item__error) {
  402. padding-left: 4px;
  403. }
  404. @media (max-width: 480px) {
  405. .auth-container {
  406. padding: 16px;
  407. }
  408. .auth-title {
  409. font-size: 24px;
  410. }
  411. .circle-1 {
  412. width: 250px;
  413. height: 250px;
  414. }
  415. .circle-2 {
  416. width: 200px;
  417. height: 200px;
  418. }
  419. .circle-3 {
  420. width: 150px;
  421. height: 150px;
  422. }
  423. }
  424. </style>