1、代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>年会抽奖系统</title>
<style>
html,body { height:100%; margin:0; background:#0a0e1a; overflow:hidden; }
#app { width:100%; height:100%; }
.header {
position:fixed; top:0; left:0; right:0; height:100px;
background:linear-gradient(180deg, rgba(10,14,26,0.95) 0%, rgba(10,14,26,0) 100%);
z-index:50; display:flex; align-items:center; justify-content:center;
}
.header-title {
font:700 48px system-ui; color:#fff;
text-shadow:0 0 30px rgba(102,126,234,0.8), 0 4px 20px rgba(0,0,0,0.5);
letter-spacing:8px;
}
.controls {
position:fixed; top:50%; left:50%; transform:translate(-50%,-50%);
z-index:100; text-align:center; pointer-events:none;
width:98vw; max-width:1800px;
display:flex;
flex-direction:column;
align-items:center;
}
.lottery-btn {
pointer-events:all; padding:24px 80px; font:700 32px system-ui;
background:linear-gradient(135deg, #f59e0b 0%, #dc2626 100%);
color:#fff; border:none; border-radius:60px; cursor:pointer;
box-shadow:0 12px 50px rgba(245,158,11,0.6), 0 0 0 0 rgba(245,158,11,0.7);
transition:all 0.3s; position:relative; overflow:hidden;
transform:translateZ(0); backface-visibility:hidden;
letter-spacing:4px;
}
.lottery-btn::before {
content:''; position:absolute; top:50%; left:50%;
width:0; height:0; border-radius:50%;
background:rgba(255,255,255,0.6);
transform:translate(-50%,-50%);
transition:width 0.6s, height 0.6s;
}
.lottery-btn:hover {
transform:translateY(-4px) scale(1.08);
box-shadow:0 18px 60px rgba(245,158,11,0.8), 0 0 40px rgba(245,158,11,0.6);
}
.lottery-btn:active {
transform:translateY(-2px) scale(1.02);
}
.lottery-btn.click-wave::before {
width:350px; height:350px; opacity:0;
transition:width 0.6s, height 0.6s, opacity 0.6s;
}
.lottery-btn.running {
background:linear-gradient(135deg, #ec4899 0%, #8b5cf6 100%);
animation:pulse 1s infinite, rotate-gradient 3s linear infinite;
}
@keyframes pulse {
0%, 100% {
box-shadow:0 12px 50px rgba(236,72,153,0.6), 0 0 30px rgba(236,72,153,0.7);
transform:scale(1);
}
50% {
box-shadow:0 18px 70px rgba(236,72,153,1), 0 0 50px rgba(236,72,153,1);
transform:scale(1.08);
}
}
@keyframes rotate-gradient {
0% { filter:hue-rotate(0deg); }
100% { filter:hue-rotate(360deg); }
}
.result-display {
margin-top:40px; color:#fff;
opacity:0; transition:opacity 0.3s;
max-width:100%; max-height:65vh;
overflow-y:auto; overflow-x:hidden;
padding:20px 80px;
pointer-events:auto;
}
.result-display::-webkit-scrollbar {
width:10px;
}
.result-display::-webkit-scrollbar-track {
background:rgba(15,23,42,0.6); border-radius:5px;
}
.result-display::-webkit-scrollbar-thumb {
background:linear-gradient(180deg, #fbbf24 0%, #f59e0b 100%);
border-radius:5px;
}
.result-display::-webkit-scrollbar-thumb:hover {
background:linear-gradient(180deg, #fcd34d 0%, #fbbf24 100%);
}
.result-display.show { opacity:1; animation:result-fade-in 0.5s ease-out; }
@keyframes result-fade-in {
0% { opacity:0; transform:translateY(-20px); }
100% { opacity:1; transform:translateY(0); }
}
.result-title {
font:700 42px system-ui; text-align:center;
margin-bottom:25px; color:#fbbf24;
text-shadow:0 0 20px rgba(251,191,36,0.8), 0 4px 15px rgba(0,0,0,0.5);
letter-spacing:3px;
}
.result-title .prize-level-badge {
display:inline-block; padding:8px 20px; margin-left:15px;
background:linear-gradient(135deg, #4ade80 0%, #22d3ee 100%);
border-radius:25px; font-size:28px;
box-shadow:0 4px 20px rgba(74,222,128,0.5);
animation:badge-glow 2s infinite;
}
@keyframes badge-glow {
0%, 100% { box-shadow:0 4px 20px rgba(74,222,128,0.5); }
50% { box-shadow:0 6px 30px rgba(74,222,128,0.8); }
}
.winner-label {
padding:30px 40px;
background:linear-gradient(135deg, rgba(251,191,36,0.3) 0%, rgba(245,158,11,0.2) 100%);
border:3px solid rgba(251,191,36,0.7);
border-radius:20px;
box-shadow:0 8px 32px rgba(251,191,36,0.4), inset 0 0 40px rgba(251,191,36,0.15);
position:relative;
color:#fff; text-align:center;
backdrop-filter:blur(15px);
transition:all 0.3s;
min-height:120px;
display:flex;
flex-direction:column;
justify-content:center;
align-items:center;
}
.winner-label.compact {
padding:15px 20px;
min-height:70px;
border-radius:12px;
}
.winner-label.single-winner {
min-height:220px;
padding:50px 80px;
background:linear-gradient(135deg, rgba(251,191,36,0.5) 0%, rgba(245,158,11,0.4) 100%);
border:5px solid #fbbf24;
box-shadow:0 10px 50px rgba(251,191,36,0.7), inset 0 0 60px rgba(251,191,36,0.35),
0 0 80px rgba(251,191,36,0.5);
animation:single-winner-glow 2s infinite;
}
@keyframes single-winner-glow {
0%, 100% {
box-shadow:0 10px 50px rgba(251,191,36,0.7), inset 0 0 60px rgba(251,191,36,0.35),
0 0 80px rgba(251,191,36,0.5);
transform:scale(1);
}
50% {
box-shadow:0 15px 70px rgba(251,191,36,0.9), inset 0 0 80px rgba(251,191,36,0.55),
0 0 120px rgba(251,191,36,0.7);
transform:scale(1.05);
}
}
.winner-label:hover {
background:linear-gradient(135deg, rgba(251,191,36,0.4) 0%, rgba(245,158,11,0.3) 100%);
border-color:#fbbf24;
box-shadow:0 10px 35px rgba(251,191,36,0.6), inset 0 0 50px rgba(251,191,36,0.25);
transform:scale(1.05);
}
.winner-label.single-winner:hover {
transform:scale(1.08);
}
.winner-label .winner-name {
color:#fff; text-shadow:0 3px 15px rgba(0,0,0,0.6), 0 0 30px rgba(251,191,36,0.5);
font-size:36px; font-weight:700; letter-spacing:4px;
}
.winner-label.compact .winner-name {
font-size:24px; letter-spacing:1px;
}
.winner-label.single-winner .winner-name {
font-size:72px; letter-spacing:12px;
text-shadow:0 5px 25px rgba(251,191,36,0.9), 0 0 50px rgba(251,191,36,0.7),
0 0 80px rgba(251,191,36,0.5);
}
.winners-container {
display:grid;
grid-template-columns:repeat(4, 1fr);
gap:20px;
width:100%;
justify-items:center;
}
.winners-container.many-winners {
grid-template-columns:repeat(auto-fill, minmax(100px, 1fr));
gap:12px;
}
.winners-container.single-winner-container {
grid-template-columns:1fr;
max-width:700px;
margin:0 auto;
}
@media (max-width: 1200px) {
.winners-container {
grid-template-columns:repeat(auto-fill, minmax(180px, 1fr));
gap:18px;
}
.winners-container.many-winners {
grid-template-columns:repeat(auto-fill, minmax(140px, 1fr));
gap:12px;
}
}
@media (max-width: 768px) {
.winners-container {
grid-template-columns:repeat(auto-fill, minmax(120px, 1fr));
gap:15px;
}
.winners-container.many-winners {
grid-template-columns:repeat(auto-fill, minmax(90px, 1fr));
gap:10px;
}
.winner-label .winner-name {
font-size:28px;
}
}
.winner-label::before {
content:'🎉'; position:absolute; left:-60px; top:50%;
transform:translateY(-50%); font-size:48px;
animation:celebrate 1s infinite;
}
.winner-label::after {
content:'🎉'; position:absolute; right:-60px; top:50%;
transform:translateY(-50%); font-size:48px;
animation:celebrate 1s infinite 0.5s;
}
@keyframes celebrate {
0%, 100% { transform:translateY(-50%) scale(1); }
50% { transform:translateY(-50%) scale(1.3); }
}
@keyframes winner-glow {
0%, 100% { box-shadow:0 12px 50px rgba(251,191,36,0.8); }
50% { box-shadow:0 18px 70px rgba(251,191,36,1), 0 0 80px rgba(251,191,36,0.8); }
}
.stats {
position:fixed; top:120px; left:30px; color:#e0e7ff;
font:16px/2 system-ui; background:rgba(15,23,42,0.85);
padding:20px 30px; border-radius:16px; backdrop-filter:blur(12px);
border:1px solid rgba(102,126,234,0.3);
box-shadow:0 8px 32px rgba(0,0,0,0.3);
}
.stats div { margin:5px 0; }
.stats span { color:#fbbf24; font-weight:700; font-size:18px; }
.draw-count-setting {
margin-top:15px; padding-top:15px;
border-top:1px solid rgba(102,126,234,0.3);
}
.draw-count-setting label {
display:block; margin-bottom:8px; color:#94a3b8;
font-size:14px;
}
.draw-count-input {
width:100%; padding:8px 12px; font-size:16px;
background:rgba(30,41,59,0.8); border:1px solid rgba(102,126,234,0.4);
border-radius:8px; color:#fbbf24; font-weight:700;
outline:none; transition:all 0.3s;
}
.draw-count-input:focus {
border-color:#667eea; box-shadow:0 0 0 3px rgba(102,126,234,0.2);
}
.draw-count-input:hover {
border-color:#667eea;
}
.prize-level-setting {
margin-top:15px; padding-top:15px;
border-top:1px solid rgba(102,126,234,0.3);
}
.prize-level-select {
width:100%; padding:8px 12px; font-size:16px;
background:rgba(30,41,59,0.8); border:1px solid rgba(102,126,234,0.4);
border-radius:8px; color:#4ade80; font-weight:700;
outline:none; transition:all 0.3s;
cursor:pointer;
}
.prize-level-select:focus {
border-color:#4ade80; box-shadow:0 0 0 3px rgba(74,222,128,0.2);
}
.prize-level-select:hover {
border-color:#4ade80;
}
.prize-level-select option {
background:#1e293b; color:#fff; padding:8px;
}
.history-btn {
width:100%; padding:10px 16px; margin-top:10px;
background:linear-gradient(135deg, rgba(102,126,234,0.6), rgba(74,144,226,0.6));
border:1px solid rgba(102,126,234,0.5);
border-radius:10px; color:#fff; font-size:14px; font-weight:600;
cursor:pointer; transition:all 0.3s;
box-shadow:0 4px 15px rgba(102,126,234,0.3);
position:relative; overflow:hidden;
}
.history-btn:hover {
background:linear-gradient(135deg, rgba(102,126,234,0.8), rgba(74,144,226,0.8));
transform:translateY(-2px);
box-shadow:0 6px 20px rgba(102,126,234,0.5);
}
.history-btn:active {
transform:translateY(0);
}
.history-btn.clear-btn {
background:linear-gradient(135deg, rgba(239,68,68,0.6), rgba(220,38,38,0.6));
border-color:rgba(239,68,68,0.5);
box-shadow:0 4px 15px rgba(239,68,68,0.3);
}
.history-btn.clear-btn:hover {
background:linear-gradient(135deg, rgba(239,68,68,0.8), rgba(220,38,38,0.8));
box-shadow:0 6px 20px rgba(239,68,68,0.5);
}
.history-modal {
position:fixed; top:0; left:0; width:100%; height:100%;
background:rgba(0,0,0,0.8); backdrop-filter:blur(8px);
display:none; justify-content:center; align-items:center;
z-index:10000;
}
.history-modal.show {
display:flex;
}
.history-content {
background:linear-gradient(135deg, rgba(15,23,42,0.95), rgba(30,41,59,0.95));
border:1px solid rgba(102,126,234,0.4);
border-radius:20px; padding:30px;
max-width:800px; width:90%; max-height:80vh;
box-shadow:0 20px 60px rgba(0,0,0,0.5);
position:relative;
}
.history-title {
font-size:24px; font-weight:700; color:#fbbf24;
margin-bottom:20px; text-align:center;
text-shadow:0 0 20px rgba(251,191,36,0.5);
}
.history-log {
background:rgba(10,14,26,0.6);
border:1px solid rgba(102,126,234,0.3);
border-radius:12px; padding:20px;
max-height:50vh; overflow-y:auto;
font-family:monospace; font-size:14px;
line-height:1.8; color:#e0e7ff;
white-space:pre-wrap; word-break:break-all;
}
.history-log::-webkit-scrollbar {
width:8px;
}
.history-log::-webkit-scrollbar-track {
background:rgba(30,41,59,0.5); border-radius:4px;
}
.history-log::-webkit-scrollbar-thumb {
background:rgba(102,126,234,0.5); border-radius:4px;
}
.history-log::-webkit-scrollbar-thumb:hover {
background:rgba(102,126,234,0.8);
}
.history-empty {
text-align:center; color:#94a3b8; padding:40px;
font-size:16px;
}
.history-close {
position:absolute; top:20px; right:20px;
width:36px; height:36px; border-radius:50%;
background:rgba(239,68,68,0.2);
border:1px solid rgba(239,68,68,0.5);
color:#ef4444; font-size:20px; font-weight:700;
cursor:pointer; transition:all 0.3s;
display:flex; align-items:center; justify-content:center;
}
.history-close:hover {
background:rgba(239,68,68,0.4);
transform:rotate(90deg);
}
.back-btn {
position:fixed; top:120px; right:30px;
padding:15px 30px; font-size:16px; font-weight:600;
background:linear-gradient(135deg, rgba(74,222,128,0.6), rgba(34,197,94,0.6));
border:1px solid rgba(74,222,128,0.5);
border-radius:12px; color:#fff;
cursor:pointer; transition:all 0.3s;
box-shadow:0 8px 32px rgba(74,222,128,0.3);
backdrop-filter:blur(12px);
display:none;
z-index:100;
}
.back-btn.show {
display:block;
}
.back-btn:hover {
background:linear-gradient(135deg, rgba(74,222,128,0.8), rgba(34,197,94,0.8));
transform:translateY(-3px);
box-shadow:0 12px 40px rgba(74,222,128,0.5);
}
.back-btn:active {
transform:translateY(-1px);
}
.hint {
position:fixed; left:50%; bottom:40px; transform:translateX(-50%);
color:#9ca3af; font:14px/1.6 system-ui; text-align:center;
background:rgba(15,23,42,0.7); padding:12px 30px;
border-radius:25px; backdrop-filter:blur(8px);
}
.firework {
position:fixed; width:8px; height:8px; border-radius:50%;
pointer-events:none; z-index:999;
}
.click-particle {
position:fixed; width:6px; height:6px; border-radius:50%;
pointer-events:none; z-index:1000;
}
.shockwave {
position:fixed; border:3px solid rgba(245,158,11,0.9);
border-radius:50%; pointer-events:none; z-index:999;
animation:shockwave-anim 0.8s ease-out forwards;
}
@keyframes shockwave-anim {
0% { width:0; height:0; opacity:1; }
100% { width:500px; height:500px; opacity:0; }
}
.energy-ring {
position:absolute; top:50%; left:50%;
width:120%; height:120%; border-radius:50%;
border:3px solid rgba(251,191,36,0.7);
transform:translate(-50%,-50%) scale(0);
opacity:0; pointer-events:none;
}
.lottery-btn.running .energy-ring {
animation:energy-pulse 1s ease-out infinite;
}
@keyframes energy-pulse {
0% { transform:translate(-50%,-50%) scale(0.8); opacity:0; }
50% { opacity:0.8; }
100% { transform:translate(-50%,-50%) scale(1.8); opacity:0; }
}
.confetti {
position:fixed; width:10px; height:10px;
pointer-events:none; z-index:1001;
}
</style>
</head>
<body>
<div id="app"></div>
<div class="header">
<div class="header-title">🎊 年会幸运抽奖 🎊</div>
</div>
<div class="controls">
<button class="lottery-btn" id="lotteryBtn">
开始抽奖
<div class="energy-ring"></div>
<div class="energy-ring" style="animation-delay:0.3s;"></div>
<div class="energy-ring" style="animation-delay:0.6s;"></div>
</button>
<div class="result-display" id="resultDisplay"></div>
</div>
<div class="stats">
<div style="font-size:18px; margin-bottom:8px; color:#fbbf24; font-weight:700;">📊 抽奖统计</div>
<div>总人数:<span id="totalCount">0</span></div>
<div>已中奖:<span id="drawnCount">0</span></div>
<div>剩余人数:<span id="remainCount">0</span></div>
<div class="draw-count-setting">
<label for="drawCountInput">🎯 每次抽取人数</label>
<input type="number" id="drawCountInput" class="draw-count-input"
value="1" min="1" max="50" />
</div>
<div class="prize-level-setting">
<label for="prizeLevelSelect">🏆 奖项等级</label>
<select id="prizeLevelSelect" class="prize-level-select">
<option value="">加载中...</option>
</select>
</div>
<div class="history-actions" style="margin-top:20px; padding-top:20px; border-top:1px solid rgba(102,126,234,0.3);">
<button class="history-btn" id="viewHistoryBtn">
📝 查看记录
</button>
<button class="history-btn clear-btn" id="clearHistoryBtn">
🗑️ 清除记录
</button>
</div>
</div>
<div class="hint">点击按钮开始抽奖 · 拖拽旋转 · 滚轮缩放</div>
<!-- 返回按钮 -->
<button class="back-btn" id="backBtn">↺ 返回</button>
<!-- 历史记录弹窗 -->
<div class="history-modal" id="historyModal">
<div class="history-content">
<div class="history-close" id="closeHistoryBtn">×</div>
<div class="history-title">📝 抽奖历史记录</div>
<div class="history-log" id="historyLog"></div>
</div>
</div>
<script type="module">
import * as THREE from '../component/threejs/build/three.module.min.js';
const container = document.getElementById('app');
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(container.clientWidth, container.clientHeight);
renderer.outputColorSpace = THREE.SRGBColorSpace;
container.appendChild(renderer.domElement);
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x0a0e1a);
scene.fog = new THREE.Fog(0x0a0e1a, 100, 1000);
// 添加星空背景
const starGeometry = new THREE.BufferGeometry();
const starCount = 1500;
const starPositions = new Float32Array(starCount * 3);
for (let i = 0; i < starCount; i++) {
starPositions[i * 3] = (Math.random() - 0.5) * 2000;
starPositions[i * 3 + 1] = (Math.random() - 0.5) * 2000;
starPositions[i * 3 + 2] = (Math.random() - 0.5) * 2000;
}
starGeometry.setAttribute('position', new THREE.BufferAttribute(starPositions, 3));
const starMaterial = new THREE.PointsMaterial({
size: 1.5,
color: 0xffffff,
transparent: true,
opacity: 0.8
});
const stars = new THREE.Points(starGeometry, starMaterial);
scene.add(stars);
const camera = new THREE.PerspectiveCamera(60, container.clientWidth / container.clientHeight, 1, 2000);
camera.position.set(0, 0, 400);
const ambient = new THREE.AmbientLight(0xffffff, 0.25);
scene.add(ambient);
const dir = new THREE.DirectionalLight(0xffffff, 0.8);
dir.position.set(200, 300, 400);
scene.add(dir);
// 添加粒子系统背景
const particleGeometry = new THREE.BufferGeometry();
const particleCount = 800;
const positions = new Float32Array(particleCount * 3);
const colors = new Float32Array(particleCount * 3);
for (let i = 0; i < particleCount; i++) {
positions[i * 3] = (Math.random() - 0.5) * 1000;
positions[i * 3 + 1] = (Math.random() - 0.5) * 1000;
positions[i * 3 + 2] = (Math.random() - 0.5) * 1000;
const color = new THREE.Color();
color.setHSL(0.6 + Math.random() * 0.2, 0.8, 0.5);
colors[i * 3] = color.r;
colors[i * 3 + 1] = color.g;
colors[i * 3 + 2] = color.b;
}
particleGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
particleGeometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
const particleMaterial = new THREE.PointsMaterial({
size: 2,
vertexColors: true,
transparent: true,
opacity: 0.6,
blending: THREE.AdditiveBlending
});
const particleSystem = new THREE.Points(particleGeometry, particleMaterial);
scene.add(particleSystem);
// 添加科技线框
const lineGeometry = new THREE.BufferGeometry();
const linePositions = [];
for (let i = 0; i < 50; i++) {
const angle = (i / 50) * Math.PI * 2;
const r = 250;
linePositions.push(Math.cos(angle) * r, 0, Math.sin(angle) * r);
}
lineGeometry.setAttribute('position', new THREE.Float32BufferAttribute(linePositions, 3));
const lineMaterial = new THREE.LineBasicMaterial({
color: 0x667eea,
transparent: true,
opacity: 0.3
});
const techCircle = new THREE.LineLoop(lineGeometry, lineMaterial);
scene.add(techCircle);
const techCircle2 = techCircle.clone();
techCircle2.rotation.x = Math.PI / 2;
scene.add(techCircle2);
const techCircle3 = techCircle.clone();
techCircle3.rotation.z = Math.PI / 2;
scene.add(techCircle3);
// 添加全息投影效果网格
const gridHelper = new THREE.GridHelper(800, 40, 0x00ffff, 0x004488);
gridHelper.position.y = -200;
gridHelper.material.transparent = true;
gridHelper.material.opacity = 0.15;
scene.add(gridHelper);
// 添加数据流线条
const dataStreams = [];
for (let i = 0; i < 20; i++) {
const curve = new THREE.CatmullRomCurve3([
new THREE.Vector3((Math.random() - 0.5) * 500, -300, (Math.random() - 0.5) * 500),
new THREE.Vector3((Math.random() - 0.5) * 300, 0, (Math.random() - 0.5) * 300),
new THREE.Vector3((Math.random() - 0.5) * 500, 300, (Math.random() - 0.5) * 500)
]);
const points = curve.getPoints(50);
const streamGeometry = new THREE.BufferGeometry().setFromPoints(points);
const streamMaterial = new THREE.LineBasicMaterial({
color: 0x00ffff,
transparent: true,
opacity: 0
});
const stream = new THREE.Line(streamGeometry, streamMaterial);
scene.add(stream);
dataStreams.push({ mesh: stream, material: streamMaterial, progress: Math.random() });
}
// 添加扫描线效果
const scanLineGeometry = new THREE.PlaneGeometry(600, 2);
const scanLineMaterial = new THREE.MeshBasicMaterial({
color: 0x00ffff,
transparent: true,
opacity: 0,
side: THREE.DoubleSide,
blending: THREE.AdditiveBlending
});
const scanLine = new THREE.Mesh(scanLineGeometry, scanLineMaterial);
scene.add(scanLine);
// 添加能量球效果
const energySphereGeometry = new THREE.SphereGeometry(280, 32, 32);
const energySphereMaterial = new THREE.MeshBasicMaterial({
color: 0x667eea,
transparent: true,
opacity: 0,
wireframe: true,
blending: THREE.AdditiveBlending
});
const energySphere = new THREE.Mesh(energySphereGeometry, energySphereMaterial);
scene.add(energySphere);
// 添加六边形网格
const hexGeometry = new THREE.TorusGeometry(300, 2, 6, 6);
const hexMaterial = new THREE.MeshBasicMaterial({
color: 0x4ade80,
transparent: true,
opacity: 0,
wireframe: true
});
const hexGrid = new THREE.Mesh(hexGeometry, hexMaterial);
hexGrid.rotation.x = Math.PI / 2;
scene.add(hexGrid);
// 添加光束效果
const beams = [];
for (let i = 0; i < 8; i++) {
const beamGeometry = new THREE.CylinderGeometry(1, 1, 500, 8);
const beamMaterial = new THREE.MeshBasicMaterial({
color: 0x667eea,
transparent: true,
opacity: 0,
blending: THREE.AdditiveBlending
});
const beam = new THREE.Mesh(beamGeometry, beamMaterial);
const angle = (i / 8) * Math.PI * 2;
beam.position.set(Math.cos(angle) * 250, 0, Math.sin(angle) * 250);
beam.rotation.z = Math.PI / 2;
beam.rotation.y = angle;
scene.add(beam);
beams.push(beam);
}
const names = [];
const drawnNames = new Set();
let isLotteryRunning = false;
let currentWinner = null;
const radius = 180;
const labels = new THREE.Group();
scene.add(labels);
// 奖项配置数据
let prizeConfig = [];
// 加载奖项配置
fetch('./server.conf')
.then(response => response.text())
.then(data => {
// 解析配置文件
const lines = data.split('\n');
const prizes = [];
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
if (line && !line.startsWith('level:')) {
// 匹配格式:奖项名称: 人数
const match = line.match(/^([^:]+):\s*(\d+)/);
if (match) {
const name = match[1].trim();
const count = parseInt(match[2]);
prizes.push({ name, count });
}
}
}
prizeConfig = prizes;
// 填充下拉框
const prizeLevelSelect = document.getElementById('prizeLevelSelect');
prizeLevelSelect.innerHTML = '';
if (prizes.length > 0) {
prizes.forEach((prize, index) => {
const option = document.createElement('option');
option.value = index;
option.textContent = `${prize.name} (${prize.count}人)`;
prizeLevelSelect.appendChild(option);
});
// 设置默认抽取人数
updateDrawCount();
} else {
prizeLevelSelect.innerHTML = '<option value="">暂无奖项配置</option>';
}
})
.catch(error => {
console.error('加载奖项配置失败:', error);
const prizeLevelSelect = document.getElementById('prizeLevelSelect');
prizeLevelSelect.innerHTML = '<option value="">加载失败</option>';
});
// 当选择奖项时,自动更新抽取人数
function updateDrawCount() {
const prizeLevelSelect = document.getElementById('prizeLevelSelect');
const drawCountInput = document.getElementById('drawCountInput');
const selectedIndex = parseInt(prizeLevelSelect.value);
if (!isNaN(selectedIndex) && prizeConfig[selectedIndex]) {
drawCountInput.value = prizeConfig[selectedIndex].count;
}
}
// 监听奖项选择变化
setTimeout(() => {
const prizeLevelSelect = document.getElementById('prizeLevelSelect');
if (prizeLevelSelect) {
prizeLevelSelect.addEventListener('change', updateDrawCount);
}
}, 100);
// 从文件读取用户名单
fetch('./user.conf')
.then(response => response.text())
.then(data => {
const users = data.split('\n').filter(line => line.trim() !== '');
names.push(...users);
console.log(`加载了 ${names.length} 个用户`);
// 更新统计信息
document.getElementById('totalCount').textContent = names.length;
document.getElementById('remainCount').textContent = names.length;
// 创建名字标签
for (let i = 0; i < names.length; i++) {
const sp = makeLabel(names[i]);
const r = radius * (0.7 + Math.random() * 0.3);
const phi = Math.random() * Math.PI * 2;
const cost = Math.random() * 2 - 1;
const sint = Math.sqrt(1 - cost * cost);
sp.position.set(r * Math.cos(phi) * sint, r * cost, r * Math.sin(phi) * sint);
sp.rotation.y = Math.random() * Math.PI * 2;
sp.userData.phi = phi;
sp.userData.r = r;
sp.userData.speed = 0.3 + Math.random() * 0.7;
sp.userData.bob = 0.3 + Math.random() * 1.0;
sp.userData.name = names[i];
sp.userData.index = i;
sp.userData.isDrawn = false;
labels.add(sp);
}
})
.catch(error => {
console.error('加载用户文件失败:', error);
// 如果加载失败,使用默认数据
for (let i = 1; i <= 500; i++) {
names.push('参与者' + i);
}
document.getElementById('totalCount').textContent = names.length;
document.getElementById('remainCount').textContent = names.length;
for (let i = 0; i < names.length; i++) {
const sp = makeLabel(names[i]);
const r = radius * (0.7 + Math.random() * 0.3);
const phi = Math.random() * Math.PI * 2;
const cost = Math.random() * 2 - 1;
const sint = Math.sqrt(1 - cost * cost);
sp.position.set(r * Math.cos(phi) * sint, r * cost, r * Math.sin(phi) * sint);
sp.rotation.y = Math.random() * Math.PI * 2;
sp.userData.phi = phi;
sp.userData.r = r;
sp.userData.speed = 0.3 + Math.random() * 0.7;
sp.userData.bob = 0.3 + Math.random() * 1.0;
sp.userData.name = names[i];
sp.userData.index = i;
sp.userData.isDrawn = false;
labels.add(sp);
}
});
function makeLabel(text) {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const padX = 50, padY = 35;
ctx.font = '700 64px system-ui, -apple-system, Segoe UI, Microsoft YaHei';
const metrics = ctx.measureText(text);
const w = Math.ceil(metrics.width) + padX * 2;
const h = 64 + padY * 2;
const dpr = Math.min(3, window.devicePixelRatio || 1);
canvas.width = Math.ceil(w * dpr);
canvas.height = Math.ceil(h * dpr);
ctx.scale(dpr, dpr);
ctx.font = '700 64px system-ui, -apple-system, Segoe UI, Microsoft YaHei';
const grd = ctx.createLinearGradient(0, 0, 0, h);
grd.addColorStop(0, '#2d3748');
grd.addColorStop(1, '#1a202c');
ctx.fillStyle = grd;
const r = 16;
ctx.beginPath();
ctx.moveTo(r, 0);
ctx.arcTo(w, 0, w, h, r);
ctx.arcTo(w, h, 0, h, r);
ctx.arcTo(0, h, 0, 0, r);
ctx.arcTo(0, 0, w, 0, r);
ctx.closePath();
ctx.fill();
ctx.shadowColor = 'rgba(0, 0, 0, 0.5)';
ctx.shadowBlur = 4;
ctx.shadowOffsetX = 0;
ctx.shadowOffsetY = 2;
ctx.strokeStyle = 'rgba(255, 255, 255, 0.15)';
ctx.lineWidth = 2;
ctx.stroke();
ctx.shadowColor = 'transparent';
ctx.fillStyle = '#f0f4f8';
ctx.textBaseline = 'middle';
ctx.fillText(text, padX, h / 2);
const tex = new THREE.CanvasTexture(canvas);
tex.anisotropy = 16;
tex.minFilter = THREE.LinearFilter;
tex.magFilter = THREE.LinearFilter;
const mat = new THREE.SpriteMaterial({ map: tex, transparent: true, depthWrite: false, color: 0xffffff });
const sp = new THREE.Sprite(mat);
const scale = 0.8;
sp.scale.set(w * scale / 32, h * scale / 32, 1);
sp.userData = { baseColor: 0xffffff };
return sp;
}
const raycaster = new THREE.Raycaster();
const pointer = new THREE.Vector2();
let highlighted = null;
function setPointer(event) {
const rect = renderer.domElement.getBoundingClientRect();
const x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
const y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
pointer.set(x, y);
}
function highlight(obj, isWinner = false) {
if (highlighted && highlighted.material && !currentWinners.includes(highlighted)) {
const color = highlighted.userData.isDrawn ? 0x666666 : highlighted.userData.baseColor;
highlighted.material.color.set(color);
highlighted.scale.set(highlighted.userData.baseScale.x, highlighted.userData.baseScale.y, 1);
}
highlighted = obj || null;
if (highlighted && highlighted.material) {
const color = isWinner ? 0xffd700 : (isLotteryRunning ? 0xff6b9d : 0x4ade80);
highlighted.material.color.set(color);
if (!highlighted.userData.baseScale) {
highlighted.userData.baseScale = { x: highlighted.scale.x, y: highlighted.scale.y };
}
const scale = isWinner ? 1.8 : 1.3;
highlighted.scale.set(highlighted.userData.baseScale.x * scale, highlighted.userData.baseScale.y * scale, 1);
}
}
function setDrawn(sprite) {
sprite.userData.isDrawn = true;
sprite.material.color.set(0x666666);
sprite.material.opacity = 0.4;
}
renderer.domElement.addEventListener('mousemove', (e) => {
setPointer(e);
});
renderer.domElement.addEventListener('wheel', (e) => {
e.preventDefault();
camera.position.z += e.deltaY * 0.2;
camera.position.z = THREE.MathUtils.clamp(camera.position.z, 120, 800);
}, { passive: false });
let drag = false, lastX = 0;
renderer.domElement.addEventListener('pointerdown', (e) => { drag = true; lastX = e.clientX; });
renderer.domElement.addEventListener('pointerup', () => { drag = false; });
renderer.domElement.addEventListener('pointerleave', () => { drag = false; });
renderer.domElement.addEventListener('pointermove', (e) => {
if (!drag) return;
const dx = e.clientX - lastX; lastX = e.clientX;
labels.rotation.y += dx * 0.002;
});
let t = 0;
let lotteryTimer = null;
let randomHighlightCount = 0;
let techModeActive = false;
let scanLineY = -300;
let currentWinners = [];
const lotteryBtn = document.getElementById('lotteryBtn');
const resultDisplay = document.getElementById('resultDisplay');
const totalCountEl = document.getElementById('totalCount');
const drawnCountEl = document.getElementById('drawnCount');
const remainCountEl = document.getElementById('remainCount');
function updateStats() {
drawnCountEl.textContent = drawnNames.size;
remainCountEl.textContent = names.length - drawnNames.size;
}
// 保存抽奖记录到localStorage
function saveLotteryHistory(prizeLevel, winners) {
const now = new Date();
const timestamp = now.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false
});
// 构建记录内容
let logContent = '';
winners.forEach(winner => {
// 格式:[时间] 奖项等级 - 姓名
logContent += `[${timestamp}] ${prizeLevel} - ${winner.userData.name}\n`;
});
// 读取现有记录(从localStorage)
let existingContent = localStorage.getItem('lotteryHistoryLog') || '';
// 追加新记录
const updatedContent = existingContent + logContent;
// 保存到localStorage
localStorage.setItem('lotteryHistoryLog', updatedContent);
console.log(`✅ 已保存抽奖记录到localStorage: ${prizeLevel} - ${winners.length}人`);
console.log('💾 累计记录行数:', updatedContent.split('\n').filter(line => line.trim()).length);
}
function createFirework(x, y) {
const colors = ['#fbbf24', '#f59e0b', '#dc2626', '#ec4899', '#8b5cf6', '#ffd700'];
for (let i = 0; i < 40; i++) {
const fw = document.createElement('div');
fw.className = 'firework';
fw.style.left = x + 'px';
fw.style.top = y + 'px';
fw.style.background = colors[Math.floor(Math.random() * colors.length)];
fw.style.boxShadow = `0 0 10px ${colors[Math.floor(Math.random() * colors.length)]}`;
document.body.appendChild(fw);
const angle = (Math.PI * 2 * i) / 40;
const velocity = 3 + Math.random() * 4;
const vx = Math.cos(angle) * velocity;
const vy = Math.sin(angle) * velocity;
let px = x, py = y, life = 80;
function animateFw() {
px += vx; py += vy + life * 0.04;
life--;
fw.style.left = px + 'px';
fw.style.top = py + 'px';
fw.style.opacity = life / 80;
if (life > 0) requestAnimationFrame(animateFw);
else fw.remove();
}
animateFw();
}
}
function createConfetti() {
const colors = ['#fbbf24', '#f59e0b', '#dc2626', '#ec4899', '#8b5cf6', '#3b82f6'];
for (let i = 0; i < 100; i++) {
setTimeout(() => {
const confetti = document.createElement('div');
confetti.className = 'confetti';
confetti.style.left = Math.random() * window.innerWidth + 'px';
confetti.style.top = '-20px';
confetti.style.background = colors[Math.floor(Math.random() * colors.length)];
confetti.style.transform = `rotate(${Math.random() * 360}deg)`;
document.body.appendChild(confetti);
let py = -20, px = parseFloat(confetti.style.left);
const vx = (Math.random() - 0.5) * 3;
const rotSpeed = (Math.random() - 0.5) * 20;
let rotation = 0;
function animateConfetti() {
py += 3;
px += vx;
rotation += rotSpeed;
confetti.style.top = py + 'px';
confetti.style.left = px + 'px';
confetti.style.transform = `rotate(${rotation}deg)`;
if (py < window.innerHeight) requestAnimationFrame(animateConfetti);
else confetti.remove();
}
animateConfetti();
}, i * 20);
}
}
function createClickEffect(x, y) {
// 冲击波效果
const shockwave = document.createElement('div');
shockwave.className = 'shockwave';
shockwave.style.left = (x - 250) + 'px';
shockwave.style.top = (y - 250) + 'px';
document.body.appendChild(shockwave);
setTimeout(() => shockwave.remove(), 800);
// 粒子爆炸效果
const colors = ['#fbbf24', '#f59e0b', '#dc2626', '#ec4899', '#8b5cf6', '#3b82f6'];
for (let i = 0; i < 25; i++) {
const particle = document.createElement('div');
particle.className = 'click-particle';
particle.style.left = x + 'px';
particle.style.top = y + 'px';
particle.style.background = colors[Math.floor(Math.random() * colors.length)];
particle.style.boxShadow = `0 0 8px ${colors[Math.floor(Math.random() * colors.length)]}`;
document.body.appendChild(particle);
const angle = (Math.PI * 2 * i) / 25 + (Math.random() - 0.5) * 0.5;
const velocity = 4 + Math.random() * 5;
const vx = Math.cos(angle) * velocity;
const vy = Math.sin(angle) * velocity;
let px = x, py = y, life = 50 + Math.random() * 30;
const maxLife = life;
function animateParticle() {
px += vx;
py += vy + (1 - life / maxLife) * 2.5;
life--;
particle.style.left = px + 'px';
particle.style.top = py + 'px';
particle.style.opacity = life / maxLife;
particle.style.transform = `scale(${life / maxLife})`;
if (life > 0) requestAnimationFrame(animateParticle);
else particle.remove();
}
animateParticle();
}
// 按钮波纹效果
lotteryBtn.classList.add('click-wave');
setTimeout(() => lotteryBtn.classList.remove('click-wave'), 600);
}
function startLottery() {
if (isLotteryRunning) return;
const available = labels.children.filter(sp => !sp.userData.isDrawn);
if (available.length === 0) {
alert('所有人都已被抽取!');
return;
}
// 获取奖项等级
const prizeLevelSelect = document.getElementById('prizeLevelSelect');
const selectedIndex = parseInt(prizeLevelSelect.value);
if (isNaN(selectedIndex) || !prizeConfig[selectedIndex]) {
alert('请选择奖项等级!');
return;
}
const prizeLevel = prizeConfig[selectedIndex].name;
// 优先使用输入框的人数,如果输入框为空或无效,则使用配置的人数
const drawCountInput = document.getElementById('drawCountInput');
let drawCount = parseInt(drawCountInput.value);
// 如果输入框的值无效,使用配置文件中的人数
if (isNaN(drawCount) || drawCount < 1) {
drawCount = prizeConfig[selectedIndex].count;
drawCountInput.value = drawCount;
}
// 限制抽取人数在合理范围内
drawCount = Math.max(1, Math.min(drawCount, available.length));
if (drawCount > available.length) {
alert(`剩余人数不足!需要${drawCount}人,剩余${available.length}人`);
return;
}
isLotteryRunning = true;
techModeActive = true;
lotteryBtn.innerHTML = '抽奖中...<div class="energy-ring"></div><div class="energy-ring" style="animation-delay:0.3s;"></div><div class="energy-ring" style="animation-delay:0.6s;"></div>';
lotteryBtn.classList.add('running');
resultDisplay.classList.remove('show');
resultDisplay.innerHTML = '';
// 场景变暗效果
scene.background.set(0x050508);
scene.fog.color.set(0x050508);
ambient.intensity = 0.1;
dir.intensity = 0.3;
// 粒子系统激活
particleMaterial.opacity = 1;
particleMaterial.size = 3;
lineMaterial.opacity = 0.8;
lineMaterial.color.set(0xfbbf24);
// 激活所有科技效果
scanLineMaterial.opacity = 0.6;
energySphereMaterial.opacity = 0.15;
hexMaterial.opacity = 0.4;
beams.forEach(beam => beam.material.opacity = 0.3);
gridHelper.material.opacity = 0.3;
dataStreams.forEach(stream => stream.material.opacity = 0.4);
scanLineY = -300;
// 播放抽奖音效提示
console.log(`🎵 抽奖开始...本次抽取${drawCount}人`);
// 清除上次中奖者高亮
if (currentWinners.length > 0) {
currentWinners.forEach(winner => setDrawn(winner));
currentWinners = [];
}
randomHighlightCount = 0;
const duration = 3000;
const startTime = Date.now();
lotteryTimer = setInterval(() => {
const elapsed = Date.now() - startTime;
const progress = elapsed / duration;
if (progress >= 1) {
clearInterval(lotteryTimer);
// 随机抽取多个获奖者
const winners = [];
const selectedIndices = new Set();
while (winners.length < drawCount && winners.length < available.length) {
const randomIndex = Math.floor(Math.random() * available.length);
if (!selectedIndices.has(randomIndex)) {
selectedIndices.add(randomIndex);
winners.push(available[randomIndex]);
}
}
currentWinners = winners;
// 高亮所有获奖者
winners.forEach(winner => {
drawnNames.add(winner.userData.name);
highlight(winner, true);
});
// 显示结果
const totalWinners = winners.length;
const isSingleWinner = totalWinners === 1;
const isManyWinners = totalWinners > 10;
let resultHTML = `<div class="result-title">🎉 中奖名单(${totalWinners}人)`;
resultHTML += `<span class="prize-level-badge">${prizeLevel}</span></div>`;
let containerClass = 'winners-container';
if (isSingleWinner) {
containerClass += ' single-winner-container';
} else if (isManyWinners) {
containerClass += ' many-winners';
}
resultHTML += `<div class="${containerClass}">`;
winners.forEach((winner, index) => {
let labelClass = 'winner-label';
if (isSingleWinner) {
labelClass += ' single-winner';
} else if (isManyWinners) {
labelClass += ' compact';
}
resultHTML += `<div class="${labelClass}" style="animation-delay:${index * 0.05}s">`;
resultHTML += `<div class="winner-name">${winner.userData.name}</div>`;
resultHTML += `</div>`;
});
resultHTML += '</div>';
resultDisplay.innerHTML = resultHTML;
resultDisplay.classList.add('show');
lotteryBtn.innerHTML = '再次抽奖<div class="energy-ring"></div><div class="energy-ring" style="animation-delay:0.3s;"></div><div class="energy-ring" style="animation-delay:0.6s;"></div>';
lotteryBtn.classList.remove('running');
isLotteryRunning = false;
techModeActive = false;
updateStats();
// 显示返回按钮
backBtn.classList.add('show');
// 保存抽奖记录到本地文件
saveLotteryHistory(prizeLevel, winners);
// 恢复场景亮度
scene.background.set(0x0a0e1a);
scene.fog.color.set(0x0a0e1a);
ambient.intensity = 0.25;
dir.intensity = 0.8;
// 粒子系统恢复
particleMaterial.opacity = 0.6;
particleMaterial.size = 2;
lineMaterial.opacity = 0.3;
lineMaterial.color.set(0x667eea);
// 关闭科技效果
scanLineMaterial.opacity = 0;
energySphereMaterial.opacity = 0;
hexMaterial.opacity = 0;
beams.forEach(beam => beam.material.opacity = 0);
gridHelper.material.opacity = 0.15;
dataStreams.forEach(stream => stream.material.opacity = 0);
// 烟花和彩带效果
for (let i = 0; i < 8; i++) {
setTimeout(() => {
createFirework(
window.innerWidth / 2 + (Math.random() - 0.5) * 600,
window.innerHeight / 2 + (Math.random() - 0.5) * 400
);
}, i * 250);
}
createConfetti();
camera.position.z = Math.max(200, camera.position.z - 100);
return;
}
const interval = 50 + progress * 200;
if (randomHighlightCount % Math.max(1, Math.floor(interval / 50)) === 0) {
const randomSprite = available[Math.floor(Math.random() * available.length)];
highlight(randomSprite, false);
}
randomHighlightCount++;
}, 50);
}
lotteryBtn.addEventListener('click', (e) => {
const rect = lotteryBtn.getBoundingClientRect();
const x = rect.left + rect.width / 2;
const y = rect.top + rect.height / 2;
createClickEffect(x, y);
// 镜头震动效果
const originalZ = camera.position.z;
let shakeTime = 0;
const shakeInterval = setInterval(() => {
shakeTime += 16;
if (shakeTime < 300) {
camera.position.x = (Math.random() - 0.5) * 8;
camera.position.y = (Math.random() - 0.5) * 8;
} else {
clearInterval(shakeInterval);
camera.position.x = 0;
camera.position.y = 0;
}
}, 16);
startLottery();
});
function resize() {
const w = container.clientWidth;
const h = container.clientHeight;
camera.aspect = w / h;
camera.updateProjectionMatrix();
renderer.setSize(w, h);
}
window.addEventListener('resize', resize);
function animate() {
requestAnimationFrame(animate);
t += 0.016;
// 名字云旋转
if (!techModeActive) {
labels.rotation.y += 0.0008;
labels.rotation.x = Math.sin(t * 0.3) * 0.1;
} else {
// 抽奖模式:从左往右快速旋转
labels.rotation.y += 0.035;
labels.rotation.x = Math.sin(t * 2) * 0.15;
labels.rotation.z = 0;
}
// 粒子系统动画
if (particleSystem) {
particleSystem.rotation.y += 0.0005;
particleSystem.rotation.x += 0.0003;
if (techModeActive) {
particleSystem.rotation.y += 0.002;
particleSystem.rotation.x += 0.001;
// 粒子颜色动态变化
const colors = particleGeometry.attributes.color;
for (let i = 0; i < particleCount; i++) {
const hue = (t * 0.1 + i * 0.001) % 1;
const color = new THREE.Color();
color.setHSL(hue, 0.8, 0.6);
colors.setXYZ(i, color.r, color.g, color.b);
}
colors.needsUpdate = true;
}
}
// 科技圆环动画
if (techCircle) {
techCircle.rotation.z += 0.001;
techCircle2.rotation.y += 0.0015;
techCircle3.rotation.x += 0.0012;
if (techModeActive) {
techCircle.rotation.z += 0.004;
techCircle2.rotation.y += 0.006;
techCircle3.rotation.x += 0.005;
// 圆环脉冲效果
const scale = 1 + Math.sin(t * 3) * 0.1;
techCircle.scale.set(scale, scale, scale);
techCircle2.scale.set(scale * 1.1, scale * 1.1, scale * 1.1);
techCircle3.scale.set(scale * 0.9, scale * 0.9, scale * 0.9);
} else {
techCircle.scale.set(1, 1, 1);
techCircle2.scale.set(1, 1, 1);
techCircle3.scale.set(1, 1, 1);
}
}
// 扫描线动画
if (scanLine && techModeActive) {
scanLineY += 4;
if (scanLineY > 300) scanLineY = -300;
scanLine.position.y = scanLineY;
scanLineMaterial.opacity = 0.6 + Math.sin(t * 5) * 0.3;
}
// 能量球动画
if (energySphere) {
energySphere.rotation.y += 0.001;
energySphere.rotation.x += 0.0005;
if (techModeActive) {
energySphere.rotation.y += 0.003;
energySphere.rotation.x += 0.002;
const pulse = 1 + Math.sin(t * 2) * 0.15;
energySphere.scale.set(pulse, pulse, pulse);
energySphereMaterial.color.setHSL((t * 0.1) % 1, 0.8, 0.5);
} else {
energySphere.scale.set(1, 1, 1);
}
}
// 六边形网格动画
if (hexGrid) {
hexGrid.rotation.z += 0.002;
if (techModeActive) {
hexGrid.rotation.z += 0.008;
const scale = 1 + Math.sin(t * 2.5) * 0.12;
hexGrid.scale.set(scale, scale, scale);
hexMaterial.color.setHSL(0.3 + Math.sin(t * 2) * 0.2, 0.8, 0.5);
} else {
hexGrid.scale.set(1, 1, 1);
}
}
// 光束动画
if (beams.length > 0 && techModeActive) {
beams.forEach((beam, i) => {
beam.rotation.y += 0.01;
const offset = (i / beams.length) * Math.PI * 2;
beam.material.opacity = 0.2 + Math.sin(t * 3 + offset) * 0.2;
beam.material.color.setHSL((t * 0.1 + i * 0.125) % 1, 0.8, 0.5);
beam.scale.y = 1 + Math.sin(t * 4 + offset) * 0.3;
});
}
// 星空闪烁
if (stars) {
stars.rotation.y += 0.0001;
starMaterial.opacity = 0.6 + Math.sin(t * 0.5) * 0.2;
}
// 网格脉冲
if (gridHelper && techModeActive) {
gridHelper.position.y = -200 + Math.sin(t * 1.5) * 20;
}
// 数据流动画
if (dataStreams.length > 0 && techModeActive) {
dataStreams.forEach(stream => {
stream.progress += 0.01;
if (stream.progress > 1) stream.progress = 0;
const opacity = Math.sin(stream.progress * Math.PI) * 0.6;
stream.material.opacity = opacity;
stream.material.color.setHSL(0.5 + stream.progress * 0.2, 0.8, 0.5);
});
}
camera.lookAt(0, 0, 0);
if (!isLotteryRunning) {
raycaster.setFromCamera(pointer, camera);
const intersects = raycaster.intersectObjects(labels.children, false);
if (intersects.length > 0 && !intersects[0].object.userData.isDrawn) {
highlight(intersects[0].object);
} else if (highlighted && !currentWinners.includes(highlighted)) {
highlight(null);
}
}
renderer.render(scene, camera);
}
animate();
// 查看历史记录功能
const historyModal = document.getElementById('historyModal');
const historyLog = document.getElementById('historyLog');
const viewHistoryBtn = document.getElementById('viewHistoryBtn');
const clearHistoryBtn = document.getElementById('clearHistoryBtn');
const closeHistoryBtn = document.getElementById('closeHistoryBtn');
const backBtn = document.getElementById('backBtn');
// 返回功能
backBtn.addEventListener('click', () => {
// 隐藏中奖结果
resultDisplay.classList.remove('show');
resultDisplay.innerHTML = '';
// 恢复按钮文字
lotteryBtn.innerHTML = '开始抽奖<div class="energy-ring"></div><div class="energy-ring" style="animation-delay:0.3s;"></div><div class="energy-ring" style="animation-delay:0.6s;"></div>';
// 隐藏返回按钮
backBtn.classList.remove('show');
// 清除当前中奖者高亮(但保留已抽取状态)
if (currentWinners.length > 0) {
currentWinners.forEach(winner => {
if (winner.userData.isDrawn) {
// 保持已抽取状态
winner.material.color.set(0x666666);
winner.material.opacity = 0.4;
}
});
currentWinners = [];
}
console.log('↺ 已返回到开始状态');
});
// 查看记录
viewHistoryBtn.addEventListener('click', () => {
const logContent = localStorage.getItem('lotteryHistoryLog') || '';
if (logContent.trim()) {
historyLog.textContent = logContent;
} else {
historyLog.innerHTML = '<div class="history-empty">📦 暂无抽奖记录</div>';
}
historyModal.classList.add('show');
});
// 关闭弹窗
closeHistoryBtn.addEventListener('click', () => {
historyModal.classList.remove('show');
});
// 点击背景关闭
historyModal.addEventListener('click', (e) => {
if (e.target === historyModal) {
historyModal.classList.remove('show');
}
});
// 清除记录
clearHistoryBtn.addEventListener('click', () => {
if (confirm('确定要清除所有抽奖记录吗?此操作不可恢复!')) {
localStorage.removeItem('lotteryHistoryLog');
alert('✅ 记录已清除');
console.log('🗑️ 抽奖历史记录已清空');
}
});
</script>
</body>
</html>
2、配置文件
1、等级配置文件
level:
特等奖: 1
一等奖: 2
二等奖: 3
三等奖: 4
幸运奖: 5
参与奖: 5
钻石奖: 5
至尊奖: 5
至尊奖: 5
2、部分功能预览
2、用户配置文件
张伟
李娜
王芳
刘洋
陈静
杨明
黄丽
周杰
吴强
徐敏
孙涛
马超
朱婷
胡军
郭鹏
.......
评论区