qr login page /reset password page

This commit is contained in:
landaiqing
2024-08-11 18:11:15 +08:00
parent ff6a4a5d09
commit b40c9c3036
20 changed files with 580 additions and 65 deletions

8
components.d.ts vendored
View File

@@ -19,6 +19,7 @@ declare module 'vue' {
AFormItem: typeof import('ant-design-vue/es')['FormItem']
AInput: typeof import('ant-design-vue/es')['Input']
AInputPassword: typeof import('ant-design-vue/es')['InputPassword']
AQrcode: typeof import('ant-design-vue/es')['QRCode']
ASelect: typeof import('ant-design-vue/es')['Select']
ASelectOption: typeof import('ant-design-vue/es')['SelectOption']
ATabPane: typeof import('ant-design-vue/es')['TabPane']
@@ -27,7 +28,14 @@ declare module 'vue' {
ATooltip: typeof import('ant-design-vue/es')['Tooltip']
BoxDog: typeof import('./src/components/BoxDog/BoxDog.vue')['default']
DynamicTitle: typeof import('./src/components/DynamicTitle/DynamicTitle.vue')['default']
FoegetPage: typeof import('./src/views/Forget/FoegetPage.vue')['default']
ForgetPage: typeof import('./src/views/Forget/ForgetPage.vue')['default']
LockOutlined: typeof import('@ant-design/icons-vue')['LockOutlined']
LoginFooter: typeof import('./src/views/Login/LoginFooter.vue')['default']
LoginPage: typeof import('./src/views/Login/LoginPage.vue')['default']
QRLogin: typeof import('./src/views/QRLogin/QRLogin.vue')['default']
QRLoginFooter: typeof import('./src/views/QRLogin/QRLoginFooter.vue')['default']
RegisterFooter: typeof import('./src/views/Register/RegisterFooter.vue')['default']
RegisterPage: typeof import('./src/views/Register/RegisterPage.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']

View File

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 55 KiB

View File

@@ -21,7 +21,6 @@ body {
background: transparent;
padding: 0;
margin: 0;
z-index: 999;
}
.container {

View File

@@ -20,7 +20,15 @@ export default {
login: "login",
register: "register",
reSendCaptcha: " re-send",
passwordRule: "The password must be 6~18 characters and must contain letters, numbers and special symbols!"
passwordRule: "The password must be 6~18 characters and must contain letters, numbers and special symbols!",
phoneLoginAndRegister: "phone login/register",
open: "Please open",
scan: "scan the code to login",
wechat: "wechat",
repassword: "confirm password",
repasswordValidate: "please enter the confirmation password",
forgetPassword: "forget password",
resetPassword: "reset password",
},
error: {
networkError: 'Network error, please try again later',

View File

@@ -1,7 +1,7 @@
// zh.ts
export default {
login: {
title: '五味子云存储',
title: '五味子云相册',
accountLogin: '账号登录',
account: '账号',
accountValidate: '请输入手机号/账号/邮箱',
@@ -9,7 +9,7 @@ export default {
passwordValidate: "请输入密码",
autoLogin: "自动登录",
forgotPassword: "忘记密码",
loginAndRegister: "登录",
loginAndRegister: "登录/注册",
qrLogin: "扫码登录",
phoneLogin: "短信登录",
phone: "手机号",
@@ -20,7 +20,15 @@ export default {
login: "登录",
register: "注册",
reSendCaptcha: "重新发送",
passwordRule: "密码要6~18位字符且必须包含字母、数字和特殊符号"
passwordRule: "密码要6~18位字符且必须包含字母、数字和特殊符号",
phoneLoginAndRegister: "短信注册/登录",
open: "请打开",
scan: "扫码登录",
wechat: "微信",
repassword: "确认密码",
repasswordValidate: "请输入确认密码",
forgetPassword: "忘记密码",
resetPassword: "重置密码",
},
error: {
networkError: '网络连接失败!',

View File

@@ -0,0 +1,11 @@
export default [
{
path: '/qrlogin',
name: 'qrlogin',
component: () => import('@/views/QRLogin/QRLogin.vue'),
meta: {
requiresAuth: false,
title: '扫码登录'
}
}
];

View File

@@ -1,11 +0,0 @@
export default [
{
path: '/register',
name: 'register',
component: () => import('@/views/Register/RegisterPage.vue'),
meta: {
requiresAuth: false,
title: '注册页'
}
}
];

View File

@@ -0,0 +1,11 @@
export default [
{
path: '/resetpass',
name: 'resetpass',
component: () => import('@/views/Forget/ForgetPage.vue'),
meta: {
requiresAuth: false,
title: '重置密码'
}
}
];

View File

@@ -7,12 +7,14 @@ import test2 from "@/router/modules/testI18n.ts";
import useStore from "@/store";
import {message} from "ant-design-vue";
import {close, start} from '@/utils/nprogress/nprogress.ts';
import register from "@/router/modules/register.ts";
import qrlogin from "@/router/modules/qrlogin.ts";
import resetpass from "@/router/modules/resetpass.ts";
const routes: Array<RouteRecordRaw> = [
...login,
...register,
...qrlogin,
...resetpass,
...test,
...test2,
];

8
src/types/user.d.ts vendored
View File

@@ -8,7 +8,15 @@ export interface AccountLogin {
account?: string
password?: string;
}
export interface PhoneLogin {
phone?: string
captcha?: string;
}
export interface ResetPassword {
phone?: string
captcha?: string;
password?: string;
repassword?: string;
}

View File

@@ -0,0 +1,209 @@
<template>
<div class="forget-main">
<div class="forget-left">
<BoxDog/>
</div>
<div class="forget-right">
<span class="forget-right-title">{{ t("login.title") }}</span>
<ACard class="forget-card" bordered :hoverable="false">
<AFlex :vertical="true">
<span class="forget-card-title">{{ t("login.forgetPassword") }}</span>
<AForm
ref="resetPasswordRef"
:model="ResetPasswordForm"
:rules="rules"
>
<AFormItem name="phone">
<span class="forget-card-span">{{ t("login.phone") }}</span>
<AInput v-model:value="ResetPasswordForm.phone" :placeholder=phoneValidate size="large" allow-clear>
<template #prefix>
<TabletOutlined/>
</template>
</AInput>
</AFormItem>
<AFormItem name="captcha">
<AFlex :vertical="true">
<span class="forget-card-span">{{ t("login.phoneCaptcha") }}</span>
<AFlex :vertical="false" align="center" justify="center">
<AInput v-model:value="ResetPasswordForm.captcha" :placeholder=captchaValidate size="large" allow-clear>
<template #prefix>
<SafetyOutlined/>
</template>
</AInput>
<AButton v-if="!state.showCountDown" @click="sendCaptcha" size="large" style="margin-left: 10px">{{
t("login.sendCaptcha")
}}
</AButton>
<AButton v-if="state.showCountDown" disabled size="large" style="margin-left: 10px">{{
state.countDownTime
}}s{{ t("login.reSendCaptcha") }}
</AButton>
</AFlex>
</AFlex>
</AFormItem>
<AFormItem name="password">
<span class="forget-card-span">{{ t("login.password") }}</span>
<AInput v-model:value="ResetPasswordForm.password" :placeholder=passwordValidate size="large" allow-clear>
<template #prefix>
<LockOutlined/>
</template>
</AInput>
</AFormItem>
<AFormItem name="repassword">
<span class="forget-card-span">{{ t("login.repassword") }}</span>
<AInput v-model:value="ResetPasswordForm.repassword" :placeholder=repasswordValidate size="large" allow-clear>
<template #prefix>
<LockOutlined/>
</template>
</AInput>
</AFormItem>
<AFormItem>
<AButton @click="resetPasswordSubmit" style="width: 100%;" type="primary" size="large">{{
t("login.resetPassword")
}}</AButton>
</AFormItem>
</AForm>
</AFlex>
<ATooltip placement="left">
<template #title>
<span>{{ t("login.phoneLogin") }}</span>
</template>
<div @click="()=>{
router.push('/login');
}" class="forget-right-qrcode"/>
</ATooltip>
</ACard>
</div>
</div>
</template>
<script setup lang="ts">
import {useI18n} from "vue-i18n";
import BoxDog from "@/components/BoxDog/BoxDog.vue";
import {useRouter} from "vue-router";
import {onMounted, reactive, ref, UnwrapRef} from "vue";
import {ResetPassword} from "@/types/user";
import {Rule} from "ant-design-vue/lib/form";
const router = useRouter();
const {t} = useI18n();
const resetPasswordRef = ref()
const ResetPasswordForm: UnwrapRef<ResetPassword> = reactive({
phone: '',
captcha: '',
password: '',
repassword: '',
});
// 表单验证提示
const passwordValidate = ref<string>(t('login.passwordValidate'));
const phoneValidate = ref<string>(t('login.phoneValidate'));
const captchaValidate = ref<string>(t('login.captchaValidate'));
const repasswordValidate = ref<string>(t('login.repasswordValidate'));
// 表单验证规则
const rules: Record<string, Rule[]> = {
password: [
{
required: true, message: t('login.passwordValidate'), trigger: 'change'
},
{
pattern: /^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!%*#?&]{6,18}$/,
message: t('login.passwordRule'),
},
],
repassword: [
{
required: true, message: t('login.repasswordValidate'), trigger: 'change'
},
{
pattern: /^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!%*#?&]{6,18}$/,
message: t('login.passwordRule'),
},
],
phone: [
{required: true, message: t('login.phoneValidate'), trigger: 'change'},
{pattern: /^1[2-9]\d{9}$/, message: t('login.phoneValidate')}
],
captcha: [
{
required: true, message: t('login.captchaValidate'), trigger: 'change'
}
]
};
const state = reactive({
countDownTime: 60,
showCountDown: false,
});
/**
* 验证码发送倒计时
*/
const countDown = () => {
const startTime = localStorage.getItem('startTimeSendCaptcha');
const nowTime = new Date().getTime();
let surplus: number = 60;
let timer: any;
if (startTime) {
surplus = 60 - Math.floor((nowTime - Number(startTime)) / 1000);
surplus = surplus <= 0 ? 0 : surplus;
} else {
localStorage.setItem('startTimeSendCaptcha', String(nowTime));
}
state.countDownTime = surplus;
if (timer) {
clearInterval(timer);
}
timer = setInterval(() => {
if (state.countDownTime <= 0) {
localStorage.removeItem('startTimeSendCaptcha');
clearInterval(timer);
state.countDownTime = 60;
state.showCountDown = false;
} else {
state.countDownTime--;
state.showCountDown = true;
}
}, 1000);
};
onMounted(() => {
const sendEndTime = localStorage.getItem('startTimeSendCaptcha');
if (sendEndTime) {
state.showCountDown = true;
countDown();
}
});
/**
* 发送验证码
*/
async function sendCaptcha() {
resetPasswordRef.value
.validateFields("phone")
.then(() => {
countDown();
console.log('values');
})
.catch((error: any) => {
console.log('error', error);
});
}
/**
* 重置密码表单提交
*/
async function resetPasswordSubmit() {
resetPasswordRef.value
.validate()
.then(() => {
console.log('values', ResetPasswordForm);
})
.catch((error: any) => {
console.log('error', error);
});
}
</script>
<style src="./index.scss" scoped>
@import "@/assets/styles/global.scss";
</style>

View File

@@ -0,0 +1,90 @@
.forget-main {
display: flex;
flex-direction: row;
//background-color: rgb(238, 255, 238);
width: 100vw;
height: 100vh;
/* 加载背景图 */
background-image: url("@/assets/images/background.png");
/* 背景图垂直、水平均居中 */
background-position: center center;
/* 背景图不平铺 */
background-repeat: no-repeat;
/* 当内容高度大于图片高度时背景图像的位置相对于viewport固定 */
background-attachment: fixed;
/* 让背景图基于容器大小伸缩 */
background-size: cover;
.forget-left {
width: 50%;
}
.forget-right {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 50%;
height: 100%;
.forget-right-title {
font-size: 32px;
font-weight: 600;
margin-bottom: 32px;
}
.forget-card-span {
color: #999ba1;
font-size: 12px;
}
.forget-card-return {
color: #999ba1;
font-size: 12px;
cursor: pointer;
}
.forget-card-return:hover {
color: #08a327;
font-size: 12px;
cursor: pointer;
}
}
.forget-card {
width: 440px;
height: 490px;
border-radius: 15px;
box-shadow: 0 2px 8px 0 rgba(0, 0, 0, .08);
.forget-card-title {
font-size: 20px;
font-weight: 600;
color: #222;
line-height: 26px;
margin-bottom: 24px
}
.forget-right-qrcode {
position: absolute;
right: 0;
top: 0;
background-position: 50%;
background-size: cover;
background-repeat: no-repeat;
width: 72px;
height: 72px;
background-image: url(data:image/png;base64,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);
cursor: pointer;
}
.forget-right-qrcode:hover {
opacity: 0.9;
}
.forget-form-bottom-button {
color: #999ba1;
}
}
}

View File

@@ -0,0 +1,25 @@
<template>
<div>
<ADivider/>
<AFlex :vertical="false" align="center" justify="space-around">
<AButton @click="()=>{
router.push('/qrlogin')
}" class="login-form-bottom-button" :icon="h(QrcodeOutlined)">{{ t("login.qrLogin") }}
</AButton>
<AButton class="login-form-bottom-button" :icon="h(QqOutlined)"></AButton>
<AButton class="login-form-bottom-button" :icon="h(GithubOutlined)"></AButton>
</AFlex>
</div>
</template>
<script setup lang="ts">
import {GithubOutlined, QqOutlined, QrcodeOutlined} from "@ant-design/icons-vue";
import {useI18n} from "vue-i18n";
import {h} from "vue";
import {useRouter} from 'vue-router'
const router = useRouter();
const {t} = useI18n();
</script>
<style src="./index.scss" scoped>
</style>

View File

@@ -20,7 +20,7 @@
name="phone">
<span class="login-card-span">{{ t("login.phone") }}</span>
<AInput v-model:value="phoneLoginForm.phone" class="login-form-input" size="large"
:placeholder=phoneValidate
:placeholder=phoneValidate allow-clear
>
<template #prefix>
<TabletOutlined/>
@@ -33,7 +33,7 @@
<AFlex :vertical="true">
<span class="login-card-span">{{ t("login.phoneCaptcha") }}</span>
<AFlex :vertical="false" align="center" justify="center">
<AInput v-model:value="phoneLoginForm.captcha" size="large" :placeholder=captchaValidate>
<AInput v-model:value="phoneLoginForm.captcha" size="large" :placeholder=captchaValidate allow-clear>
<template #prefix>
<SafetyOutlined/>
</template>
@@ -53,22 +53,16 @@
<AFormItem>
<AFlex :vertical="false" justify="space-between">
<ACheckbox>{{ t("login.autoLogin") }}</ACheckbox>
<a>{{ t("login.forgotPassword") }}</a>
</AFlex>
</AFormItem>
<AFormItem>
<AButton @click="phoneLoginSubmit" type="primary" size="large" class="login-form-button">
{{ t("login.login") }}
{{ t("login.loginAndRegister") }}
</AButton>
</AFormItem>
</AForm>
<ADivider/>
<AFlex :vertical="false" align="center" justify="space-around">
<AButton class="login-form-bottom-button" :icon="h(QrcodeOutlined)">{{ t("login.qrLogin") }}</AButton>
<AButton class="login-form-bottom-button" :icon="h(QqOutlined)"></AButton>
<AButton class="login-form-bottom-button" :icon="h(GithubOutlined)"></AButton>
</AFlex>
<LoginFooter/>
</ATabPane>
<!-- 账号登录 -->
<ATabPane key="accountLogin">
@@ -83,7 +77,7 @@
name="account">
<span class="login-card-span">{{ t("login.account") }}</span>
<AInput v-model:value="accountLoginForm.account" class="login-form-input" size="large"
:placeholder=accountValidate>
:placeholder=accountValidate allow-clear>
<template #prefix>
<user-outlined/>
</template>
@@ -95,7 +89,7 @@
<AFlex :vertical="true">
<span class="login-card-span">{{ t("login.password") }}</span>
<AInputPassword v-model:value="accountLoginForm.password" class="login-form-input" size="large"
:placeholder=passwordValidate>
:placeholder=passwordValidate allow-clear>
<template #prefix>
<SafetyOutlined/>
</template>
@@ -105,32 +99,30 @@
<AFormItem>
<AFlex :vertical="false" justify="space-between">
<ACheckbox>{{ t("login.autoLogin") }}</ACheckbox>
<a>{{ t("login.forgotPassword") }}</a>
<a @click="()=>{
router.push('/resetpass')
}">{{ t("login.forgotPassword") }}</a>
</AFlex>
</AFormItem>
<AFormItem>
<AButton @click="accountLoginSubmit" type="primary" size="large" class="login-form-button">
{{
t("login.loginAndRegister")
t("login.login")
}}
</AButton>
</AFormItem>
</AForm>
<ADivider/>
<AFlex :vertical="false" align="center" justify="space-around">
<AButton class="login-form-bottom-button" :icon="h(QrcodeOutlined)">{{ t("login.qrLogin") }}</AButton>
<AButton class="login-form-bottom-button" :icon="h(QqOutlined)"></AButton>
<AButton class="login-form-bottom-button" :icon="h(GithubOutlined)"></AButton>
</AFlex>
<LoginFooter/>
</ATabPane>
</ATabs>
<ATooltip placement="left">
<template #title>
<span>{{ t("login.qrLogin") }}</span>
</template>
<div class="login-right-qrcode"/>
<div @click="()=>{
router.push('/qrlogin');
}" class="login-right-qrcode"/>
</ATooltip>
</ACard>
</div>
@@ -138,14 +130,16 @@
</template>
<script setup lang="ts">
import {Rule} from "ant-design-vue/lib/form";
import {h, onMounted, reactive, ref, UnwrapRef} from "vue";
import {onMounted, reactive, ref, UnwrapRef} from "vue";
import {AccountLogin, PhoneLogin} from "@/types/user";
import {GithubOutlined, QqOutlined, QrcodeOutlined} from "@ant-design/icons-vue";
import {useI18n} from "vue-i18n";
import BoxDog from "@/components/BoxDog/BoxDog.vue";
import LoginFooter from "@/views/Login/LoginFooter.vue";
import {useRouter} from "vue-router";
const router = useRouter();
const {t} = useI18n();
const accountLoginFormRef = ref();
const accountLoginFormRef = ref<any>();
const phoneLoginFormRef = ref<any>();
// 账号登录表单数据
const accountLoginForm: UnwrapRef<AccountLogin> = reactive({

View File

@@ -5,7 +5,7 @@
width: 100vw;
height: 100vh;
/* 加载背景图 */
background-image: url("@/assets/images/login-back.png");
background-image: url("@/assets/images/background.png");
/* 背景图垂直、水平均居中 */
background-position: center center;
/* 背景图不平铺 */
@@ -82,10 +82,6 @@
.login-form-bottom-button {
color: #999ba1;
}
.login-form-bottom-button:hover {
color: rgba(15, 15, 16, 0.3);
}
}
}

View File

@@ -0,0 +1,51 @@
<template>
<div class="qrlogin-main">
<div class="qrlogin-left">
<BoxDog/>
</div>
<div class="qrlogin-right">
<span class="qrlogin-right-title">{{ t("login.title") }}</span>
<ACard class="qrlogin-card" bordered :hoverable="false">
<AFlex :vertical="true" align="center">
<span class="qrlogin-card-item-span">{{ t("login.qrLogin") }}</span>
<span class="qrlogin-card-item-info">
{{ t("login.open") }}
<span class="qrlogin-card-wechat">{{ t("login.wechat") }}</span>
{{ t("login.scan") }}
</span>
<AQrcode
class="qrlogin-card-qr"
:size="230"
:error-level="'H'"
value="https://www.antdv.com"
icon="https://www.antdv.com/assets/logo.1ef800a8.svg"
/>
<ACheckbox class="qrlogin-card-auto-login">{{ t("login.autoLogin") }}</ACheckbox>
</AFlex>
<QRLoginFooter/>
<ATooltip placement="left">
<template #title>
<span>{{ t("login.phoneLogin") }}</span>
</template>
<div @click="()=>{
router.push('/login')
}" class="qrlogin-right-qrcode"/>
</ATooltip>
</ACard>
</div>
</div>
</template>
<script setup lang="ts">
import {useI18n} from "vue-i18n";
import BoxDog from "@/components/BoxDog/BoxDog.vue";
import QRLoginFooter from "@/views/QRLogin/QRLoginFooter.vue";
import {useRouter} from 'vue-router'
const {t} = useI18n();
const router = useRouter();
</script>
<style src="./index.scss" scoped>
@import "@/assets/styles/global.scss";
</style>

View File

@@ -0,0 +1,25 @@
<template>
<div>
<ADivider/>
<AFlex :vertical="false" align="center" justify="space-around">
<AButton @click="()=>{
router.push('/login')
}" class="qrlogin-form-bottom-button" :icon="h(TabletOutlined)">{{ t("login.phoneLoginAndRegister") }}
</AButton>
<AButton class="qrlogin-form-bottom-button" :icon="h(QqOutlined)"></AButton>
<AButton class="qrlogin-form-bottom-button" :icon="h(GithubOutlined)"></AButton>
</AFlex>
</div>
</template>
<script setup lang="ts">
import {GithubOutlined, QqOutlined, TabletOutlined} from "@ant-design/icons-vue";
import {useI18n} from "vue-i18n";
import {h} from "vue";
import {useRouter} from 'vue-router'
const router = useRouter();
const {t} = useI18n();
</script>
<style src="./index.scss" scoped>
</style>

View File

@@ -0,0 +1,98 @@
.qrlogin-main {
display: flex;
flex-direction: row;
//background-color: rgb(238, 255, 238);
width: 100vw;
height: 100vh;
/* 加载背景图 */
background-image: url("@/assets/images/background.png");
/* 背景图垂直、水平均居中 */
background-position: center center;
/* 背景图不平铺 */
background-repeat: no-repeat;
/* 当内容高度大于图片高度时背景图像的位置相对于viewport固定 */
background-attachment: fixed;
/* 让背景图基于容器大小伸缩 */
background-size: cover;
.qrlogin-left {
width: 50%;
}
.qrlogin-right {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 50%;
height: 100%;
.qrlogin-right-title {
font-size: 32px;
font-weight: 600;
margin-bottom: 32px;
}
}
.qrlogin-card {
width: 440px;
height: 490px;
border-radius: 15px;
box-shadow: 0 2px 8px 0 rgba(0, 0, 0, .08);
.qrlogin-card-item-span {
font-size: 20px;
font-weight: 600;
color: #222;
line-height: 26px;
}
.qrlogin-card-item-info {
margin-top: 12px;
font-size: 12px;
font-weight: 400;
color: #999;
line-height: 18px;
margin-bottom: 24px;
.qrlogin-card-wechat {
color: #7acc35;
margin-left: 4px;
margin-right: 4px;
cursor: pointer;
font-weight: 600;
}
}
.qrlogin-card-qr {
margin-top: 24px;
}
.qrlogin-card-auto-login {
margin-top: 24px;
}
.qrlogin-right-qrcode {
position: absolute;
right: 0;
top: 0;
background-position: 50%;
background-size: cover;
background-repeat: no-repeat;
width: 72px;
height: 72px;
background-image: url(data:image/png;base64,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);
cursor: pointer;
}
.qrlogin-right-qrcode:hover {
opacity: 0.9;
}
.qrlogin-form-bottom-button {
color: #999ba1;
}
}
}

View File

@@ -1,17 +0,0 @@
<script lang="ts">
import {defineComponent} from 'vue';
export default defineComponent({
name: "RegisterPage"
});
</script>
<template>
<div>
</div>
</template>
<style src="./index.scss" scoped>
</style>