| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443 |
- <template>
- <div class="welcome">
- <el-container style="height: 100%">
- <el-header>
- <div style="
- display: flex;
- align-items: center;
- margin-top: 11px;
- margin-left: 11px;
- gap: 10px;
- ">
- <img loading="lazy" alt="" src="@/assets/xiaozhi-logo.png" style="width: 42px; height: 42px" />
- <img loading="lazy" alt="" :src="xiaozhiAiIcon" style="height: 20px" />
- </div>
- </el-header>
- <div class="login-person">
- <img loading="lazy" alt="" src="@/assets/login/login-person.png" style="width: 100%" />
- </div>
- <el-main style="position: relative">
- <div class="login-box" @keyup.enter="login">
- <div style="
- display: flex;
- align-items: center;
- gap: 20px;
- margin-bottom: 39px;
- padding: 0 30px;
- ">
- <img loading="lazy" alt="" src="@/assets/login/hi.png" style="width: 34px; height: 34px" />
- <div class="login-text">{{ $t("login.title") }}</div>
- <div class="login-welcome">
- {{ $t("login.welcome") }}
- </div>
- <!-- 语言切换下拉菜单 -->
- <el-dropdown trigger="click" class="title-language-dropdown"
- @visible-change="handleLanguageDropdownVisibleChange">
- <span class="el-dropdown-link">
- <span class="current-language-text">{{ currentLanguageText }}</span>
- <i class="el-icon-arrow-down el-icon--right" :class="{ 'rotate-down': languageDropdownVisible }"></i>
- </span>
- <el-dropdown-menu slot="dropdown">
- <el-dropdown-item @click.native="changeLanguage('zh_CN')">
- {{ $t("language.zhCN") }}
- </el-dropdown-item>
- <el-dropdown-item @click.native="changeLanguage('zh_TW')">
- {{ $t("language.zhTW") }}
- </el-dropdown-item>
- <el-dropdown-item @click.native="changeLanguage('en')">
- {{ $t("language.en") }}
- </el-dropdown-item>
- <el-dropdown-item @click.native="changeLanguage('de')">
- {{ $t("language.de") }}
- </el-dropdown-item>
- <el-dropdown-item @click.native="changeLanguage('vi')">
- {{ $t("language.vi") }}
- </el-dropdown-item>
- <el-dropdown-item @click.native="changeLanguage('pt_BR')">
- {{ $t("language.ptBR") }}
- </el-dropdown-item>
- </el-dropdown-menu>
- </el-dropdown>
- </div>
- <div style="padding: 0 30px">
- <!-- 用户名登录 -->
- <template v-if="!isMobileLogin">
- <div class="input-box">
- <img loading="lazy" alt="" class="input-icon" src="@/assets/login/username.png" />
- <el-input v-model="form.username" :placeholder="$t('login.usernamePlaceholder')" />
- </div>
- </template>
- <!-- 手机号登录 -->
- <template v-else>
- <div class="input-box">
- <div style="display: flex; align-items: center; width: 100%">
- <el-select v-model="form.areaCode" style="width: 220px; margin-right: 10px">
- <el-option v-for="item in mobileAreaList" :key="item.key" :label="`${item.name} (${item.key})`"
- :value="item.key" />
- </el-select>
- <el-input v-model="form.mobile" :placeholder="$t('login.mobilePlaceholder')" />
- </div>
- </div>
- </template>
- <div class="input-box">
- <img loading="lazy" alt="" class="input-icon" src="@/assets/login/password.png" />
- <el-input v-model="form.password" :placeholder="$t('login.passwordPlaceholder')" type="password"
- show-password />
- </div>
- <div style="
- display: flex;
- align-items: center;
- margin-top: 20px;
- width: 100%;
- gap: 10px;
- ">
- <div class="input-box" style="width: calc(100% - 130px); margin-top: 0">
- <img loading="lazy" alt="" class="input-icon" src="@/assets/login/shield.png" />
- <el-input v-model="form.captcha" :placeholder="$t('login.captchaPlaceholder')" style="flex: 1" />
- </div>
- <img loading="lazy" v-if="captchaUrl" :src="captchaUrl" alt="验证码"
- style="width: 150px; height: 40px; cursor: pointer" @click="fetchCaptcha" />
- </div>
- <div style="
- font-weight: 400;
- font-size: 14px;
- text-align: left;
- color: #5778ff;
- display: flex;
- justify-content: space-between;
- margin-top: 20px;
- ">
- <div v-if="allowUserRegister" style="cursor: pointer" @click="goToRegister">
- {{ $t("login.register") }}
- </div>
- <div style="cursor: pointer" @click="goToForgetPassword" v-if="enableMobileRegister">
- {{ $t("login.forgetPassword") }}
- </div>
- </div>
- </div>
- <div class="login-btn" @click="login">{{ $t("login.login") }}</div>
- <!-- 登录方式切换按钮 -->
- <div class="login-type-container" v-if="enableMobileRegister">
- <div style="display: flex; gap: 10px">
- <el-tooltip :content="$t('login.mobileLogin')" placement="bottom">
- <el-button :type="isMobileLogin ? 'primary' : 'default'" icon="el-icon-mobile" circle
- @click="switchLoginType('mobile')"></el-button>
- </el-tooltip>
- <el-tooltip :content="$t('login.usernameLogin')" placement="bottom">
- <el-button :type="!isMobileLogin ? 'primary' : 'default'" icon="el-icon-user" circle
- @click="switchLoginType('username')"></el-button>
- </el-tooltip>
- </div>
- </div>
- <div style="font-size: 14px; color: #979db1">
- {{ $t("login.agreeTo") }}
- <div style="display: inline-block; color: #5778ff; cursor: pointer" @click="openPage('/user-agreement.html')">
- {{ $t("login.userAgreement") }}
- </div>
- {{ $t("login.and") }}
- <div style="display: inline-block; color: #5778ff; cursor: pointer" @click="openPage('/privacy-policy.html')">
- {{ $t("login.privacyPolicy") }}
- </div>
- </div>
- </div>
- </el-main>
- <el-footer>
- <version-footer />
- </el-footer>
- </el-container>
- </div>
- </template>
- <script>
- import Api from "@/apis/api";
- import VersionFooter from "@/components/VersionFooter.vue";
- import i18n, { changeLanguage } from "@/i18n";
- import { getUUID, goToPage, showDanger, showSuccess, sm2Encrypt, validateMobile } from "@/utils";
- import { mapState } from "vuex";
- import featureManager from "@/utils/featureManager";
- export default {
- name: "login",
- components: {
- VersionFooter,
- },
- computed: {
- ...mapState({
- allowUserRegister: (state) => state.pubConfig.allowUserRegister,
- enableMobileRegister: (state) => state.pubConfig.enableMobileRegister,
- mobileAreaList: (state) => state.pubConfig.mobileAreaList,
- sm2PublicKey: (state) => state.pubConfig.sm2PublicKey,
- }),
- // 获取当前语言
- currentLanguage() {
- return i18n.locale || "zh_CN";
- },
- // 获取当前语言显示文本
- currentLanguageText() {
- const currentLang = this.currentLanguage;
- switch (currentLang) {
- case "zh_CN":
- return this.$t("language.zhCN");
- case "zh_TW":
- return this.$t("language.zhTW");
- case "en":
- return this.$t("language.en");
- case "de":
- return this.$t("language.de");
- case "vi":
- return this.$t("language.vi");
- case "pt_BR":
- return this.$t("language.ptBR");
- default:
- return this.$t("language.zhCN");
- }
- },
- // 根据当前语言获取对应的xiaozhi-ai图标
- xiaozhiAiIcon() {
- const currentLang = this.currentLanguage;
- switch (currentLang) {
- case "zh_CN":
- return require("@/assets/xiaozhi-ai.png");
- case "zh_TW":
- return require("@/assets/xiaozhi-ai_zh_TW.png");
- case "en":
- return require("@/assets/xiaozhi-ai_en.png");
- case "de":
- return require("@/assets/xiaozhi-ai_de.png");
- case "vi":
- return require("@/assets/xiaozhi-ai_vi.png");
- default:
- return require("@/assets/xiaozhi-ai.png");
- }
- },
- },
- data() {
- return {
- activeName: "username",
- form: {
- username: "",
- password: "",
- captcha: "",
- captchaId: "",
- areaCode: "+86",
- mobile: "",
- },
- captchaUuid: "",
- captchaUrl: "",
- isMobileLogin: false,
- languageDropdownVisible: false,
- };
- },
- mounted() {
- this.fetchCaptcha();
- this.$store.dispatch("fetchPubConfig").then(() => {
- // 根据配置决定默认登录方式
- this.isMobileLogin = this.enableMobileRegister;
- });
- },
- methods: {
- openPage(url) {
- const lang = this.$i18n ? this.$i18n.locale : 'zh_CN';
- if (!lang.startsWith('zh')) {
- url = url.replace('.html', '-en.html');
- }
- window.open(url, '_blank');
- },
- fetchCaptcha() {
- // 处理手动清空localstorage导致无法获取验证码的问题
- const token = localStorage.getItem('token')
- if (token) {
- if (this.$route.path !== "/home") {
- this.$router.push("/home");
- }
- } else {
- this.captchaUuid = getUUID();
- Api.user.getCaptcha(this.captchaUuid, (res) => {
- if (res.status === 200) {
- const blob = new Blob([res.data], { type: res.data.type });
- this.captchaUrl = URL.createObjectURL(blob);
- } else {
- showDanger("验证码加载失败,点击刷新");
- }
- });
- }
- },
- // 切换语言下拉菜单的可见状态变化
- handleLanguageDropdownVisibleChange(visible) {
- this.languageDropdownVisible = visible;
- },
- // 切换语言
- changeLanguage(lang) {
- changeLanguage(lang);
- this.languageDropdownVisible = false;
- this.$message.success({
- message: this.$t("message.success"),
- showClose: true,
- });
- },
- // 切换登录方式
- switchLoginType(type) {
- this.isMobileLogin = type === "mobile";
- // 清空表单
- this.form.username = "";
- this.form.mobile = "";
- this.form.password = "";
- this.form.captcha = "";
- this.fetchCaptcha();
- },
- // 封装输入验证逻辑
- validateInput(input, messageKey) {
- if (!input.trim()) {
- showDanger(this.$t(messageKey));
- return false;
- }
- return true;
- },
-
- getUserInfo() {
- Api.user.getUserInfo(({ data }) => {
- if (data.code === 0) {
- this.$store.commit("setUserInfo", data.data);
- goToPage("/home");
- } else {
- showDanger("用户信息获取失败");
- }
- });
- },
- async login() {
- if (this.isMobileLogin) {
- // 手机号登录验证
- if (!validateMobile(this.form.mobile, this.form.areaCode)) {
- showDanger(this.$t('login.requiredMobile'));
- return;
- }
- // 拼接手机号作为用户名
- this.form.username = this.form.areaCode + this.form.mobile;
- } else {
- // 用户名登录验证
- if (!this.validateInput(this.form.username, 'login.requiredUsername')) {
- return;
- }
- }
- // 验证密码
- if (!this.validateInput(this.form.password, 'login.requiredPassword')) {
- return;
- }
- // 验证验证码
- if (!this.validateInput(this.form.captcha, 'login.requiredCaptcha')) {
- return;
- }
- // 加密密码
- let encryptedPassword;
- try {
- // 拼接验证码和密码
- const captchaAndPassword = this.form.captcha + this.form.password;
- encryptedPassword = sm2Encrypt(this.sm2PublicKey, captchaAndPassword);
- } catch (error) {
- console.error("密码加密失败:", error);
- showDanger(this.$t('sm2.encryptionFailed'));
- return;
- }
- const plainUsername = this.form.username;
- this.form.captchaId = this.captchaUuid;
- // 加密
- const loginData = {
- username: plainUsername,
- password: encryptedPassword,
- captchaId: this.form.captchaId
- };
- Api.user.login(
- loginData,
- ({ data }) => {
- showSuccess(this.$t('login.loginSuccess'));
- this.$store.commit("setToken", JSON.stringify(data.data));
- this.getUserInfo();
- },
- (err) => {
- // 直接使用后端返回的国际化消息
- let errorMessage = err.data.msg || "登录失败";
- showDanger(errorMessage);
- }
- );
- // 重新获取验证码
- setTimeout(() => {
- this.fetchCaptcha();
- }, 1000);
- },
- goToRegister() {
- goToPage("/register");
- },
- goToForgetPassword() {
- goToPage("/retrieve-password");
- }
- },
- };
- </script>
- <style lang="scss" scoped>
- @import "./auth.scss";
- .login-type-container {
- margin: 10px 20px;
- display: flex;
- justify-content: center;
- }
- .title-language-dropdown {
- margin-left: auto;
- }
- .current-language-text {
- margin-left: 4px;
- margin-right: 4px;
- font-size: 12px;
- color: #3d4566;
- }
- .language-dropdown {
- margin-left: auto;
- }
- .rotate-down {
- transform: rotate(180deg);
- transition: transform 0.3s ease;
- }
- .el-icon-arrow-down {
- transition: transform 0.3s ease;
- }
- :deep(.el-button--primary) {
- background-color: #5778ff;
- border-color: #5778ff;
- &:hover,
- &:focus {
- background-color: #4a6ae8;
- border-color: #4a6ae8;
- }
- &:active {
- background-color: #3d5cd6;
- border-color: #3d5cd6;
- }
- }
- </style>
|