mirror of
				https://github.com/continew-org/continew-admin.git
				synced 2025-11-04 10:57:10 +08:00 
			
		
		
		
	refactor: 重构登录页面 UI 以适配多因子认证、第三方登录等场景
This commit is contained in:
		@@ -5,6 +5,7 @@ import { UserState } from '@/store/modules/login/types';
 | 
				
			|||||||
const BASE_URL = '/auth';
 | 
					const BASE_URL = '/auth';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface LoginReq {
 | 
					export interface LoginReq {
 | 
				
			||||||
 | 
					  phone?: string;
 | 
				
			||||||
  username: string;
 | 
					  username: string;
 | 
				
			||||||
  password: string;
 | 
					  password: string;
 | 
				
			||||||
  captcha: string;
 | 
					  captcha: string;
 | 
				
			||||||
 
 | 
				
			|||||||
										
											Binary file not shown.
										
									
								
							| 
		 Before Width: | Height: | Size: 245 KiB  | 
										
											Binary file not shown.
										
									
								
							| 
		 Before Width: | Height: | Size: 257 KiB  | 
										
											Binary file not shown.
										
									
								
							| 
		 Before Width: | Height: | Size: 353 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								continew-admin-ui/src/assets/images/login/login-bg.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								continew-admin-ui/src/assets/images/login/login-bg.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 551 KiB  | 
@@ -11,7 +11,7 @@ export default function useUser() {
 | 
				
			|||||||
  const logout = async (logoutTo?: string) => {
 | 
					  const logout = async (logoutTo?: string) => {
 | 
				
			||||||
    await loginStore.logout();
 | 
					    await loginStore.logout();
 | 
				
			||||||
    const currentRoute = router.currentRoute.value;
 | 
					    const currentRoute = router.currentRoute.value;
 | 
				
			||||||
    Notification.success(t('login.form.logout.success'));
 | 
					    Notification.success(t('login.logout.success'));
 | 
				
			||||||
    router.push({
 | 
					    router.push({
 | 
				
			||||||
      name: logoutTo && typeof logoutTo === 'string' ? logoutTo : 'login',
 | 
					      name: logoutTo && typeof logoutTo === 'string' ? logoutTo : 'login',
 | 
				
			||||||
      query: {
 | 
					      query: {
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										210
									
								
								continew-admin-ui/src/views/login/components/account-login.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										210
									
								
								continew-admin-ui/src/views/login/components/account-login.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,210 @@
 | 
				
			|||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <a-form
 | 
				
			||||||
 | 
					    ref="formRef"
 | 
				
			||||||
 | 
					    :model="form"
 | 
				
			||||||
 | 
					    :rules="rules"
 | 
				
			||||||
 | 
					    layout="vertical"
 | 
				
			||||||
 | 
					    size="large"
 | 
				
			||||||
 | 
					    class="login-form"
 | 
				
			||||||
 | 
					    @submit="handleLogin"
 | 
				
			||||||
 | 
					  >
 | 
				
			||||||
 | 
					    <a-form-item field="username" hide-label>
 | 
				
			||||||
 | 
					      <a-input
 | 
				
			||||||
 | 
					        v-model="form.username"
 | 
				
			||||||
 | 
					        :placeholder="$t('login.account.placeholder.username')"
 | 
				
			||||||
 | 
					        :max-length="64"
 | 
				
			||||||
 | 
					        allow-clear
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					    </a-form-item>
 | 
				
			||||||
 | 
					    <a-form-item field="password" hide-label>
 | 
				
			||||||
 | 
					      <a-input-password
 | 
				
			||||||
 | 
					        v-model="form.password"
 | 
				
			||||||
 | 
					        :placeholder="$t('login.account.placeholder.password')"
 | 
				
			||||||
 | 
					        :max-length="32"
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					    </a-form-item>
 | 
				
			||||||
 | 
					    <a-form-item field="captcha" hide-label>
 | 
				
			||||||
 | 
					      <a-input
 | 
				
			||||||
 | 
					        v-model="form.captcha"
 | 
				
			||||||
 | 
					        :placeholder="$t('login.account.placeholder.captcha')"
 | 
				
			||||||
 | 
					        :max-length="4"
 | 
				
			||||||
 | 
					        allow-clear
 | 
				
			||||||
 | 
					        style="flex: 1 1"
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					      <img
 | 
				
			||||||
 | 
					        :src="captchaImgBase64"
 | 
				
			||||||
 | 
					        :alt="$t('login.captcha')"
 | 
				
			||||||
 | 
					        class="captcha"
 | 
				
			||||||
 | 
					        @click="getCaptcha"
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					    </a-form-item>
 | 
				
			||||||
 | 
					    <div class="remember-me">
 | 
				
			||||||
 | 
					      <a-checkbox
 | 
				
			||||||
 | 
					        :model-value="loginConfig.rememberMe"
 | 
				
			||||||
 | 
					        @change="setRememberMe as any"
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        {{ $t('login.rememberMe') }}
 | 
				
			||||||
 | 
					      </a-checkbox>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					    <a-button class="btn" :loading="loading" type="primary" html-type="submit"
 | 
				
			||||||
 | 
					      >{{ $t('login.button') }}
 | 
				
			||||||
 | 
					    </a-button>
 | 
				
			||||||
 | 
					  </a-form>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script lang="ts" setup>
 | 
				
			||||||
 | 
					  import { getCurrentInstance, ref, toRefs, reactive } from 'vue';
 | 
				
			||||||
 | 
					  import { useI18n } from 'vue-i18n';
 | 
				
			||||||
 | 
					  import { useStorage } from '@vueuse/core';
 | 
				
			||||||
 | 
					  import { useLoginStore } from '@/store';
 | 
				
			||||||
 | 
					  import { LoginReq } from '@/api/auth/login';
 | 
				
			||||||
 | 
					  import { ValidatedError } from '@arco-design/web-vue';
 | 
				
			||||||
 | 
					  import { encryptByRsa } from '@/utils/encrypt';
 | 
				
			||||||
 | 
					  import { useRouter } from 'vue-router';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const { proxy } = getCurrentInstance() as any;
 | 
				
			||||||
 | 
					  const { t } = useI18n();
 | 
				
			||||||
 | 
					  const router = useRouter();
 | 
				
			||||||
 | 
					  const loginStore = useLoginStore();
 | 
				
			||||||
 | 
					  const loading = ref(false);
 | 
				
			||||||
 | 
					  const captchaImgBase64 = ref();
 | 
				
			||||||
 | 
					  const loginConfig = useStorage('login-config', {
 | 
				
			||||||
 | 
					    rememberMe: true,
 | 
				
			||||||
 | 
					    username: 'admin', // 演示默认值
 | 
				
			||||||
 | 
					    password: 'admin123', // 演示默认值
 | 
				
			||||||
 | 
					    // username: debug ? 'admin' : '', // 演示默认值
 | 
				
			||||||
 | 
					    // password: debug ? 'admin123' : '', // 演示默认值
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					  const data = reactive({
 | 
				
			||||||
 | 
					    form: {
 | 
				
			||||||
 | 
					      username: loginConfig.value.username,
 | 
				
			||||||
 | 
					      password: loginConfig.value.password,
 | 
				
			||||||
 | 
					      captcha: '',
 | 
				
			||||||
 | 
					      uuid: '',
 | 
				
			||||||
 | 
					    } as LoginReq,
 | 
				
			||||||
 | 
					    rules: {
 | 
				
			||||||
 | 
					      username: [
 | 
				
			||||||
 | 
					        { required: true, message: t('login.account.error.required.username') },
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      password: [
 | 
				
			||||||
 | 
					        { required: true, message: t('login.account.error.required.password') },
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      captcha: [
 | 
				
			||||||
 | 
					        { required: true, message: t('login.account.error.required.captcha') },
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					  const { form, rules } = toRefs(data);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * 获取验证码
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  const getCaptcha = () => {
 | 
				
			||||||
 | 
					    loginStore.getImgCaptcha().then((res) => {
 | 
				
			||||||
 | 
					      form.value.uuid = res.data.uuid;
 | 
				
			||||||
 | 
					      captchaImgBase64.value = res.data.img;
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					  getCaptcha();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * 登录
 | 
				
			||||||
 | 
					   *
 | 
				
			||||||
 | 
					   * @param errors 表单验证错误
 | 
				
			||||||
 | 
					   * @param values 表单数据
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  const handleLogin = ({
 | 
				
			||||||
 | 
					    errors,
 | 
				
			||||||
 | 
					    values,
 | 
				
			||||||
 | 
					  }: {
 | 
				
			||||||
 | 
					    errors: Record<string, ValidatedError> | undefined;
 | 
				
			||||||
 | 
					    values: Record<string, any>;
 | 
				
			||||||
 | 
					  }) => {
 | 
				
			||||||
 | 
					    if (loading.value) return;
 | 
				
			||||||
 | 
					    if (!errors) {
 | 
				
			||||||
 | 
					      loading.value = true;
 | 
				
			||||||
 | 
					      loginStore
 | 
				
			||||||
 | 
					        .login({
 | 
				
			||||||
 | 
					          username: values.username,
 | 
				
			||||||
 | 
					          password: encryptByRsa(values.password) || '',
 | 
				
			||||||
 | 
					          captcha: values.captcha,
 | 
				
			||||||
 | 
					          uuid: values.uuid,
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        .then(() => {
 | 
				
			||||||
 | 
					          const { redirect, ...othersQuery } = router.currentRoute.value.query;
 | 
				
			||||||
 | 
					          router.push({
 | 
				
			||||||
 | 
					            name: (redirect as string) || 'Workplace',
 | 
				
			||||||
 | 
					            query: {
 | 
				
			||||||
 | 
					              ...othersQuery,
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
 | 
					          const { rememberMe } = loginConfig.value;
 | 
				
			||||||
 | 
					          const { username } = values;
 | 
				
			||||||
 | 
					          loginConfig.value.username = rememberMe ? username : '';
 | 
				
			||||||
 | 
					          proxy.$notification.success(t('login.success'));
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        .catch(() => {
 | 
				
			||||||
 | 
					          getCaptcha();
 | 
				
			||||||
 | 
					          form.value.captcha = '';
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        .finally(() => {
 | 
				
			||||||
 | 
					          loading.value = false;
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * 记住我
 | 
				
			||||||
 | 
					   *
 | 
				
			||||||
 | 
					   * @param value 是否记住我
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  const setRememberMe = (value: boolean) => {
 | 
				
			||||||
 | 
					    loginConfig.value.rememberMe = value;
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style lang="less" scoped>
 | 
				
			||||||
 | 
					  .login-form {
 | 
				
			||||||
 | 
					    box-sizing: border-box;
 | 
				
			||||||
 | 
					    padding: 0 5px;
 | 
				
			||||||
 | 
					    margin-top: 16px;
 | 
				
			||||||
 | 
					    .arco-input-wrapper,
 | 
				
			||||||
 | 
					    :deep(.arco-select-view-single) {
 | 
				
			||||||
 | 
					      background-color: var(--color-bg-white);
 | 
				
			||||||
 | 
					      border: 1px solid var(--color-border-3);
 | 
				
			||||||
 | 
					      height: 40px;
 | 
				
			||||||
 | 
					      border-radius: 4px;
 | 
				
			||||||
 | 
					      font-size: 13px;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    .arco-input-wrapper.arco-input-error {
 | 
				
			||||||
 | 
					      background-color: var(--color-danger-light-1);
 | 
				
			||||||
 | 
					      border-color: var(--color-danger-light-4);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .captcha {
 | 
				
			||||||
 | 
					      width: 111px;
 | 
				
			||||||
 | 
					      height: 36px;
 | 
				
			||||||
 | 
					      margin: 0 0 0 5px;
 | 
				
			||||||
 | 
					      cursor: pointer;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .remember-me {
 | 
				
			||||||
 | 
					      display: flex;
 | 
				
			||||||
 | 
					      justify-content: space-between;
 | 
				
			||||||
 | 
					      .arco-checkbox {
 | 
				
			||||||
 | 
					        padding-left: 0;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .btn {
 | 
				
			||||||
 | 
					      border-radius: 4px;
 | 
				
			||||||
 | 
					      box-shadow: 0 0 0 1px #05f, 0 2px 1px rgba(0, 0, 0, 0.15);
 | 
				
			||||||
 | 
					      font-size: 14px;
 | 
				
			||||||
 | 
					      font-weight: 500;
 | 
				
			||||||
 | 
					      height: 40px;
 | 
				
			||||||
 | 
					      line-height: 22px;
 | 
				
			||||||
 | 
					      margin: 20px 0 12px;
 | 
				
			||||||
 | 
					      width: 100%;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
@@ -1,87 +0,0 @@
 | 
				
			|||||||
<template>
 | 
					 | 
				
			||||||
  <div class="banner">
 | 
					 | 
				
			||||||
    <div class="banner-inner">
 | 
					 | 
				
			||||||
      <a-carousel class="carousel" animation-name="fade">
 | 
					 | 
				
			||||||
        <a-carousel-item v-for="item in carouselItem" :key="item.slogan">
 | 
					 | 
				
			||||||
          <div :key="item.slogan" class="carousel-item">
 | 
					 | 
				
			||||||
            <div class="carousel-title">{{ item.slogan }}</div>
 | 
					 | 
				
			||||||
            <div class="carousel-sub-title">{{ item.subSlogan }}</div>
 | 
					 | 
				
			||||||
            <img class="carousel-image" :src="item.image" />
 | 
					 | 
				
			||||||
          </div>
 | 
					 | 
				
			||||||
        </a-carousel-item>
 | 
					 | 
				
			||||||
      </a-carousel>
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
  </div>
 | 
					 | 
				
			||||||
</template>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<script lang="ts" setup>
 | 
					 | 
				
			||||||
  import { computed } from 'vue';
 | 
					 | 
				
			||||||
  import { useI18n } from 'vue-i18n';
 | 
					 | 
				
			||||||
  import bannerImage1 from '@/assets/images/login/banner1.png';
 | 
					 | 
				
			||||||
  import bannerImage2 from '@/assets/images/login/banner2.png';
 | 
					 | 
				
			||||||
  import bannerImage3 from '@/assets/images/login/banner3.png';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const { t } = useI18n();
 | 
					 | 
				
			||||||
  const carouselItem = computed(() => [
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
      slogan: t('login.banner.slogan1'),
 | 
					 | 
				
			||||||
      subSlogan: t('login.banner.subSlogan1'),
 | 
					 | 
				
			||||||
      image: bannerImage1,
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
      slogan: t('login.banner.slogan2'),
 | 
					 | 
				
			||||||
      subSlogan: t('login.banner.subSlogan2'),
 | 
					 | 
				
			||||||
      image: bannerImage2,
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
      slogan: t('login.banner.slogan3'),
 | 
					 | 
				
			||||||
      subSlogan: t('login.banner.subSlogan3'),
 | 
					 | 
				
			||||||
      image: bannerImage3,
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
  ]);
 | 
					 | 
				
			||||||
</script>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<style lang="less" scoped>
 | 
					 | 
				
			||||||
  .banner {
 | 
					 | 
				
			||||||
    display: flex;
 | 
					 | 
				
			||||||
    align-items: center;
 | 
					 | 
				
			||||||
    justify-content: center;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    &-inner {
 | 
					 | 
				
			||||||
      flex: 1;
 | 
					 | 
				
			||||||
      height: 100%;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  .carousel {
 | 
					 | 
				
			||||||
    height: 100%;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    &-item {
 | 
					 | 
				
			||||||
      display: flex;
 | 
					 | 
				
			||||||
      flex-direction: column;
 | 
					 | 
				
			||||||
      align-items: center;
 | 
					 | 
				
			||||||
      justify-content: center;
 | 
					 | 
				
			||||||
      height: 100%;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    &-title {
 | 
					 | 
				
			||||||
      color: var(--color-fill-1);
 | 
					 | 
				
			||||||
      font-weight: 500;
 | 
					 | 
				
			||||||
      font-size: 20px;
 | 
					 | 
				
			||||||
      line-height: 28px;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    &-sub-title {
 | 
					 | 
				
			||||||
      margin-top: 8px;
 | 
					 | 
				
			||||||
      margin-left: 30px;
 | 
					 | 
				
			||||||
      color: var(--color-text-3);
 | 
					 | 
				
			||||||
      font-size: 14px;
 | 
					 | 
				
			||||||
      line-height: 22px;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    &-image {
 | 
					 | 
				
			||||||
      width: 360px;
 | 
					 | 
				
			||||||
      margin-top: 30px;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
</style>
 | 
					 | 
				
			||||||
@@ -1,216 +0,0 @@
 | 
				
			|||||||
<template>
 | 
					 | 
				
			||||||
  <div class="login-form-wrapper">
 | 
					 | 
				
			||||||
    <div class="login-form-title">登录 {{ appStore.getTitle }}</div>
 | 
					 | 
				
			||||||
    <a-form
 | 
					 | 
				
			||||||
      ref="formRef"
 | 
					 | 
				
			||||||
      :model="form"
 | 
					 | 
				
			||||||
      :rules="rules"
 | 
					 | 
				
			||||||
      layout="vertical"
 | 
					 | 
				
			||||||
      size="large"
 | 
					 | 
				
			||||||
      class="login-form"
 | 
					 | 
				
			||||||
      @submit="handleLogin"
 | 
					 | 
				
			||||||
    >
 | 
					 | 
				
			||||||
      <a-form-item field="username" hide-label>
 | 
					 | 
				
			||||||
        <a-input
 | 
					 | 
				
			||||||
          v-model="form.username"
 | 
					 | 
				
			||||||
          :placeholder="$t('login.form.placeholder.username')"
 | 
					 | 
				
			||||||
          :max-length="64"
 | 
					 | 
				
			||||||
        >
 | 
					 | 
				
			||||||
          <template #prefix><icon-user /></template>
 | 
					 | 
				
			||||||
        </a-input>
 | 
					 | 
				
			||||||
      </a-form-item>
 | 
					 | 
				
			||||||
      <a-form-item field="password" hide-label>
 | 
					 | 
				
			||||||
        <a-input-password
 | 
					 | 
				
			||||||
          v-model="form.password"
 | 
					 | 
				
			||||||
          :placeholder="$t('login.form.placeholder.password')"
 | 
					 | 
				
			||||||
          :max-length="32"
 | 
					 | 
				
			||||||
          allow-clear
 | 
					 | 
				
			||||||
        >
 | 
					 | 
				
			||||||
          <template #prefix><icon-lock /></template>
 | 
					 | 
				
			||||||
        </a-input-password>
 | 
					 | 
				
			||||||
      </a-form-item>
 | 
					 | 
				
			||||||
      <a-form-item class="login-form-captcha" field="captcha" hide-label>
 | 
					 | 
				
			||||||
        <a-input
 | 
					 | 
				
			||||||
          v-model="form.captcha"
 | 
					 | 
				
			||||||
          :placeholder="$t('login.form.placeholder.captcha')"
 | 
					 | 
				
			||||||
          :max-length="4"
 | 
					 | 
				
			||||||
          allow-clear
 | 
					 | 
				
			||||||
          style="width: 63%"
 | 
					 | 
				
			||||||
        >
 | 
					 | 
				
			||||||
          <template #prefix><icon-check-circle /></template>
 | 
					 | 
				
			||||||
        </a-input>
 | 
					 | 
				
			||||||
        <img
 | 
					 | 
				
			||||||
          :src="captchaImgBase64"
 | 
					 | 
				
			||||||
          :alt="$t('login.form.captcha')"
 | 
					 | 
				
			||||||
          @click="getCaptcha"
 | 
					 | 
				
			||||||
        />
 | 
					 | 
				
			||||||
      </a-form-item>
 | 
					 | 
				
			||||||
      <a-space :size="16" direction="vertical">
 | 
					 | 
				
			||||||
        <div class="login-form-remember-me">
 | 
					 | 
				
			||||||
          <a-checkbox
 | 
					 | 
				
			||||||
            checked="rememberMe"
 | 
					 | 
				
			||||||
            :model-value="loginConfig.rememberMe"
 | 
					 | 
				
			||||||
            @change="setRememberMe as any"
 | 
					 | 
				
			||||||
          >
 | 
					 | 
				
			||||||
            {{ $t('login.form.rememberMe') }}
 | 
					 | 
				
			||||||
          </a-checkbox>
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
        <a-button :loading="loading" type="primary" long html-type="submit">
 | 
					 | 
				
			||||||
          {{ $t('login.form.login') }}
 | 
					 | 
				
			||||||
        </a-button>
 | 
					 | 
				
			||||||
      </a-space>
 | 
					 | 
				
			||||||
    </a-form>
 | 
					 | 
				
			||||||
  </div>
 | 
					 | 
				
			||||||
</template>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<script lang="ts" setup>
 | 
					 | 
				
			||||||
  import { getCurrentInstance, ref, toRefs, reactive, computed } from 'vue';
 | 
					 | 
				
			||||||
  import { FieldRule, ValidatedError } from '@arco-design/web-vue';
 | 
					 | 
				
			||||||
  import { LoginReq } from '@/api/auth/login';
 | 
					 | 
				
			||||||
  import { useI18n } from 'vue-i18n';
 | 
					 | 
				
			||||||
  import { useRouter } from 'vue-router';
 | 
					 | 
				
			||||||
  import { useStorage } from '@vueuse/core';
 | 
					 | 
				
			||||||
  import { useLoginStore, useAppStore } from '@/store';
 | 
					 | 
				
			||||||
  import { encryptByRsa } from '@/utils/encrypt';
 | 
					 | 
				
			||||||
  // import debug from '@/utils/env';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const { proxy } = getCurrentInstance() as any;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const captchaImgBase64 = ref('');
 | 
					 | 
				
			||||||
  const loginStore = useLoginStore();
 | 
					 | 
				
			||||||
  const appStore = useAppStore();
 | 
					 | 
				
			||||||
  const loading = ref(false);
 | 
					 | 
				
			||||||
  const { t } = useI18n();
 | 
					 | 
				
			||||||
  const router = useRouter();
 | 
					 | 
				
			||||||
  const loginConfig = useStorage('login-config', {
 | 
					 | 
				
			||||||
    rememberMe: true,
 | 
					 | 
				
			||||||
    username: 'admin', // 演示默认值
 | 
					 | 
				
			||||||
    password: 'admin123', // 演示默认值
 | 
					 | 
				
			||||||
    // username: debug ? 'admin' : '', // 演示默认值
 | 
					 | 
				
			||||||
    // password: debug ? 'admin123' : '', // 演示默认值
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const data = reactive({
 | 
					 | 
				
			||||||
    // 表单数据
 | 
					 | 
				
			||||||
    form: {
 | 
					 | 
				
			||||||
      username: loginConfig.value.username,
 | 
					 | 
				
			||||||
      password: loginConfig.value.password,
 | 
					 | 
				
			||||||
      captcha: '',
 | 
					 | 
				
			||||||
      uuid: '',
 | 
					 | 
				
			||||||
    } as LoginReq,
 | 
					 | 
				
			||||||
    // 表单验证规则
 | 
					 | 
				
			||||||
    rules: computed((): Record<string, FieldRule[]> => {
 | 
					 | 
				
			||||||
      return {
 | 
					 | 
				
			||||||
        username: [
 | 
					 | 
				
			||||||
          { required: true, message: t('login.form.error.required.username') },
 | 
					 | 
				
			||||||
        ],
 | 
					 | 
				
			||||||
        password: [
 | 
					 | 
				
			||||||
          { required: true, message: t('login.form.error.required.password') },
 | 
					 | 
				
			||||||
        ],
 | 
					 | 
				
			||||||
        captcha: [
 | 
					 | 
				
			||||||
          { required: true, message: t('login.form.error.required.captcha') },
 | 
					 | 
				
			||||||
        ],
 | 
					 | 
				
			||||||
      };
 | 
					 | 
				
			||||||
    }),
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
  const { form, rules } = toRefs(data);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  /**
 | 
					 | 
				
			||||||
   * 获取验证码
 | 
					 | 
				
			||||||
   */
 | 
					 | 
				
			||||||
  const getCaptcha = () => {
 | 
					 | 
				
			||||||
    loginStore.getImgCaptcha().then((res) => {
 | 
					 | 
				
			||||||
      form.value.uuid = res.data.uuid;
 | 
					 | 
				
			||||||
      captchaImgBase64.value = res.data.img;
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
  getCaptcha();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  /**
 | 
					 | 
				
			||||||
   * 登录
 | 
					 | 
				
			||||||
   *
 | 
					 | 
				
			||||||
   * @param errors 表单验证错误
 | 
					 | 
				
			||||||
   * @param values 表单数据
 | 
					 | 
				
			||||||
   */
 | 
					 | 
				
			||||||
  const handleLogin = ({
 | 
					 | 
				
			||||||
    errors,
 | 
					 | 
				
			||||||
    values,
 | 
					 | 
				
			||||||
  }: {
 | 
					 | 
				
			||||||
    errors: Record<string, ValidatedError> | undefined;
 | 
					 | 
				
			||||||
    values: Record<string, any>;
 | 
					 | 
				
			||||||
  }) => {
 | 
					 | 
				
			||||||
    if (loading.value) return;
 | 
					 | 
				
			||||||
    if (!errors) {
 | 
					 | 
				
			||||||
      loading.value = true;
 | 
					 | 
				
			||||||
      loginStore
 | 
					 | 
				
			||||||
        .login({
 | 
					 | 
				
			||||||
          username: values.username,
 | 
					 | 
				
			||||||
          password: encryptByRsa(values.password) || '',
 | 
					 | 
				
			||||||
          captcha: values.captcha,
 | 
					 | 
				
			||||||
          uuid: values.uuid,
 | 
					 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
        .then(() => {
 | 
					 | 
				
			||||||
          const { redirect, ...othersQuery } = router.currentRoute.value.query;
 | 
					 | 
				
			||||||
          router.push({
 | 
					 | 
				
			||||||
            name: (redirect as string) || 'Workplace',
 | 
					 | 
				
			||||||
            query: {
 | 
					 | 
				
			||||||
              ...othersQuery,
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
          });
 | 
					 | 
				
			||||||
          const { rememberMe } = loginConfig.value;
 | 
					 | 
				
			||||||
          const { username } = values;
 | 
					 | 
				
			||||||
          loginConfig.value.username = rememberMe ? username : '';
 | 
					 | 
				
			||||||
          proxy.$notification.success(t('login.form.login.success'));
 | 
					 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
        .catch(() => {
 | 
					 | 
				
			||||||
          getCaptcha();
 | 
					 | 
				
			||||||
          form.value.captcha = '';
 | 
					 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
        .finally(() => {
 | 
					 | 
				
			||||||
          loading.value = false;
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  /**
 | 
					 | 
				
			||||||
   * 记住我
 | 
					 | 
				
			||||||
   *
 | 
					 | 
				
			||||||
   * @param value 是否记住我
 | 
					 | 
				
			||||||
   */
 | 
					 | 
				
			||||||
  const setRememberMe = (value: boolean) => {
 | 
					 | 
				
			||||||
    loginConfig.value.rememberMe = value;
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
</script>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<style lang="less" scoped>
 | 
					 | 
				
			||||||
  .login-form {
 | 
					 | 
				
			||||||
    margin-top: 15px;
 | 
					 | 
				
			||||||
    &-wrapper {
 | 
					 | 
				
			||||||
      width: 320px;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    &-title {
 | 
					 | 
				
			||||||
      color: var(--color-text-1);
 | 
					 | 
				
			||||||
      font-weight: 500;
 | 
					 | 
				
			||||||
      font-size: 24px;
 | 
					 | 
				
			||||||
      line-height: 32px;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    &-sub-title {
 | 
					 | 
				
			||||||
      color: var(--color-text-3);
 | 
					 | 
				
			||||||
      font-size: 16px;
 | 
					 | 
				
			||||||
      line-height: 24px;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    &-captcha img {
 | 
					 | 
				
			||||||
      width: 111px;
 | 
					 | 
				
			||||||
      height: 36px;
 | 
					 | 
				
			||||||
      margin: 0 0 0 10px;
 | 
					 | 
				
			||||||
      cursor: pointer;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    &-remember-me {
 | 
					 | 
				
			||||||
      display: flex;
 | 
					 | 
				
			||||||
      justify-content: space-between;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    &-register-btn {
 | 
					 | 
				
			||||||
      color: var(--color-text-3) !important;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
</style>
 | 
					 | 
				
			||||||
							
								
								
									
										158
									
								
								continew-admin-ui/src/views/login/components/phone-login.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										158
									
								
								continew-admin-ui/src/views/login/components/phone-login.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,158 @@
 | 
				
			|||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <a-form
 | 
				
			||||||
 | 
					    ref="formRef"
 | 
				
			||||||
 | 
					    :model="form"
 | 
				
			||||||
 | 
					    :rules="rules"
 | 
				
			||||||
 | 
					    layout="vertical"
 | 
				
			||||||
 | 
					    size="large"
 | 
				
			||||||
 | 
					    class="login-form"
 | 
				
			||||||
 | 
					  >
 | 
				
			||||||
 | 
					    <a-form-item field="phone" hide-label>
 | 
				
			||||||
 | 
					      <a-select :options="['+86']" style="flex: 1 1" default-value="+86" />
 | 
				
			||||||
 | 
					      <a-input
 | 
				
			||||||
 | 
					        v-model="form.phone"
 | 
				
			||||||
 | 
					        :placeholder="$t('login.phone.placeholder.phone')"
 | 
				
			||||||
 | 
					        :max-length="11"
 | 
				
			||||||
 | 
					        allow-clear
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					    </a-form-item>
 | 
				
			||||||
 | 
					    <a-form-item field="captcha" hide-label>
 | 
				
			||||||
 | 
					      <a-input
 | 
				
			||||||
 | 
					        v-model="form.captcha"
 | 
				
			||||||
 | 
					        :placeholder="$t('login.phone.placeholder.captcha')"
 | 
				
			||||||
 | 
					        :max-length="6"
 | 
				
			||||||
 | 
					        allow-clear
 | 
				
			||||||
 | 
					        style="flex: 1 1"
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					      <a-button
 | 
				
			||||||
 | 
					        class="captcha-btn"
 | 
				
			||||||
 | 
					        :loading="captchaLoading"
 | 
				
			||||||
 | 
					        :disabled="captchaDisable"
 | 
				
			||||||
 | 
					        @click="handleSendCaptcha"
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        {{ captchaBtnName }}
 | 
				
			||||||
 | 
					      </a-button>
 | 
				
			||||||
 | 
					    </a-form-item>
 | 
				
			||||||
 | 
					    <a-button class="btn" :loading="loading" type="primary" html-type="submit"
 | 
				
			||||||
 | 
					      >{{ $t('login.button') }}(即将开放)
 | 
				
			||||||
 | 
					    </a-button>
 | 
				
			||||||
 | 
					  </a-form>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script lang="ts" setup>
 | 
				
			||||||
 | 
					  import { getCurrentInstance, ref, toRefs, reactive, computed } from 'vue';
 | 
				
			||||||
 | 
					  import { useI18n } from 'vue-i18n';
 | 
				
			||||||
 | 
					  import { useLoginStore } from '@/store';
 | 
				
			||||||
 | 
					  import { LoginReq } from '@/api/auth/login';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const { proxy } = getCurrentInstance() as any;
 | 
				
			||||||
 | 
					  const { t } = useI18n();
 | 
				
			||||||
 | 
					  const loginStore = useLoginStore();
 | 
				
			||||||
 | 
					  const loading = ref(false);
 | 
				
			||||||
 | 
					  const captchaLoading = ref(false);
 | 
				
			||||||
 | 
					  const captchaDisable = ref(false);
 | 
				
			||||||
 | 
					  const captchaTime = ref(60);
 | 
				
			||||||
 | 
					  const captchaTimer = ref();
 | 
				
			||||||
 | 
					  const captchaBtnNameKey = ref('login.phone.captcha');
 | 
				
			||||||
 | 
					  const captchaBtnName = computed(() => t(captchaBtnNameKey.value));
 | 
				
			||||||
 | 
					  const data = reactive({
 | 
				
			||||||
 | 
					    form: {} as LoginReq,
 | 
				
			||||||
 | 
					    rules: {
 | 
				
			||||||
 | 
					      phone: [
 | 
				
			||||||
 | 
					        { required: true, message: t('login.phone.error.required.phone') },
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      captcha: [
 | 
				
			||||||
 | 
					        { required: true, message: t('login.phone.error.required.captcha') },
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					  const { form, rules } = toRefs(data);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * 重置验证码
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  const resetCaptcha = () => {
 | 
				
			||||||
 | 
					    window.clearInterval(captchaTimer.value);
 | 
				
			||||||
 | 
					    captchaTime.value = 60;
 | 
				
			||||||
 | 
					    captchaBtnNameKey.value = 'login.phone.captcha';
 | 
				
			||||||
 | 
					    captchaDisable.value = false;
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * 发送验证码
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  const handleSendCaptcha = () => {
 | 
				
			||||||
 | 
					    if (captchaLoading.value) return;
 | 
				
			||||||
 | 
					    proxy.$refs.formRef.validateField('phone', (valid: any) => {
 | 
				
			||||||
 | 
					      if (!valid) {
 | 
				
			||||||
 | 
					        captchaLoading.value = true;
 | 
				
			||||||
 | 
					        captchaBtnNameKey.value = 'login.phone.captcha.ing';
 | 
				
			||||||
 | 
					        captchaLoading.value = false;
 | 
				
			||||||
 | 
					        captchaDisable.value = true;
 | 
				
			||||||
 | 
					        captchaBtnNameKey.value = `${t(
 | 
				
			||||||
 | 
					          'login.phone.reCaptcha'
 | 
				
			||||||
 | 
					        )}(${(captchaTime.value -= 1)}s)`;
 | 
				
			||||||
 | 
					        captchaTimer.value = window.setInterval(() => {
 | 
				
			||||||
 | 
					          captchaTime.value -= 1;
 | 
				
			||||||
 | 
					          captchaBtnNameKey.value = `${t('login.phone.reCaptcha')}(${
 | 
				
			||||||
 | 
					            captchaTime.value
 | 
				
			||||||
 | 
					          }s)`;
 | 
				
			||||||
 | 
					          if (captchaTime.value < 0) {
 | 
				
			||||||
 | 
					            window.clearInterval(captchaTimer.value);
 | 
				
			||||||
 | 
					            captchaTime.value = 60;
 | 
				
			||||||
 | 
					            captchaBtnNameKey.value = t('login.phone.reCaptcha');
 | 
				
			||||||
 | 
					            captchaDisable.value = false;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }, 1000);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style lang="less" scoped>
 | 
				
			||||||
 | 
					  .login-form {
 | 
				
			||||||
 | 
					    box-sizing: border-box;
 | 
				
			||||||
 | 
					    padding: 0 5px;
 | 
				
			||||||
 | 
					    margin-top: 16px;
 | 
				
			||||||
 | 
					    .arco-input-wrapper,
 | 
				
			||||||
 | 
					    :deep(.arco-select-view-single) {
 | 
				
			||||||
 | 
					      background-color: var(--color-bg-white);
 | 
				
			||||||
 | 
					      border: 1px solid var(--color-border-3);
 | 
				
			||||||
 | 
					      height: 40px;
 | 
				
			||||||
 | 
					      border-radius: 4px;
 | 
				
			||||||
 | 
					      font-size: 13px;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    .arco-input-wrapper.arco-input-error {
 | 
				
			||||||
 | 
					      background-color: var(--color-danger-light-1);
 | 
				
			||||||
 | 
					      border-color: var(--color-danger-light-4);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .captcha-btn {
 | 
				
			||||||
 | 
					      height: 40px;
 | 
				
			||||||
 | 
					      margin-left: 12px;
 | 
				
			||||||
 | 
					      min-width: 98px;
 | 
				
			||||||
 | 
					      border-radius: 4px;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .arco-btn-secondary:not(.arco-btn-disabled) {
 | 
				
			||||||
 | 
					      background-color: #f6f8fa;
 | 
				
			||||||
 | 
					      border: 1px solid #dde2e9;
 | 
				
			||||||
 | 
					      color: #41464f;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    .arco-btn-secondary:not(.arco-btn-disabled):hover {
 | 
				
			||||||
 | 
					      background-color: transparent;
 | 
				
			||||||
 | 
					      border: 1px solid rgb(var(--primary-6));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .btn {
 | 
				
			||||||
 | 
					      border-radius: 4px;
 | 
				
			||||||
 | 
					      box-shadow: 0 0 0 1px #05f, 0 2px 1px rgba(0, 0, 0, 0.15);
 | 
				
			||||||
 | 
					      font-size: 14px;
 | 
				
			||||||
 | 
					      font-weight: 500;
 | 
				
			||||||
 | 
					      height: 40px;
 | 
				
			||||||
 | 
					      line-height: 22px;
 | 
				
			||||||
 | 
					      margin: 20px 0 12px;
 | 
				
			||||||
 | 
					      width: 100%;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
@@ -1,75 +1,264 @@
 | 
				
			|||||||
<template>
 | 
					<template>
 | 
				
			||||||
  <div class="container">
 | 
					  <div class="root">
 | 
				
			||||||
    <div class="logo">
 | 
					    <div class="header">
 | 
				
			||||||
      <img :src="getFile(appStore.getLogo)" alt="logo" height="33" />
 | 
					      <img
 | 
				
			||||||
 | 
					        :src="getFile(appStore.getLogo) ?? './logo.svg'"
 | 
				
			||||||
 | 
					        alt="logo"
 | 
				
			||||||
 | 
					        height="33"
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
      <div class="logo-text">{{ appStore.getTitle }}</div>
 | 
					      <div class="logo-text">{{ appStore.getTitle }}</div>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
    <LoginBanner />
 | 
					    <div class="container">
 | 
				
			||||||
    <div class="content">
 | 
					      <div class="left-banner"></div>
 | 
				
			||||||
      <div class="content-inner">
 | 
					      <div class="login-card">
 | 
				
			||||||
        <LoginForm />
 | 
					        <div class="title"
 | 
				
			||||||
 | 
					          >{{ $t('login.welcome') }} {{ appStore.getTitle }}</div
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					        <a-tabs class="account-tab" default-active-key="1">
 | 
				
			||||||
 | 
					          <a-tab-pane key="1" :title="$t('login.account')"
 | 
				
			||||||
 | 
					            ><AccountLogin
 | 
				
			||||||
 | 
					          /></a-tab-pane>
 | 
				
			||||||
 | 
					          <a-tab-pane key="2" :title="$t('login.phone')"
 | 
				
			||||||
 | 
					            ><PhoneLogin
 | 
				
			||||||
 | 
					          /></a-tab-pane>
 | 
				
			||||||
 | 
					        </a-tabs>
 | 
				
			||||||
 | 
					        <div class="oauth">
 | 
				
			||||||
 | 
					          <a-divider class="text" orientation="center">{{
 | 
				
			||||||
 | 
					            $t('login.other')
 | 
				
			||||||
 | 
					          }}</a-divider>
 | 
				
			||||||
 | 
					          <div class="idps">
 | 
				
			||||||
 | 
					            <a-tooltip content="邮箱登录(即将开放)" mini>
 | 
				
			||||||
 | 
					              <div class="mail app">
 | 
				
			||||||
 | 
					                <icon-email /> {{ $t('login.email.txt') }}
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					            </a-tooltip>
 | 
				
			||||||
 | 
					            <a-tooltip content="Gitee(即将开放)" mini>
 | 
				
			||||||
 | 
					              <a href="javascript: void(0);" class="app">
 | 
				
			||||||
 | 
					                <svg
 | 
				
			||||||
 | 
					                  class="icon"
 | 
				
			||||||
 | 
					                  fill="#C71D23"
 | 
				
			||||||
 | 
					                  role="img"
 | 
				
			||||||
 | 
					                  viewBox="0 0 24 24"
 | 
				
			||||||
 | 
					                  xmlns="http://www.w3.org/2000/svg"
 | 
				
			||||||
 | 
					                >
 | 
				
			||||||
 | 
					                  <path
 | 
				
			||||||
 | 
					                    d="M11.984 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.016 0zm6.09 5.333c.328 0 .593.266.592.593v1.482a.594.594 0 0 1-.593.592H9.777c-.982 0-1.778.796-1.778 1.778v5.63c0 .327.266.592.593.592h5.63c.982 0 1.778-.796 1.778-1.778v-.296a.593.593 0 0 0-.592-.593h-4.15a.592.592 0 0 1-.592-.592v-1.482a.593.593 0 0 1 .593-.592h6.815c.327 0 .593.265.593.592v3.408a4 4 0 0 1-4 4H5.926a.593.593 0 0 1-.593-.593V9.778a4.444 4.444 0 0 1 4.445-4.444h8.296Z"
 | 
				
			||||||
 | 
					                  />
 | 
				
			||||||
 | 
					                </svg>
 | 
				
			||||||
 | 
					              </a>
 | 
				
			||||||
 | 
					            </a-tooltip>
 | 
				
			||||||
 | 
					            <a-tooltip content="GitHub(即将开放)" mini>
 | 
				
			||||||
 | 
					              <a href="javascript: void(0);" class="app">
 | 
				
			||||||
 | 
					                <svg
 | 
				
			||||||
 | 
					                  class="icon"
 | 
				
			||||||
 | 
					                  role="img"
 | 
				
			||||||
 | 
					                  viewBox="0 0 24 24"
 | 
				
			||||||
 | 
					                  xmlns="http://www.w3.org/2000/svg"
 | 
				
			||||||
 | 
					                >
 | 
				
			||||||
 | 
					                  <path
 | 
				
			||||||
 | 
					                    d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"
 | 
				
			||||||
 | 
					                  />
 | 
				
			||||||
 | 
					                </svg>
 | 
				
			||||||
 | 
					              </a>
 | 
				
			||||||
 | 
					            </a-tooltip>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
    <div class="footer">
 | 
					    <div class="footer">
 | 
				
			||||||
        <Footer />
 | 
					      <div class="beian">
 | 
				
			||||||
 | 
					        <div class="below text" v-html="appStore.getCopyright"></div>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script lang="ts" setup>
 | 
					<script lang="ts" setup>
 | 
				
			||||||
  import Footer from '@/components/footer/index.vue';
 | 
					  import { useI18n } from 'vue-i18n';
 | 
				
			||||||
  import { useAppStore } from '@/store';
 | 
					  import { useAppStore } from '@/store';
 | 
				
			||||||
  import getFile from '@/utils/file';
 | 
					  import getFile from '@/utils/file';
 | 
				
			||||||
  import LoginBanner from './components/banner.vue';
 | 
					  import AccountLogin from './components/account-login.vue';
 | 
				
			||||||
  import LoginForm from './components/login-form.vue';
 | 
					  import PhoneLogin from './components/phone-login.vue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const { t } = useI18n();
 | 
				
			||||||
  const appStore = useAppStore();
 | 
					  const appStore = useAppStore();
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<style lang="less" scoped>
 | 
					<style lang="less" scoped>
 | 
				
			||||||
 | 
					  .root {
 | 
				
			||||||
 | 
					    background-image: url(../../assets/images/login/login-bg.png);
 | 
				
			||||||
 | 
					    background-repeat: no-repeat;
 | 
				
			||||||
 | 
					    background-size: cover;
 | 
				
			||||||
 | 
					    min-height: 100vh;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    a {
 | 
				
			||||||
 | 
					      color: #3370ff;
 | 
				
			||||||
 | 
					      cursor: pointer !important;
 | 
				
			||||||
 | 
					      text-decoration: none;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    a:hover {
 | 
				
			||||||
 | 
					      color: #6694ff;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .header {
 | 
				
			||||||
 | 
					      padding: 32px 40px 0;
 | 
				
			||||||
 | 
					      img {
 | 
				
			||||||
 | 
					        vertical-align: middle;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      .logo-text {
 | 
				
			||||||
 | 
					        display: inline-block;
 | 
				
			||||||
 | 
					        margin-right: 4px;
 | 
				
			||||||
 | 
					        margin-left: 4px;
 | 
				
			||||||
 | 
					        color: var(--color-text-1);
 | 
				
			||||||
 | 
					        font-size: 24px;
 | 
				
			||||||
 | 
					        vertical-align: middle;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    .container {
 | 
					    .container {
 | 
				
			||||||
    display: flex;
 | 
					 | 
				
			||||||
    height: 100vh;
 | 
					 | 
				
			||||||
    .banner {
 | 
					 | 
				
			||||||
      width: 550px;
 | 
					 | 
				
			||||||
      background: linear-gradient(163.85deg, #1d2129 0%, #00308f 100%);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    .content {
 | 
					 | 
				
			||||||
      position: relative;
 | 
					 | 
				
			||||||
      display: flex;
 | 
					 | 
				
			||||||
      flex: 1;
 | 
					 | 
				
			||||||
      align-items: center;
 | 
					      align-items: center;
 | 
				
			||||||
 | 
					      box-sizing: border-box;
 | 
				
			||||||
 | 
					      display: flex;
 | 
				
			||||||
 | 
					      height: calc(100vh - 100px);
 | 
				
			||||||
      justify-content: center;
 | 
					      justify-content: center;
 | 
				
			||||||
      padding-bottom: 40px;
 | 
					      margin: 0 auto;
 | 
				
			||||||
    }
 | 
					      max-width: 1200px;
 | 
				
			||||||
    .footer {
 | 
					      min-height: 650px;
 | 
				
			||||||
 | 
					      .left-banner {
 | 
				
			||||||
 | 
					        flex: 1 1;
 | 
				
			||||||
 | 
					        height: 100%;
 | 
				
			||||||
 | 
					        max-height: 700px;
 | 
				
			||||||
 | 
					        position: relative;
 | 
				
			||||||
 | 
					        img {
 | 
				
			||||||
 | 
					          height: 100%;
 | 
				
			||||||
 | 
					          left: 0;
 | 
				
			||||||
 | 
					          max-height: 350px;
 | 
				
			||||||
 | 
					          max-width: 500px;
 | 
				
			||||||
 | 
					          object-fit: contain;
 | 
				
			||||||
          position: absolute;
 | 
					          position: absolute;
 | 
				
			||||||
      right: 0;
 | 
					          top: 5%;
 | 
				
			||||||
      bottom: 0;
 | 
					 | 
				
			||||||
          width: 100%;
 | 
					          width: 100%;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					      .login-card {
 | 
				
			||||||
 | 
					        display: flex;
 | 
				
			||||||
 | 
					        background: #fff;
 | 
				
			||||||
 | 
					        border-radius: 20px;
 | 
				
			||||||
 | 
					        box-shadow: 0 5px 15px rgba(0, 0, 0, 0.05);
 | 
				
			||||||
 | 
					        box-sizing: border-box;
 | 
				
			||||||
 | 
					        min-height: 500px;
 | 
				
			||||||
 | 
					        position: relative;
 | 
				
			||||||
 | 
					        width: 450px;
 | 
				
			||||||
 | 
					        flex-direction: column;
 | 
				
			||||||
 | 
					        margin-bottom: 53px;
 | 
				
			||||||
 | 
					        padding: 48px 43px 32px;
 | 
				
			||||||
 | 
					        .title {
 | 
				
			||||||
 | 
					          color: #020814;
 | 
				
			||||||
 | 
					          font-size: 24px;
 | 
				
			||||||
 | 
					          font-weight: 500;
 | 
				
			||||||
 | 
					          letter-spacing: 0.003em;
 | 
				
			||||||
 | 
					          line-height: 32px;
 | 
				
			||||||
 | 
					          padding: 0 5px;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        .account-tab {
 | 
				
			||||||
 | 
					          margin-top: 36px;
 | 
				
			||||||
 | 
					          :deep(.arco-tabs-nav::before) {
 | 
				
			||||||
 | 
					            display: none;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          :deep(.arco-tabs-tab-title) {
 | 
				
			||||||
 | 
					            font-size: 16px;
 | 
				
			||||||
 | 
					            font-weight: 500;
 | 
				
			||||||
 | 
					            line-height: 22px;
 | 
				
			||||||
 | 
					            display: inline-block;
 | 
				
			||||||
 | 
					            padding: 1px 0;
 | 
				
			||||||
 | 
					            position: relative;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          :deep(.arco-tabs-tab-title:hover) {
 | 
				
			||||||
 | 
					            color: rgb(var(--primary-6));
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          :deep(.arco-tabs-tab-title:before) {
 | 
				
			||||||
 | 
					            display: none;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          :deep(.arco-tabs-tab) {
 | 
				
			||||||
 | 
					            margin: 0 30px 0 6px;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  .logo {
 | 
					        .oauth {
 | 
				
			||||||
    position: fixed;
 | 
					          margin-top: 20px;
 | 
				
			||||||
    top: 24px;
 | 
					          padding: 0 5px;
 | 
				
			||||||
    left: 22px;
 | 
					          :deep(.arco-divider-text) {
 | 
				
			||||||
    z-index: 1;
 | 
					            color: #80838a;
 | 
				
			||||||
    display: inline-flex;
 | 
					            font-size: 12px;
 | 
				
			||||||
 | 
					            font-weight: 400;
 | 
				
			||||||
 | 
					            line-height: 20px;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          :deep(.arco-divider) {
 | 
				
			||||||
 | 
					            margin-bottom: 25px;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          .idps {
 | 
				
			||||||
            align-items: center;
 | 
					            align-items: center;
 | 
				
			||||||
 | 
					            display: flex;
 | 
				
			||||||
    &-text {
 | 
					            justify-content: center;
 | 
				
			||||||
      margin-right: 4px;
 | 
					            width: 100%;
 | 
				
			||||||
      margin-left: 4px;
 | 
					            .app {
 | 
				
			||||||
      color: var(--color-fill-1);
 | 
					              margin-right: 12px;
 | 
				
			||||||
      font-size: 20px;
 | 
					              align-items: center;
 | 
				
			||||||
 | 
					              border: 1px solid #eaedf1;
 | 
				
			||||||
 | 
					              border-radius: 32px;
 | 
				
			||||||
 | 
					              box-sizing: border-box;
 | 
				
			||||||
 | 
					              display: flex;
 | 
				
			||||||
 | 
					              height: 32px;
 | 
				
			||||||
 | 
					              justify-content: center;
 | 
				
			||||||
 | 
					              width: 32px;
 | 
				
			||||||
 | 
					              cursor: pointer;
 | 
				
			||||||
 | 
					              .icon {
 | 
				
			||||||
 | 
					                width: 21px;
 | 
				
			||||||
 | 
					                height: 20px;
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            .app:hover {
 | 
				
			||||||
 | 
					              background: #f3f7ff;
 | 
				
			||||||
 | 
					              border: 1px solid #97bcff;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            .mail {
 | 
				
			||||||
 | 
					              min-width: 81px;
 | 
				
			||||||
 | 
					              width: 81px;
 | 
				
			||||||
 | 
					              color: #41464f;
 | 
				
			||||||
 | 
					              font-size: 12px;
 | 
				
			||||||
 | 
					              font-weight: 400;
 | 
				
			||||||
 | 
					              line-height: 20px;
 | 
				
			||||||
 | 
					              padding: 6px 10px;
 | 
				
			||||||
 | 
					              svg {
 | 
				
			||||||
 | 
					                color: #000;
 | 
				
			||||||
 | 
					                font-size: 16px;
 | 
				
			||||||
 | 
					                margin-right: 10px;
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @media (max-width: @screen-lg) {
 | 
					    .footer {
 | 
				
			||||||
    .container {
 | 
					      align-items: center;
 | 
				
			||||||
      .banner {
 | 
					      box-sizing: border-box;
 | 
				
			||||||
        width: 25%;
 | 
					      display: flex;
 | 
				
			||||||
 | 
					      justify-content: center;
 | 
				
			||||||
 | 
					      .beian {
 | 
				
			||||||
 | 
					        .text {
 | 
				
			||||||
 | 
					          color: #41464f;
 | 
				
			||||||
 | 
					          font-size: 12px;
 | 
				
			||||||
 | 
					          font-weight: 400;
 | 
				
			||||||
 | 
					          letter-spacing: 0.2px;
 | 
				
			||||||
 | 
					          line-height: 20px;
 | 
				
			||||||
 | 
					          text-align: center;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        .below {
 | 
				
			||||||
 | 
					          align-items: center;
 | 
				
			||||||
 | 
					          display: flex;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,29 +1,32 @@
 | 
				
			|||||||
export default {
 | 
					export default {
 | 
				
			||||||
  'login.form.placeholder.username': 'Please enter username',
 | 
					  'login.welcome': 'Welcome to',
 | 
				
			||||||
  'login.form.placeholder.password': 'Please enter password',
 | 
					  'login.account': 'Account Login',
 | 
				
			||||||
  'login.form.placeholder.captcha': 'Please enter captcha',
 | 
					  'login.phone': 'Phone Login',
 | 
				
			||||||
 | 
					  'login.email': 'Email Login',
 | 
				
			||||||
 | 
					  'login.other': 'Other Login',
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  'login.form.error.required.username': 'Please enter username',
 | 
					  'login.account.placeholder.username': 'Please enter username',
 | 
				
			||||||
  'login.form.error.required.password': 'Please enter password',
 | 
					  'login.account.placeholder.password': 'Please enter password',
 | 
				
			||||||
  'login.form.error.required.captcha': 'Please enter captcha',
 | 
					  'login.account.placeholder.captcha': 'Please enter captcha',
 | 
				
			||||||
 | 
					  'login.phone.placeholder.phone': 'Please enter phone',
 | 
				
			||||||
 | 
					  'login.phone.placeholder.captcha': 'Please enter captcha',
 | 
				
			||||||
 | 
					  'login.phone.captcha': 'Get captcha',
 | 
				
			||||||
 | 
					  'login.phone.captcha.ing': 'Sending...',
 | 
				
			||||||
 | 
					  'login.phone.reCaptcha': 'Resend captcha',
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  'login.form.captcha': 'Captcha',
 | 
					  'login.account.error.required.username': 'Please enter username',
 | 
				
			||||||
  'login.form.rememberMe': 'Remember me',
 | 
					  'login.account.error.required.password': 'Please enter password',
 | 
				
			||||||
  'login.form.login': 'Login',
 | 
					  'login.account.error.required.captcha': 'Please enter captcha',
 | 
				
			||||||
  'login.form.register': 'Register account',
 | 
					  'login.phone.error.required.phone': 'Please enter phone',
 | 
				
			||||||
  'login.form.forgetPassword': 'Forgot password',
 | 
					  'login.phone.error.required.captcha': 'Please enter captcha',
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  'login.form.login.success': 'Welcome to use',
 | 
					  'login.captcha': 'Captcha',
 | 
				
			||||||
  'login.form.login.error': 'Login error, refresh and try again',
 | 
					  'login.rememberMe': 'Remember me',
 | 
				
			||||||
  'login.form.logout.success': 'Logout success',
 | 
					  'login.button': 'Login',
 | 
				
			||||||
 | 
					  'login.email.txt': 'Email',
 | 
				
			||||||
 | 
					  'login.account.txt': 'Account/Phone Login',
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  'login.banner.slogan1': 'Middle and background management framework',
 | 
					  'login.success': 'Welcome to use',
 | 
				
			||||||
  'login.banner.subSlogan1':
 | 
					  'login.error': 'Login error, refresh and try again',
 | 
				
			||||||
    'Continue New Admin, continue to build on the latest popular technology stack',
 | 
					  'login.logout.success': 'Logout success',
 | 
				
			||||||
  'login.banner.slogan2': 'Built-in solutions to common problems',
 | 
					 | 
				
			||||||
  'login.banner.subSlogan2':
 | 
					 | 
				
			||||||
    'Rich basic functions, low threshold to use, enterprise rapid development scaffolding',
 | 
					 | 
				
			||||||
  'login.banner.slogan3': 'The code is standard and open source and free',
 | 
					 | 
				
			||||||
  'login.banner.subSlogan3':
 | 
					 | 
				
			||||||
    'The backend code is fully compliant with the Alibaba coding specification, and the frontend code is strictly ESLint checked',
 | 
					 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,28 +1,32 @@
 | 
				
			|||||||
export default {
 | 
					export default {
 | 
				
			||||||
  'login.form.placeholder.username': '请输入用户名',
 | 
					  'login.welcome': '欢迎来到',
 | 
				
			||||||
  'login.form.placeholder.password': '请输入密码',
 | 
					  'login.account': '账号登录',
 | 
				
			||||||
  'login.form.placeholder.captcha': '请输入验证码',
 | 
					  'login.phone': '手机号登录',
 | 
				
			||||||
 | 
					  'login.email': '邮箱登录',
 | 
				
			||||||
 | 
					  'login.other': '其他登录方式',
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  'login.form.error.required.username': '请输入用户名',
 | 
					  'login.account.placeholder.username': '请输入用户名',
 | 
				
			||||||
  'login.form.error.required.password': '请输入密码',
 | 
					  'login.account.placeholder.password': '请输入密码',
 | 
				
			||||||
  'login.form.error.required.captcha': '请输入验证码',
 | 
					  'login.account.placeholder.captcha': '请输入验证码',
 | 
				
			||||||
 | 
					  'login.phone.placeholder.phone': '请输入手机号',
 | 
				
			||||||
 | 
					  'login.phone.placeholder.captcha': '请输入验证码',
 | 
				
			||||||
 | 
					  'login.phone.captcha': '获取验证码',
 | 
				
			||||||
 | 
					  'login.phone.captcha.ing': '发送中...',
 | 
				
			||||||
 | 
					  'login.phone.reCaptcha': '重新发送',
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  'login.form.captcha': '验证码',
 | 
					  'login.account.error.required.username': '请输入用户名',
 | 
				
			||||||
  'login.form.rememberMe': '记住我',
 | 
					  'login.account.error.required.password': '请输入密码',
 | 
				
			||||||
  'login.form.login': '登录',
 | 
					  'login.account.error.required.captcha': '请输入验证码',
 | 
				
			||||||
  'login.form.register': '注册账号',
 | 
					  'login.phone.error.required.phone': '请输入手机号',
 | 
				
			||||||
  'login.form.forgetPassword': '忘记密码',
 | 
					  'login.phone.error.required.captcha': '请输入验证码',
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  'login.form.login.success': '欢迎使用',
 | 
					  'login.captcha': '验证码',
 | 
				
			||||||
  'login.form.login.error': '登录出错,请刷新重试',
 | 
					  'login.rememberMe': '记住我',
 | 
				
			||||||
  'login.form.logout.success': '退出成功',
 | 
					  'login.button': '立即登录',
 | 
				
			||||||
 | 
					  'login.email.txt': '邮箱',
 | 
				
			||||||
 | 
					  'login.account.txt': '账号/手机号登录',
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  'login.banner.slogan1': '中后台管理框架',
 | 
					  'login.success': '欢迎使用',
 | 
				
			||||||
  'login.banner.subSlogan1':
 | 
					  'login.error': '登录出错,请刷新重试',
 | 
				
			||||||
    'Continue New Admin,持续以最新流行技术栈构建,拥抱变化,迭代优化',
 | 
					  'login.logout.success': '退出成功',
 | 
				
			||||||
  'login.banner.slogan2': '内置了常见问题的解决方案',
 | 
					 | 
				
			||||||
  'login.banner.subSlogan2': '基础功能丰富,使用门槛低,企业级快速开发脚手架',
 | 
					 | 
				
			||||||
  'login.banner.slogan3': '代码规范且开源免费',
 | 
					 | 
				
			||||||
  'login.banner.subSlogan3':
 | 
					 | 
				
			||||||
    '后端代码完全遵循阿里巴巴编码规范,前端代码使用严格的 ESLint 检查',
 | 
					 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user