refactor: 登录页面 UI 新增邮箱登录模式

This commit is contained in:
Charles7c 2023-09-28 19:33:19 +08:00
parent 3f92660e9f
commit a5a4cd4964
6 changed files with 237 additions and 35 deletions

View File

@ -6,10 +6,11 @@ const BASE_URL = '/auth';
export interface LoginReq { export interface LoginReq {
phone?: string; phone?: string;
username: string; email?: string;
password: string; username?: string;
password?: string;
captcha: string; captcha: string;
uuid: string; uuid?: string;
} }
export interface LoginRes { export interface LoginRes {

View File

@ -0,0 +1,156 @@
<template>
<a-form
ref="formRef"
:model="form"
:rules="rules"
layout="vertical"
size="large"
class="login-form"
>
<a-form-item field="email" hide-label>
<a-input
v-model="form.email"
:placeholder="$t('login.email.placeholder.email')"
allow-clear
/>
</a-form-item>
<a-form-item field="captcha" hide-label>
<a-input
v-model="form.captcha"
:placeholder="$t('login.email.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.captcha.get');
const captchaBtnName = computed(() => t(captchaBtnNameKey.value));
const data = reactive({
form: {} as LoginReq,
rules: {
email: [
{ required: true, message: t('login.email.error.required.email') },
],
captcha: [
{ required: true, message: t('login.email.error.required.captcha') },
],
},
});
const { form, rules } = toRefs(data);
/**
* 重置验证码
*/
const resetCaptcha = () => {
window.clearInterval(captchaTimer.value);
captchaTime.value = 60;
captchaBtnNameKey.value = 'login.captcha.get';
captchaDisable.value = false;
};
/**
* 发送验证码
*/
const handleSendCaptcha = () => {
if (captchaLoading.value) return;
proxy.$refs.formRef.validateField('email', (valid: any) => {
if (!valid) {
captchaLoading.value = true;
captchaBtnNameKey.value = 'login.captcha.ing';
captchaLoading.value = false;
captchaDisable.value = true;
captchaBtnNameKey.value = `${t(
'login.captcha.get'
)}(${(captchaTime.value -= 1)}s)`;
captchaTimer.value = window.setInterval(() => {
captchaTime.value -= 1;
captchaBtnNameKey.value = `${t('login.captcha.get')}(${
captchaTime.value
}s)`;
if (captchaTime.value < 0) {
window.clearInterval(captchaTimer.value);
captchaTime.value = 60;
captchaBtnNameKey.value = t('login.captcha.get');
captchaDisable.value = false;
}
}, 1000);
}
});
};
</script>
<style lang="less" scoped>
.login-form {
box-sizing: border-box;
padding: 32px 5px 0;
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

@ -53,13 +53,17 @@
const captchaDisable = ref(false); const captchaDisable = ref(false);
const captchaTime = ref(60); const captchaTime = ref(60);
const captchaTimer = ref(); const captchaTimer = ref();
const captchaBtnNameKey = ref('login.phone.captcha'); const captchaBtnNameKey = ref('login.captcha.get');
const captchaBtnName = computed(() => t(captchaBtnNameKey.value)); const captchaBtnName = computed(() => t(captchaBtnNameKey.value));
const data = reactive({ const data = reactive({
form: {} as LoginReq, form: {} as LoginReq,
rules: { rules: {
phone: [ phone: [
{ required: true, message: t('login.phone.error.required.phone') }, { required: true, message: t('login.phone.error.required.phone') },
{
match: /^1[3-9]\d{9}$/,
message: t('login.phone.error.match.phone'),
},
], ],
captcha: [ captcha: [
{ required: true, message: t('login.phone.error.required.captcha') }, { required: true, message: t('login.phone.error.required.captcha') },
@ -74,7 +78,7 @@
const resetCaptcha = () => { const resetCaptcha = () => {
window.clearInterval(captchaTimer.value); window.clearInterval(captchaTimer.value);
captchaTime.value = 60; captchaTime.value = 60;
captchaBtnNameKey.value = 'login.phone.captcha'; captchaBtnNameKey.value = 'login.captcha.get';
captchaDisable.value = false; captchaDisable.value = false;
}; };
@ -86,21 +90,21 @@
proxy.$refs.formRef.validateField('phone', (valid: any) => { proxy.$refs.formRef.validateField('phone', (valid: any) => {
if (!valid) { if (!valid) {
captchaLoading.value = true; captchaLoading.value = true;
captchaBtnNameKey.value = 'login.phone.captcha.ing'; captchaBtnNameKey.value = 'login.captcha.ing';
captchaLoading.value = false; captchaLoading.value = false;
captchaDisable.value = true; captchaDisable.value = true;
captchaBtnNameKey.value = `${t( captchaBtnNameKey.value = `${t(
'login.phone.reCaptcha' 'login.captcha.get'
)}(${(captchaTime.value -= 1)}s)`; )}(${(captchaTime.value -= 1)}s)`;
captchaTimer.value = window.setInterval(() => { captchaTimer.value = window.setInterval(() => {
captchaTime.value -= 1; captchaTime.value -= 1;
captchaBtnNameKey.value = `${t('login.phone.reCaptcha')}(${ captchaBtnNameKey.value = `${t('login.captcha.get')}(${
captchaTime.value captchaTime.value
}s)`; }s)`;
if (captchaTime.value < 0) { if (captchaTime.value < 0) {
window.clearInterval(captchaTimer.value); window.clearInterval(captchaTimer.value);
captchaTime.value = 60; captchaTime.value = 60;
captchaBtnNameKey.value = t('login.phone.reCaptcha'); captchaBtnNameKey.value = t('login.captcha.get');
captchaDisable.value = false; captchaDisable.value = false;
} }
}, 1000); }, 1000);

View File

@ -12,26 +12,34 @@
<div class="left-banner"></div> <div class="left-banner"></div>
<div class="login-card"> <div class="login-card">
<div class="title" <div class="title"
>{{ $t('login.welcome') }} {{ appStore.getTitle }}</div >{{ $t('login.welcome') }} {{ appStore.getTitle }}</div
> >
<a-tabs class="account-tab" default-active-key="1"> <EmailLogin v-if="isEmailLogin" />
<a-tab-pane key="1" :title="$t('login.account')" <a-tabs v-else class="account-tab" default-active-key="1">
><AccountLogin <a-tab-pane
/></a-tab-pane> key="1"
<a-tab-pane key="2" :title="$t('login.phone')" :title="$t('login.account')"
><PhoneLogin >
/></a-tab-pane> <AccountLogin />
</a-tab-pane>
<a-tab-pane
key="2"
:title="$t('login.phone')"
>
<PhoneLogin />
</a-tab-pane>
</a-tabs> </a-tabs>
<div class="oauth"> <div class="oauth">
<a-divider class="text" orientation="center">{{ <a-divider class="text" orientation="center">{{
$t('login.other') $t('login.other')
}}</a-divider> }}</a-divider>
<div class="idps"> <div class="idps">
<a-tooltip content="邮箱登录(即将开放)" mini> <div v-if="!isEmailLogin" class="mail app" @click="toggleLoginMode">
<div class="mail app"> <icon-email /> {{ $t('login.email.txt') }}
<icon-email /> {{ $t('login.email.txt') }} </div>
</div> <div v-else class="account app" @click="toggleLoginMode">
</a-tooltip> <icon-user /> {{ $t('login.account.txt') }}
</div>
<a-tooltip content="Gitee即将开放" mini> <a-tooltip content="Gitee即将开放" mini>
<a href="javascript: void(0);" class="app"> <a href="javascript: void(0);" class="app">
<svg <svg
@ -65,7 +73,7 @@
</div> </div>
</div> </div>
</div> </div>
<div class="footer"> <div v-if="appStore.device === 'desktop'" class="footer">
<div class="beian"> <div class="beian">
<div class="below text" v-html="appStore.getCopyright"></div> <div class="below text" v-html="appStore.getCopyright"></div>
</div> </div>
@ -74,14 +82,23 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useAppStore } from '@/store'; import { useAppStore } from '@/store';
import getFile from '@/utils/file'; import getFile from '@/utils/file';
import useResponsive from '@/hooks/responsive';
import AccountLogin from './components/account-login.vue'; import AccountLogin from './components/account-login.vue';
import PhoneLogin from './components/phone-login.vue'; import PhoneLogin from './components/phone-login.vue';
import EmailLogin from './components/email-login.vue';
const { t } = useI18n(); const { t } = useI18n();
const appStore = useAppStore(); const appStore = useAppStore();
useResponsive(true);
const isEmailLogin = ref(false);
const toggleLoginMode = () => {
isEmailLogin.value = !isEmailLogin.value;
};
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
@ -137,7 +154,7 @@
max-width: 500px; max-width: 500px;
object-fit: contain; object-fit: contain;
position: absolute; position: absolute;
top: 5%; top: 4.5%;
width: 100%; width: 100%;
} }
} }
@ -225,16 +242,31 @@
.mail { .mail {
min-width: 81px; min-width: 81px;
width: 81px; width: 81px;
}
.account {
min-width: 147px;
width: 147px;
}
.mail,
.account {
color: #41464f; color: #41464f;
font-size: 12px; font-size: 12px;
font-weight: 400; font-weight: 400;
line-height: 20px; line-height: 20px;
padding: 6px 10px; padding: 6px 10px;
svg { }
color: #000; .mail svg,
font-size: 16px; .account svg {
margin-right: 10px; font-size: 16px;
} margin-right: 10px;
}
.mail:hover,
.account:hover {
color: rgb(var(--primary-6));
}
.mail svg:hover,
.account svg:hover {
color: rgb(var(--primary-6));
} }
} }
} }

View File

@ -10,15 +10,20 @@ export default {
'login.account.placeholder.captcha': 'Please enter captcha', 'login.account.placeholder.captcha': 'Please enter captcha',
'login.phone.placeholder.phone': 'Please enter phone', 'login.phone.placeholder.phone': 'Please enter phone',
'login.phone.placeholder.captcha': 'Please enter captcha', 'login.phone.placeholder.captcha': 'Please enter captcha',
'login.phone.captcha': 'Get captcha', 'login.email.placeholder.email': 'Please enter email',
'login.phone.captcha.ing': 'Sending...', 'login.email.placeholder.captcha': 'Please enter captcha',
'login.phone.reCaptcha': 'Resend captcha', 'login.captcha.get': 'Get captcha',
'login.captcha.ing': 'Sending...',
'login.account.error.required.username': 'Please enter username', 'login.account.error.required.username': 'Please enter username',
'login.account.error.required.password': 'Please enter password', 'login.account.error.required.password': 'Please enter password',
'login.account.error.required.captcha': 'Please enter captcha', 'login.account.error.required.captcha': 'Please enter captcha',
'login.phone.error.required.phone': 'Please enter phone', 'login.phone.error.required.phone': 'Please enter phone',
'login.phone.error.match.phone':
'Please enter the correct mobile phone number',
'login.phone.error.required.captcha': 'Please enter captcha', 'login.phone.error.required.captcha': 'Please enter captcha',
'login.email.error.required.email': 'Please enter email',
'login.email.error.required.captcha': 'Please enter captcha',
'login.captcha': 'Captcha', 'login.captcha': 'Captcha',
'login.rememberMe': 'Remember me', 'login.rememberMe': 'Remember me',

View File

@ -10,15 +10,19 @@ export default {
'login.account.placeholder.captcha': '请输入验证码', 'login.account.placeholder.captcha': '请输入验证码',
'login.phone.placeholder.phone': '请输入手机号', 'login.phone.placeholder.phone': '请输入手机号',
'login.phone.placeholder.captcha': '请输入验证码', 'login.phone.placeholder.captcha': '请输入验证码',
'login.phone.captcha': '获取验证码', 'login.email.placeholder.email': '请输入邮箱',
'login.phone.captcha.ing': '发送中...', 'login.email.placeholder.captcha': '请输入验证码',
'login.phone.reCaptcha': '重新发送', 'login.captcha.get': '获取验证码',
'login.captcha.ing': '发送中...',
'login.account.error.required.username': '请输入用户名', 'login.account.error.required.username': '请输入用户名',
'login.account.error.required.password': '请输入密码', 'login.account.error.required.password': '请输入密码',
'login.account.error.required.captcha': '请输入验证码', 'login.account.error.required.captcha': '请输入验证码',
'login.phone.error.required.phone': '请输入手机号', 'login.phone.error.required.phone': '请输入手机号',
'login.phone.error.match.phone': '请输入正确的手机号',
'login.phone.error.required.captcha': '请输入验证码', 'login.phone.error.required.captcha': '请输入验证码',
'login.email.error.required.email': '请输入邮箱',
'login.email.error.required.captcha': '请输入验证码',
'login.captcha': '验证码', 'login.captcha': '验证码',
'login.rememberMe': '记住我', 'login.rememberMe': '记住我',