login.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443
  1. <template>
  2. <div class="welcome">
  3. <el-container style="height: 100%">
  4. <el-header>
  5. <div style="
  6. display: flex;
  7. align-items: center;
  8. margin-top: 11px;
  9. margin-left: 11px;
  10. gap: 10px;
  11. ">
  12. <img loading="lazy" alt="" src="@/assets/xiaozhi-logo.png" style="width: 42px; height: 42px" />
  13. <img loading="lazy" alt="" :src="xiaozhiAiIcon" style="height: 20px" />
  14. </div>
  15. </el-header>
  16. <div class="login-person">
  17. <img loading="lazy" alt="" src="@/assets/login/login-person.png" style="width: 100%" />
  18. </div>
  19. <el-main style="position: relative">
  20. <div class="login-box" @keyup.enter="login">
  21. <div style="
  22. display: flex;
  23. align-items: center;
  24. gap: 20px;
  25. margin-bottom: 39px;
  26. padding: 0 30px;
  27. ">
  28. <img loading="lazy" alt="" src="@/assets/login/hi.png" style="width: 34px; height: 34px" />
  29. <div class="login-text">{{ $t("login.title") }}</div>
  30. <div class="login-welcome">
  31. {{ $t("login.welcome") }}
  32. </div>
  33. <!-- 语言切换下拉菜单 -->
  34. <el-dropdown trigger="click" class="title-language-dropdown"
  35. @visible-change="handleLanguageDropdownVisibleChange">
  36. <span class="el-dropdown-link">
  37. <span class="current-language-text">{{ currentLanguageText }}</span>
  38. <i class="el-icon-arrow-down el-icon--right" :class="{ 'rotate-down': languageDropdownVisible }"></i>
  39. </span>
  40. <el-dropdown-menu slot="dropdown">
  41. <el-dropdown-item @click.native="changeLanguage('zh_CN')">
  42. {{ $t("language.zhCN") }}
  43. </el-dropdown-item>
  44. <el-dropdown-item @click.native="changeLanguage('zh_TW')">
  45. {{ $t("language.zhTW") }}
  46. </el-dropdown-item>
  47. <el-dropdown-item @click.native="changeLanguage('en')">
  48. {{ $t("language.en") }}
  49. </el-dropdown-item>
  50. <el-dropdown-item @click.native="changeLanguage('de')">
  51. {{ $t("language.de") }}
  52. </el-dropdown-item>
  53. <el-dropdown-item @click.native="changeLanguage('vi')">
  54. {{ $t("language.vi") }}
  55. </el-dropdown-item>
  56. <el-dropdown-item @click.native="changeLanguage('pt_BR')">
  57. {{ $t("language.ptBR") }}
  58. </el-dropdown-item>
  59. </el-dropdown-menu>
  60. </el-dropdown>
  61. </div>
  62. <div style="padding: 0 30px">
  63. <!-- 用户名登录 -->
  64. <template v-if="!isMobileLogin">
  65. <div class="input-box">
  66. <img loading="lazy" alt="" class="input-icon" src="@/assets/login/username.png" />
  67. <el-input v-model="form.username" :placeholder="$t('login.usernamePlaceholder')" />
  68. </div>
  69. </template>
  70. <!-- 手机号登录 -->
  71. <template v-else>
  72. <div class="input-box">
  73. <div style="display: flex; align-items: center; width: 100%">
  74. <el-select v-model="form.areaCode" style="width: 220px; margin-right: 10px">
  75. <el-option v-for="item in mobileAreaList" :key="item.key" :label="`${item.name} (${item.key})`"
  76. :value="item.key" />
  77. </el-select>
  78. <el-input v-model="form.mobile" :placeholder="$t('login.mobilePlaceholder')" />
  79. </div>
  80. </div>
  81. </template>
  82. <div class="input-box">
  83. <img loading="lazy" alt="" class="input-icon" src="@/assets/login/password.png" />
  84. <el-input v-model="form.password" :placeholder="$t('login.passwordPlaceholder')" type="password"
  85. show-password />
  86. </div>
  87. <div style="
  88. display: flex;
  89. align-items: center;
  90. margin-top: 20px;
  91. width: 100%;
  92. gap: 10px;
  93. ">
  94. <div class="input-box" style="width: calc(100% - 130px); margin-top: 0">
  95. <img loading="lazy" alt="" class="input-icon" src="@/assets/login/shield.png" />
  96. <el-input v-model="form.captcha" :placeholder="$t('login.captchaPlaceholder')" style="flex: 1" />
  97. </div>
  98. <img loading="lazy" v-if="captchaUrl" :src="captchaUrl" alt="验证码"
  99. style="width: 150px; height: 40px; cursor: pointer" @click="fetchCaptcha" />
  100. </div>
  101. <div style="
  102. font-weight: 400;
  103. font-size: 14px;
  104. text-align: left;
  105. color: #5778ff;
  106. display: flex;
  107. justify-content: space-between;
  108. margin-top: 20px;
  109. ">
  110. <div v-if="allowUserRegister" style="cursor: pointer" @click="goToRegister">
  111. {{ $t("login.register") }}
  112. </div>
  113. <div style="cursor: pointer" @click="goToForgetPassword" v-if="enableMobileRegister">
  114. {{ $t("login.forgetPassword") }}
  115. </div>
  116. </div>
  117. </div>
  118. <div class="login-btn" @click="login">{{ $t("login.login") }}</div>
  119. <!-- 登录方式切换按钮 -->
  120. <div class="login-type-container" v-if="enableMobileRegister">
  121. <div style="display: flex; gap: 10px">
  122. <el-tooltip :content="$t('login.mobileLogin')" placement="bottom">
  123. <el-button :type="isMobileLogin ? 'primary' : 'default'" icon="el-icon-mobile" circle
  124. @click="switchLoginType('mobile')"></el-button>
  125. </el-tooltip>
  126. <el-tooltip :content="$t('login.usernameLogin')" placement="bottom">
  127. <el-button :type="!isMobileLogin ? 'primary' : 'default'" icon="el-icon-user" circle
  128. @click="switchLoginType('username')"></el-button>
  129. </el-tooltip>
  130. </div>
  131. </div>
  132. <div style="font-size: 14px; color: #979db1">
  133. {{ $t("login.agreeTo") }}
  134. <div style="display: inline-block; color: #5778ff; cursor: pointer" @click="openPage('/user-agreement.html')">
  135. {{ $t("login.userAgreement") }}
  136. </div>
  137. {{ $t("login.and") }}
  138. <div style="display: inline-block; color: #5778ff; cursor: pointer" @click="openPage('/privacy-policy.html')">
  139. {{ $t("login.privacyPolicy") }}
  140. </div>
  141. </div>
  142. </div>
  143. </el-main>
  144. <el-footer>
  145. <version-footer />
  146. </el-footer>
  147. </el-container>
  148. </div>
  149. </template>
  150. <script>
  151. import Api from "@/apis/api";
  152. import VersionFooter from "@/components/VersionFooter.vue";
  153. import i18n, { changeLanguage } from "@/i18n";
  154. import { getUUID, goToPage, showDanger, showSuccess, sm2Encrypt, validateMobile } from "@/utils";
  155. import { mapState } from "vuex";
  156. import featureManager from "@/utils/featureManager";
  157. export default {
  158. name: "login",
  159. components: {
  160. VersionFooter,
  161. },
  162. computed: {
  163. ...mapState({
  164. allowUserRegister: (state) => state.pubConfig.allowUserRegister,
  165. enableMobileRegister: (state) => state.pubConfig.enableMobileRegister,
  166. mobileAreaList: (state) => state.pubConfig.mobileAreaList,
  167. sm2PublicKey: (state) => state.pubConfig.sm2PublicKey,
  168. }),
  169. // 获取当前语言
  170. currentLanguage() {
  171. return i18n.locale || "zh_CN";
  172. },
  173. // 获取当前语言显示文本
  174. currentLanguageText() {
  175. const currentLang = this.currentLanguage;
  176. switch (currentLang) {
  177. case "zh_CN":
  178. return this.$t("language.zhCN");
  179. case "zh_TW":
  180. return this.$t("language.zhTW");
  181. case "en":
  182. return this.$t("language.en");
  183. case "de":
  184. return this.$t("language.de");
  185. case "vi":
  186. return this.$t("language.vi");
  187. case "pt_BR":
  188. return this.$t("language.ptBR");
  189. default:
  190. return this.$t("language.zhCN");
  191. }
  192. },
  193. // 根据当前语言获取对应的xiaozhi-ai图标
  194. xiaozhiAiIcon() {
  195. const currentLang = this.currentLanguage;
  196. switch (currentLang) {
  197. case "zh_CN":
  198. return require("@/assets/xiaozhi-ai.png");
  199. case "zh_TW":
  200. return require("@/assets/xiaozhi-ai_zh_TW.png");
  201. case "en":
  202. return require("@/assets/xiaozhi-ai_en.png");
  203. case "de":
  204. return require("@/assets/xiaozhi-ai_de.png");
  205. case "vi":
  206. return require("@/assets/xiaozhi-ai_vi.png");
  207. default:
  208. return require("@/assets/xiaozhi-ai.png");
  209. }
  210. },
  211. },
  212. data() {
  213. return {
  214. activeName: "username",
  215. form: {
  216. username: "",
  217. password: "",
  218. captcha: "",
  219. captchaId: "",
  220. areaCode: "+86",
  221. mobile: "",
  222. },
  223. captchaUuid: "",
  224. captchaUrl: "",
  225. isMobileLogin: false,
  226. languageDropdownVisible: false,
  227. };
  228. },
  229. mounted() {
  230. this.fetchCaptcha();
  231. this.$store.dispatch("fetchPubConfig").then(() => {
  232. // 根据配置决定默认登录方式
  233. this.isMobileLogin = this.enableMobileRegister;
  234. });
  235. },
  236. methods: {
  237. openPage(url) {
  238. const lang = this.$i18n ? this.$i18n.locale : 'zh_CN';
  239. if (!lang.startsWith('zh')) {
  240. url = url.replace('.html', '-en.html');
  241. }
  242. window.open(url, '_blank');
  243. },
  244. fetchCaptcha() {
  245. // 处理手动清空localstorage导致无法获取验证码的问题
  246. const token = localStorage.getItem('token')
  247. if (token) {
  248. if (this.$route.path !== "/home") {
  249. this.$router.push("/home");
  250. }
  251. } else {
  252. this.captchaUuid = getUUID();
  253. Api.user.getCaptcha(this.captchaUuid, (res) => {
  254. if (res.status === 200) {
  255. const blob = new Blob([res.data], { type: res.data.type });
  256. this.captchaUrl = URL.createObjectURL(blob);
  257. } else {
  258. showDanger("验证码加载失败,点击刷新");
  259. }
  260. });
  261. }
  262. },
  263. // 切换语言下拉菜单的可见状态变化
  264. handleLanguageDropdownVisibleChange(visible) {
  265. this.languageDropdownVisible = visible;
  266. },
  267. // 切换语言
  268. changeLanguage(lang) {
  269. changeLanguage(lang);
  270. this.languageDropdownVisible = false;
  271. this.$message.success({
  272. message: this.$t("message.success"),
  273. showClose: true,
  274. });
  275. },
  276. // 切换登录方式
  277. switchLoginType(type) {
  278. this.isMobileLogin = type === "mobile";
  279. // 清空表单
  280. this.form.username = "";
  281. this.form.mobile = "";
  282. this.form.password = "";
  283. this.form.captcha = "";
  284. this.fetchCaptcha();
  285. },
  286. // 封装输入验证逻辑
  287. validateInput(input, messageKey) {
  288. if (!input.trim()) {
  289. showDanger(this.$t(messageKey));
  290. return false;
  291. }
  292. return true;
  293. },
  294. getUserInfo() {
  295. Api.user.getUserInfo(({ data }) => {
  296. if (data.code === 0) {
  297. this.$store.commit("setUserInfo", data.data);
  298. goToPage("/home");
  299. } else {
  300. showDanger("用户信息获取失败");
  301. }
  302. });
  303. },
  304. async login() {
  305. if (this.isMobileLogin) {
  306. // 手机号登录验证
  307. if (!validateMobile(this.form.mobile, this.form.areaCode)) {
  308. showDanger(this.$t('login.requiredMobile'));
  309. return;
  310. }
  311. // 拼接手机号作为用户名
  312. this.form.username = this.form.areaCode + this.form.mobile;
  313. } else {
  314. // 用户名登录验证
  315. if (!this.validateInput(this.form.username, 'login.requiredUsername')) {
  316. return;
  317. }
  318. }
  319. // 验证密码
  320. if (!this.validateInput(this.form.password, 'login.requiredPassword')) {
  321. return;
  322. }
  323. // 验证验证码
  324. if (!this.validateInput(this.form.captcha, 'login.requiredCaptcha')) {
  325. return;
  326. }
  327. // 加密密码
  328. let encryptedPassword;
  329. try {
  330. // 拼接验证码和密码
  331. const captchaAndPassword = this.form.captcha + this.form.password;
  332. encryptedPassword = sm2Encrypt(this.sm2PublicKey, captchaAndPassword);
  333. } catch (error) {
  334. console.error("密码加密失败:", error);
  335. showDanger(this.$t('sm2.encryptionFailed'));
  336. return;
  337. }
  338. const plainUsername = this.form.username;
  339. this.form.captchaId = this.captchaUuid;
  340. // 加密
  341. const loginData = {
  342. username: plainUsername,
  343. password: encryptedPassword,
  344. captchaId: this.form.captchaId
  345. };
  346. Api.user.login(
  347. loginData,
  348. ({ data }) => {
  349. showSuccess(this.$t('login.loginSuccess'));
  350. this.$store.commit("setToken", JSON.stringify(data.data));
  351. this.getUserInfo();
  352. },
  353. (err) => {
  354. // 直接使用后端返回的国际化消息
  355. let errorMessage = err.data.msg || "登录失败";
  356. showDanger(errorMessage);
  357. }
  358. );
  359. // 重新获取验证码
  360. setTimeout(() => {
  361. this.fetchCaptcha();
  362. }, 1000);
  363. },
  364. goToRegister() {
  365. goToPage("/register");
  366. },
  367. goToForgetPassword() {
  368. goToPage("/retrieve-password");
  369. }
  370. },
  371. };
  372. </script>
  373. <style lang="scss" scoped>
  374. @import "./auth.scss";
  375. .login-type-container {
  376. margin: 10px 20px;
  377. display: flex;
  378. justify-content: center;
  379. }
  380. .title-language-dropdown {
  381. margin-left: auto;
  382. }
  383. .current-language-text {
  384. margin-left: 4px;
  385. margin-right: 4px;
  386. font-size: 12px;
  387. color: #3d4566;
  388. }
  389. .language-dropdown {
  390. margin-left: auto;
  391. }
  392. .rotate-down {
  393. transform: rotate(180deg);
  394. transition: transform 0.3s ease;
  395. }
  396. .el-icon-arrow-down {
  397. transition: transform 0.3s ease;
  398. }
  399. :deep(.el-button--primary) {
  400. background-color: #5778ff;
  401. border-color: #5778ff;
  402. &:hover,
  403. &:focus {
  404. background-color: #4a6ae8;
  405. border-color: #4a6ae8;
  406. }
  407. &:active {
  408. background-color: #3d5cd6;
  409. border-color: #3d5cd6;
  410. }
  411. }
  412. </style>