WholeProcessPlatform/frontend/src/views/login/index.vue
2026-07-10 17:11:58 +08:00

1072 lines
30 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="login-container">
<h1>
<div>
<img :src="loginImg" alt="Logo" class="logo-img" />
<a class="system-title">{{ $t("login.title") }}</a>
</div>
</h1>
<div class="login-wrapper">
<!-- 左侧背景图区域 -->
<div class="left-section">
<div class="slogan">
<p>绿水青山就是金山银山</p>
</div>
</div>
<!-- 右侧登录表单区域 -->
<div class="right-section">
<!-- 忘记密码页面 -->
<div v-if="showForgotPassword" class="forgot-password-container">
<div class="forgot-password-header">
<a-button type="link" @click="backToLogin" class="back-button">
<span>← 返回</span>
</a-button>
<!-- <h3 class="forgot-password-title">忘记密码</h3> -->
</div>
<a-form
:model="forgotPasswordForm"
:rules="forgotPasswordRules"
layout="vertical"
class="form-container"
>
<a-form-item label="" name="phone">
<a-input
v-model:value="forgotPasswordForm.phone"
placeholder="请输入手机号"
size="large"
>
<template #prefix>
<MobileOutlined />
</template>
</a-input>
</a-form-item>
<a-form-item label="" name="captcha">
<div class="captcha-row">
<a-input
v-model:value="forgotPasswordForm.captcha"
placeholder="请输入验证码"
size="large"
autocomplete="current-captcha"
class="captcha-input"
>
<template #prefix>
<LockOutlined />
</template>
<template #suffix>
<a-button
type="text"
size="small"
@click="sendForgotPasswordSms"
:disabled="smsButtonDisabled"
:loading="loading"
>
{{
smsCountdown > 0 ? `${smsCountdown}秒后重新获取` : "获取验证码"
}}
</a-button>
</template>
</a-input>
</div>
</a-form-item>
<a-form-item label="" name="newPassword">
<a-input-password
v-model:value="forgotPasswordForm.newPassword"
autocomplete="current-password"
placeholder="请输入新密码"
size="large"
>
<template #prefix>
<LockOutlined />
</template>
</a-input-password>
</a-form-item>
<a-form-item label="" name="confirmPassword">
<a-input-password
v-model:value="forgotPasswordForm.confirmPassword"
autocomplete="current-confirmPassword"
placeholder="请再次输入密码"
size="large"
>
<template #prefix>
<LockOutlined />
</template>
</a-input-password>
</a-form-item>
<a-button
type="primary"
size="large"
block
@click="handleResetPassword"
:loading="loading"
>
<span>提交</span>
</a-button>
</a-form>
</div>
<!-- 登录页面 -->
<div v-else>
<!-- Tabs 切换:账号登录 / 短信登录 -->
<a-tabs v-model:activeKey="activeTab" class="login-tabs">
<a-tab-pane key="account" tab="账号登录">
<a-form
:model="loginData"
:rules="loginRules"
layout="vertical"
class="form-container"
@finish="onFinish"
>
<!-- 用户名/账号/手机号输入框 -->
<a-form-item label="" name="username">
<a-input
ref="username"
v-model:value="loginData.username"
clearable
type="text"
placeholder="请输入用户账号/手机号"
size="large"
autocomplete="current-username"
>
<template #prefix>
<UserOutlined />
</template>
</a-input>
</a-form-item>
<!-- 密码输入框 -->
<a-form-item label="" name="password">
<a-input-password
type="password"
v-model:value="loginData.password"
placeholder="请输入密码"
size="large"
autocomplete="current-password"
>
<template #prefix>
<LockOutlined />
</template>
</a-input-password>
</a-form-item>
<!-- 验证码区域 -->
<a-form-item label="" name="captcha">
<div class="captcha-row">
<a-row :gutter="24" align="middle">
<a-col :span="10"
><a-input
v-model:value="loginData.code"
placeholder="请输入验证码"
autocomplete="current-captcha"
size="large"
class="captcha-input"
/></a-col>
<a-col :span="10"
><img :src="codeUrl" alt="验证码" class="captcha-img"
/></a-col>
<a-col :span="4"> <a @click="getCode">换一张</a></a-col>
</a-row>
</div>
</a-form-item>
<a-button
type="primary"
size="large"
block
htmlType="submit"
:loading="loading"
>
<span>登录</span>
</a-button>
<div
style="
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
"
>
<a-checkbox v-model:checked="remember" style="margin-top: 10px"
>记住密码</a-checkbox
>
<a-button
type="link"
size="mini"
@click="showForgotPasswordPage"
:style="{ marginTop: '10px', border: 'none' }"
>
忘记密码
</a-button>
<!-- <a-button
type="link"
size="mini"
@click="goRegister"
:style="{ marginTop: '10px', border: 'none' }"
>
用户注册
</a-button> -->
</div>
<!-- 忘记密码 -->
</a-form>
</a-tab-pane>
<!-- 短信登录 Tab (占位) -->
<a-tab-pane key="sms" tab="短信登录">
<a-form
:model="loginData"
:rules="state.smsLoginRules"
layout="vertical"
class="form-container"
@finish="onSmsLogin"
>
<a-form-item label="" name="username">
<a-input
v-model:value="loginData.username"
placeholder="请输入手机号"
size="large"
>
<template #prefix>
<MobileOutlined />
</template>
</a-input>
</a-form-item>
<a-form-item label="" name="captcha">
<div class="captcha-row">
<a-input
v-model:value="loginData.code"
placeholder="请输入验证码"
size="large"
class="captcha-input"
>
<template #prefix>
<LockOutlined />
</template>
<template #suffix>
<a-button
type="text"
size="small"
@click="sendSms"
:disabled="smsButtonDisabled"
:loading="loading"
>
{{
smsCountdown > 0
? `${smsCountdown}秒后重新获取`
: "获取验证码"
}}
</a-button>
</template>
</a-input>
</div>
</a-form-item>
<a-button
type="primary"
size="large"
block
htmlType="submit"
:loading="loading"
>
<span>登录</span>
</a-button>
<div
style="
width: 100%;
display: flex;
align-items: center;
justify-content: start;
"
>
<a-button
type="link"
size="mini"
block
@click="showForgotPasswordPage"
:style="{ marginTop: '10px', border: 'none' }"
style="width: 20%;"
>
忘记密码
</a-button>
<!-- <a-button
type="link"
size="mini"
block
@click="goRegister"
:style="{ marginTop: '10px', border: 'none' }"
>
注册
</a-button> -->
</div>
</a-form>
</a-tab-pane>
</a-tabs>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { onMounted, reactive, ref, toRefs, watch } from "vue";
import loginImg from "@/assets/images/logo.png";
import { UserOutlined, LockOutlined, MobileOutlined } from "@ant-design/icons-vue";
import { getCaptcha, sendSmsCode, smsLoginApi, resetPassword } from "@/api/auth";
import { message } from "ant-design-vue";
import { setToken } from "@/utils/auth";
import Cookies from "js-cookie";
// 组件依赖
import router from "@/router";
// API依赖
import { useRoute } from "vue-router";
import { LoginData } from "@/api/auth/types";
//密码加密
import { encrypt, decrypt } from "@/utils/rsaEncrypt";
// 状态管理依赖
import { useUserStore } from "@/store/modules/user";
import { usePermissionStoreHook } from "@/store/modules/permission";
// 国际化
import { useI18n } from "vue-i18n";
const { t } = useI18n();
const userStore = useUserStore();
const route = useRoute();
// 图片验证码
const codeUrl = ref("");
const smsCountdown = ref(0); // 短信验证码倒计时
const smsButtonDisabled = ref(false); // 短信按钮禁用状态
// 记住密码
let remember = ref(false);
// 忘记密码表单数据
const forgotPasswordForm = ref({
phone: "",
captcha: "",
newPassword: "",
confirmPassword: "",
});
// 忘记密码
const showForgotPassword = ref(false);
// 登录方式
const activeTab = ref("account");
const state = reactive({
redirect: "",
loginData: {
uuid: "",
username: "",
password: "",
code: "",
} as LoginData,
loginRules: {
username: [{ required: true, trigger: "blur", message: t("login.rulesUsername") }],
password: [{ required: true, trigger: "blur", message: t("login.rulesPassword") }],
code: [{ required: true, trigger: "blur", message: "请输入验证码" }],
},
smsLoginRules: {
username: [{ required: true, trigger: "blur", message: "请输入手机号" }],
code: [{ required: true, trigger: "blur", message: "请输入验证码" }],
},
loginImg: loginImg[0],
loading: false,
passwordType: "password",
// 大写提示禁用
capslockTooltipDisabled: true,
otherQuery: {},
clientHeight: document.documentElement.clientHeight,
showDialog: false,
cookiePass: "",
});
// 忘记密码表单校验规则
const forgotPasswordRules = ref({
phone: [
{ required: true, message: "手机号不能为空", trigger: "blur" },
{ pattern: /^1[3-9]\d{9}$/, message: "请输入正确的手机号", trigger: "blur" },
],
captcha: [{ required: true, message: "验证码不能为空", trigger: "blur" }],
newPassword: [
{ required: true, message: "新密码不能为空", trigger: "blur" },
{ min: 10, message: "密码长度不能少于10位", trigger: "blur" },
{
validator: (rule: any, value: string) => {
if (!value) return Promise.resolve();
// 检查密码复杂度:至少包含四类字符中的三类
const hasUpperCase = /[A-Z]/.test(value);
const hasLowerCase = /[a-z]/.test(value);
const hasNumber = /\d/.test(value);
const hasSpecialChar = /[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(value);
const typeCount = [hasUpperCase, hasLowerCase, hasNumber, hasSpecialChar].filter(
Boolean
).length;
if (typeCount < 3) {
return Promise.reject(
new Error("密码必须包含大写字母、小写字母、数字、特殊字符中的至少三种")
);
}
// 检查连续重复字符
if (/(.)\1{1,}/.test(value)) {
return Promise.reject(new Error("密码不能包含连续重复字符"));
}
// 检查连续递增/递减数字
for (let i = 0; i < value.length - 1; i++) {
if (/\d/.test(value[i]) && /\d/.test(value[i + 1])) {
const diff = value.charCodeAt(i + 1) - value.charCodeAt(i);
if (Math.abs(diff) === 1) {
return Promise.reject(new Error("密码不能包含连续递增或递减的数字"));
}
}
}
// 检查连续递增/递减字母
for (let i = 0; i < value.length - 1; i++) {
if (/[a-zA-Z]/.test(value[i]) && /[a-zA-Z]/.test(value[i + 1])) {
const diff = value.charCodeAt(i + 1) - value.charCodeAt(i);
if (Math.abs(diff) === 1) {
return Promise.reject(new Error("密码不能包含连续递增或递减的字母"));
}
}
}
return Promise.resolve();
},
trigger: "blur",
},
],
confirmPassword: [
// { required: true, message: "请再次输入密码", trigger: "blur" },
{
validator: (rule: any, value: string) => {
if (!value) {
return Promise.reject(new Error("请再次输入密码"));
}
if (value !== forgotPasswordForm.value.newPassword) {
return Promise.reject(new Error("两次输入的密码不一致"));
}
return Promise.resolve();
},
trigger: "blur",
},
],
});
const {
loginData,
loginRules,
loading,
// passwordType, capslockTooltipDisabled
} = toRefs(state);
// function checkCapslock(e: any) {
// const { key } = e;
// state.capslockTooltipDisabled = key && key.length === 1 && key >= "A" && key <= "Z";
// }
// function showPwd() {
// if (passwordType.value === "password") {
// passwordType.value = "";
// } else {
// passwordType.value = "password";
// }
// nextTick(() => {
// passwordRef.value.focus();
// });
// }
/**
* 登录
*/
function onFinish() {
state.loading = true;
const user = {
username: state.loginData.username,
password: state.loginData.password,
// rememberMe: state.loginData.rememberMe,
code: state.loginData.code,
uuid: state.loginData.uuid,
};
if (user.password !== state.cookiePass) {
user.password = encrypt(user.password);
}
userStore
.login(user)
.then(() => {
if (remember.value == true) {
Cookies.set("username", user.username, { expires: 30 });
Cookies.set("password", user.password, { expires: 30 });
Cookies.set("rememberMe", String(remember.value), { expires: 30 });
} else {
// 如果用户取消勾选记住密码,应该移除 cookie
Cookies.remove("username");
Cookies.remove("password");
Cookies.remove("rememberMe");
}
router.push({ path: "/" });
state.loading = false;
message.success("登录成功");
})
.catch(() => {
getCode();
state.loading = false;
});
}
/**
* 短信验证码登录
*/
const onSmsLogin = async () => {
try {
// ========== 第一部分:表单验证 ==========
const phoneRegex = /^1[3-9]\d{9}$/;
if (!loginData.value.username || !phoneRegex.test(loginData.value.username)) {
message.error("请输入正确的手机号");
return;
}
if (!loginData.value.code || loginData.value.code.trim() === "") {
message.error("请输入验证码");
return;
}
// ========== 第二部分:设置加载状态 ==========
state.loading = true;
// ========== 第三部分:调用短信登录 API ==========
console.log("开始调用短信登录API, 手机号:", loginData.value.username);
const res: any = await smsLoginApi(
loginData.value.username, // 手机号
loginData.value.code // 短信验证码
);
console.log("短信登录API响应:", res);
// ========== 第四部分:检查响应结果 ==========
console.log("检查响应结果, res.code:", res.code, "类型:", typeof res.code);
// 注意code 可能是字符串 "0" 或数字 0需要兼容处理
if (res.code !== 0 && res.code !== "0") {
console.error("短信登录失败, 错误码:", res.code, "错误信息:", res.msg);
message.error(res.msg || "登录失败");
return;
}
// ========== 第五部分:提取并存储 Token ==========
console.log("开始提取Token, res.data:", res.data);
const token = res.data?.token || res.data; // 兼容两种结构
if (!token) {
console.error("Token为空, res.data结构:", JSON.stringify(res.data));
message.error("登录失败:未获取到Token");
return;
}
console.log("Token获取成功:", token);
// 存储到 Cookie
setToken(token);
// 更新 Store 状态(注意是大写 Token
userStore.Token = token;
console.log("Token已存储到Store");
// ========== 第六部分:获取用户信息和角色 ==========
console.log("开始获取用户信息...");
const userInfo = await userStore.getInfo();
console.log("用户信息获取成功:", userInfo);
const roles = userInfo.roles;
console.log("用户角色:", roles);
// ========== 第七部分:生成路由(获取菜单)==========
console.log("开始生成路由...");
const permissionStore = usePermissionStoreHook();
const accessRoutes: any = await permissionStore.generateRoutes(roles);
console.log("路由生成成功, 路由数量:", accessRoutes.length);
// ========== 第八部分:动态添加路由 ==========
accessRoutes.forEach((route: any) => {
router.addRoute(route);
});
// ========== 第九部分:设置路径并跳转 ==========
router.push({ path: accessRoutes[0].children[0].opturl });
message.success("登录成功");
} catch (error: any) {
// ========== 第十部分:错误处理 ==========
console.error("短信登录失败", error);
message.error(error.message || "登录失败,请重试");
} finally {
// ========== 第十一部分:关闭加载状态 ==========
state.loading = false;
}
};
watch(
route,
() => {
const query = route.query;
if (query) {
state.redirect = query.redirect as string;
state.otherQuery = getOtherQuery(query);
}
},
{
immediate: true,
}
);
function getOtherQuery(query: any) {
return Object.keys(query).reduce((acc: any, cur: any) => {
if (cur !== "redirect") {
acc[cur] = query[cur];
}
return acc;
}, {});
}
function getCookie() {
const username = Cookies.get("username");
let password = Cookies.get("password");
const rememberMe = Cookies.get("rememberMe");
rememberMe == "true" ? (remember.value = Boolean(rememberMe)) : false;
// 保存cookie里面的加密后的密码
state.cookiePass = password === undefined ? "" : password;
password = password === undefined ? state.loginData.password : password;
state.loginData = {
username: username === undefined ? state.loginData.username : username,
password: decrypt(password),
code: "",
uuid: "",
};
remember.value = rememberMe === undefined ? false : Boolean(rememberMe);
}
function getCode() {
getCaptcha().then((result: any) => {
codeUrl.value = result.data.img;
state.loginData.uuid = result.data.uuid;
});
}
// 开始倒计时
const startCountdown = () => {
smsCountdown.value = 60;
const timer = setInterval(() => {
smsCountdown.value--;
if (smsCountdown.value <= 0) {
clearInterval(timer);
smsButtonDisabled.value = false;
}
}, 1000);
};
// 显示忘记密码页面
const showForgotPasswordPage = () => {
showForgotPassword.value = true;
};
//注册用户页面
const goRegister = () => {
router.push({ path: "/register" });
};
// 返回登录页面
const backToLogin = () => {
showForgotPassword.value = false;
// 重置忘记密码表单
forgotPasswordForm.value = {
phone: "",
captcha: "",
newPassword: "",
confirmPassword: "",
};
};
// 发送短信验证码
const sendSms = async () => {
// 检查手机号是否为空
if (!loginData.value.username) {
message.error("请输入手机号");
return;
}
// 检查手机号格式
const phoneRegex = /^1[3-9]\d{9}$/;
if (!phoneRegex.test(loginData.value.username)) {
message.error("请输入正确的手机号");
return;
}
// 如果正在倒计时,不允许重复发送
if (smsCountdown.value > 0) {
return;
}
loading.value = true;
smsButtonDisabled.value = true;
try {
// 调用发送短信验证码接口type默认为1注册类型
const res: any = await sendSmsCode(loginData.value.username, 3);
if (res.code == 1) {
message.success(res.msg);
return;
}
// 发送成功
message.success("验证码发送成功");
// 开始倒计时
startCountdown();
} catch (error) {
console.error("发送验证码失败", error);
message.error("验证码发送失败,请重试");
smsButtonDisabled.value = false;
} finally {
loading.value = false;
}
};
// 发送忘记密码短信验证码
const sendForgotPasswordSms = async () => {
// 检查手机号是否为空
if (!forgotPasswordForm.value.phone) {
message.error("请输入手机号");
return;
}
// 检查手机号格式
const phoneRegex = /^1[3-9]\d{9}$/;
if (!phoneRegex.test(forgotPasswordForm.value.phone)) {
message.error("请输入正确的手机号");
return;
}
// 如果正在倒计时,不允许重复发送
if (smsCountdown.value > 0) {
return;
}
loading.value = true;
smsButtonDisabled.value = true;
try {
// 调用发送短信验证码接口type=3表示找回密码类型
const res: any = await sendSmsCode(forgotPasswordForm.value.phone, 2);
if (res.code == 1) {
message.error(res.msg);
smsButtonDisabled.value = false;
return;
}
// 发送成功
message.success("验证码发送成功");
// 开始倒计时
startCountdown();
} catch (error: any) {
// console.error("发送验证码失败", error);
// message.error(error.message || "验证码发送失败,请重试");
smsButtonDisabled.value = false;
} finally {
loading.value = false;
}
};
// 处理密码重置
const handleResetPassword = async () => {
// 先进行表单验证
try {
// 手动验证各个字段
const phoneRegex = /^1[3-9]\d{9}$/;
if (
!forgotPasswordForm.value.phone ||
!phoneRegex.test(forgotPasswordForm.value.phone)
) {
message.error("请输入正确的手机号");
return;
}
if (!forgotPasswordForm.value.captcha) {
message.error("请输入验证码");
return;
}
if (!forgotPasswordForm.value.newPassword) {
message.error("请输入新密码");
return;
}
// 验证确认密码
if (!forgotPasswordForm.value.confirmPassword) {
message.error("请再次输入密码");
return;
}
// 验证两次密码是否一致
if (
forgotPasswordForm.value.newPassword !== forgotPasswordForm.value.confirmPassword
) {
message.error("两次输入的密码不一致");
return;
}
// 密码复杂度校验
const password = forgotPasswordForm.value.newPassword;
if (password.length < 10) {
message.error("密码长度不能少于10位");
return;
}
const hasUpperCase = /[A-Z]/.test(password);
const hasLowerCase = /[a-z]/.test(password);
const hasNumber = /\d/.test(password);
const hasSpecialChar = /[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(password);
const typeCount = [hasUpperCase, hasLowerCase, hasNumber, hasSpecialChar].filter(
Boolean
).length;
if (typeCount < 3) {
message.error("密码必须包含大写字母、小写字母、数字、特殊字符中的至少三种");
return;
}
// 检查连续重复字符
if (/(.)\1{1,}/.test(password)) {
message.error("密码不能包含连续重复字符");
return;
}
// 检查连续递增/递减数字
for (let i = 0; i < password.length - 1; i++) {
if (/\d/.test(password[i]) && /\d/.test(password[i + 1])) {
const diff = password.charCodeAt(i + 1) - password.charCodeAt(i);
if (Math.abs(diff) === 1) {
message.error("密码不能包含连续递增或递减的数字");
return;
}
}
}
// 检查连续递增/递减字母
for (let i = 0; i < password.length - 1; i++) {
if (/[a-zA-Z]/.test(password[i]) && /[a-zA-Z]/.test(password[i + 1])) {
const diff = password.charCodeAt(i + 1) - password.charCodeAt(i);
if (Math.abs(diff) === 1) {
message.error("密码不能包含连续递增或递减的字母");
return;
}
}
}
} catch (error) {
console.error("表单验证失败", error);
return;
}
loading.value = true;
try {
// 对密码进行 RSA 加密
const encryptedPassword = encrypt(forgotPasswordForm.value.newPassword);
// 调用重置密码接口,使用加密后的密码
const res: any = await resetPassword(
forgotPasswordForm.value.phone,
forgotPasswordForm.value.captcha,
encryptedPassword
);
if (res.code == 0) {
message.success("密码重置成功");
// 返回登录页面
backToLogin();
} else {
message.error(res.msg || "密码重置失败");
}
} catch (error: any) {
// console.error("密码重置失败", error);
// message.error(error.message || "密码重置失败,请重试");
} finally {
loading.value = false;
}
};
onMounted(() => {
getCode();
getCookie();
});
</script>
<style scoped lang="scss">
.login-container {
margin: 0px auto;
position: relative;
width: 100%;
height: 100%;
min-width: 1500px;
background-color: rgb(255, 255, 255);
h1 {
position: relative;
padding: 66px 0px 2px 80px;
margin-bottom: 0px;
height: 155px;
div {
position: relative;
line-height: 71px;
}
.logo-img {
position: absolute;
left: 0px;
top: 10px;
width: 55px;
}
.system-title {
margin-left: 71px;
font-weight: 700;
font-size: 35px;
color: rgb(47, 107, 152);
}
}
:deep(.ant-tabs-tab) {
padding: 8px 0;
}
:deep(.ant-input-prefix) {
display: flex;
width: 26px;
svg {
width: 18px;
height: 18px;
margin-right: 4px;
}
}
.login-wrapper {
position: relative;
width: 100%;
height: 72%;
min-height: 600px;
background: url("@/assets/images/login-bg.jpg");
background-repeat: no-repeat;
background-size: 100% 100%;
}
// 左侧背景区域
.left-section {
.slogan {
position: absolute;
top: 28%;
left: 18%;
width: 440px;
height: 112px;
color: rgb(255, 255, 255);
font-size: 40px;
}
}
// 右侧登录卡片区域
.right-section {
position: absolute;
left: 74%;
top: 20%;
width: 20%;
max-height: 402px;
max-width: 400px;
min-height: 362px;
border-radius: 3px;
padding: 15px 24px 24px;
background-color: rgb(255, 255, 255);
.ant-tabs-nav {
height: 38px;
}
}
/* 验证码布局 */
.captcha-row {
display: flex;
.ant-row {
display: flex;
align-items: start;
}
/* gap: 12px;
align-items: center; */
.captcha-img {
width: 100%;
height: 40px;
line-height: 40px;
background-repeat: no-repeat;
background-size: contain;
background-position: center center;
}
a {
line-height: 50px;
float: right;
color: #1890ff;
text-decoration: none;
background-color: transparent;
outline: none;
cursor: pointer;
transition: color 0.3s;
white-space: nowrap;
}
}
// 忘记密码样式
.forgot-link {
float: right;
color: #1677ff;
font-size: 14px;
}
// 短信登录占位提示
.sms-login-tip {
padding: 40px 20px;
text-align: center;
color: #999;
}
// 忘记密码页面样式
.forgot-password-container {
.forgot-password-header {
display: flex;
align-items: center;
margin-bottom: 24px;
.back-button {
padding: 0;
margin-right: 12px;
color: #1677ff;
font-size: 14px;
&:hover {
color: #4096ff;
}
}
.forgot-password-title {
margin: 0;
font-size: 18px;
font-weight: 600;
color: #333;
}
}
}
}
</style>