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

3 months ago
<!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">&times;</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地址
3 months ago
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>