Files
Web_BLVLOG_Vue3_Prod/src/pages/login/index.vue

567 lines
14 KiB
Vue
Raw Normal View History

2025-11-20 16:21:56 +08:00
<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>