You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
986 lines
31 KiB
986 lines
31 KiB
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>客户关系管理系统 - 登录</title>
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
|
<style>
|
|
/* 基础样式保持不变 */
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
}
|
|
|
|
body {
|
|
overflow-x: hidden;
|
|
color: #333;
|
|
background: linear-gradient(135deg, #ebc4d2 0%, #c094ed 100%);
|
|
min-height: 100vh;
|
|
}
|
|
|
|
canvas {
|
|
display: block;
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
z-index: -1;
|
|
}
|
|
|
|
.login-container {
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
min-height: 100vh;
|
|
padding: 20px;
|
|
}
|
|
|
|
.login-panel {
|
|
background: rgba(161, 164, 163, 0);
|
|
border-radius: 20px;
|
|
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
|
|
padding: 40px;
|
|
width: 100%;
|
|
max-width: 450px;
|
|
backdrop-filter: blur(3px);
|
|
}
|
|
|
|
.login-title {
|
|
font-size: 32px;
|
|
font-weight: 700;
|
|
text-align: center;
|
|
margin-bottom: 30px;
|
|
color: #333;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 15px;
|
|
}
|
|
|
|
.login-title i {
|
|
color: #e86a92;
|
|
}
|
|
|
|
.form-group {
|
|
margin-bottom: 25px;
|
|
position: relative;
|
|
}
|
|
|
|
.form-label {
|
|
display: block;
|
|
margin-bottom: 8px;
|
|
font-weight: 500;
|
|
color: #555;
|
|
}
|
|
|
|
.form-input {
|
|
width: 100%;
|
|
padding: 15px 20px;
|
|
border-radius: 12px;
|
|
border: 1px solid rgba(0, 0, 0, 0.1);
|
|
background: rgba(255, 255, 255, 0.7);
|
|
font-size: 16px;
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.form-input:focus {
|
|
outline: none;
|
|
border-color: #e86a92;
|
|
box-shadow: 0 0 0 4px rgba(232, 106, 146, 0.2);
|
|
}
|
|
|
|
.login-btn {
|
|
width: 100%;
|
|
padding: 16px;
|
|
border-radius: 12px;
|
|
border: none;
|
|
background: linear-gradient(45deg, #e86a92, #8e44ad);
|
|
color: white;
|
|
font-size: 18px;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
transition: all 0.3s ease;
|
|
margin-top: 10px;
|
|
position: relative;
|
|
}
|
|
|
|
.login-btn:hover:not(:disabled) {
|
|
transform: translateY(-3px);
|
|
box-shadow: 0 8px 20px rgba(232, 106, 146, 0.4);
|
|
}
|
|
|
|
.login-btn:disabled {
|
|
opacity: 0.7;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.login-btn .spinner {
|
|
display: none;
|
|
width: 20px;
|
|
height: 20px;
|
|
border: 3px solid rgba(255, 255, 255, 0.3);
|
|
border-radius: 50%;
|
|
border-top-color: white;
|
|
animation: spin 1s ease-in-out infinite;
|
|
position: absolute;
|
|
left: calc(50% - 10px);
|
|
}
|
|
|
|
@keyframes spin {
|
|
to { transform: rotate(360deg); }
|
|
}
|
|
|
|
.error-message {
|
|
color: #cc2e1d;
|
|
font-size: 14px;
|
|
text-align: center;
|
|
margin-top: 15px;
|
|
padding: 10px;
|
|
border-radius: 8px;
|
|
background: rgba(231, 76, 60, 0.1);
|
|
display: none;
|
|
}
|
|
|
|
.error-message.show {
|
|
display: block;
|
|
}
|
|
|
|
.form-input.error {
|
|
border-color: #e74c3c;
|
|
box-shadow: 0 0 0 3px rgba(231, 76, 60, 0.2);
|
|
}
|
|
|
|
.field-error {
|
|
color: #e74c3c;
|
|
font-size: 12px;
|
|
margin-top: 5px;
|
|
display: none;
|
|
}
|
|
|
|
.field-error.show {
|
|
display: block;
|
|
}
|
|
|
|
.main-app {
|
|
display: none;
|
|
}
|
|
|
|
.main-app.active {
|
|
display: block;
|
|
}
|
|
|
|
.modal-overlay {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
background-color: rgba(0, 0, 0, 0.5);
|
|
backdrop-filter: blur(4px);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
z-index: 1000;
|
|
display: none;
|
|
}
|
|
|
|
.modal {
|
|
background: linear-gradient(135deg, #ebc4d2 0%, #c094ed 100%);
|
|
border-radius: 20px;
|
|
padding: 30px;
|
|
max-width: 400px;
|
|
width: 90%;
|
|
box-shadow: 0 10px 40px rgba(195, 194, 194, 0.5);
|
|
border: 1px solid rgba(255, 255, 255, 0.3);
|
|
backdrop-filter: blur(3px);
|
|
color: #333;
|
|
}
|
|
|
|
.modal-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 20px;
|
|
padding-bottom: 15px;
|
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
|
}
|
|
|
|
.modal-title {
|
|
font-size: 22px;
|
|
text-align: center;
|
|
justify-content: space-between;
|
|
font-weight: 600;
|
|
color: #AB2524;
|
|
text-shadow: 1px 1px 3px rgba(0, 0, 0, 0.2);
|
|
}
|
|
|
|
.modal-close {
|
|
background: none;
|
|
border: none;
|
|
font-size: 24px;
|
|
cursor: pointer;
|
|
color: rgba(66, 239, 224, 0.8);
|
|
transition: color 0.3s;
|
|
}
|
|
|
|
.modal-close:hover {
|
|
color: #2771d1;
|
|
}
|
|
|
|
.modal-body {
|
|
margin-bottom: 25px;
|
|
line-height: 1.6;
|
|
text-align: center;
|
|
font-size: 16px;
|
|
color: #EAF044;
|
|
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
.modal-footer {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
}
|
|
|
|
.modal-btn {
|
|
padding: 10px 20px;
|
|
border-radius: 8px;
|
|
border: none;
|
|
background: linear-gradient(45deg, #e86a92, #8e44ad);
|
|
color: white;
|
|
cursor: pointer;
|
|
font-weight: 500;
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.modal-btn:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
|
|
}
|
|
|
|
.remember-me {
|
|
display: flex;
|
|
align-items: center;
|
|
margin-top: 15px;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.remember-me input {
|
|
margin-right: 8px;
|
|
}
|
|
|
|
.remember-me label {
|
|
font-size: 14px;
|
|
color: #555;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.clear-saved {
|
|
display: block;
|
|
text-align: right;
|
|
font-size: 12px;
|
|
color: #8e44ad;
|
|
cursor: pointer;
|
|
margin-top: 5px;
|
|
text-decoration: underline;
|
|
}
|
|
|
|
.clear-saved:hover {
|
|
color: #e86a92;
|
|
}
|
|
</style>
|
|
</head>
|
|
|
|
<body>
|
|
<canvas id="particleCanvas"></canvas>
|
|
|
|
<div id="login-page" class="login-container">
|
|
<div class="login-panel">
|
|
<div class="login-title">
|
|
<i class="fas fa-users"></i>
|
|
<span>客户关系管理系统</span>
|
|
</div>
|
|
|
|
<form id="login-form" novalidate>
|
|
<div class="form-group">
|
|
<label class="form-label" for="projectName">工位名</label>
|
|
<input type="text" id="projectName" class="form-input" placeholder="请输入工位名" required>
|
|
<div class="field-error" id="projectName-error">请输入工位名</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label class="form-label" for="userName">用户名</label>
|
|
<input type="text" id="userName" class="form-input" placeholder="请输入用户名" required>
|
|
<div class="field-error" id="userName-error">请输入用户名</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label class="form-label" for="password">密码</label>
|
|
<input type="password" id="password" class="form-input" placeholder="请输入密码" required>
|
|
<div class="field-error" id="password-error">请输入密码</div>
|
|
</div>
|
|
|
|
<div class="remember-me">
|
|
<input type="checkbox" id="rememberMe">
|
|
<label for="rememberMe">记住用户名和工位名</label>
|
|
</div>
|
|
|
|
<span class="clear-saved" id="clearSaved">清除已保存的信息</span>
|
|
|
|
<button type="submit" class="login-btn" id="login-button">
|
|
<span class="spinner" id="login-spinner"></span>
|
|
<span id="login-text"><i class="fas fa-sign-in-alt"></i> 登录</span>
|
|
</button>
|
|
|
|
<div class="error-message" id="login-error"></div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="main-app" class="main-app">
|
|
<!-- 主应用内容 -->
|
|
</div>
|
|
|
|
<div class="modal-overlay" id="modal-overlay">
|
|
<div class="modal">
|
|
<div class="modal-header">
|
|
<h3 class="modal-title" id="modal-title">登录失败</h3>
|
|
<button class="modal-close" id="modal-close">×</button>
|
|
</div>
|
|
<div class="modal-body" id="modal-message">
|
|
账号或密码不正确,请重新输入。
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button class="modal-btn" id="modal-confirm">确定</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/three@0.132.2/build/three.min.js"></script>
|
|
|
|
<script>
|
|
// 登录状态管理
|
|
let isLoggedIn = false;//登录状态记录
|
|
let currentUser = null;//存储信息
|
|
|
|
// 检查本地存储中的登录状态
|
|
function checkLoginStatus() {
|
|
const token = localStorage.getItem('authToken');//获取token
|
|
const user = localStorage.getItem('currentUser');//获取user
|
|
if (token && user)
|
|
{
|
|
isLoggedIn = true;
|
|
currentUser = JSON.parse(user);//将user字符串解析为对象
|
|
showMainApp();
|
|
}
|
|
else
|
|
{
|
|
showLoginPage();
|
|
}
|
|
}
|
|
|
|
// 检查是否有保存的用户名和工位名
|
|
function checkSavedLoginInfo()
|
|
{
|
|
const savedLoginInfo = localStorage.getItem('savedLoginInfo');
|
|
if (savedLoginInfo)
|
|
{
|
|
try {
|
|
const { projectName, userName } = JSON.parse(savedLoginInfo);//解析保存的登录信息
|
|
if (projectName)
|
|
{
|
|
document.getElementById('projectName').value = projectName;
|
|
}
|
|
if (userName)
|
|
{
|
|
document.getElementById('userName').value = userName;
|
|
}
|
|
document.getElementById('rememberMe').checked = true;//记住用户名和工位名
|
|
document.getElementById('clearSaved').style.display = 'block';//显示清除已保存的信息按钮
|
|
}
|
|
catch (e)
|
|
{
|
|
console.error('解析保存的登录信息失败', e);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
document.getElementById('clearSaved').style.display = 'none';
|
|
}
|
|
}
|
|
|
|
// 清除已保存的用户名和工位名
|
|
function clearSavedLoginInfo() {
|
|
localStorage.removeItem('savedLoginInfo');
|
|
document.getElementById('projectName').value = '';//清空工位名输入框
|
|
document.getElementById('userName').value = '';
|
|
document.getElementById('rememberMe').checked = false;//取消记住用户名和工位名
|
|
document.getElementById('clearSaved').style.display = 'none';//隐藏清除已保存的信息按钮
|
|
}
|
|
|
|
// 显示登录页面
|
|
function showLoginPage() {
|
|
document.getElementById('login-page').style.display = 'flex';//显示登录页面
|
|
document.getElementById('main-app').classList.remove('active');//隐藏主应用页面
|
|
}
|
|
|
|
// 显示主应用
|
|
function showMainApp() {
|
|
document.getElementById('login-page').style.display = 'none';//隐藏登录页面
|
|
document.getElementById('main-app').classList.add('active');//显示主应用页面
|
|
|
|
// 生成主应用内容
|
|
document.getElementById('main-app').innerHTML = `
|
|
<div style="text-align: center; padding: 50px;">
|
|
<h2>退出成功!</h2>
|
|
<p>欢迎下次回来,${currentUser.userName} (${currentUser.projectName})</p>
|
|
<button id="logout-btn" style="margin-top: 20px; padding: 10px 20px; background: #e86a92; color: white; border: none; border-radius: 8px; cursor: pointer;">
|
|
确定
|
|
</button>
|
|
</div>
|
|
`;
|
|
|
|
// 添加退出登录按钮事件监听
|
|
document.getElementById('logout-btn').addEventListener('click', logout);
|
|
}
|
|
|
|
// 退出登录
|
|
function logout() {
|
|
localStorage.removeItem('authToken');//删除token
|
|
localStorage.removeItem('currentUser');
|
|
isLoggedIn = false;
|
|
currentUser = null;
|
|
showLoginPage();
|
|
}
|
|
|
|
// 显示弹窗
|
|
function showModal(title, message) {
|
|
document.getElementById('modal-title').textContent = title;//设置弹窗标题
|
|
document.getElementById('modal-message').textContent = message;//设置弹窗消息
|
|
document.getElementById('modal-overlay').style.display = 'flex';//显示弹窗
|
|
}
|
|
|
|
// 隐藏弹窗
|
|
function hideModal() {
|
|
document.getElementById('modal-overlay').style.display = 'none';
|
|
}
|
|
|
|
// 设置登录按钮状态
|
|
function setLoginButtonState(isLoading) {
|
|
const loginButton = document.getElementById('login-button');//获取登录按钮
|
|
const loginSpinner = document.getElementById('login-spinner');//获取加载动画
|
|
const loginText = document.getElementById('login-text');//获取登录按钮文字
|
|
|
|
if (isLoading) {
|
|
loginButton.disabled = true;
|
|
loginSpinner.style.display = 'block';
|
|
loginText.style.opacity = '0';
|
|
} else {
|
|
loginButton.disabled = false;
|
|
loginSpinner.style.display = 'none';
|
|
loginText.style.opacity = '1';
|
|
}
|
|
}
|
|
|
|
// 验证单个字段
|
|
function validateField(fieldId) {
|
|
const field = document.getElementById(fieldId);//获取字段
|
|
const errorElement = document.getElementById(`${fieldId}-error`);//获取错误提示元素
|
|
|
|
if (!field.value.trim()) {
|
|
field.classList.add('error');//添加错误样式
|
|
errorElement.classList.add('show');//显示错误提示
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
field.classList.remove('error');
|
|
errorElement.classList.remove('show');
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// 验证所有字段
|
|
function validateForm() {
|
|
let isValid = true;//初始化为true
|
|
if (!validateField('projectName')) isValid = false;
|
|
if (!validateField('userName')) isValid = false;
|
|
if (!validateField('password')) isValid = false;
|
|
return isValid;
|
|
}
|
|
|
|
// 清除字段错误状态
|
|
function clearFieldError(fieldId) {
|
|
const field = document.getElementById(fieldId);
|
|
const errorElement = document.getElementById(`${fieldId}-error`);
|
|
field.classList.remove('error');
|
|
errorElement.classList.remove('show');
|
|
}
|
|
|
|
const API_BASE_URL = 'http://8.137.125.67:8080/DL'; // 服务器API地址
|
|
async function sendLoginRequest(projectName, userName, password) {
|
|
try {
|
|
// 使用URL编码的表单数据
|
|
const params = new URLSearchParams();
|
|
params.append('projectName', projectName);
|
|
params.append('userName', userName);
|
|
params.append('password', password);
|
|
|
|
const response = await fetch(`${API_BASE_URL}/logins`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
},
|
|
body: params.toString()
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
const data = await response.json();
|
|
return data;
|
|
} catch (error) {
|
|
console.error('登录请求失败:', error);
|
|
throw new Error('网络错误,请检查网络连接后重试');
|
|
}
|
|
}
|
|
|
|
// 处理登录表单提交
|
|
document.getElementById('login-form').addEventListener('submit', async function (e) {
|
|
e.preventDefault();
|
|
// 重置错误消息
|
|
document.getElementById('login-error').classList.remove('show');
|
|
|
|
// 验证表单
|
|
if (!validateForm()) {
|
|
return;
|
|
}
|
|
|
|
// 获取表单值
|
|
const projectName = document.getElementById('projectName').value;
|
|
const userName = document.getElementById('userName').value;
|
|
const password = document.getElementById('password').value;
|
|
const rememberMe = document.getElementById('rememberMe').checked;
|
|
|
|
// 显示加载状态
|
|
setLoginButtonState(true);
|
|
|
|
try {
|
|
// 发送登录请求到后端
|
|
const response = await sendLoginRequest(projectName, userName, password);
|
|
|
|
if (response.success) {
|
|
// 登录成功
|
|
if (rememberMe) {
|
|
const loginInfo = { projectName, userName };
|
|
localStorage.setItem('savedLoginInfo', JSON.stringify(loginInfo));
|
|
document.getElementById('clearSaved').style.display = 'block';
|
|
} else {
|
|
localStorage.removeItem('savedLoginInfo');
|
|
document.getElementById('clearSaved').style.display = 'none';
|
|
}
|
|
|
|
// 保存认证信息
|
|
localStorage.setItem('authToken', response.token);
|
|
localStorage.setItem('currentUser', JSON.stringify(response.user));
|
|
|
|
// 根据角色跳转到不同的页面,并传递用户信息
|
|
const root = response.root;
|
|
const userParams = new URLSearchParams({
|
|
managerId:encodeURIComponent(response.user.managerId),
|
|
managercompany:encodeURIComponent(response.user.managercompany),
|
|
managerdepartment:encodeURIComponent(response.user.managerdepartment),
|
|
organization:encodeURIComponent(response.user.organization),
|
|
role: encodeURIComponent(response.user.role),//角色
|
|
userName: encodeURIComponent(response.user.userName),//用户名
|
|
assistant:encodeURIComponent(response.user.assistant),//助理
|
|
token: response.token//认证信息
|
|
}).toString();
|
|
|
|
if (root === 1) {
|
|
// 总经理跳转到
|
|
window.location.href = `mainapp(2).html?${userParams}`;
|
|
}
|
|
else if (root === 2) {
|
|
// 采购员跳转
|
|
// 销售员跳转
|
|
console.log('负责人id',response.user.managerId);
|
|
console.log('所属部门', response.user.managerdepartment);
|
|
console.log('所属公司', response.user.managercompany);
|
|
console.log('所属组织', response.user.organization);
|
|
console.log('角色', response.user.role);
|
|
console.log('用户名', response.user.userName);
|
|
console.log('助理', response.user.assistant);
|
|
window.location.href = `mainapp-supplys.html?${userParams}`;
|
|
}
|
|
else if (root === 3) {
|
|
// 销售员跳转
|
|
console.log('负责人id',response.user.managerId);
|
|
console.log('所属部门', response.user.managerdepartment);
|
|
console.log('所属公司', response.user.managercompany);
|
|
console.log('所属组织', response.user.organization);
|
|
console.log('角色', response.user.role);
|
|
console.log('用户名', response.user.userName);
|
|
console.log('助理', response.user.assistant);
|
|
window.location.href = `mainapp-sells.html?${userParams}`;
|
|
} else {
|
|
// 未知角色,显示错误或跳转到默认页面
|
|
showModal('登录失败', '未知用户角色,请联系管理员');
|
|
}
|
|
} else {
|
|
// 登录失败
|
|
if (response.errorType === 'password') {
|
|
document.getElementById('password').value = '';
|
|
showModal('登录失败', '密码错误,请重新输入');
|
|
} else {
|
|
showModal('登录失败', response.message || '登录失败,请检查您的账号和密码');
|
|
}
|
|
}
|
|
} catch (error) {
|
|
// 异常处理
|
|
showModal('登录错误', error.message || '登录过程中发生错误,请稍后重试');
|
|
console.error('Login error:', error);
|
|
} finally {
|
|
// 隐藏加载状态
|
|
setLoginButtonState(false);
|
|
}
|
|
});
|
|
|
|
// 为输入字段添加事件监听器,失去焦点时验证
|
|
document.getElementById('projectName').addEventListener('blur', function() {
|
|
validateField('projectName');
|
|
});
|
|
|
|
document.getElementById('userName').addEventListener('blur', function() {
|
|
validateField('userName');
|
|
});
|
|
|
|
document.getElementById('password').addEventListener('blur', function() {
|
|
validateField('password');
|
|
});
|
|
|
|
// 为输入字段添加事件监听器,输入时清除错误状态
|
|
document.getElementById('projectName').addEventListener('input', function() {
|
|
clearFieldError('projectName');
|
|
});
|
|
|
|
document.getElementById('userName').addEventListener('input', function() {
|
|
clearFieldError('userName');
|
|
});
|
|
|
|
document.getElementById('password').addEventListener('input', function() {
|
|
clearFieldError('password');
|
|
});
|
|
|
|
// 清除已保存的用户名和工位名
|
|
document.getElementById('clearSaved').addEventListener('click', clearSavedLoginInfo);
|
|
|
|
// 弹窗事件处理
|
|
document.getElementById('modal-close').addEventListener('click', hideModal);
|
|
document.getElementById('modal-confirm').addEventListener('click', hideModal);
|
|
|
|
// 页面加载时检查登录状态
|
|
window.addEventListener('DOMContentLoaded', function () {
|
|
checkLoginStatus();
|
|
checkSavedLoginInfo();
|
|
initParticleBackground(); // 初始化粒子背景
|
|
});
|
|
|
|
// 粒子背景初始化函数
|
|
function initParticleBackground() {
|
|
// 3D粒子背景特效
|
|
const scene = new THREE.Scene(); // 创建场景
|
|
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 2000); // 创建透视相机
|
|
const renderer = new THREE.WebGLRenderer({
|
|
canvas: document.getElementById('particleCanvas'), // 指定渲染画布
|
|
antialias: true, // 开启抗锯齿
|
|
alpha: true // 开启透明度
|
|
});
|
|
renderer.setSize(window.innerWidth, window.innerHeight); // 设置渲染器尺寸
|
|
renderer.setClearColor(0x000000, 0); // 设置清除颜色(透明)
|
|
|
|
camera.position.z = 5; // 设置相机位置
|
|
|
|
// 粒子系统参数
|
|
const particleCount = 120; // 粒子数量
|
|
const minDistance = 2.5; // 最小距离
|
|
const particleSize = 0.2; // 粒子大小
|
|
const particles = new THREE.BufferGeometry(); // 创建粒子几何体
|
|
const positions = new Float32Array(particleCount * 3); // 位置数组
|
|
const colors = new Float32Array(particleCount * 3); // 颜色数组
|
|
const sizes = new Float32Array(particleCount); // 大小数组
|
|
const velocities = new Float32Array(particleCount * 3); // 速度数组
|
|
const opacities = new Float32Array(particleCount); // 透明度数组
|
|
const lifeCycles = new Float32Array(particleCount); // 生命周期数组
|
|
const resetOffsets = new Float32Array(particleCount); // 重置偏移数组
|
|
|
|
// 粒子系统常量
|
|
const maxLife = 5000; // 最大生命周期
|
|
const fadeRange = 600; // 淡入淡出范围
|
|
const baseSpeed = 0.007; // 基础速度
|
|
const frustum = new THREE.Frustum(); // 视锥体
|
|
const distanceThreshold = 200; // 距离阈值
|
|
const distributionRadius = 14; // 分布半径
|
|
|
|
// 颜色调色板
|
|
const colorPalette = [
|
|
new THREE.Color(0xe86a92),
|
|
new THREE.Color(0xd68fb7),
|
|
new THREE.Color(0x8e44ad),
|
|
new THREE.Color(0xe74c3c)
|
|
];
|
|
|
|
// 检查位置是否有效(避免粒子过于接近)
|
|
function isPositionValid(x, y, z, existingParticles, index) {
|
|
for (let i = 0; i < index; i++) {
|
|
const i3 = i * 3;
|
|
const dx = x - existingParticles[i3];
|
|
const dy = y - existingParticles[i3 + 1];
|
|
const dz = z - existingParticles[i3 + 2];
|
|
const distance = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
|
|
if (distance < minDistance) {
|
|
return false; // 距离太近,位置无效
|
|
}
|
|
}
|
|
return true; // 位置有效
|
|
}
|
|
|
|
let particleIndex = 0; // 粒子索引
|
|
const maxAttempts = 100; // 最大尝试次数
|
|
const rings = 6; // 环数
|
|
const particlesPerRing = Math.ceil(particleCount / rings); // 每环粒子数
|
|
|
|
// 初始化粒子位置
|
|
for (let ring = 1; ring <= rings; ring++) {
|
|
const radius = (distributionRadius / (rings + 1)) * ring; // 计算环半径
|
|
|
|
for (let p = 0; p < particlesPerRing && particleIndex < particleCount; p++) {
|
|
let validPosition = false; // 位置有效性标志
|
|
let x, y, z; // 位置坐标
|
|
let attempts = 0; // 尝试次数
|
|
|
|
// 尝试找到有效位置
|
|
while (!validPosition && attempts < maxAttempts) {
|
|
const angle = (p / particlesPerRing) * Math.PI * 2 + (Math.random() * 0.1); // 计算角度
|
|
x = Math.cos(angle) * radius; // 计算x坐标
|
|
y = Math.sin(angle) * radius; // 计算y坐标
|
|
z = -Math.random() * 4; // 计算z坐标
|
|
|
|
validPosition = isPositionValid(x, y, z, positions, particleIndex); // 检查位置有效性
|
|
attempts++; // 增加尝试次数
|
|
}
|
|
|
|
if (validPosition) {
|
|
addParticle(x, y, z); // 添加粒子
|
|
} else {
|
|
addParticle(x, y, z); // 即使位置无效也添加粒子
|
|
}
|
|
}
|
|
}
|
|
|
|
// 添加粒子到系统
|
|
function addParticle(x, y, z) {
|
|
const i3 = particleIndex * 3; // 计算数组索引
|
|
|
|
// 设置位置
|
|
positions[i3] = x;
|
|
positions[i3 + 1] = y;
|
|
positions[i3 + 2] = z;
|
|
|
|
// 计算速度因子
|
|
const distanceFromCenter = Math.sqrt(x * x + y * y) || 1;
|
|
const speedFactor = 0.025 / distanceFromCenter;
|
|
|
|
// 设置速度
|
|
velocities[i3] = x * speedFactor * baseSpeed;
|
|
velocities[i3 + 1] = y * speedFactor * baseSpeed;
|
|
velocities[i3 + 2] = baseSpeed * (1 + Math.random() * 0.2);
|
|
|
|
// 设置大小
|
|
sizes[particleIndex] = particleSize * (0.9 + Math.random() * 0.2);
|
|
|
|
// 设置颜色
|
|
const color = colorPalette[Math.floor(Math.random() * colorPalette.length)];
|
|
colors[i3] = color.r;
|
|
colors[i3 + 1] = color.g;
|
|
colors[i3 + 2] = color.b;
|
|
|
|
// 设置生命周期和透明度
|
|
lifeCycles[particleIndex] = Math.random() * maxLife;
|
|
resetOffsets[particleIndex] = Math.random() * 1000;
|
|
opacities[particleIndex] = 1;
|
|
|
|
particleIndex++; // 增加粒子索引
|
|
}
|
|
|
|
// 设置几何体属性
|
|
particles.setAttribute('position', new THREE.BufferAttribute(positions, 3));
|
|
particles.setAttribute('color', new THREE.BufferAttribute(colors, 3));
|
|
particles.setAttribute('size', new THREE.BufferAttribute(sizes, 1));
|
|
particles.setAttribute('opacity', new THREE.BufferAttribute(opacities, 1));
|
|
|
|
// 创建粒子材质
|
|
const particleMaterial = new THREE.ShaderMaterial({
|
|
uniforms: {
|
|
color: { value: new THREE.Color(0xffffff) } // 颜色统一值
|
|
},
|
|
// 顶点着色器
|
|
vertexShader: `
|
|
attribute float size;
|
|
attribute float opacity;
|
|
varying float vOpacity;
|
|
void main() {
|
|
vOpacity = opacity;
|
|
vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
|
|
gl_PointSize = size * (150.0 / -mvPosition.z);
|
|
gl_Position = projectionMatrix * mvPosition;
|
|
}
|
|
`,
|
|
// 片段着色器
|
|
fragmentShader: `
|
|
uniform vec3 color;
|
|
varying float vOpacity;
|
|
void main() {
|
|
vec2 coord = gl_PointCoord - vec2(0.5);
|
|
float distance = length(coord);
|
|
|
|
float smoothness = 0.2;
|
|
float alpha = vOpacity * (1.0 - smoothstep(0.5 - smoothness, 0.5, distance));
|
|
|
|
alpha *= 1.0 - (distance * 1.5);
|
|
|
|
if (alpha < 0.05) discard;
|
|
|
|
gl_FragColor = vec4(color, alpha);
|
|
}
|
|
`,
|
|
transparent: true, // 启用透明度
|
|
blending: THREE.AdditiveBlending, // 使用相加混合
|
|
alphaTest: 0.05 // alpha测试阈值
|
|
});
|
|
|
|
// 创建粒子系统
|
|
const particleSystem = new THREE.Points(particles, particleMaterial);
|
|
scene.add(particleSystem); // 将粒子系统添加到场景
|
|
|
|
const vector = new THREE.Vector3(); // 创建临时向量
|
|
|
|
// 动画循环函数
|
|
function animate() {
|
|
requestAnimationFrame(animate); // 请求下一帧动画
|
|
|
|
// 更新视锥体
|
|
camera.updateMatrixWorld();
|
|
frustum.setFromProjectionMatrix(
|
|
new THREE.Matrix4().multiplyMatrices(
|
|
camera.projectionMatrix,
|
|
camera.matrixWorldInverse
|
|
)
|
|
);
|
|
|
|
const positions = particles.attributes.position.array; // 获取位置数组
|
|
const opacities = particles.attributes.opacity.array; // 获取透明度数组
|
|
|
|
// 更新每个粒子
|
|
for (let i = 0; i < particleCount; i++) {
|
|
const i3 = i * 3; // 计算数组索引
|
|
|
|
lifeCycles[i]++; // 增加生命周期
|
|
|
|
// 计算有效最大生命周期
|
|
const effectiveMaxLife = maxLife + resetOffsets[i];
|
|
if (lifeCycles[i] < fadeRange) {
|
|
// 淡入阶段
|
|
opacities[i] = lifeCycles[i] / fadeRange;
|
|
} else if (lifeCycles[i] > effectiveMaxLife - fadeRange) {
|
|
// 淡出阶段
|
|
opacities[i] = (effectiveMaxLife - lifeCycles[i]) / fadeRange;
|
|
} else {
|
|
// 完全可见阶段
|
|
opacities[i] = 1;
|
|
}
|
|
|
|
// 更新位置
|
|
positions[i3] += velocities[i3];
|
|
positions[i3 + 1] += velocities[i3 + 1];
|
|
positions[i3 + 2] += velocities[i3 + 2];
|
|
|
|
// 检查粒子是否需要重置
|
|
vector.set(positions[i3], positions[i3 + 1], positions[i3 + 2]);
|
|
const isOutsideFrustum = !frustum.containsPoint(vector); // 是否在视锥体外
|
|
const distanceFromCamera = vector.distanceTo(camera.position); // 与相机的距离
|
|
|
|
// 重置条件:生命周期结束、距离过远或在视锥体外
|
|
if (lifeCycles[i] > effectiveMaxLife ||
|
|
distanceFromCamera > distanceThreshold ||
|
|
isOutsideFrustum) {
|
|
|
|
let x, y, z; // 新位置坐标
|
|
let validPosition = false; // 位置有效性标志
|
|
let attempts = 0; // 尝试次数
|
|
|
|
// 尝试找到有效的新位置
|
|
while (!validPosition && attempts < maxAttempts) {
|
|
const ring = Math.floor(Math.random() * rings) + 1; // 随机选择环
|
|
const radius = (distributionRadius / (rings + 1)) * ring; // 计算环半径
|
|
const angle = Math.random() * Math.PI * 2; // 随机角度
|
|
|
|
x = Math.cos(angle) * radius; // 计算x坐标
|
|
y = Math.sin(angle) * radius; // 计算y坐标
|
|
z = -Math.random() * 4; // 计算z坐标
|
|
|
|
validPosition = true; // 假设位置有效
|
|
// 检查与其他粒子的距离
|
|
for (let j = 0; j < particleCount; j++) {
|
|
if (j === i) continue; // 跳过自身
|
|
const j3 = j * 3;
|
|
const dx = x - positions[j3];
|
|
const dy = y - positions[j3 + 1];
|
|
const dz = z - positions[j3 + 2];
|
|
const distance = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
|
|
// 如果距离太近且粒子处于活跃状态,则位置无效
|
|
if (distance < minDistance && lifeCycles[j] > fadeRange && lifeCycles[j] < effectiveMaxLife - fadeRange) {
|
|
validPosition = false;
|
|
break;
|
|
}
|
|
}
|
|
attempts++; // 增加尝试次数
|
|
}
|
|
|
|
// 设置新位置
|
|
positions[i3] = x;
|
|
positions[i3 + 1] = y;
|
|
positions[i3 + 2] = z;
|
|
lifeCycles[i] = 0; // 重置生命周期
|
|
}
|
|
}
|
|
|
|
// 标记需要更新属性
|
|
particles.attributes.position.needsUpdate = true;
|
|
particles.attributes.opacity.needsUpdate = true;
|
|
|
|
// 缓慢旋转粒子系统
|
|
particleSystem.rotation.x += 0.0000005;
|
|
particleSystem.rotation.y += 0.000001;
|
|
|
|
renderer.render(scene, camera); // 渲染场景
|
|
}
|
|
|
|
// 窗口大小调整事件处理
|
|
window.addEventListener('resize', () => {
|
|
camera.aspect = window.innerWidth / window.innerHeight; // 更新相机宽高比
|
|
camera.updateProjectionMatrix(); // 更新相机投影矩阵
|
|
renderer.setSize(window.innerWidth, window.innerHeight); // 更新渲染器尺寸
|
|
});
|
|
|
|
// 启动动画
|
|
animate();
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|