loginView.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537
  1. <template>
  2. <div class="auth-wrapper">
  3. <!-- 左侧背景图区域 -->
  4. <div class="auth-left"></div>
  5. <!-- 右侧表单容器 -->
  6. <div class="auth-form-container">
  7. <!-- 登录表单 -->
  8. <el-form
  9. v-if="isLogin"
  10. ref="formRef"
  11. :model="form"
  12. :rules="rules"
  13. label-width="116px"
  14. class="login-form"
  15. >
  16. <div class="form-header">
  17. <h2 class="form-title">
  18. {{
  19. userType === "user" ? t("login.userTitle") : t("login.adminTitle")
  20. }}
  21. </h2>
  22. <el-button class="user-type-toggle" @click="toggleUserType" link>
  23. <el-icon><User /></el-icon>
  24. <span>{{ currentUserTypeName }}</span>
  25. </el-button>
  26. </div>
  27. <div class="input-frame">
  28. <el-form-item label="账号:" prop="name">
  29. <el-input v-model="form.name" />
  30. </el-form-item>
  31. </div>
  32. <div class="input-frame">
  33. <el-form-item label="密码:" prop="password">
  34. <el-input type="password" v-model="form.password" />
  35. </el-form-item>
  36. </div>
  37. <!-- 错误提示区域 -->
  38. <div v-if="showError" class="error-message">
  39. {{ errorMessage }}
  40. </div>
  41. <div class="language-toggle-wrapper">
  42. <span class="text-toggle" @click="toggleLanguage">{{
  43. currentLanguageName
  44. }}</span>
  45. </div>
  46. <el-form-item>
  47. <div class="button-group">
  48. <el-button
  49. type="primary"
  50. @click="onSubmit"
  51. :loading="loading"
  52. class="login-button"
  53. >
  54. {{ t("login.loginButton") }}
  55. </el-button>
  56. </div>
  57. <div class="text-link-wrapper">
  58. <span class="text-toggle" @click="toggleForm">{{
  59. t("login.registerLink")
  60. }}</span>
  61. </div>
  62. </el-form-item>
  63. </el-form>
  64. <!-- 注册表单 -->
  65. <el-form
  66. v-else
  67. ref="registerFormRef"
  68. :model="registerForm"
  69. :rules="registerRules"
  70. label-width="116px"
  71. class="login-form"
  72. >
  73. <div class="form-header">
  74. <h2 class="form-title">{{ t("register.title") }}</h2>
  75. <el-button class="user-type-toggle" @click="toggleUserType" link>
  76. <el-icon><User /></el-icon>
  77. <span>{{ currentUserTypeName }}</span>
  78. </el-button>
  79. </div>
  80. <div class="input-frame">
  81. <el-form-item label="账号:" prop="name">
  82. <el-input v-model="registerForm.name" />
  83. </el-form-item>
  84. </div>
  85. <div class="input-frame">
  86. <el-form-item label="密码:" prop="password">
  87. <el-input type="password" v-model="registerForm.password" />
  88. </el-form-item>
  89. </div>
  90. <div class="input-frame">
  91. <el-form-item label="确认密码:" prop="confirmPassword">
  92. <el-input type="password" v-model="registerForm.confirmPassword" />
  93. </el-form-item>
  94. </div>
  95. <!-- 错误提示区域 -->
  96. <div v-if="showError" class="error-message">
  97. {{ errorMessage }}
  98. </div>
  99. <div class="language-toggle-wrapper">
  100. <span class="text-toggle" @click="toggleLanguage">{{
  101. currentLanguageName
  102. }}</span>
  103. </div>
  104. <el-form-item>
  105. <div class="button-group">
  106. <el-button
  107. type="primary"
  108. @click="onRegister"
  109. :loading="loading"
  110. class="login-button"
  111. >
  112. {{ t("register.registerButton") }}
  113. </el-button>
  114. </div>
  115. <div class="button-group register-link-container">
  116. <span @click="toggleForm" class="register-button">{{
  117. t("register.backToLoginButton")
  118. }}</span>
  119. </div>
  120. </el-form-item>
  121. </el-form>
  122. </div>
  123. </div>
  124. </template>
  125. <script setup lang="ts">
  126. import { reactive, ref, computed, watch, onMounted } from "vue";
  127. import { ElForm } from "element-plus";
  128. import type { FormRules } from "element-plus";
  129. import { login, register } from "@/API/users";
  130. import { useTokenStore } from "@/stores/mytoken";
  131. import { useI18n } from "vue-i18n";
  132. import { User } from "@element-plus/icons-vue";
  133. import { useRouter } from "vue-router";
  134. // ============ 类型定义 ============
  135. interface LoginForm {
  136. name: string;
  137. password: string;
  138. }
  139. interface RegisterForm {
  140. name: string;
  141. password: string;
  142. confirmPassword: string;
  143. }
  144. // ============ 核心实例 ============
  145. const store = useTokenStore();
  146. const { t, locale } = useI18n();
  147. const router = useRouter();
  148. // ============ 状态 ============
  149. const isLogin = ref(true);
  150. const userType = ref<"user" | "admin">("user");
  151. const loading = ref(false);
  152. const showError = ref(false);
  153. const errorMessage = ref("");
  154. const form = reactive<LoginForm>({ name: "", password: "" });
  155. const registerForm = reactive<RegisterForm>({
  156. name: "",
  157. password: "",
  158. confirmPassword: "",
  159. });
  160. const formRef = ref<InstanceType<typeof ElForm> | null>(null);
  161. const registerFormRef = ref<InstanceType<typeof ElForm> | null>(null);
  162. // ============ 表单切换 ============
  163. const toggleForm = () => {
  164. isLogin.value = !isLogin.value;
  165. showError.value = false; // 切换表单时隐藏错误提示
  166. };
  167. const toggleUserType = () => {
  168. userType.value = userType.value === "user" ? "admin" : "user";
  169. };
  170. const toggleLanguage = () => {
  171. locale.value = locale.value === "zh" ? "en" : "zh";
  172. localStorage.setItem("lang", locale.value);
  173. };
  174. // ============ 计算属性 ============
  175. const currentLanguageName = computed(() =>
  176. locale.value === "zh" ? "English" : "中文"
  177. );
  178. const currentUserTypeName = computed(() =>
  179. userType.value === "user" ? t("login.switchToAdmin") : t("login.switchToUser")
  180. );
  181. // ============ 显示错误消息 ============
  182. const showErrorMsg = (message: string) => {
  183. errorMessage.value = message;
  184. showError.value = true;
  185. // 3秒后自动隐藏消息
  186. setTimeout(() => {
  187. showError.value = false;
  188. }, 3000);
  189. };
  190. // ============ 登录逻辑 ============
  191. const onSubmit = async () => {
  192. if (!formRef.value) return;
  193. try {
  194. // 验证表单
  195. await formRef.value.validate();
  196. loading.value = true;
  197. showError.value = false; // 开始验证时隐藏错误提示
  198. // 调用登录API
  199. const response = await login({
  200. name: form.name,
  201. password: form.password,
  202. usertype: userType.value,
  203. });
  204. console.log('完整登录响应:', response);
  205. // 检查响应结构
  206. if (!response.data?.user) {
  207. throw new Error('后端返回的用户信息不完整');
  208. }
  209. // 提取用户信息
  210. const userData = response.data.user;
  211. if (!userData.id || !userData.name) {
  212. throw new Error('缺少必要的用户字段');
  213. }
  214. // 保存用户信息到store
  215. store.saveToken({
  216. userId: Number(userData.id),
  217. name: userData.name,
  218. loginType: userData.userType || userType.value, // 优先使用后端返回的userType
  219. });
  220. // 跳转到目标页面
  221. await router.push({ name: 'CropCadmiumPrediction' });
  222. } catch (error: any) {
  223. console.error('登录失败:', {
  224. error: error.message,
  225. stack: error.stack,
  226. response: error.response?.data
  227. });
  228. // 显示错误消息
  229. const errorMsg = error.response?.data.message ||
  230. error.message ||
  231. '登录失败,请检查用户名和密码';
  232. showErrorMsg(errorMsg);
  233. } finally {
  234. loading.value = false;
  235. }
  236. };
  237. // ============ 注册逻辑 ============
  238. const onRegister = async () => {
  239. if (!registerFormRef.value) return;
  240. try {
  241. await registerFormRef.value.validate();
  242. loading.value = true;
  243. showError.value = false; // 开始验证时隐藏错误提示
  244. const res = await register({
  245. name: registerForm.name,
  246. password: registerForm.password,
  247. userType: userType.value,
  248. });
  249. if (res.data?.message) {
  250. // 注册成功后自动登录
  251. try {
  252. // 调用登录API
  253. const loginResponse = await login({
  254. name: registerForm.name,
  255. password: registerForm.password,
  256. usertype: userType.value,
  257. });
  258. // 检查登录响应结构
  259. if (loginResponse.data?.user) {
  260. const userData = loginResponse.data.user;
  261. // 保存用户信息到store
  262. store.saveToken({
  263. userId: Number(userData.id),
  264. name: userData.name,
  265. loginType: userData.userType || userType.value,
  266. });
  267. // 跳转到目标页面
  268. await router.push({ name: 'CropCadmiumPrediction' });
  269. } else {
  270. showErrorMsg(loginResponse.data?.message || '自动登录失败,请手动登录');
  271. toggleForm(); // 返回登录页面
  272. }
  273. } catch (loginError: any) {
  274. console.error('自动登录失败:', loginError);
  275. showErrorMsg(
  276. loginError?.response?.data?.message || '自动登录失败,请手动登录'
  277. );
  278. toggleForm(); // 返回登录页面
  279. }
  280. } else {
  281. showErrorMsg(res.data?.message || t("register.registerFailed"));
  282. }
  283. } catch (error: any) {
  284. console.error("注册异常:", error);
  285. showErrorMsg(
  286. error?.response?.data?.message || t("register.registerFailed")
  287. );
  288. } finally {
  289. loading.value = false;
  290. }
  291. };
  292. // ============ 校验规则 ============
  293. const createRules = (): { rules: FormRules; registerRules: FormRules } => {
  294. const rules: FormRules = {
  295. name: [
  296. {
  297. required: true,
  298. message: t("validation.usernameRequired"),
  299. trigger: "blur",
  300. },
  301. ],
  302. password: [
  303. {
  304. required: true,
  305. message: t("validation.passwordRequired"),
  306. trigger: "blur",
  307. },
  308. {
  309. min: 3,
  310. max: 16,
  311. message: t("validation.passwordLength"),
  312. trigger: "blur",
  313. },
  314. ],
  315. };
  316. const registerRules: FormRules = {
  317. name: rules.name,
  318. password: rules.password,
  319. confirmPassword: [
  320. {
  321. required: true,
  322. message: t("validation.confirmPasswordRequired"),
  323. trigger: "blur",
  324. },
  325. {
  326. validator: (_rule, value: string, callback) => {
  327. if (value !== registerForm.password)
  328. callback(new Error(t("validation.passwordMismatch")));
  329. else callback();
  330. },
  331. trigger: "blur",
  332. },
  333. ],
  334. };
  335. return { rules, registerRules };
  336. };
  337. let { rules, registerRules } = createRules();
  338. watch(locale, () => {
  339. const newRules = createRules();
  340. rules = newRules.rules;
  341. registerRules = newRules.registerRules;
  342. });
  343. watch(
  344. () => registerForm.password,
  345. () => {
  346. registerFormRef.value?.validateField("confirmPassword");
  347. }
  348. );
  349. onMounted(() => {
  350. console.log("登录/注册页初始化", {
  351. form,
  352. registerForm,
  353. userType: userType.value,
  354. locale: locale.value,
  355. });
  356. });
  357. </script>
  358. <style scoped>
  359. .auth-wrapper {
  360. display: flex;
  361. height: 100vh;
  362. background-color: #f6f6f6;
  363. position: relative;
  364. }
  365. .auth-left {
  366. width: 35%;
  367. background: url("@/assets/login-bg.png") no-repeat center/cover;
  368. }
  369. .auth-form-container {
  370. width: 55%;
  371. padding: 0 40px 0 60px;
  372. display: flex;
  373. justify-content: center;
  374. align-items: center;
  375. flex-direction: column;
  376. }
  377. .login-form {
  378. width: 100%;
  379. max-width: 700px;
  380. padding: 40px 30px;
  381. margin-top: 50px;
  382. background: rgba(255, 255, 255, 0.9);
  383. border-radius: 15px;
  384. }
  385. .form-header {
  386. display: flex;
  387. justify-content: space-between;
  388. align-items: center;
  389. margin-bottom: 40px;
  390. margin-top: 20px;
  391. }
  392. .form-title {
  393. font-size: 32px;
  394. font-weight: 600;
  395. color: #333;
  396. }
  397. .user-type-toggle {
  398. font-size: 36px;
  399. color: #333;
  400. }
  401. .user-type-toggle span {
  402. margin-left: 6px;
  403. font-size: 36px;
  404. }
  405. .language-toggle-wrapper {
  406. text-align: right;
  407. margin: 15px 0 20px;
  408. }
  409. :deep(.el-form-item__label) {
  410. float: none;
  411. display: block;
  412. text-align: left;
  413. font-size: 24px;
  414. padding-bottom: 8px;
  415. color: #7e7878;
  416. }
  417. :deep(.el-input .el-input__inner) {
  418. height: 50px;
  419. font-size: 20px;
  420. border-radius: 0;
  421. border: 1px solid #dcdfe6;
  422. background-color: #fff;
  423. padding: 0 15px;
  424. }
  425. .login-button {
  426. background: linear-gradient(to right, #8df9f0, #26b046);
  427. width: 100%;
  428. max-width: 400px;
  429. height: 56px;
  430. color: white;
  431. border: none;
  432. border-radius: 20px;
  433. font-size: 24px;
  434. cursor: pointer;
  435. margin-top: 10px;
  436. }
  437. .login-button:hover {
  438. opacity: 0.9;
  439. }
  440. .register-button {
  441. display: block;
  442. text-align: center;
  443. color: #478bf0;
  444. font-size: 18px;
  445. cursor: pointer;
  446. padding: 10px 0;
  447. width: 100%;
  448. }
  449. .register-button:hover {
  450. color: #357ae8;
  451. text-decoration: underline;
  452. }
  453. .button-group {
  454. display: flex;
  455. justify-content: center;
  456. width: 100%;
  457. }
  458. .text-toggle {
  459. color: #478bf0;
  460. font-size: 16px;
  461. cursor: pointer;
  462. }
  463. .text-toggle:hover {
  464. color: #357ae8;
  465. text-decoration: underline;
  466. }
  467. .text-link-wrapper {
  468. text-align: center;
  469. margin-top: 20px;
  470. }
  471. .input-frame {
  472. background-color: #fff;
  473. width: 100%;
  474. padding: 15px 10px;
  475. margin-bottom: 20px;
  476. max-width: 600px; /* 限制最大宽度 */
  477. }
  478. /* 错误提示样式 */
  479. .error-message {
  480. color: #f56c6c;
  481. font-size: 16px;
  482. text-align: center;
  483. margin: 15px 0;
  484. animation: fadeIn 0.3s ease-out forwards;
  485. }
  486. @keyframes fadeIn {
  487. from { opacity: 0; }
  488. to { opacity: 1; }
  489. }
  490. </style>