-
+
+
+
+
请使用微信扫码登录
+
请前往大屏幕扫描二维码进入年会
-
- 进入年会
-
+
+
+ 1
+ 前往大屏幕
+
+
+ 2
+ 微信扫描二维码
+
+
+ 3
+ 授权后自动登录
+
+
@@ -240,31 +202,58 @@ async function handleEnter() {
}
}
-.form-section {
+.scan-section {
margin-bottom: $spacing-xl;
+ text-align: center;
- .input-wrapper {
- background: $color-bg-card;
- margin-bottom: $spacing-md;
- padding: $spacing-xs;
+ .scan-hint {
+ margin-bottom: $spacing-xl;
- :deep(.van-field) {
- background: transparent;
+ .hint-title {
+ font-size: $font-size-xl;
+ font-weight: bold;
+ color: $color-text-primary;
+ margin: $spacing-md 0 $spacing-xs;
+ }
- .van-field__control {
- text-align: center;
- font-size: $font-size-lg;
- }
+ .hint-desc {
+ font-size: $font-size-md;
+ color: $color-text-secondary;
}
}
- .enter-btn {
- background: linear-gradient(135deg, $color-primary-light 0%, $color-primary 100%);
- border: none;
- height: 48px;
- font-size: $font-size-lg;
- font-weight: 500;
- box-shadow: $shadow-md;
+ .steps {
+ display: flex;
+ justify-content: center;
+ gap: $spacing-lg;
+ padding: $spacing-md;
+ background: rgba($color-primary, 0.05);
+ border-radius: $radius-lg;
+ }
+
+ .step {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: $spacing-xs;
+ }
+
+ .step-num {
+ width: 28px;
+ height: 28px;
+ border-radius: 50%;
+ background: $color-primary;
+ color: white;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-weight: bold;
+ font-size: $font-size-sm;
+ }
+
+ .step-text {
+ font-size: $font-size-xs;
+ color: $color-text-secondary;
}
}
diff --git a/packages/client-screen/src/components/QRCodeLogin.vue b/packages/client-screen/src/components/QRCodeLogin.vue
index 1f4a469..f92b48b 100644
--- a/packages/client-screen/src/components/QRCodeLogin.vue
+++ b/packages/client-screen/src/components/QRCodeLogin.vue
@@ -6,7 +6,7 @@ import { SOCKET_EVENTS } from '@gala/shared/constants';
import type { ScanStatusUpdatePayload, ScanLoginStatus } from '@gala/shared/types';
const emit = defineEmits<{
- (e: 'login-success', data: { userId: string; userName: string; department: string; sessionToken: string }): void;
+ (e: 'login-success', data: { userId: string; userName: string; sessionToken: string }): void;
(e: 'login-cancel'): void;
}>();
@@ -125,7 +125,6 @@ function handleStatusUpdate(data: ScanStatusUpdatePayload) {
emit('login-success', {
userId: data.userInfo.userId,
userName: data.userInfo.userName,
- department: data.userInfo.department,
sessionToken: data.userInfo.sessionToken,
});
}
diff --git a/packages/client-screen/src/pixi/BackgroundEffect.ts b/packages/client-screen/src/pixi/BackgroundEffect.ts
index 0dde0d6..d6f45c8 100644
--- a/packages/client-screen/src/pixi/BackgroundEffect.ts
+++ b/packages/client-screen/src/pixi/BackgroundEffect.ts
@@ -6,8 +6,22 @@ const COLORS = {
gold: 0xf0c239,
goldDark: 0xd4a84b,
goldLight: 0xffd700,
+ // Firework colors
+ fireworkRed: 0xff4444,
+ fireworkOrange: 0xff8844,
+ fireworkYellow: 0xffdd44,
+ fireworkPink: 0xff66aa,
+ fireworkWhite: 0xffffee,
};
+// Firework color palettes
+const FIREWORK_PALETTES = [
+ [COLORS.gold, COLORS.goldLight, COLORS.fireworkYellow],
+ [COLORS.fireworkRed, COLORS.fireworkOrange, COLORS.fireworkYellow],
+ [COLORS.fireworkPink, COLORS.fireworkWhite, COLORS.gold],
+ [COLORS.goldDark, COLORS.gold, COLORS.fireworkWhite],
+];
+
interface Particle {
x: number;
y: number;
@@ -17,9 +31,11 @@ interface Particle {
alpha: number;
rotation: number;
rotationSpeed: number;
- type: 'dust' | 'symbol' | 'streak';
+ type: 'dust' | 'symbol' | 'streak' | 'firework-trail' | 'firework-spark';
life: number;
maxLife: number;
+ color?: number;
+ gravity?: number;
}
export class BackgroundEffect {
@@ -29,6 +45,7 @@ export class BackgroundEffect {
private dustLayer: Graphics;
private symbolLayer: Graphics;
private streakLayer: Graphics;
+ private fireworkLayer: Graphics;
private time = 0;
private windOffset = 0;
private isDestroyed = false;
@@ -37,6 +54,7 @@ export class BackgroundEffect {
private readonly DUST_COUNT = 120;
private readonly SYMBOL_COUNT = 25;
private readonly STREAK_INTERVAL = 3000; // ms between streaks
+ private readonly FIREWORK_INTERVAL = 4000; // ms between fireworks
constructor() {
this.app = new Application();
@@ -44,6 +62,7 @@ export class BackgroundEffect {
this.dustLayer = new Graphics();
this.symbolLayer = new Graphics();
this.streakLayer = new Graphics();
+ this.fireworkLayer = new Graphics();
}
async init(canvas: HTMLCanvasElement): Promise
{
@@ -61,6 +80,7 @@ export class BackgroundEffect {
this.container.addChild(this.dustLayer);
this.container.addChild(this.symbolLayer);
this.container.addChild(this.streakLayer);
+ this.container.addChild(this.fireworkLayer);
this.app.stage.addChild(this.container);
// Draw vignette overlay
@@ -76,6 +96,9 @@ export class BackgroundEffect {
// Spawn streaks periodically
this.spawnStreakLoop();
+ // Spawn fireworks periodically
+ this.spawnFireworkLoop();
+
// Handle resize
window.addEventListener('resize', this.handleResize.bind(this));
}
@@ -157,6 +180,78 @@ export class BackgroundEffect {
setTimeout(() => this.spawnStreakLoop(), this.STREAK_INTERVAL + Math.random() * 2000);
}
+ private spawnFireworkLoop(): void {
+ if (this.isDestroyed) return;
+
+ this.launchFirework();
+ setTimeout(() => this.spawnFireworkLoop(), this.FIREWORK_INTERVAL + Math.random() * 3000);
+ }
+
+ private launchFirework(): void {
+ if (this.isDestroyed) return;
+
+ const w = this.app.screen.width;
+ const h = this.app.screen.height;
+
+ // Random launch position from bottom
+ const startX = w * 0.2 + Math.random() * w * 0.6;
+ const startY = h;
+
+ // Target explosion point
+ const targetY = h * 0.2 + Math.random() * h * 0.3;
+
+ // Calculate velocity to reach target
+ const flightTime = 60 + Math.random() * 30; // frames
+ const vy = -(startY - targetY) / flightTime - 0.5 * 0.15 * flightTime;
+
+ // Select color palette for this firework
+ const palette = FIREWORK_PALETTES[Math.floor(Math.random() * FIREWORK_PALETTES.length)];
+
+ // Create trail particle (the rising firework)
+ this.particles.push({
+ x: startX,
+ y: startY,
+ vx: (Math.random() - 0.5) * 0.5,
+ vy: vy,
+ size: 3,
+ alpha: 1,
+ rotation: 0,
+ rotationSpeed: 0,
+ type: 'firework-trail',
+ life: 0,
+ maxLife: flightTime,
+ color: palette[0],
+ gravity: 0.15,
+ });
+ }
+
+ private explodeFirework(x: number, y: number): void {
+ const palette = FIREWORK_PALETTES[Math.floor(Math.random() * FIREWORK_PALETTES.length)];
+ const sparkCount = 40 + Math.floor(Math.random() * 30);
+
+ for (let i = 0; i < sparkCount; i++) {
+ const angle = (Math.PI * 2 * i) / sparkCount + (Math.random() - 0.5) * 0.3;
+ const speed = 3 + Math.random() * 4;
+ const color = palette[Math.floor(Math.random() * palette.length)];
+
+ this.particles.push({
+ x: x,
+ y: y,
+ vx: Math.cos(angle) * speed,
+ vy: Math.sin(angle) * speed,
+ size: 2 + Math.random() * 2,
+ alpha: 1,
+ rotation: 0,
+ rotationSpeed: 0,
+ type: 'firework-spark',
+ life: 0,
+ maxLife: 50 + Math.random() * 30,
+ color: color,
+ gravity: 0.08,
+ });
+ }
+ }
+
private update(ticker: Ticker): void {
if (this.isDestroyed) return;
@@ -167,6 +262,7 @@ export class BackgroundEffect {
this.dustLayer.clear();
this.symbolLayer.clear();
this.streakLayer.clear();
+ this.fireworkLayer.clear();
const w = this.app.screen.width;
const h = this.app.screen.height;
@@ -185,6 +281,25 @@ export class BackgroundEffect {
return false;
}
p.alpha = 0.9 * (1 - p.life / p.maxLife);
+ } else if (p.type === 'firework-trail') {
+ // Apply gravity
+ p.vy += p.gravity || 0.15;
+ if (p.life >= p.maxLife) {
+ // Explode at the end
+ this.explodeFirework(p.x, p.y);
+ return false;
+ }
+ p.alpha = 1;
+ } else if (p.type === 'firework-spark') {
+ // Apply gravity and friction
+ p.vy += p.gravity || 0.08;
+ p.vx *= 0.98;
+ p.vy *= 0.98;
+ if (p.life >= p.maxLife) {
+ return false;
+ }
+ p.alpha = 1 - (p.life / p.maxLife);
+ p.size *= 0.995;
} else if (p.type === 'symbol') {
// Reset symbols that go off top
if (p.y < -20) {
@@ -210,7 +325,9 @@ export class BackgroundEffect {
? this.dustLayer
: p.type === 'symbol'
? this.symbolLayer
- : this.streakLayer;
+ : (p.type === 'firework-trail' || p.type === 'firework-spark')
+ ? this.fireworkLayer
+ : this.streakLayer;
if (p.type === 'dust') {
// Simple gold circle
@@ -250,6 +367,35 @@ export class BackgroundEffect {
// Draw head
layer.circle(p.x, p.y, p.size);
layer.fill({ color: COLORS.goldLight, alpha: p.alpha });
+ } else if (p.type === 'firework-trail') {
+ // Rising firework with sparkle trail
+ const trailLength = 30;
+ for (let i = 0; i < 8; i++) {
+ const ratio = i / 8;
+ const tx = p.x + (Math.random() - 0.5) * 3;
+ const ty = p.y + ratio * trailLength;
+ const ta = p.alpha * (1 - ratio) * 0.6;
+ const ts = p.size * (1 - ratio * 0.5);
+ layer.circle(tx, ty, ts);
+ layer.fill({ color: p.color || COLORS.gold, alpha: ta });
+ }
+ // Bright head
+ layer.circle(p.x, p.y, p.size + 1);
+ layer.fill({ color: COLORS.fireworkWhite, alpha: p.alpha });
+ } else if (p.type === 'firework-spark') {
+ // Exploding spark with small trail
+ const trailLen = 5;
+ for (let i = 0; i < 3; i++) {
+ const ratio = i / 3;
+ const tx = p.x - p.vx * ratio * trailLen;
+ const ty = p.y - p.vy * ratio * trailLen;
+ const ta = p.alpha * (1 - ratio) * 0.5;
+ layer.circle(tx, ty, p.size * (1 - ratio * 0.3));
+ layer.fill({ color: p.color || COLORS.gold, alpha: ta });
+ }
+ // Spark head
+ layer.circle(p.x, p.y, p.size);
+ layer.fill({ color: p.color || COLORS.gold, alpha: p.alpha });
}
}
diff --git a/packages/client-screen/src/views/MainDisplay.vue b/packages/client-screen/src/views/MainDisplay.vue
index 32d0dbd..83ee6cf 100644
--- a/packages/client-screen/src/views/MainDisplay.vue
+++ b/packages/client-screen/src/views/MainDisplay.vue
@@ -13,7 +13,7 @@ const showQRLogin = ref(false);
// Mobile URL for entry QR code - use environment variable or fallback to current origin
const mobileUrl = import.meta.env.VITE_MOBILE_URL || window.location.origin;
-function handleLoginSuccess(data: { userId: string; userName: string; department: string; sessionToken: string }) {
+function handleLoginSuccess(data: { userId: string; userName: string; sessionToken: string }) {
console.log('Login success:', data);
showQRLogin.value = false;
// 可以在这里处理登录成功后的逻辑
diff --git a/packages/server/src/app.ts b/packages/server/src/app.ts
index 97c9391..cd5e975 100644
--- a/packages/server/src/app.ts
+++ b/packages/server/src/app.ts
@@ -2,6 +2,7 @@ import express, { Application } from 'express';
import cors from 'cors';
import helmet from 'helmet';
import compression from 'compression';
+import path from 'path';
import { config } from './config';
import { logger } from './utils/logger';
import { errorHandler } from './middleware/errorHandler';
@@ -51,6 +52,9 @@ app.use(express.urlencoded({ extended: true }));
// Request logging
app.use(requestLogger);
+// Static files (for WeChat domain verification, etc.)
+app.use(express.static(path.join(__dirname, '../public')));
+
// Health check
app.get('/health', (_req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() });
diff --git a/packages/shared/src/types/scan-login.types.ts b/packages/shared/src/types/scan-login.types.ts
index 4c9cfa6..82aaee6 100644
--- a/packages/shared/src/types/scan-login.types.ts
+++ b/packages/shared/src/types/scan-login.types.ts
@@ -11,7 +11,6 @@ export interface ScanTokenData {
userInfo?: {
userId: string;
userName: string;
- department: string;
};
}
@@ -38,7 +37,6 @@ export interface ValidateTokenResponse {
export interface ScanConfirmPayload {
scanToken: string;
userName: string;
- department: string;
}
export interface ScanConfirmResponse {
@@ -56,7 +54,6 @@ export interface ScanStatusUpdatePayload {
userInfo?: {
userId: string;
userName: string;
- department: string;
sessionToken: string;
};
}