loginView.vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666
  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">{{ t("login.userTitle") }}</h2>
  18. </div>
  19. <div class="input-frame login-input-frame">
  20. <el-form-item label="账号:" prop="name">
  21. <el-input v-model="form.name" />
  22. </el-form-item>
  23. </div>
  24. <div class="input-frame login-input-frame">
  25. <el-form-item label="密码:" prop="password">
  26. <!-- 使用 Element Plus 的密码输入框 -->
  27. <el-input v-model="form.password" type="password" show-password />
  28. </el-form-item>
  29. </div>
  30. <!-- 错误提示区域 -->
  31. <div v-if="showError" class="error-message">
  32. {{ errorMessage }}
  33. </div>
  34. <div class="language-toggle-wrapper">
  35. <span class="text-toggle" @click="toggleLanguage">{{
  36. currentLanguageName
  37. }}</span>
  38. </div>
  39. <el-form-item>
  40. <div class="button-group">
  41. <el-button
  42. type="primary"
  43. @click="onSubmit"
  44. :loading="loading"
  45. class="login-button"
  46. >
  47. {{ t("login.loginButton") }}
  48. </el-button>
  49. </div>
  50. <div class="text-link-wrapper">
  51. <span class="text-toggle" @click="toggleForm">{{
  52. t("login.registerLink")
  53. }}</span>
  54. </div>
  55. </el-form-item>
  56. </el-form>
  57. <!-- 注册表单 -->
  58. <el-form
  59. v-else
  60. ref="registerFormRef"
  61. :model="registerForm"
  62. :rules="registerRules"
  63. class="login-form register-form"
  64. >
  65. <div class="form-header">
  66. <h2 class="form-title">{{ t("register.title") }}</h2>
  67. </div>
  68. <!-- 调整后的注册账号输入框 -->
  69. <div class="input-frame register-input-frame register-account-frame">
  70. <el-form-item label="账号:" prop="name">
  71. <el-input v-model="registerForm.name" />
  72. </el-form-item>
  73. </div>
  74. <!-- 调整后的注册密码输入框 -->
  75. <div class="input-frame register-input-frame register-password-frame">
  76. <el-form-item label="密码:" prop="password">
  77. <!-- 使用 Element Plus 的密码输入框 -->
  78. <el-input
  79. v-model="registerForm.password"
  80. type="password"
  81. show-password
  82. />
  83. </el-form-item>
  84. </div>
  85. <div class="input-frame register-input-frame">
  86. <el-form-item
  87. label="确认密码:"
  88. prop="confirmPassword"
  89. >
  90. <!-- 使用 Element Plus 的密码输入框 -->
  91. <el-input
  92. v-model="registerForm.confirmPassword"
  93. type="password"
  94. show-password
  95. />
  96. </el-form-item>
  97. </div>
  98. <!-- 成功/错误提示区域 -->
  99. <div v-if="showMessage" :class="messageType">
  100. {{ messageContent }}
  101. </div>
  102. <div class="language-toggle-wrapper">
  103. <span class="text-toggle" @click="toggleLanguage">{{
  104. currentLanguageName
  105. }}</span>
  106. </div>
  107. <el-form-item>
  108. <div class="button-group">
  109. <el-button
  110. type="primary"
  111. @click="onRegister"
  112. :loading="loading"
  113. class="login-button register-button-adjusted"
  114. >
  115. {{ t("register.registerButton") }}
  116. </el-button>
  117. </div>
  118. <div class="button-group register-link-container">
  119. <span @click="toggleForm" class="register-button">{{
  120. t("register.backToLoginButton")
  121. }}</span>
  122. </div>
  123. </el-form-item>
  124. </el-form>
  125. </div>
  126. </div>
  127. </template>
  128. <script setup lang="ts">
  129. import { reactive, ref, computed, watch, onMounted } from "vue";
  130. import { ElForm, ElMessage } from "element-plus";
  131. import type { FormRules } from "element-plus";
  132. import { login, register } from "@/API/users";
  133. import { changeLanguageAPI } from "@/API/admin";
  134. import { useTokenStore } from "@/stores/mytoken";
  135. import { useI18n } from "vue-i18n";
  136. import { useRouter } from "vue-router";
  137. // ============ 类型定义 ============
  138. interface LoginForm {
  139. name: string;
  140. password: string;
  141. }
  142. interface RegisterForm {
  143. name: string;
  144. password: string;
  145. confirmPassword: string;
  146. }
  147. // ============ 核心实例 ============
  148. const store = useTokenStore();
  149. const { t, locale } = useI18n();
  150. const router = useRouter();
  151. // ============ 状态 ============
  152. const isLogin = ref(true);
  153. const loading = ref(false);
  154. const showError = ref(false);
  155. const errorMessage = ref("");
  156. // 新增状态用于注册成功/失败提示
  157. const showMessage = ref(false);
  158. const messageContent = ref("");
  159. const messageType = ref<"success-message" | "error-message">("error-message"); // 'success-message' 或 'error-message'
  160. const form = reactive<LoginForm>({ name: "", password: "" });
  161. const registerForm = reactive<RegisterForm>({
  162. name: "",
  163. password: "",
  164. confirmPassword: "",
  165. });
  166. const formRef = ref<InstanceType<typeof ElForm> | null>(null);
  167. const registerFormRef = ref<InstanceType<typeof ElForm> | null>(null);
  168. // ============ 表单切换 ============
  169. const toggleForm = () => {
  170. isLogin.value = !isLogin.value;
  171. showError.value = false; // 切换表单时隐藏错误提示
  172. showMessage.value = false; // 切换表单时也隐藏注册消息
  173. };
  174. /**
  175. * 切换语言并通知后端
  176. */
  177. const toggleLanguage = async () => {
  178. // 计算新的语言
  179. const newLocale = locale.value === "zh" ? "en" : "zh";
  180. // 更新本地语言设置
  181. locale.value = newLocale;
  182. localStorage.setItem("lang", newLocale);
  183. try {
  184. // 发送语言切换信息到后端
  185. const response = await changeLanguageAPI({
  186. language: newLocale,
  187. userId: store.userInfo?.userId, // 修复:使用 userInfo?.userId
  188. timestamp: Date.now(), // 添加时间戳防止重复请求
  189. });
  190. if (!response.success) {
  191. console.warn('语言切换通知后端失败:', response.message);
  192. // 即使后端请求失败,前端语言切换仍然生效
  193. } else {
  194. console.log('语言切换已通知后端:', newLocale);
  195. // 可选:如果后端需要返回特定数据,可以处理响应
  196. if (response.message) {
  197. console.log('后端响应:', response.message);
  198. }
  199. }
  200. } catch (error) {
  201. console.error('发送语言切换请求到后端时出错:', error);
  202. // 错误处理,但不影响前端语言切换
  203. }
  204. };
  205. // ============ 计算属性 ============
  206. const currentLanguageName = computed(() =>
  207. locale.value === "zh" ? "English" : "中文"
  208. );
  209. // ============ 显示错误消息 (保留原逻辑) ============
  210. const showErrorMsg = (message: string) => {
  211. errorMessage.value = message;
  212. showError.value = true;
  213. // 3秒后自动隐藏消息
  214. setTimeout(() => {
  215. showError.value = false;
  216. }, 3000);
  217. };
  218. // ============ 显示通用消息 (新增) ============
  219. const showMsg = (content: string, type: "success" | "error" = "success") => {
  220. messageContent.value = content;
  221. messageType.value = type === "success" ? "success-message" : "error-message";
  222. showMessage.value = true;
  223. // 3秒后自动隐藏消息
  224. setTimeout(() => {
  225. showMessage.value = false;
  226. }, 3000);
  227. };
  228. // ============ 登录逻辑 ============
  229. const onSubmit = async () => {
  230. if (!formRef.value) return;
  231. try {
  232. // 验证表单
  233. await formRef.value.validate();
  234. loading.value = true;
  235. showError.value = false; // 开始验证时隐藏错误提示
  236. // 调用登录API
  237. const response = await login({
  238. name: form.name,
  239. password: form.password,
  240. });
  241. console.log("完整登录响应:", response);
  242. // 检查响应结构
  243. if (!response.data?.user) {
  244. throw new Error("后端返回的用户信息不完整");
  245. }
  246. // 提取用户信息
  247. const userData = response.data.user;
  248. if (!userData.id || !userData.name) {
  249. throw new Error("缺少必要的用户字段");
  250. }
  251. // --- 修改部分开始 (解决 TypeScript 错误) ---
  252. // 确定 loginType 的值
  253. // 假设后端返回的字段是 userType
  254. let loginTypeFromResponse = userData.userType;
  255. // 如果后端没有返回 userType,使用默认值 'user' 以避免程序崩溃
  256. // 你可以根据实际业务需求调整默认值或处理逻辑
  257. if (!loginTypeFromResponse) {
  258. console.warn("后端未返回 userType,使用默认值 'user'");
  259. loginTypeFromResponse = 'user';
  260. }
  261. // 保存用户信息到store (包含必需的 loginType)
  262. store.saveToken({
  263. userId: Number(userData.id),
  264. name: userData.name,
  265. loginType: loginTypeFromResponse, // 确保提供 loginType
  266. // 如果 UserInfo 还有其他必需字段,也需要在这里提供
  267. });
  268. // --- 修改部分结束 ---
  269. // 使用 Element Plus 的成功提示
  270. ElMessage.success(t("login.loginSuccess"));
  271. // 跳转到目标页面
  272. await router.push({ name: "samplingMethodDevice1" });
  273. } catch (error: any) {
  274. console.error("登录失败:", {
  275. error: error.message,
  276. stack: error.stack,
  277. response: error.response?.data,
  278. });
  279. // 显示错误消息
  280. const errorMsg =
  281. error.response?.data?.message ||
  282. error.message ||
  283. t("login.loginFailed"); // 使用翻译键
  284. showErrorMsg(errorMsg);
  285. // 使用 Element Plus 的错误提示
  286. ElMessage.error(errorMsg);
  287. } finally {
  288. loading.value = false;
  289. }
  290. };
  291. // ============ 注册逻辑 ============
  292. const onRegister = async () => {
  293. if (!registerFormRef.value) return;
  294. try {
  295. await registerFormRef.value.validate();
  296. loading.value = true;
  297. showError.value = false; // 开始验证时隐藏错误提示
  298. showMessage.value = false; // 开始验证时隐藏消息
  299. // 调用注册API
  300. const res = await register({
  301. name: registerForm.name,
  302. password: registerForm.password,
  303. });
  304. // 注册成功
  305. if (res.data?.message) {
  306. // 显示注册成功消息
  307. showMsg(t("register.registerSuccess"), "success");
  308. ElMessage.success(t("register.registerSuccess"));
  309. // 延迟一段时间后自动跳转到登录页并填充信息
  310. setTimeout(() => {
  311. // 切换到登录表单
  312. isLogin.value = true;
  313. // 将注册的用户名和密码填入登录表单
  314. form.name = registerForm.name;
  315. form.password = registerForm.password; // 填充密码
  316. // 清空注册表单
  317. registerForm.name = "";
  318. registerForm.password = "";
  319. registerForm.confirmPassword = "";
  320. // 可选:给用户一个提示
  321. ElMessage.info(t("register.autoLoginPrompt"));
  322. }, 1500); // 1.5秒后执行
  323. } else {
  324. const errorMsg = res.data?.message || t("register.registerFailed");
  325. showMsg(errorMsg, "error");
  326. ElMessage.error(errorMsg);
  327. }
  328. } catch (error: any) {
  329. console.error("注册异常:", error);
  330. const errorMsg =
  331. error?.response?.data?.message || t("register.registerFailed");
  332. showMsg(errorMsg, "error");
  333. ElMessage.error(errorMsg);
  334. } finally {
  335. loading.value = false;
  336. }
  337. };
  338. // ============ 校验规则 ============
  339. const createRules = (): { rules: FormRules; registerRules: FormRules } => {
  340. const rules: FormRules = {
  341. name: [
  342. {
  343. required: true,
  344. message: t("validation.usernameRequired"),
  345. trigger: "blur",
  346. },
  347. ],
  348. password: [
  349. {
  350. required: true,
  351. message: t("validation.passwordRequired"),
  352. trigger: "blur",
  353. },
  354. {
  355. min: 3,
  356. max: 16,
  357. message: t("validation.passwordLength"),
  358. trigger: "blur",
  359. },
  360. ],
  361. };
  362. const registerRules: FormRules = {
  363. name: rules.name,
  364. password: rules.password,
  365. confirmPassword: [
  366. {
  367. required: true,
  368. message: t("validation.confirmPasswordRequired"),
  369. trigger: "blur",
  370. },
  371. {
  372. validator: (_rule, value: string, callback) => {
  373. if (value !== registerForm.password)
  374. callback(new Error(t("validation.passwordMismatch")));
  375. else callback();
  376. },
  377. trigger: "blur",
  378. },
  379. ],
  380. };
  381. return { rules, registerRules };
  382. };
  383. let { rules, registerRules } = createRules();
  384. watch(locale, () => {
  385. const newRules = createRules();
  386. rules = newRules.rules;
  387. registerRules = newRules.registerRules;
  388. });
  389. watch(
  390. () => registerForm.password,
  391. () => {
  392. registerFormRef.value?.validateField("confirmPassword");
  393. }
  394. );
  395. onMounted(() => {
  396. console.log("登录/注册页初始化", {
  397. form,
  398. registerForm,
  399. locale: locale.value,
  400. });
  401. });
  402. </script>
  403. <style scoped>
  404. .auth-wrapper {
  405. display: flex;
  406. height: 100vh;
  407. background-color: #f6f6f6;
  408. position: relative;
  409. }
  410. .auth-left {
  411. width: 35%;
  412. background: url("@/assets/login-bg.png") no-repeat center/cover;
  413. }
  414. .auth-form-container {
  415. width: 55%;
  416. padding: 0 40px 0 60px;
  417. display: flex;
  418. justify-content: center;
  419. align-items: center;
  420. flex-direction: column;
  421. }
  422. .login-form {
  423. width: 100%;
  424. max-width: 700px;
  425. padding: 40px 30px;
  426. margin-top: 50px;
  427. background: rgba(255, 255, 255, 0.9);
  428. border-radius: 15px;
  429. }
  430. .form-header {
  431. display: flex;
  432. justify-content: space-between;
  433. align-items: center;
  434. margin-bottom: 40px;
  435. margin-top: 20px;
  436. }
  437. .form-title {
  438. font-size: 32px;
  439. font-weight: 600;
  440. color: #333;
  441. }
  442. .language-toggle-wrapper {
  443. text-align: right;
  444. margin: 15px 0 20px;
  445. }
  446. :deep(.el-form-item__label) {
  447. float: none;
  448. display: block;
  449. text-align: left;
  450. font-size: 24px;
  451. padding-bottom: 8px;
  452. color: #7e7878;
  453. white-space: nowrap; /* 防止标签内文字换行 */
  454. overflow: hidden;
  455. text-overflow: ellipsis;
  456. }
  457. /* 更新输入框样式以适应 Element Plus 的结构 */
  458. :deep(.el-input__wrapper) {
  459. box-shadow: 0 0 0 1px #dcdfe6 inset; /* 模拟之前的边框 */
  460. border-radius: 0;
  461. }
  462. :deep(.el-input__inner) {
  463. height: 50px;
  464. font-size: 20px;
  465. background-color: #fff;
  466. padding: 0 15px;
  467. }
  468. /* 更新密码图标按钮样式 */
  469. :deep(.el-input__suffix) {
  470. cursor: pointer;
  471. font-size: 20px;
  472. color: #aaa;
  473. margin-right: 10px;
  474. }
  475. .login-button {
  476. background: linear-gradient(to right, #8df9f0, #26b046);
  477. width: 100%;
  478. max-width: 300px; /* 缩小按钮宽度 */
  479. height: 56px;
  480. color: white;
  481. border: none;
  482. border-radius: 20px;
  483. font-size: 24px;
  484. cursor: pointer;
  485. margin-top: 10px;
  486. margin: 10px auto; /* 居中显示 */
  487. display: block; /* 使 margin: auto 生效 */
  488. }
  489. .login-button:hover {
  490. opacity: 0.9;
  491. }
  492. .register-button {
  493. display: block;
  494. text-align: center;
  495. color: #478bf0;
  496. font-size: 18px;
  497. cursor: pointer;
  498. padding: 10px 0;
  499. width: 100%;
  500. }
  501. .register-button:hover {
  502. color: #357ae8;
  503. text-decoration: underline;
  504. }
  505. .button-group {
  506. display: flex;
  507. justify-content: center;
  508. width: 100%;
  509. }
  510. .text-toggle {
  511. color: #478bf0;
  512. font-size: 16px;
  513. cursor: pointer;
  514. }
  515. .text-toggle:hover {
  516. color: #357ae8;
  517. text-decoration: underline;
  518. }
  519. .text-link-wrapper {
  520. text-align: center;
  521. margin-top: 20px;
  522. }
  523. /* 错误提示样式 */
  524. .error-message,
  525. .success-message {
  526. font-size: 16px;
  527. text-align: center;
  528. margin: 15px 0;
  529. animation: fadeIn 0.3s ease-out forwards;
  530. padding: 10px;
  531. border-radius: 4px;
  532. }
  533. .error-message {
  534. color: #f56c6c;
  535. background-color: #fef0f0;
  536. border: 1px solid #fde2e2;
  537. }
  538. .success-message {
  539. color: #67c23a;
  540. background-color: #f0f9ec;
  541. border: 1px solid #e1f3d8;
  542. }
  543. @keyframes fadeIn {
  544. from {
  545. opacity: 0;
  546. }
  547. to {
  548. opacity: 1;
  549. }
  550. }
  551. /* 针对登录和注册表单中的标签 */
  552. .login-form :deep(.el-form-item__label),
  553. .register-form :deep(.el-form-item__label) {
  554. /* 为登录和注册表单中的标签设置一个稍大的最大宽度,以适应"确认密码"等较长标签 */
  555. max-width: 200px; /* 根据实际需要调整 */
  556. width: auto;
  557. text-align: right; /* 标签靠右对齐 */
  558. flex-shrink: 0; /* 防止标签被压缩 */
  559. margin-right: 15px; /* 标签和输入框之间的间距 */
  560. white-space: nowrap;
  561. overflow: hidden;
  562. text-overflow: ellipsis;
  563. }
  564. /* 针对登录和注册表单中的内容区域 */
  565. .login-form :deep(.el-form-item__content),
  566. .register-form :deep(.el-form-item__content) {
  567. display: flex;
  568. justify-content: flex-end; /* 内容整体靠右 */
  569. width: 100%;
  570. }
  571. /* 针对登录和注册表单中的输入框包装器 */
  572. .login-form :deep(.el-input__wrapper),
  573. .register-form :deep(.el-input__wrapper) {
  574. max-width: 350px; /* 设置你想要的统一缩短后的宽度 */
  575. width: 100%;
  576. margin-left: 0;
  577. }
  578. /* 特别为登录和注册表单的输入框框架增加类名并设置间距 */
  579. .login-input-frame,
  580. .register-input-frame {
  581. margin-left: 48px;
  582. }
  583. /* --- 修改部分:为注册表单的账号和密码输入框容器添加特定类名并设置新的左边距 --- */
  584. .register-form .register-account-frame,
  585. .register-form .register-password-frame {
  586. margin-left: 96px; /* 原来的 48px + 新增的 20px */
  587. }
  588. /* --------------------------------------------------------------------------------- */
  589. /* 确认密码输入框间距保持不变 */
  590. .register-form .input-frame:last-of-type {
  591. margin-bottom: 20px; /* 或者你希望的其他值 */
  592. }
  593. /* 注册按钮特殊调整 */
  594. .register-button-adjusted {
  595. max-width: 250px;
  596. }
  597. </style>