2026-03-25 10:02:19 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<div class="login-container">
|
2026-03-27 14:50:35 +08:00
|
|
|
|
<h1>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<img :src="loginImg" alt="Logo" class="logo-img" />
|
|
|
|
|
|
<a class="system-title">{{ $t("login.title") }}</a>
|
2026-03-25 10:02:19 +08:00
|
|
|
|
</div>
|
2026-03-27 14:50:35 +08:00
|
|
|
|
</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>
|
2026-07-10 17:11:58 +08:00
|
|
|
|
<!-- <h3 class="forgot-password-title">忘记密码</h3> -->
|
2026-03-25 10:02:19 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-03-27 14:50:35 +08:00
|
|
|
|
<a-form
|
|
|
|
|
|
:model="forgotPasswordForm"
|
|
|
|
|
|
:rules="forgotPasswordRules"
|
|
|
|
|
|
layout="vertical"
|
|
|
|
|
|
class="form-container"
|
2026-03-25 10:02:19 +08:00
|
|
|
|
>
|
2026-03-27 14:50:35 +08:00
|
|
|
|
<a-form-item label="" name="phone">
|
2026-07-10 17:11:58 +08:00
|
|
|
|
<a-input
|
|
|
|
|
|
v-model:value="forgotPasswordForm.phone"
|
|
|
|
|
|
placeholder="请输入手机号"
|
|
|
|
|
|
size="large"
|
|
|
|
|
|
>
|
2026-03-27 14:50:35 +08:00
|
|
|
|
<template #prefix>
|
|
|
|
|
|
<MobileOutlined />
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</a-input>
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
|
|
|
|
|
|
<a-form-item label="" name="captcha">
|
|
|
|
|
|
<div class="captcha-row">
|
|
|
|
|
|
<a-input
|
2026-07-10 17:11:58 +08:00
|
|
|
|
v-model:value="forgotPasswordForm.captcha"
|
2026-03-27 14:50:35 +08:00
|
|
|
|
placeholder="请输入验证码"
|
|
|
|
|
|
size="large"
|
2026-07-10 17:11:58 +08:00
|
|
|
|
autocomplete="current-captcha"
|
2026-03-27 14:50:35 +08:00
|
|
|
|
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
|
2026-07-10 17:11:58 +08:00
|
|
|
|
v-model:value="forgotPasswordForm.newPassword"
|
|
|
|
|
|
autocomplete="current-password"
|
2026-03-27 14:50:35 +08:00
|
|
|
|
placeholder="请输入新密码"
|
|
|
|
|
|
size="large"
|
2026-03-25 10:02:19 +08:00
|
|
|
|
>
|
2026-03-27 14:50:35 +08:00
|
|
|
|
<template #prefix>
|
|
|
|
|
|
<LockOutlined />
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</a-input-password>
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
|
2026-07-10 17:11:58 +08:00
|
|
|
|
<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>
|
|
|
|
|
|
|
2026-03-27 14:50:35 +08:00
|
|
|
|
<a-button
|
|
|
|
|
|
type="primary"
|
2026-03-25 10:02:19 +08:00
|
|
|
|
size="large"
|
2026-03-27 14:50:35 +08:00
|
|
|
|
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"
|
2026-05-07 15:40:18 +08:00
|
|
|
|
placeholder="请输入用户账号/手机号"
|
2026-03-27 14:50:35 +08:00
|
|
|
|
size="large"
|
2026-06-26 18:16:16 +08:00
|
|
|
|
autocomplete="current-username"
|
2026-03-27 14:50:35 +08:00
|
|
|
|
>
|
|
|
|
|
|
<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"
|
2026-06-26 18:16:16 +08:00
|
|
|
|
autocomplete="current-password"
|
2026-03-27 14:50:35 +08:00
|
|
|
|
>
|
|
|
|
|
|
<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="请输入验证码"
|
2026-06-26 18:16:16 +08:00
|
|
|
|
autocomplete="current-captcha"
|
2026-03-27 14:50:35 +08:00
|
|
|
|
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>
|
2026-07-10 17:11:58 +08:00
|
|
|
|
<div
|
|
|
|
|
|
style="
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
"
|
2026-03-27 14:50:35 +08:00
|
|
|
|
>
|
2026-07-10 17:11:58 +08:00
|
|
|
|
<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>
|
2026-03-27 14:50:35 +08:00
|
|
|
|
|
|
|
|
|
|
<!-- 忘记密码 -->
|
|
|
|
|
|
</a-form>
|
|
|
|
|
|
</a-tab-pane>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 短信登录 Tab (占位) -->
|
2026-07-10 17:11:58 +08:00
|
|
|
|
<a-tab-pane key="sms" tab="短信登录">
|
2026-03-27 14:50:35 +08:00
|
|
|
|
<a-form
|
|
|
|
|
|
:model="loginData"
|
2026-07-10 17:11:58 +08:00
|
|
|
|
:rules="state.smsLoginRules"
|
2026-03-27 14:50:35 +08:00
|
|
|
|
layout="vertical"
|
|
|
|
|
|
class="form-container"
|
2026-07-10 17:11:58 +08:00
|
|
|
|
@finish="onSmsLogin"
|
2026-03-27 14:50:35 +08:00
|
|
|
|
>
|
|
|
|
|
|
<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
|
2026-07-10 17:11:58 +08:00
|
|
|
|
v-model:value="loginData.code"
|
2026-03-27 14:50:35 +08:00
|
|
|
|
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>
|
2026-07-10 17:11:58 +08:00
|
|
|
|
<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>
|
2026-03-27 14:50:35 +08:00
|
|
|
|
</a-form>
|
2026-07-10 17:11:58 +08:00
|
|
|
|
</a-tab-pane>
|
2026-03-27 14:50:35 +08:00
|
|
|
|
</a-tabs>
|
2026-03-25 10:02:19 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
2026-04-22 17:53:20 +08:00
|
|
|
|
import { onMounted, reactive, ref, toRefs, watch } from "vue";
|
2026-03-27 14:50:35 +08:00
|
|
|
|
import loginImg from "@/assets/images/logo.png";
|
|
|
|
|
|
import { UserOutlined, LockOutlined, MobileOutlined } from "@ant-design/icons-vue";
|
2026-07-10 17:11:58 +08:00
|
|
|
|
import { getCaptcha, sendSmsCode, smsLoginApi, resetPassword } from "@/api/auth";
|
2026-03-27 14:50:35 +08:00
|
|
|
|
import { message } from "ant-design-vue";
|
2026-07-10 17:11:58 +08:00
|
|
|
|
import { setToken } from "@/utils/auth";
|
|
|
|
|
|
import Cookies from "js-cookie";
|
2026-03-25 10:02:19 +08:00
|
|
|
|
// 组件依赖
|
2026-07-10 17:11:58 +08:00
|
|
|
|
|
2026-03-27 14:50:35 +08:00
|
|
|
|
import router from "@/router";
|
2026-03-25 10:02:19 +08:00
|
|
|
|
// API依赖
|
2026-03-27 14:50:35 +08:00
|
|
|
|
import { useRoute } from "vue-router";
|
|
|
|
|
|
import { LoginData } from "@/api/auth/types";
|
2026-03-25 10:02:19 +08:00
|
|
|
|
|
|
|
|
|
|
//密码加密
|
2026-07-10 17:11:58 +08:00
|
|
|
|
import { encrypt, decrypt } from "@/utils/rsaEncrypt";
|
2026-03-25 10:02:19 +08:00
|
|
|
|
|
|
|
|
|
|
// 状态管理依赖
|
2026-03-27 14:50:35 +08:00
|
|
|
|
import { useUserStore } from "@/store/modules/user";
|
2026-07-10 17:11:58 +08:00
|
|
|
|
import { usePermissionStoreHook } from "@/store/modules/permission";
|
2026-03-25 10:02:19 +08:00
|
|
|
|
|
|
|
|
|
|
// 国际化
|
2026-03-27 14:50:35 +08:00
|
|
|
|
import { useI18n } from "vue-i18n";
|
2026-03-25 10:02:19 +08:00
|
|
|
|
const { t } = useI18n();
|
|
|
|
|
|
const userStore = useUserStore();
|
|
|
|
|
|
const route = useRoute();
|
|
|
|
|
|
// 图片验证码
|
2026-03-27 14:50:35 +08:00
|
|
|
|
const codeUrl = ref("");
|
2026-03-25 10:02:19 +08:00
|
|
|
|
|
2026-03-27 14:50:35 +08:00
|
|
|
|
const smsCountdown = ref(0); // 短信验证码倒计时
|
|
|
|
|
|
const smsButtonDisabled = ref(false); // 短信按钮禁用状态
|
2026-03-25 10:02:19 +08:00
|
|
|
|
// 记住密码
|
|
|
|
|
|
let remember = ref(false);
|
|
|
|
|
|
|
2026-03-27 14:50:35 +08:00
|
|
|
|
// 忘记密码表单数据
|
|
|
|
|
|
const forgotPasswordForm = ref({
|
|
|
|
|
|
phone: "",
|
|
|
|
|
|
captcha: "",
|
|
|
|
|
|
newPassword: "",
|
2026-07-10 17:11:58 +08:00
|
|
|
|
confirmPassword: "",
|
2026-03-27 14:50:35 +08:00
|
|
|
|
});
|
|
|
|
|
|
// 忘记密码
|
|
|
|
|
|
const showForgotPassword = ref(false);
|
|
|
|
|
|
// 登录方式
|
|
|
|
|
|
const activeTab = ref("account");
|
|
|
|
|
|
|
2026-03-25 10:02:19 +08:00
|
|
|
|
const state = reactive({
|
2026-03-27 14:50:35 +08:00
|
|
|
|
redirect: "",
|
2026-03-25 10:02:19 +08:00
|
|
|
|
loginData: {
|
2026-03-27 14:50:35 +08:00
|
|
|
|
uuid: "",
|
2026-04-27 13:43:34 +08:00
|
|
|
|
username: "",
|
|
|
|
|
|
password: "",
|
2026-03-27 14:50:35 +08:00
|
|
|
|
code: "",
|
2026-03-25 10:02:19 +08:00
|
|
|
|
} as LoginData,
|
|
|
|
|
|
loginRules: {
|
2026-03-27 14:50:35 +08:00
|
|
|
|
username: [{ required: true, trigger: "blur", message: t("login.rulesUsername") }],
|
|
|
|
|
|
password: [{ required: true, trigger: "blur", message: t("login.rulesPassword") }],
|
|
|
|
|
|
code: [{ required: true, trigger: "blur", message: "请输入验证码" }],
|
2026-03-25 10:02:19 +08:00
|
|
|
|
},
|
2026-07-10 17:11:58 +08:00
|
|
|
|
smsLoginRules: {
|
|
|
|
|
|
username: [{ required: true, trigger: "blur", message: "请输入手机号" }],
|
|
|
|
|
|
code: [{ required: true, trigger: "blur", message: "请输入验证码" }],
|
|
|
|
|
|
},
|
2026-03-25 10:02:19 +08:00
|
|
|
|
loginImg: loginImg[0],
|
|
|
|
|
|
loading: false,
|
2026-03-27 14:50:35 +08:00
|
|
|
|
passwordType: "password",
|
2026-03-25 10:02:19 +08:00
|
|
|
|
// 大写提示禁用
|
|
|
|
|
|
capslockTooltipDisabled: true,
|
|
|
|
|
|
otherQuery: {},
|
|
|
|
|
|
clientHeight: document.documentElement.clientHeight,
|
|
|
|
|
|
showDialog: false,
|
|
|
|
|
|
|
2026-03-27 14:50:35 +08:00
|
|
|
|
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" },
|
2026-07-10 17:11:58 +08:00
|
|
|
|
{ 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",
|
|
|
|
|
|
},
|
2026-03-27 14:50:35 +08:00
|
|
|
|
],
|
2026-03-25 10:02:19 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-07-10 17:11:58 +08:00
|
|
|
|
const {
|
|
|
|
|
|
loginData,
|
|
|
|
|
|
loginRules,
|
|
|
|
|
|
loading,
|
|
|
|
|
|
// passwordType, capslockTooltipDisabled
|
|
|
|
|
|
} = toRefs(state);
|
2026-03-25 10:02:19 +08:00
|
|
|
|
|
2026-04-22 17:53:20 +08:00
|
|
|
|
// function checkCapslock(e: any) {
|
|
|
|
|
|
// const { key } = e;
|
|
|
|
|
|
// state.capslockTooltipDisabled = key && key.length === 1 && key >= "A" && key <= "Z";
|
|
|
|
|
|
// }
|
2026-03-25 10:02:19 +08:00
|
|
|
|
|
2026-04-22 17:53:20 +08:00
|
|
|
|
// function showPwd() {
|
|
|
|
|
|
// if (passwordType.value === "password") {
|
|
|
|
|
|
// passwordType.value = "";
|
|
|
|
|
|
// } else {
|
|
|
|
|
|
// passwordType.value = "password";
|
|
|
|
|
|
// }
|
|
|
|
|
|
// nextTick(() => {
|
|
|
|
|
|
// passwordRef.value.focus();
|
|
|
|
|
|
// });
|
|
|
|
|
|
// }
|
2026-03-25 10:02:19 +08:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 登录
|
|
|
|
|
|
*/
|
2026-03-27 14:50:35 +08:00
|
|
|
|
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(() => {
|
2026-07-10 17:11:58 +08:00
|
|
|
|
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");
|
|
|
|
|
|
}
|
2026-03-27 14:50:35 +08:00
|
|
|
|
router.push({ path: "/" });
|
|
|
|
|
|
state.loading = false;
|
2026-07-10 17:11:58 +08:00
|
|
|
|
message.success("登录成功");
|
2026-03-27 14:50:35 +08:00
|
|
|
|
})
|
|
|
|
|
|
.catch(() => {
|
|
|
|
|
|
getCode();
|
|
|
|
|
|
state.loading = false;
|
|
|
|
|
|
});
|
2026-03-25 10:02:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-10 17:11:58 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 短信验证码登录
|
|
|
|
|
|
*/
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-25 10:02:19 +08:00
|
|
|
|
watch(
|
|
|
|
|
|
route,
|
|
|
|
|
|
() => {
|
|
|
|
|
|
const query = route.query;
|
|
|
|
|
|
if (query) {
|
|
|
|
|
|
state.redirect = query.redirect as string;
|
|
|
|
|
|
state.otherQuery = getOtherQuery(query);
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
2026-03-27 14:50:35 +08:00
|
|
|
|
immediate: true,
|
2026-03-25 10:02:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
function getOtherQuery(query: any) {
|
|
|
|
|
|
return Object.keys(query).reduce((acc: any, cur: any) => {
|
2026-03-27 14:50:35 +08:00
|
|
|
|
if (cur !== "redirect") {
|
2026-03-25 10:02:19 +08:00
|
|
|
|
acc[cur] = query[cur];
|
|
|
|
|
|
}
|
|
|
|
|
|
return acc;
|
|
|
|
|
|
}, {});
|
|
|
|
|
|
}
|
2026-07-10 17:11:58 +08:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
2026-03-25 10:02:19 +08:00
|
|
|
|
function getCode() {
|
2026-03-27 14:50:35 +08:00
|
|
|
|
getCaptcha().then((result: any) => {
|
|
|
|
|
|
codeUrl.value = result.data.img;
|
2026-03-25 10:02:19 +08:00
|
|
|
|
state.loginData.uuid = result.data.uuid;
|
2026-03-27 14:50:35 +08:00
|
|
|
|
});
|
2026-03-25 10:02:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-27 14:50:35 +08:00
|
|
|
|
// 开始倒计时
|
|
|
|
|
|
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;
|
|
|
|
|
|
};
|
2026-07-10 17:11:58 +08:00
|
|
|
|
//注册用户页面
|
|
|
|
|
|
const goRegister = () => {
|
|
|
|
|
|
router.push({ path: "/register" });
|
|
|
|
|
|
};
|
2026-03-27 14:50:35 +08:00
|
|
|
|
// 返回登录页面
|
|
|
|
|
|
const backToLogin = () => {
|
|
|
|
|
|
showForgotPassword.value = false;
|
|
|
|
|
|
// 重置忘记密码表单
|
|
|
|
|
|
forgotPasswordForm.value = {
|
|
|
|
|
|
phone: "",
|
|
|
|
|
|
captcha: "",
|
|
|
|
|
|
newPassword: "",
|
2026-07-10 17:11:58 +08:00
|
|
|
|
confirmPassword: "",
|
2026-03-27 14:50:35 +08:00
|
|
|
|
};
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 发送短信验证码
|
|
|
|
|
|
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 {
|
2026-07-10 17:11:58 +08:00
|
|
|
|
// 调用发送短信验证码接口,type默认为1(注册类型)
|
|
|
|
|
|
const res: any = await sendSmsCode(loginData.value.username, 3);
|
|
|
|
|
|
if (res.code == 1) {
|
|
|
|
|
|
message.success(res.msg);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
// 发送成功
|
2026-03-27 14:50:35 +08:00
|
|
|
|
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 {
|
2026-07-10 17:11:58 +08:00
|
|
|
|
// 调用发送短信验证码接口,type=3表示找回密码类型
|
|
|
|
|
|
const res: any = await sendSmsCode(forgotPasswordForm.value.phone, 2);
|
|
|
|
|
|
|
|
|
|
|
|
if (res.code == 1) {
|
|
|
|
|
|
message.error(res.msg);
|
|
|
|
|
|
smsButtonDisabled.value = false;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-03-27 14:50:35 +08:00
|
|
|
|
|
2026-07-10 17:11:58 +08:00
|
|
|
|
// 发送成功
|
2026-03-27 14:50:35 +08:00
|
|
|
|
message.success("验证码发送成功");
|
|
|
|
|
|
|
|
|
|
|
|
// 开始倒计时
|
|
|
|
|
|
startCountdown();
|
2026-07-10 17:11:58 +08:00
|
|
|
|
} catch (error: any) {
|
|
|
|
|
|
// console.error("发送验证码失败", error);
|
|
|
|
|
|
// message.error(error.message || "验证码发送失败,请重试");
|
2026-03-27 14:50:35 +08:00
|
|
|
|
smsButtonDisabled.value = false;
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
loading.value = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
// 处理密码重置
|
|
|
|
|
|
const handleResetPassword = async () => {
|
2026-07-10 17:11:58 +08:00
|
|
|
|
// 先进行表单验证
|
2026-03-27 14:50:35 +08:00
|
|
|
|
try {
|
2026-07-10 17:11:58 +08:00
|
|
|
|
// 手动验证各个字段
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-27 14:50:35 +08:00
|
|
|
|
} catch (error) {
|
2026-07-10 17:11:58 +08:00
|
|
|
|
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 || "密码重置失败,请重试");
|
2026-03-27 14:50:35 +08:00
|
|
|
|
} finally {
|
|
|
|
|
|
loading.value = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-25 10:02:19 +08:00
|
|
|
|
onMounted(() => {
|
|
|
|
|
|
getCode();
|
2026-07-10 17:11:58 +08:00
|
|
|
|
getCookie();
|
2026-03-25 10:02:19 +08:00
|
|
|
|
});
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
2026-03-27 14:50:35 +08:00
|
|
|
|
<style scoped lang="scss">
|
2026-03-25 10:02:19 +08:00
|
|
|
|
.login-container {
|
2026-03-27 14:50:35 +08:00
|
|
|
|
margin: 0px auto;
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
min-width: 1500px;
|
|
|
|
|
|
background-color: rgb(255, 255, 255);
|
|
|
|
|
|
|
|
|
|
|
|
h1 {
|
2026-03-25 10:02:19 +08:00
|
|
|
|
position: relative;
|
2026-03-27 14:50:35 +08:00
|
|
|
|
padding: 66px 0px 2px 80px;
|
|
|
|
|
|
margin-bottom: 0px;
|
|
|
|
|
|
height: 155px;
|
2026-03-25 10:02:19 +08:00
|
|
|
|
|
2026-03-27 14:50:35 +08:00
|
|
|
|
div {
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
line-height: 71px;
|
2026-03-25 10:02:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-27 14:50:35 +08:00
|
|
|
|
.logo-img {
|
2026-03-25 10:02:19 +08:00
|
|
|
|
position: absolute;
|
2026-03-27 14:50:35 +08:00
|
|
|
|
left: 0px;
|
|
|
|
|
|
top: 10px;
|
|
|
|
|
|
width: 55px;
|
2026-03-25 10:02:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-27 14:50:35 +08:00
|
|
|
|
.system-title {
|
|
|
|
|
|
margin-left: 71px;
|
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
font-size: 35px;
|
|
|
|
|
|
color: rgb(47, 107, 152);
|
2026-03-25 10:02:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-27 14:50:35 +08:00
|
|
|
|
:deep(.ant-tabs-tab) {
|
|
|
|
|
|
padding: 8px 0;
|
2026-03-25 10:02:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-27 14:50:35 +08:00
|
|
|
|
:deep(.ant-input-prefix) {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
width: 26px;
|
2026-03-25 10:02:19 +08:00
|
|
|
|
|
2026-03-27 14:50:35 +08:00
|
|
|
|
svg {
|
|
|
|
|
|
width: 18px;
|
|
|
|
|
|
height: 18px;
|
|
|
|
|
|
margin-right: 4px;
|
2026-03-25 10:02:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-27 14:50:35 +08:00
|
|
|
|
.login-wrapper {
|
2026-07-10 17:11:58 +08:00
|
|
|
|
position: relative;
|
2026-03-27 14:50:35 +08:00
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 72%;
|
|
|
|
|
|
min-height: 600px;
|
|
|
|
|
|
background: url("@/assets/images/login-bg.jpg");
|
|
|
|
|
|
background-repeat: no-repeat;
|
|
|
|
|
|
background-size: 100% 100%;
|
2026-03-25 10:02:19 +08:00
|
|
|
|
}
|
2026-03-27 14:50:35 +08:00
|
|
|
|
|
|
|
|
|
|
// 左侧背景区域
|
|
|
|
|
|
.left-section {
|
2026-07-10 17:11:58 +08:00
|
|
|
|
.slogan {
|
2026-03-27 14:50:35 +08:00
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: 28%;
|
|
|
|
|
|
left: 18%;
|
|
|
|
|
|
width: 440px;
|
|
|
|
|
|
height: 112px;
|
|
|
|
|
|
color: rgb(255, 255, 255);
|
|
|
|
|
|
font-size: 40px;
|
|
|
|
|
|
}
|
2026-03-25 10:02:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-27 14:50:35 +08:00
|
|
|
|
// 右侧登录卡片区域
|
|
|
|
|
|
.right-section {
|
2026-03-25 10:02:19 +08:00
|
|
|
|
position: absolute;
|
2026-03-27 14:50:35 +08:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
2026-03-25 10:02:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-27 14:50:35 +08:00
|
|
|
|
/* 验证码布局 */
|
|
|
|
|
|
.captcha-row {
|
|
|
|
|
|
display: flex;
|
2026-07-10 17:11:58 +08:00
|
|
|
|
|
2026-03-27 14:50:35 +08:00
|
|
|
|
.ant-row {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: start;
|
|
|
|
|
|
}
|
2026-07-10 17:11:58 +08:00
|
|
|
|
|
2026-03-27 14:50:35 +08:00
|
|
|
|
/* gap: 12px;
|
|
|
|
|
|
align-items: center; */
|
2026-03-25 10:02:19 +08:00
|
|
|
|
|
2026-03-27 14:50:35 +08:00
|
|
|
|
.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;
|
2026-03-25 10:02:19 +08:00
|
|
|
|
cursor: pointer;
|
2026-03-27 14:50:35 +08:00
|
|
|
|
transition: color 0.3s;
|
|
|
|
|
|
white-space: nowrap;
|
2026-03-25 10:02:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-27 14:50:35 +08:00
|
|
|
|
// 忘记密码样式
|
|
|
|
|
|
.forgot-link {
|
|
|
|
|
|
float: right;
|
|
|
|
|
|
color: #1677ff;
|
|
|
|
|
|
font-size: 14px;
|
2026-03-25 10:02:19 +08:00
|
|
|
|
}
|
2026-03-27 14:50:35 +08:00
|
|
|
|
|
|
|
|
|
|
// 短信登录占位提示
|
|
|
|
|
|
.sms-login-tip {
|
|
|
|
|
|
padding: 40px 20px;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
color: #999;
|
2026-03-25 10:02:19 +08:00
|
|
|
|
}
|
2026-03-27 14:50:35 +08:00
|
|
|
|
|
|
|
|
|
|
// 忘记密码页面样式
|
|
|
|
|
|
.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;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-25 10:02:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|