refactor: 重构登录页面 UI 以适配多因子认证、第三方登录等场景

This commit is contained in:
Charles7c 2023-09-27 23:19:24 +08:00
parent d356a6ad04
commit d40d5b4ae6
13 changed files with 662 additions and 400 deletions

View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 551 KiB

View File

@ -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: {

View 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>

View File

@ -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>

View File

@ -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>

View 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>

View File

@ -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 class="footer"> </div>
<Footer /> <div class="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>
.container { .root {
display: flex; background-image: url(../../assets/images/login/login-bg.png);
height: 100vh; background-repeat: no-repeat;
.banner { background-size: cover;
width: 550px; min-height: 100vh;
background: linear-gradient(163.85deg, #1d2129 0%, #00308f 100%);
}
.content {
position: relative;
display: flex;
flex: 1;
align-items: center;
justify-content: center;
padding-bottom: 40px;
}
.footer {
position: absolute;
right: 0;
bottom: 0;
width: 100%;
}
}
.logo { a {
position: fixed; color: #3370ff;
top: 24px; cursor: pointer !important;
left: 22px; text-decoration: none;
z-index: 1; }
display: inline-flex;
align-items: center; a:hover {
color: #6694ff;
&-text { }
margin-right: 4px;
margin-left: 4px; .header {
color: var(--color-fill-1); padding: 32px 40px 0;
font-size: 20px; 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;
}
} }
}
@media (max-width: @screen-lg) {
.container { .container {
.banner { align-items: center;
width: 25%; box-sizing: border-box;
display: flex;
height: calc(100vh - 100px);
justify-content: center;
margin: 0 auto;
max-width: 1200px;
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;
top: 5%;
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;
}
}
.oauth {
margin-top: 20px;
padding: 0 5px;
:deep(.arco-divider-text) {
color: #80838a;
font-size: 12px;
font-weight: 400;
line-height: 20px;
}
:deep(.arco-divider) {
margin-bottom: 25px;
}
.idps {
align-items: center;
display: flex;
justify-content: center;
width: 100%;
.app {
margin-right: 12px;
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;
}
}
}
}
}
}
.footer {
align-items: center;
box-sizing: border-box;
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;
}
} }
} }
} }

View File

@ -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',
}; };

View File

@ -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 检查',
}; };