This commit is contained in:
2026-04-01 10:12:52 +08:00
parent f9db706769
commit 90b290af1f
13 changed files with 999 additions and 40 deletions
+183 -25
View File
@@ -20,6 +20,10 @@
<div class="login-panel">
<h2 class="login-title">欢迎登录</h2>
<div class="login-desc">请填写您的账号信息</div>
<div class="mode-switch">
<button class="mode-btn" :class="{ active: loginMode === 'password' }" @click="switchMode('password')">账号密码登录</button>
<button class="mode-btn" :class="{ active: loginMode === 'sms' }" @click="switchMode('sms')">手机号验证码登录</button>
</div>
<div class="form-group icon-input-group">
<span class="input-icon">
<i class="fa-solid fa-building-columns"></i>
@@ -27,13 +31,13 @@
<input v-model="tenant_name" type="text" placeholder="租户名称" autocomplete="tenant_name"
class="input input-with-icon" />
</div>
<div class="form-group icon-input-group">
<div v-if="loginMode === 'password'" class="form-group icon-input-group">
<span class="input-icon">
<i class="fa-solid fa-user"></i>
</span>
<input v-model="account" type="text" placeholder="用户名" autocomplete="account" class="input input-with-icon" />
</div>
<div class="form-group icon-input-group">
<div v-if="loginMode === 'password'" class="form-group icon-input-group">
<span class="input-icon">
<i class="fa-solid fa-lock"></i>
</span>
@@ -45,16 +49,29 @@
<i v-else class="fa-solid fa-eye-slash"></i>
</span>
</div>
<div v-if="loginMode === 'sms'" class="form-group icon-input-group">
<span class="input-icon">
<i class="fa-solid fa-mobile-screen-button"></i>
</span>
<input v-model="phone" type="text" placeholder="手机号" autocomplete="tel" class="input input-with-icon" />
</div>
<div v-if="loginMode === 'sms'" class="form-group code-row">
<input v-model="smsCode" type="text" placeholder="短信验证码" class="input code-input" />
<button class="code-btn" :disabled="codeLoading || countdown > 0" @click="handleSendLoginCode">
{{ countdown > 0 ? `${countdown}s` : (codeLoading ? "发送中..." : "发送验证码") }}
</button>
</div>
<!-- 极验验证码容器 -->
<div style="display: none;" v-if="showCaptchaContainer" class="geetest-container" ref="captchaContainer"></div>
<div class="remember-me-row">
<label class="remember-me-label">
<label v-if="loginMode === 'password'" class="remember-me-label">
<input type="checkbox" v-model="rememberMe" class="remember-me-checkbox" @change="handleRememberMeChange" />
<span>记住我</span>
</label>
<span v-else></span>
<div class="action-links">
<a class="register-link" @click.prevent="goRegister">注册账号</a>
<span class="divider">|</span>
@@ -76,10 +93,10 @@
</template>
<script setup lang="ts">
import { ref, onMounted, nextTick } from "vue";
import { ref, onMounted, nextTick, onUnmounted } from "vue";
import { useRouter } from "vue-router";
import { useAuthStore } from "@/stores/auth";
import { login, getOpenVerify, getGeetest4Infos } from "@/api/login";
import { login, getOpenVerify, getGeetest4Infos, sendLoginCode, loginBySms } from "@/api/login";
import "@/assets/js/gt4.js";
import { ElMessageBox, ElMessage } from "element-plus";
@@ -88,11 +105,17 @@ const authStore = useAuthStore();
// --- 表单数据 ---
const tenant_name = ref("");
const loginMode = ref<"password" | "sms">("password");
const account = ref("");
const password = ref("");
const phone = ref("");
const smsCode = ref("");
const passwordVisible = ref(false);
const rememberMe = ref(false);
const loading = ref(false);
const codeLoading = ref(false);
const countdown = ref(0);
let countdownTimer: ReturnType<typeof setInterval> | null = null;
const errorMsg = ref("");
// --- 极验相关变量 ---
@@ -127,6 +150,25 @@ const cleanCaptchaInstance = () => {
// --- 执行登录请求 ---
const performLoginRequest = async () => {
if (loginMode.value === "sms") {
const res = await loginBySms({
tenant_name: tenant_name.value,
phone: phone.value,
sms_code: smsCode.value,
});
if (res && res.code === 200) {
authStore.setLoginInfo(res.data);
const { useTabsStore } = await import("@/stores");
const tabsStore = useTabsStore();
tabsStore.resetTabs();
router.push({ path: "/home" });
ElMessage.success("登录成功!");
return true;
}
errorMsg.value = res.msg || "登录失败";
return false;
}
const res = await login({
tenant_name: tenant_name.value,
account: account.value,
@@ -163,6 +205,53 @@ const performLoginRequest = async () => {
}
};
const switchMode = (mode: "password" | "sms") => {
loginMode.value = mode;
errorMsg.value = "";
};
const startCountdown = () => {
countdown.value = 60;
if (countdownTimer) clearInterval(countdownTimer);
countdownTimer = setInterval(() => {
countdown.value -= 1;
if (countdown.value <= 0) {
if (countdownTimer) clearInterval(countdownTimer);
countdownTimer = null;
}
}, 1000);
};
const handleSendLoginCode = async () => {
if (codeLoading.value || countdown.value > 0) return;
if (!tenant_name.value.trim()) {
errorMsg.value = "请输入租户名称";
return;
}
if (!phone.value.trim()) {
errorMsg.value = "请输入手机号";
return;
}
codeLoading.value = true;
errorMsg.value = "";
try {
const res = await sendLoginCode({
tenant_name: tenant_name.value,
phone: phone.value,
});
if (res && res.code === 200) {
ElMessage.success("验证码已发送");
startCountdown();
} else {
errorMsg.value = res?.msg || "发送验证码失败";
}
} catch (err: any) {
errorMsg.value = err?.response?.data?.msg || err?.message || "发送验证码失败";
} finally {
codeLoading.value = false;
}
};
// --- 初始化极验4.0 ---
const startGeetest4 = async () => {
showCaptchaContainer.value = true;
@@ -277,34 +366,50 @@ const handleLogin = async () => {
errorMsg.value = "请输入租户名称";
return;
}
if (!account.value.trim()) {
errorMsg.value = "请输入用户名";
return;
}
if (!password.value.trim()) {
errorMsg.value = "请输入密码";
return;
if (loginMode.value === "password") {
if (!account.value.trim()) {
errorMsg.value = "请输入用户名";
return;
}
if (!password.value.trim()) {
errorMsg.value = "请输入密码";
return;
}
} else {
if (!phone.value.trim()) {
errorMsg.value = "请输入手机号";
return;
}
if (!smsCode.value.trim()) {
errorMsg.value = "请输入短信验证码";
return;
}
}
if (loading.value) return;
loading.value = true;
try {
// 先判断是否开启验证
const verifyRes = await getOpenVerify();
let openVerify = "0";
if (loginMode.value === "password") {
// 先判断是否开启验证
const verifyRes = await getOpenVerify();
let openVerify = "0";
if (verifyRes && verifyRes.code === 200 && Array.isArray(verifyRes.data)) {
verifyRes.data.forEach((item: any) => {
if (item.label === "openVerify") {
openVerify = item.value || "0";
}
});
}
if (verifyRes && verifyRes.code === 200 && Array.isArray(verifyRes.data)) {
verifyRes.data.forEach((item: any) => {
if (item.label === "openVerify") {
openVerify = item.value || "0";
}
});
}
// 如果开启了验证,使用极验4.0
if (openVerify === "1") {
await startGeetest4();
// 如果开启了验证,使用极验4.0
if (openVerify === "1") {
await startGeetest4();
} else {
// 直接登录
await performLoginRequest();
}
} else {
// 直接登录
await performLoginRequest();
@@ -348,6 +453,10 @@ onMounted(() => {
}
});
onUnmounted(() => {
if (countdownTimer) clearInterval(countdownTimer);
});
const goRegister = () => router.push("/register");
const goForget = () => router.push("/forget");
</script>
@@ -447,6 +556,29 @@ const goForget = () => router.push("/forget");
letter-spacing: 0.2px;
}
.mode-switch {
display: flex;
gap: 8px;
margin-bottom: 14px;
}
.mode-btn {
flex: 1;
padding: 8px 10px;
border: 1px solid #d6e6fa;
border-radius: 7px;
background: #f7fbfe;
color: #407ad6;
cursor: pointer;
}
.mode-btn.active {
background: #e8f3ff;
border-color: #7cb8ff;
color: #1d63c2;
font-weight: 600;
}
.form-group {
margin-bottom: 15px;
position: relative;
@@ -520,6 +652,32 @@ const goForget = () => router.push("/forget");
box-shadow: 0 0 0 2px #e3f2ffb1;
}
.code-row {
display: flex;
gap: 8px;
align-items: center;
}
.code-input {
margin-bottom: 0;
flex: 1;
}
.code-btn {
width: 120px;
height: 44px;
border: 1.3px solid #d6e6fa;
border-radius: 7px;
background: #fff;
color: #3a78ca;
cursor: pointer;
}
.code-btn:disabled {
opacity: 0.65;
cursor: not-allowed;
}
/* 极验验证码容器样式 */
.geetest-container {
margin: 10px 0;