feat: implement QR code scan login system with admin control

- Add scan login service with Redis-based token management
- Add scan login API routes for token generation and validation
- Add QRCodeLogin component for PC-side QR code display
- Add EntryQRCode component for mass login scenarios
- Add ScanLoginView for mobile-side login form
- Add localStorage persistence for user identity
- Add logout functionality to mobile client
- Add auto-redirect for logged-in users
- Add admin console control for QR code display on big screen
- Add socket event forwarding from admin to screen display

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
empty
2026-01-25 21:44:52 +08:00
parent 75570af8bc
commit f4736b6ebd
26 changed files with 1925 additions and 119 deletions

View File

@@ -443,16 +443,10 @@ export class LotteryMachine {
p.text.y = centerY + y1 * scale;
p.text.scale.set(scale * 0.8);
// Depth-based alpha and color
// Depth-based alpha - all participants show gold color
const depthAlpha = (z2 + this.sphereRadius) / (this.sphereRadius * 2);
p.text.alpha = p.isEligible ? 0.3 + depthAlpha * 0.7 : 0.15;
// Dim ineligible names
if (!p.isEligible) {
p.text.style.fill = 0x666666;
} else {
p.text.style.fill = COLORS.gold;
}
p.text.alpha = 0.3 + depthAlpha * 0.7;
p.text.style.fill = COLORS.gold;
// Z-sorting
p.text.zIndex = Math.floor(z2);
@@ -462,46 +456,65 @@ export class LotteryMachine {
}
// ============================================================================
// Storm Phase (Tornado)
// Storm Phase (Fast Spinning Sphere)
// ============================================================================
private updateStorm(deltaMs: number): void {
const centerX = this.app.screen.width / 2;
const centerY = this.app.screen.height / 2;
// Ramp up storm intensity
this.stormIntensity = Math.min(1, this.stormIntensity + deltaMs * 0.001);
this.stormAngle += deltaMs * 0.01 * this.stormIntensity;
// Ramp up rotation speed
this.stormIntensity = Math.min(1, this.stormIntensity + deltaMs * 0.002);
// Apply motion blur based on intensity
this.blurFilter.strength = this.stormIntensity * 8;
// Fast sphere rotation - much faster than galaxy phase
this.sphereRotationY += deltaMs * 0.008 * (1 + this.stormIntensity * 3);
this.sphereRotationX += deltaMs * 0.003 * this.stormIntensity;
// Light motion blur for speed effect
this.blurFilter.strength = this.stormIntensity * 2;
this.nameParticles.forEach((p, index) => {
if (!p.text) return;
// Tornado vortex motion
const baseAngle = this.stormAngle + (index / this.nameParticles.length) * Math.PI * 2;
const verticalPos = ((this.time * 0.001 + index * 0.1) % 2) - 1; // -1 to 1
const radius = 100 + Math.abs(verticalPos) * 200 * this.stormIntensity;
// Get original sphere position from fibonacci distribution
const phi = Math.acos(1 - 2 * (index + 0.5) / this.nameParticles.length);
const theta = Math.PI * (1 + Math.sqrt(5)) * index;
const targetX = centerX + Math.cos(baseAngle) * radius;
const targetY = centerY + verticalPos * 300;
const sphereX = this.sphereRadius * Math.sin(phi) * Math.cos(theta);
const sphereY = this.sphereRadius * Math.sin(phi) * Math.sin(theta);
const sphereZ = this.sphereRadius * Math.cos(phi);
// Smooth interpolation
p.x += (targetX - p.x) * 0.1;
p.y += (targetY - p.y) * 0.1;
// Apply 3D rotation (Y axis then X axis)
const cosY = Math.cos(this.sphereRotationY);
const sinY = Math.sin(this.sphereRotationY);
const cosX = Math.cos(this.sphereRotationX);
const sinX = Math.sin(this.sphereRotationX);
p.text.x = p.x;
p.text.y = p.y;
p.text.rotation += p.rotationSpeed * this.stormIntensity * 3;
p.text.alpha = p.isEligible ? 0.8 : 0.2;
// Rotate around Y axis
const x1 = sphereX * cosY - sphereZ * sinY;
const z1 = sphereX * sinY + sphereZ * cosY;
// Scale based on position
const distFromCenter = Math.sqrt(
Math.pow(p.x - centerX, 2) + Math.pow(p.y - centerY, 2)
);
p.text.scale.set(0.5 + (1 - distFromCenter / 400) * 0.5);
// Rotate around X axis
const y1 = sphereY * cosX - z1 * sinX;
const z2 = sphereY * sinX + z1 * cosX;
// Project to 2D with perspective
const perspective = 800;
const scale = perspective / (perspective + z2);
p.text.x = centerX + x1 * scale;
p.text.y = centerY + y1 * scale;
p.text.scale.set(scale * 0.9);
// Depth-based alpha - all participants show same
const depthAlpha = (z2 + this.sphereRadius) / (this.sphereRadius * 2);
p.text.alpha = 0.4 + depthAlpha * 0.6;
// Z-sorting
p.text.zIndex = Math.floor(z2);
});
this.galaxyContainer.sortChildren();
}
// ============================================================================