初始化
This commit is contained in:
566
src/pages/login/index.vue
Normal file
566
src/pages/login/index.vue
Normal file
@@ -0,0 +1,566 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user