ERP/frontend/login.html

581 lines
20 KiB
HTML
Executable File
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.

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>登录 - 韬智生产管理系统</title>
<link rel="icon" type="image/x-icon" href="../icon.ico" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="./assets/login-animated.css?v=20251231-1427" />
</head>
<body>
<div class="login-page">
<!-- 左侧角色动画区域 -->
<div class="characters-section">
<div class="brand-logo">
<img src="../icon.ico" alt="Logo" class="logo-icon" />
<span>韬智生产管理系统</span>
</div>
<div class="characters-container" id="characters-container">
<!-- 紫色角色 -->
<div class="character purple" id="char-purple">
<div class="eyes">
<div class="eye" id="purple-eye-left">
<div class="pupil" id="purple-pupil-left"></div>
</div>
<div class="eye" id="purple-eye-right">
<div class="pupil" id="purple-pupil-right"></div>
</div>
</div>
</div>
<!-- 黑色角色 -->
<div class="character black" id="char-black">
<div class="eyes">
<div class="eye" id="black-eye-left">
<div class="pupil" id="black-pupil-left"></div>
</div>
<div class="eye" id="black-eye-right">
<div class="pupil" id="black-pupil-right"></div>
</div>
</div>
</div>
<!-- 橙色角色 -->
<div class="character orange" id="char-orange">
<div class="eyes-simple">
<div class="pupil-simple" id="orange-pupil-left"></div>
<div class="pupil-simple" id="orange-pupil-right"></div>
</div>
</div>
<!-- 黄色角色 -->
<div class="character yellow" id="char-yellow">
<div class="eyes-simple">
<div class="pupil-simple" id="yellow-pupil-left"></div>
<div class="pupil-simple" id="yellow-pupil-right"></div>
</div>
<div class="mouth"></div>
</div>
</div>
<div class="footer-links">
<a href="#">隐私政策</a>
<a href="#">服务条款</a>
<a href="#">联系我们</a>
</div>
</div>
<!-- 右侧登录表单区域 -->
<div class="login-section">
<div class="login-container">
<div class="login-header">
<h1>欢迎回来!</h1>
<p>请输入您的登录信息</p>
</div>
<form class="login-form" onsubmit="return false;">
<div class="form-group">
<label for="username">用户名</label>
<input type="text" id="username" class="form-input" placeholder="请输入用户名" required autocomplete="username">
</div>
<div class="form-group">
<label for="password">密码</label>
<div class="password-wrapper">
<input type="text" id="password" class="form-input" placeholder="请输入密码" required autocomplete="current-password">
<button type="button" class="password-toggle" id="password-toggle" aria-label="隐藏密码">
<svg class="eye-icon eye-open" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
<circle cx="12" cy="12" r="3"></circle>
</svg>
<svg class="eye-icon eye-closed" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="display:none;">
<path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"></path>
<line x1="1" y1="1" x2="23" y2="23"></line>
</svg>
</button>
</div>
</div>
<div class="form-group captcha-group">
<div class="captcha-input-wrapper">
<label for="captcha">验证码</label>
<input type="text" id="captcha" class="form-input" placeholder="请输入验证码" required maxlength="4" autocomplete="off">
</div>
<div class="captcha-image-wrapper" id="captcha-image-wrapper" title="点击刷新验证码">
<img id="captcha-image" class="captcha-image" alt="验证码" />
</div>
</div>
<div id="error-message" class="error-message" style="display:none;"></div>
<button type="button" id="login-btn" class="login-button">
<span id="login-text">登录</span>
<span id="login-loader" class="btn-loader" style="display:none;">
<span class="dot"></span>
<span class="dot"></span>
<span class="dot"></span>
</span>
</button>
<div class="form-footer">
<span class="copyright">© 2025 韬智科技</span>
</div>
</form>
</div>
</div>
</div>
<script>
// 检查是否已登录
async function checkAuth() {
try {
const response = await fetch('/api/auth/me', {
credentials: 'include'
});
if (response.ok) {
const user = await response.json();
if (user && user.username) {
window.location.replace('./index.html#/dashboard');
}
}
} catch(e) {
// 未登录,继续显示登录页面
console.log('未登录,显示登录页面');
}
}
checkAuth();
// 登录处理
const usernameInput = document.getElementById('username');
const passwordInput = document.getElementById('password');
const passwordToggle = document.getElementById('password-toggle');
const captchaInput = document.getElementById('captcha');
const captchaImage = document.getElementById('captcha-image');
const captchaImageWrapper = document.getElementById('captcha-image-wrapper');
const loginBtn = document.getElementById('login-btn');
const loginText = document.getElementById('login-text');
const loginLoader = document.getElementById('login-loader');
const errorMessage = document.getElementById('error-message');
function showError(message) {
errorMessage.textContent = message;
errorMessage.style.display = 'block';
setTimeout(() => {
errorMessage.style.display = 'none';
}, 3000);
}
function setLoading(loading) {
if (loading) {
loginBtn.disabled = true;
loginText.style.display = 'none';
loginLoader.style.display = 'flex';
} else {
loginBtn.disabled = false;
loginText.style.display = 'inline';
loginLoader.style.display = 'none';
}
}
// 加载验证码
async function loadCaptcha() {
try {
const response = await fetch('/api/auth/captcha', {
credentials: 'include'
});
if (response.ok) {
const data = await response.json();
if (data.image) {
captchaImage.src = data.image;
}
}
} catch(e) {
console.error('加载验证码失败:', e);
}
}
// 点击图片刷新验证码
captchaImageWrapper.addEventListener('click', () => {
captchaInput.value = '';
captchaImageWrapper.classList.add('refreshing');
loadCaptcha();
setTimeout(() => {
captchaImageWrapper.classList.remove('refreshing');
}, 300);
});
// 页面加载时获取验证码
loadCaptcha();
async function handleLogin() {
const username = usernameInput.value.trim();
const password = passwordInput.value.trim();
const captcha = captchaInput.value.trim();
if (!username) {
showError('请输入用户名');
usernameInput.focus();
return;
}
if (!password) {
showError('请输入密码');
passwordInput.focus();
return;
}
if (!captcha) {
showError('请输入验证码');
captchaInput.focus();
return;
}
if (captcha.length !== 4) {
showError('验证码为4位数字');
captchaInput.focus();
return;
}
setLoading(true);
try {
// 直接使用 fetch避免 API.login 的 overlay 问题
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ username, password, captcha })
});
const result = await response.json();
if (!response.ok) {
throw new Error(result.error || '登录失败');
}
if (result.ok) {
// 登录成功,跳转到主页面
// 使用 replace 避免 Safari 缓存问题
window.location.replace('./index.html#/dashboard');
} else {
throw new Error('登录失败');
}
} catch(e) {
showError(e.message || '登录失败');
setLoading(false);
// 刷新验证码
captchaInput.value = '';
loadCaptcha();
}
}
loginBtn.addEventListener('click', handleLogin);
// 回车键登录
usernameInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
passwordInput.focus();
}
});
passwordInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
captchaInput.focus();
}
});
captchaInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
handleLogin();
}
});
// 自动聚焦用户名输入框
usernameInput.focus();
// ============ 角色动画系统 ============
let mouseX = 0, mouseY = 0;
let isTyping = false;
let showPassword = false;
// 获取所有角色元素
const charPurple = document.getElementById('char-purple');
const charBlack = document.getElementById('char-black');
const charOrange = document.getElementById('char-orange');
const charYellow = document.getElementById('char-yellow');
const purpleEyeLeft = document.getElementById('purple-eye-left');
const purpleEyeRight = document.getElementById('purple-eye-right');
const purplePupilLeft = document.getElementById('purple-pupil-left');
const purplePupilRight = document.getElementById('purple-pupil-right');
const blackEyeLeft = document.getElementById('black-eye-left');
const blackEyeRight = document.getElementById('black-eye-right');
const blackPupilLeft = document.getElementById('black-pupil-left');
const blackPupilRight = document.getElementById('black-pupil-right');
const orangePupilLeft = document.getElementById('orange-pupil-left');
const orangePupilRight = document.getElementById('orange-pupil-right');
const yellowPupilLeft = document.getElementById('yellow-pupil-left');
const yellowPupilRight = document.getElementById('yellow-pupil-right');
// 鼠标跟踪 - 使用requestAnimationFrame节流
let rafId = null;
window.addEventListener('mousemove', (e) => {
mouseX = e.clientX;
mouseY = e.clientY;
if (!rafId) {
rafId = requestAnimationFrame(() => {
updateCharacterPositions();
updatePupilPositions();
rafId = null;
});
}
});
// 计算瞳孔位置
function calculatePupilPosition(element, maxDistance = 5) {
if (!element) return { x: 0, y: 0 };
const rect = element.getBoundingClientRect();
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
const deltaX = mouseX - centerX;
const deltaY = mouseY - centerY;
const distance = Math.min(Math.sqrt(deltaX ** 2 + deltaY ** 2), maxDistance);
const angle = Math.atan2(deltaY, deltaX);
return {
x: Math.cos(angle) * distance,
y: Math.sin(angle) * distance
};
}
// 计算身体倾斜角度和脸部位置
function calculatePosition(element) {
if (!element) return { faceX: 0, faceY: 0, bodySkew: 0 };
const rect = element.getBoundingClientRect();
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 3; // 聚焦头部区域
const deltaX = mouseX - centerX;
const deltaY = mouseY - centerY;
// 脸部移动(用于眼睛位置)
const faceX = Math.max(-15, Math.min(15, deltaX / 20));
const faceY = Math.max(-10, Math.min(10, deltaY / 30));
// 身体倾斜(负值表示向鼠标方向倾斜)
const bodySkew = Math.max(-6, Math.min(6, -deltaX / 120));
return { faceX, faceY, bodySkew };
}
// 更新角色身体位置
function updateCharacterPositions() {
const hasPassword = passwordInput.value.length > 0;
const isPasswordVisible = passwordInput.getAttribute('type') === 'text';
// 紫色角色
const purplePos = calculatePosition(charPurple);
if (hasPassword && isPasswordVisible) {
// 密码可见时 - 站高,不倾斜
charPurple.style.height = '440px';
charPurple.style.transform = `skewX(0deg)`;
} else if (isTyping || (hasPassword && !isPasswordVisible)) {
// 输入邮箱或密码隐藏时 - 站高并倾斜
charPurple.style.height = '440px';
charPurple.style.transform = `skewX(${purplePos.bodySkew - 12}deg) translateX(40px)`;
} else {
// 正常状态 - 根据鼠标晃动
charPurple.style.height = '400px';
charPurple.style.transform = `skewX(${purplePos.bodySkew}deg)`;
}
// 黑色角色
const blackPos = calculatePosition(charBlack);
if (hasPassword && isPasswordVisible) {
// 密码可见时 - 不倾斜
charBlack.style.transform = `skewX(0deg)`;
} else if (isLookingAtEachOther) {
// 互看时 - 倾斜并移动
charBlack.style.transform = `skewX(${blackPos.bodySkew * 1.5 + 10}deg) translateX(20px)`;
} else if (isTyping || (hasPassword && !isPasswordVisible)) {
charBlack.style.transform = `skewX(${blackPos.bodySkew * 1.5}deg)`;
} else {
charBlack.style.transform = `skewX(${blackPos.bodySkew}deg)`;
}
// 橙色和黄色角色
const orangePos = calculatePosition(charOrange);
const yellowPos = calculatePosition(charYellow);
if (hasPassword && isPasswordVisible) {
// 密码可见时 - 不倾斜
charOrange.style.transform = `skewX(0deg)`;
charYellow.style.transform = `skewX(0deg)`;
} else {
charOrange.style.transform = `skewX(${orangePos.bodySkew}deg)`;
charYellow.style.transform = `skewX(${yellowPos.bodySkew}deg)`;
}
}
// 更新瞳孔位置
function updatePupilPositions() {
const hasPassword = passwordInput.value.length > 0;
const isPasswordVisible = passwordInput.getAttribute('type') === 'text';
// 根据状态决定眼睛方向
if (isLookingAtEachOther) {
// 输入邮箱时 - 互相看向对方
purplePupilLeft.style.transform = 'translate(3px, 4px)';
purplePupilRight.style.transform = 'translate(3px, 4px)';
blackPupilLeft.style.transform = 'translate(0px, -4px)';
blackPupilRight.style.transform = 'translate(0px, -4px)';
// 橙色和黄色跟随鼠标
const orangeLeftPos = calculatePupilPosition(orangePupilLeft, 5);
const orangeRightPos = calculatePupilPosition(orangePupilRight, 5);
orangePupilLeft.style.transform = `translate(${orangeLeftPos.x}px, ${orangeLeftPos.y}px)`;
orangePupilRight.style.transform = `translate(${orangeRightPos.x}px, ${orangeRightPos.y}px)`;
const yellowLeftPos = calculatePupilPosition(yellowPupilLeft, 5);
const yellowRightPos = calculatePupilPosition(yellowPupilRight, 5);
yellowPupilLeft.style.transform = `translate(${yellowLeftPos.x}px, ${yellowLeftPos.y}px)`;
yellowPupilRight.style.transform = `translate(${yellowRightPos.x}px, ${yellowRightPos.y}px)`;
} else if (hasPassword && isPasswordVisible) {
// 密码可见时 - 所有小人看向左侧
purplePupilLeft.style.transform = 'translate(-5px, 0px)';
purplePupilRight.style.transform = 'translate(-5px, 0px)';
blackPupilLeft.style.transform = 'translate(-4px, 0px)';
blackPupilRight.style.transform = 'translate(-4px, 0px)';
orangePupilLeft.style.transform = 'translate(-5px, 0px)';
orangePupilRight.style.transform = 'translate(-5px, 0px)';
yellowPupilLeft.style.transform = 'translate(-5px, 0px)';
yellowPupilRight.style.transform = 'translate(-5px, 0px)';
} else if (hasPassword && !isPasswordVisible) {
// 密码隐藏时 - 所有小人看向右下侧
purplePupilLeft.style.transform = 'translate(4px, 4px)';
purplePupilRight.style.transform = 'translate(4px, 4px)';
blackPupilLeft.style.transform = 'translate(4px, 4px)';
blackPupilRight.style.transform = 'translate(4px, 4px)';
orangePupilLeft.style.transform = 'translate(5px, 4px)';
orangePupilRight.style.transform = 'translate(5px, 4px)';
yellowPupilLeft.style.transform = 'translate(5px, 4px)';
yellowPupilRight.style.transform = 'translate(5px, 4px)';
} else {
// 正常状态 - 跟随鼠标
const purpleLeftPos = calculatePupilPosition(purpleEyeLeft, 5);
const purpleRightPos = calculatePupilPosition(purpleEyeRight, 5);
purplePupilLeft.style.transform = `translate(${purpleLeftPos.x}px, ${purpleLeftPos.y}px)`;
purplePupilRight.style.transform = `translate(${purpleRightPos.x}px, ${purpleRightPos.y}px)`;
const blackLeftPos = calculatePupilPosition(blackEyeLeft, 4);
const blackRightPos = calculatePupilPosition(blackEyeRight, 4);
blackPupilLeft.style.transform = `translate(${blackLeftPos.x}px, ${blackLeftPos.y}px)`;
blackPupilRight.style.transform = `translate(${blackRightPos.x}px, ${blackRightPos.y}px)`;
const orangeLeftPos = calculatePupilPosition(orangePupilLeft, 5);
const orangeRightPos = calculatePupilPosition(orangePupilRight, 5);
orangePupilLeft.style.transform = `translate(${orangeLeftPos.x}px, ${orangeLeftPos.y}px)`;
orangePupilRight.style.transform = `translate(${orangeRightPos.x}px, ${orangeRightPos.y}px)`;
const yellowLeftPos = calculatePupilPosition(yellowPupilLeft, 5);
const yellowRightPos = calculatePupilPosition(yellowPupilRight, 5);
yellowPupilLeft.style.transform = `translate(${yellowLeftPos.x}px, ${yellowLeftPos.y}px)`;
yellowPupilRight.style.transform = `translate(${yellowRightPos.x}px, ${yellowRightPos.y}px)`;
}
}
// 眨眼动画
function blinkEye(eyeLeft, eyeRight) {
eyeLeft.style.height = '2px';
eyeRight.style.height = '2px';
setTimeout(() => {
eyeLeft.style.height = '';
eyeRight.style.height = '';
}, 150);
}
// 随机眨眼
function scheduleRandomBlink(eyeLeft, eyeRight) {
const delay = Math.random() * 4000 + 3000;
setTimeout(() => {
if (Math.random() > 0.3) {
blinkEye(eyeLeft, eyeRight);
}
scheduleRandomBlink(eyeLeft, eyeRight);
}, delay);
}
scheduleRandomBlink(purpleEyeLeft, purpleEyeRight);
scheduleRandomBlink(blackEyeLeft, blackEyeRight);
// 添加isLookingAtEachOther状态
let isLookingAtEachOther = false;
// 输入框聚焦 - 角色互看
usernameInput.addEventListener('focus', () => {
isTyping = true;
isLookingAtEachOther = true;
// 短暂互看后恢复
setTimeout(() => {
isLookingAtEachOther = false;
}, 800);
});
usernameInput.addEventListener('blur', () => {
if (!passwordInput.value) {
isTyping = false;
}
isLookingAtEachOther = false;
});
// 密码输入
passwordInput.addEventListener('focus', () => {
isTyping = true;
});
passwordInput.addEventListener('blur', () => {
if (!passwordInput.value) {
isTyping = false;
}
});
passwordInput.addEventListener('input', () => {
updateCharacterPositions();
updatePupilPositions();
});
// 密码可见性切换(默认显示,点击隐藏)
passwordToggle.addEventListener('click', () => {
const type = passwordInput.getAttribute('type');
const eyeOpen = passwordToggle.querySelector('.eye-open');
const eyeClosed = passwordToggle.querySelector('.eye-closed');
if (type === 'text') {
// 点击后隐藏密码
passwordInput.setAttribute('type', 'password');
eyeOpen.style.display = 'none';
eyeClosed.style.display = 'block';
passwordToggle.setAttribute('aria-label', '显示密码');
} else {
// 点击后显示密码
passwordInput.setAttribute('type', 'text');
eyeOpen.style.display = 'block';
eyeClosed.style.display = 'none';
passwordToggle.setAttribute('aria-label', '隐藏密码');
}
updatePupilPositions();
});
</script>
</body>
</html>