refactor: 重构登录页面 UI 以适配多因子认证、第三方登录等场景
This commit is contained in:
parent
d356a6ad04
commit
d40d5b4ae6
@ -5,6 +5,7 @@ import { UserState } from '@/store/modules/login/types';
|
||||
const BASE_URL = '/auth';
|
||||
|
||||
export interface LoginReq {
|
||||
phone?: string;
|
||||
username: string;
|
||||
password: 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) => {
|
||||
await loginStore.logout();
|
||||
const currentRoute = router.currentRoute.value;
|
||||
Notification.success(t('login.form.logout.success'));
|
||||
Notification.success(t('login.logout.success'));
|
||||
router.push({
|
||||
name: logoutTo && typeof logoutTo === 'string' ? logoutTo : 'login',
|
||||
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>
|
||||
<div class="container">
|
||||
<div class="logo">
|
||||
<img :src="getFile(appStore.getLogo)" alt="logo" height="33" />
|
||||
<div class="root">
|
||||
<div class="header">
|
||||
<img
|
||||
:src="getFile(appStore.getLogo) ?? './logo.svg'"
|
||||
alt="logo"
|
||||
height="33"
|
||||
/>
|
||||
<div class="logo-text">{{ appStore.getTitle }}</div>
|
||||
</div>
|
||||
<LoginBanner />
|
||||
<div class="content">
|
||||
<div class="content-inner">
|
||||
<LoginForm />
|
||||
<div class="container">
|
||||
<div class="left-banner"></div>
|
||||
<div class="login-card">
|
||||
<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 class="footer">
|
||||
<Footer />
|
||||
</div>
|
||||
<div class="footer">
|
||||
<div class="beian">
|
||||
<div class="below text" v-html="appStore.getCopyright"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import Footer from '@/components/footer/index.vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useAppStore } from '@/store';
|
||||
import getFile from '@/utils/file';
|
||||
import LoginBanner from './components/banner.vue';
|
||||
import LoginForm from './components/login-form.vue';
|
||||
import AccountLogin from './components/account-login.vue';
|
||||
import PhoneLogin from './components/phone-login.vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
const appStore = useAppStore();
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.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;
|
||||
justify-content: center;
|
||||
padding-bottom: 40px;
|
||||
}
|
||||
.footer {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
.root {
|
||||
background-image: url(../../assets/images/login/login-bg.png);
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
min-height: 100vh;
|
||||
|
||||
.logo {
|
||||
position: fixed;
|
||||
top: 24px;
|
||||
left: 22px;
|
||||
z-index: 1;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
&-text {
|
||||
margin-right: 4px;
|
||||
margin-left: 4px;
|
||||
color: var(--color-fill-1);
|
||||
font-size: 20px;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: @screen-lg) {
|
||||
.container {
|
||||
.banner {
|
||||
width: 25%;
|
||||
align-items: center;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,29 +1,32 @@
|
||||
export default {
|
||||
'login.form.placeholder.username': 'Please enter username',
|
||||
'login.form.placeholder.password': 'Please enter password',
|
||||
'login.form.placeholder.captcha': 'Please enter captcha',
|
||||
'login.welcome': 'Welcome to',
|
||||
'login.account': 'Account Login',
|
||||
'login.phone': 'Phone Login',
|
||||
'login.email': 'Email Login',
|
||||
'login.other': 'Other Login',
|
||||
|
||||
'login.form.error.required.username': 'Please enter username',
|
||||
'login.form.error.required.password': 'Please enter password',
|
||||
'login.form.error.required.captcha': 'Please enter captcha',
|
||||
'login.account.placeholder.username': 'Please enter username',
|
||||
'login.account.placeholder.password': 'Please enter password',
|
||||
'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.form.rememberMe': 'Remember me',
|
||||
'login.form.login': 'Login',
|
||||
'login.form.register': 'Register account',
|
||||
'login.form.forgetPassword': 'Forgot password',
|
||||
'login.account.error.required.username': 'Please enter username',
|
||||
'login.account.error.required.password': 'Please enter password',
|
||||
'login.account.error.required.captcha': 'Please enter captcha',
|
||||
'login.phone.error.required.phone': 'Please enter phone',
|
||||
'login.phone.error.required.captcha': 'Please enter captcha',
|
||||
|
||||
'login.form.login.success': 'Welcome to use',
|
||||
'login.form.login.error': 'Login error, refresh and try again',
|
||||
'login.form.logout.success': 'Logout success',
|
||||
'login.captcha': 'Captcha',
|
||||
'login.rememberMe': 'Remember me',
|
||||
'login.button': 'Login',
|
||||
'login.email.txt': 'Email',
|
||||
'login.account.txt': 'Account/Phone Login',
|
||||
|
||||
'login.banner.slogan1': 'Middle and background management framework',
|
||||
'login.banner.subSlogan1':
|
||||
'Continue New Admin, continue to build on the latest popular technology stack',
|
||||
'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',
|
||||
'login.success': 'Welcome to use',
|
||||
'login.error': 'Login error, refresh and try again',
|
||||
'login.logout.success': 'Logout success',
|
||||
};
|
||||
|
@ -1,28 +1,32 @@
|
||||
export default {
|
||||
'login.form.placeholder.username': '请输入用户名',
|
||||
'login.form.placeholder.password': '请输入密码',
|
||||
'login.form.placeholder.captcha': '请输入验证码',
|
||||
'login.welcome': '欢迎来到',
|
||||
'login.account': '账号登录',
|
||||
'login.phone': '手机号登录',
|
||||
'login.email': '邮箱登录',
|
||||
'login.other': '其他登录方式',
|
||||
|
||||
'login.form.error.required.username': '请输入用户名',
|
||||
'login.form.error.required.password': '请输入密码',
|
||||
'login.form.error.required.captcha': '请输入验证码',
|
||||
'login.account.placeholder.username': '请输入用户名',
|
||||
'login.account.placeholder.password': '请输入密码',
|
||||
'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.form.rememberMe': '记住我',
|
||||
'login.form.login': '登录',
|
||||
'login.form.register': '注册账号',
|
||||
'login.form.forgetPassword': '忘记密码',
|
||||
'login.account.error.required.username': '请输入用户名',
|
||||
'login.account.error.required.password': '请输入密码',
|
||||
'login.account.error.required.captcha': '请输入验证码',
|
||||
'login.phone.error.required.phone': '请输入手机号',
|
||||
'login.phone.error.required.captcha': '请输入验证码',
|
||||
|
||||
'login.form.login.success': '欢迎使用',
|
||||
'login.form.login.error': '登录出错,请刷新重试',
|
||||
'login.form.logout.success': '退出成功',
|
||||
'login.captcha': '验证码',
|
||||
'login.rememberMe': '记住我',
|
||||
'login.button': '立即登录',
|
||||
'login.email.txt': '邮箱',
|
||||
'login.account.txt': '账号/手机号登录',
|
||||
|
||||
'login.banner.slogan1': '中后台管理框架',
|
||||
'login.banner.subSlogan1':
|
||||
'Continue New Admin,持续以最新流行技术栈构建,拥抱变化,迭代优化',
|
||||
'login.banner.slogan2': '内置了常见问题的解决方案',
|
||||
'login.banner.subSlogan2': '基础功能丰富,使用门槛低,企业级快速开发脚手架',
|
||||
'login.banner.slogan3': '代码规范且开源免费',
|
||||
'login.banner.subSlogan3':
|
||||
'后端代码完全遵循阿里巴巴编码规范,前端代码使用严格的 ESLint 检查',
|
||||
'login.success': '欢迎使用',
|
||||
'login.error': '登录出错,请刷新重试',
|
||||
'login.logout.success': '退出成功',
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user