Files
Web_BLVLOG_Vue3_Prod/src/pages/login/index.vue
2025-11-20 16:21:56 +08:00

567 lines
14 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 style="height:100vh">
<div class="container">
<div class="logo-container">
<img src="../../../public/logobig.svg" class="log-img" />
<!--<div class="brand-logo">
<div class="text-container">
<div class="main-text">
inHaos
<span class="tm">TM</span>
</div>
<div class="sub-text">Everything done in house</div>
</div>
</div>-->
</div>
<div class="login-container">
<h1>宝来威设备监控平台</h1>
<el-form :model="form"
status-icon
:disabled="isLocked"
class="login-form"
@submit.prevent="handleSubmit">
<!-- 账号输入 -->
<el-form-item prop="username">
<el-input v-model="form.username"
placeholder="请输入账号"
clearable
@focus="scrollToFormBottom"
:prefix-icon="User" />
</el-form-item>
<!-- 密码输入 -->
<el-form-item prop="password">
<el-input v-model="form.password"
type="password"
placeholder="请输入密码"
show-password
@focus="scrollToFormBottom"
:prefix-icon="Lock" />
</el-form-item>
<!-- 验证码区域 -->
<el-form-item prop="code" class="captcha-container">
<div class="captcha-input">
<el-input v-model="form.code"
@focus="scrollToFormBottom"
placeholder="请输入验证码"
:prefix-icon="Key"
maxlength="4" />
<img :src="captchaSrc"
class="captcha-image"
alt="验证码"
@click="generateCaptcha" />
</div>
</el-form-item>
<!-- 记住我 & 操作按钮 -->
<el-form-item>
<div class="form-actions">
<el-checkbox v-model="form.remember">记住我</el-checkbox>
<el-button type="primary"
native-type="submit"
:loading="loading"
:disabled="submitDisabled">
{{ isLocked ? `请等待${lockRemainTime}后重试` : '立即登录' }}
</el-button>
</div>
</el-form-item>
<!-- 错误提示 -->
<div v-if="errorAttempts > 0" class="error-tip">
已错误尝试 {{ errorAttempts }} 5次后将锁定账号
</div>
</el-form>
</div>
</div>
</div>
</template>
<script setup>
import { ref, inject, onMounted, computed } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import { User, Lock, Key } from '@element-plus/icons-vue'
import { useNow } from '@vueuse/core';
const checkLoginStatus = inject('checkLoginStatus');
const calculateTimeDiff = inject('calculateTimeDiff');
const router = useRouter()
const $http = inject('$http')
// 响应式状态
const form = ref({
username: localStorage.getItem('rememberedUsername') || '',
password: localStorage.getItem('rememberedPassword') || '',
remember: localStorage.getItem('rememberedUsername') ?
Boolean(localStorage.getItem('rememberedUsername')) :
true,
code: ''
})
const isDarkMode = ref(window.matchMedia('(prefers-color-scheme: dark)').matches)
const loading = ref(false);
const submitDisabled = ref(false);
const isLocked = ref(false);
const errorAttempts = ref(parseInt(localStorage.getItem('loginErrorAttempts')) || 0);
const lockUntil = ref(parseInt(localStorage.getItem('loginLockUntil')) || 0);
const captchaSrc = ref('');
const captchaValue = ref('');
// 计算属性
const lockRemainTime = computed(() => {
if (!isLocked) return '00:00'
const remain = (lockUntil.value - Date.now()) / 1000
return `${Math.floor(remain / 60)}${Math.floor(remain % 60)}`
})
// 方法实现
const initAuthState = () => {
if (lockUntil.value && Date.now() < lockUntil.value) {
setupUnlockTimer(lockUntil.value - Date.now())
}
}
// 新增深色模式状态
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
isDarkMode.value = true;
} else {
isDarkMode.value = false;
}
const setupUnlockTimer = (duration) => {
isLocked.value = true
setTimeout(() => {
isLocked.value = false
errorAttempts.value = 0
localStorage.removeItem('loginLockUntil')
localStorage.removeItem('loginErrorAttempts')
}, duration)
}
// 生成验证码
const generateCaptcha = () => {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const chars = '0123456789';
let captcha = '';
canvas.width = 86;
canvas.height = 30;
// 生成验证码字符串
for (let i = 0; i < 4; i++) {
captcha += chars.charAt(Math.floor(Math.random() * chars.length));
}
captchaValue.value = captcha; // 存储验证码值用于验证
// 绘制背景
ctx.fillStyle = isDarkMode.value ? '#242424' : '#FFFFFF';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// 绘制噪点
ctx.fillStyle = isDarkMode.value ? '#FFFFFF' : '#000000';
for (let i = 0; i < 33; i++) {
const x = Math.random() * canvas.width;
const y = Math.random() * canvas.height;
ctx.beginPath();
ctx.arc(x, y, 1, 0, Math.PI * 2, false);
ctx.fill();
}
// 绘制验证码
ctx.font = '24px Arial';
let xnd = 10;
for (let i = 0; i < captcha.length; i++) {
const angle = (Math.random() * 30) - 15; // -10度到+10度
// 保存当前状态
ctx.save();
// 旋转并绘制文字
ctx.translate(xnd, 20);
ctx.rotate(angle * Math.PI / 180);
ctx.fillText(captcha[i], 0, 0);
// 恢复状态
ctx.restore();
// 更新下一个字符的x位置
xnd += 15 + Math.abs(angle) / 2; // 根据角度调整字符间距
}
// 添加5-10条干扰条纹
ctx.strokeStyle = '#6F4A2F';
const bug = Math.floor(Math.random() * 6) + 3
for (let i = 0; i < bug; i++) {
ctx.beginPath();
ctx.moveTo(Math.random() * canvas.width, Math.random() * canvas.height);
ctx.lineTo(Math.random() * canvas.width, Math.random() * canvas.height);
ctx.stroke();
}
// 将canvas转换为图片URL
captchaSrc.value = canvas.toDataURL('image/png');
}
// 仅供展示
import config from '../../../public/config.js';
// 登录方法
const handleSubmit = async () => {
if (isLocked.value || submitDisabled.value) return
if (!validateForm()) return
try {
loading.value = true
/* const response = await $http.post('LeiDa/Login', {
username: form.value.username,
password: form.value.password
})
console.log(response)*/
// 仅供展示使用
form.value = {
username: 'admin',
password: 'blw@123',
remember: true,
}
const response = config.adminLogin
//
setTimeout(() => {
handleLoginResponse(response.data)
}, 555)
//handleLoginResponse(response.data)
} catch (error) {
handleLoginError(error)
} finally {
loading.value = false
}
}
// 新增表单验证方法
const validateForm = () => {
if (!form.value.username.trim()) {
ElMessage.error('请输入账号!')
return false
}
if (!form.value.password.trim()) {
ElMessage.error('请输入密码!')
return false
}
if (!form.value.code.trim()) {
ElMessage.error('请输入验证码!')
return false
}
if (form.value.code !== captchaValue.value) {
ElMessage.error('验证码错误!')
generateCaptcha()
return false
}
return true
}
// 新增错误处理方法
const handleLoginError = (error) => {
console.error('登录请求失败:', error)
ElMessage.error('网络请求失败,请检查网络连接')
errorAttempts.value++
}
const handleLoginResponse = (data) => {
if (data.isok) {
handleLoginSuccess(JSON.parse(data.response))
} else {
handleLoginFailure(data.message)
}
}
// 滚动到表单底部(用于移动端输入框聚焦)
const scrollToFormBottom = () => {
window.scrollTo({
top: document.body.scrollHeight,
behavior: 'smooth'
});
}
const handleLoginSuccess = (userData) => {
if (!userData.isok) {
handleLoginFailure(userData.message)
return
}
// 登录成功处理...
errorAttempts.value = 0
if (localStorage.getItem('ATTEMPTS_STORAGE_KEY')) {
localStorage.removeItem(ATTEMPTS_STORAGE_KEY)
}
if (localStorage.getItem('LOCK_STORAGE_KEY')) {
localStorage.removeItem(LOCK_STORAGE_KEY)
}
localStorage.removeItem('loginErrorAttempts')
localStorage.setItem('username', form.value.username)
localStorage.setItem('login', true)
//localStorage.setItem('permission', userData.response)
localStorage.setItem('AccessibleHotels', JSON.stringify(userData.response))
localStorage.setItem("TokenT", new Date())
// 记住账号和密码
if (form.value.remember) {
localStorage.setItem('rememberedUsername', form.value.username)
localStorage.setItem('rememberedPassword', form.value.password)
} else {
if (localStorage.getItem('rememberedUsername')) {
localStorage.removeItem('rememberedPassword')
}
}
ElMessage.success(`登录成功,欢迎:${form.value.username}`)
router.push('/home')
}
const handleLoginFailure = (message) => {
ElMessage.error(message)
errorAttempts.value++
localStorage.setItem('loginErrorAttempts', errorAttempts.value)
if (errorAttempts.value >= 5) {
const lockUntil = Date.now() + 3600000
lockUntil.value = lockUntil
localStorage.setItem('loginLockUntil', lockUntil)
setupUnlockTimer(3600000)
}
}
// 验证时间
const TokenTCheck = () => {
generateCaptcha()
//console.log(localStorage.getItem("TokenT"))
if (localStorage.getItem("TokenT")) {
if (calculateTimeDiff(localStorage.getItem("TokenT")) < 660000) {
form.value.code = captchaValue.value
if (validateForm()) {
handleSubmit()
}
} else {
localStorage.removeItem("TokenT")
}
}
}
// 初始化
onMounted(() => {
localStorage.removeItem('AccessibleHotels')
localStorage.setItem('login', false)
TokenTCheck()
initAuthState()
checkLoginStatus()
})
</script>
<style scoped>
.container {
display: flex;
height: 100%;
width: 100%;
}
.logo-container, .login-container {
flex: 1; /* 各占50% */
display: flex;
flex-direction: column;
align-items: center; /* 水平居中 */
justify-content: center; /* 垂直居中 */
/*min-height: 100vh;*/
}
.logo-image {
width: 80%;
max-width: 400px;
height: auto;
}
.login-title {
margin: 2rem 0;
text-align: center;
}
/* 外层容器样式 */
div[style*="display: flex"] {
display: flex;
justify-content: space-between;
align-items: flex-start;
padding: 20px;
}
/* 通用样式保持 */
.captcha-container {
margin-bottom: 18px;
}
.captcha-input {
display: flex;
gap: 10px;
}
.captcha-image {
height: 40px;
cursor: pointer;
border-radius: 4px;
border: 1px solid var(--el-border-color);
}
.form-actions {
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
}
.error-tip {
color: var(--el-color-danger);
font-size: 12px;
text-align: center;
margin-top: -10px;
margin-bottom: 10px;
}
.brand-logo {
display: flex;
align-items: center;
padding: 20px;
font-family: Arial, sans-serif;
cursor: default; /* 禁用文本选择光标 */
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
}
.symbol-container {
position: relative;
margin-right: 25px;
pointer-events: none; /* 禁用图形部分交互 */
}
.text-container {
display: flex;
flex-direction: column;
pointer-events: none; /* 禁用文字部分交互 */
}
.main-text {
font-size: 88px;
font-weight: 900; /* 加粗程度提升 */
font-style: italic; /* 新增斜体效果 */
margin-bottom: 5px;
font-family: Arial, sans-serif; /* 确保斜体生效 */
transform: skewX(-10deg);
}
/* 其他保持原有样式 */
h1 {
margin: 100px 0px 100px 0px;
text-align: center
}
.sub-text {
font-size: 12px;
letter-spacing: 0.5px;
text-align: left;
}
.log-img {
width: 400px;
height: auto;
}
/* 移动端适配 */
@media (max-width: 768px) {
.log-img {
width: 220px;
}
div [style*="display: flex"] {
flex-direction: column; /* 改为垂直布局 */
padding: 100px;
}
/* 图片容器调整 */
div[style*="display: flex"] > div:first-child {
width: 100%;
margin: 0 auto;
text-align: center;
}
/* 图片样式调整 */
img[src*="logobig.svg"] {
/* width: 80% !important;*/
max-width: 300px;
margin: 0 auto !important;
display: block;
}
.login-container {
flex: 9;
}
.logo-container,
.login-container {
padding: 0rem;
justify-content: start;
}
.main-text {
font-size: 66px;
}
/* 验证码输入容器 */
/* .captcha-input {
flex-direction: column;
gap: 10px;
}*/
/* 验证码图片 */
.captcha-image {
width: 120px;
height: auto;
}
.container {
flex-direction: column;
}
.logo-container,
.login-container {
width: 100%;
min-height: auto;
padding: 2rem;
}
.logo-image {
max-width: 280px;
margin-top: 2rem;
}
.login-container {
box-shadow: none;
}
h1 {
margin: 10px 0px 50px 0px;
}
.login-form {
padding: 0;
}
}
</style>