567 lines
14 KiB
Vue
567 lines
14 KiB
Vue
<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>
|