feat: initialize Annual Gala Interactive System monorepo

- Set up pnpm workspace with 4 packages: shared, server, client-mobile, client-screen
- Implement Redis atomic voting with Lua scripts (HINCRBY + distributed lock)
- Add optimistic UI with IndexedDB queue for offline resilience
- Configure Socket.io with auto-reconnection (infinite retries)
- Separate mobile (Vant) and big screen (Pixi.js) dependencies

Tech stack:
- Frontend Mobile: Vue 3 + Vant + Socket.io-client
- Frontend Screen: Vue 3 + Pixi.js + GSAP
- Backend: Express + Socket.io + Redis + Prisma/MySQL

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
empty
2026-01-15 01:19:36 +08:00
commit e7397d22a9
74 changed files with 14088 additions and 0 deletions

View File

@@ -0,0 +1,158 @@
import { defineStore } from 'pinia';
import { ref, computed, shallowRef } from 'vue';
import { io, Socket } from 'socket.io-client';
import type {
ServerToClientEvents,
ClientToServerEvents,
DrawStartPayload,
DrawWinnerPayload,
VoteUpdatePayload,
} from '@gala/shared/types';
import { SOCKET_EVENTS } from '@gala/shared/constants';
type GalaSocket = Socket<ServerToClientEvents, ClientToServerEvents>;
export const useDisplayStore = defineStore('display', () => {
// State - use shallowRef for socket to avoid deep reactivity issues
const socket = shallowRef<GalaSocket | null>(null);
const isConnected = ref(false);
const isConnecting = ref(false);
const onlineUsers = ref(0);
const currentMode = ref<'idle' | 'voting' | 'draw' | 'results'>('idle');
// Draw state
const isDrawing = ref(false);
const currentPrize = ref<string | null>(null);
const currentWinner = ref<{
userId: string;
userName: string;
department: string;
avatar?: string;
} | null>(null);
// Computed
const connectionStatus = computed(() => {
if (isConnected.value) return 'connected';
if (isConnecting.value) return 'connecting';
return 'disconnected';
});
/**
* Connect to WebSocket server
*/
function connect() {
if (socket.value?.connected || isConnecting.value) {
return;
}
isConnecting.value = true;
const socketInstance = io(import.meta.env.VITE_SOCKET_URL || '', {
reconnection: true,
reconnectionAttempts: Infinity,
reconnectionDelay: 1000,
reconnectionDelayMax: 5000,
timeout: 10000,
transports: ['websocket', 'polling'],
});
// Connection events
socketInstance.on('connect', () => {
console.log('[Screen] Connected');
isConnected.value = true;
isConnecting.value = false;
// Join as screen
socketInstance.emit(SOCKET_EVENTS.CONNECTION_JOIN as any, {
userId: 'screen_main',
userName: 'Main Display',
role: 'screen',
}, () => {});
});
socketInstance.on('disconnect', (reason) => {
console.log('[Screen] Disconnected:', reason);
isConnected.value = false;
});
socketInstance.on('connect_error', (error) => {
console.error('[Screen] Connection error:', error);
isConnecting.value = false;
});
// User count updates
socketInstance.on('connection:users_count' as any, (count: number) => {
onlineUsers.value = count;
});
// Draw events
socketInstance.on('draw:started' as any, (data: DrawStartPayload) => {
isDrawing.value = true;
currentPrize.value = data.prizeName;
currentWinner.value = null;
currentMode.value = 'draw';
});
socketInstance.on('draw:winner' as any, (data: DrawWinnerPayload) => {
currentWinner.value = data.winner;
});
socketInstance.on('draw:ended' as any, () => {
isDrawing.value = false;
});
// Vote updates
socketInstance.on('vote:updated' as any, (data: VoteUpdatePayload) => {
// Emit custom event for components to handle
window.dispatchEvent(new CustomEvent('vote:updated', { detail: data }));
});
socket.value = socketInstance as GalaSocket;
}
/**
* Disconnect from server
*/
function disconnect() {
if (socket.value) {
socket.value.disconnect();
socket.value = null;
}
isConnected.value = false;
isConnecting.value = false;
}
/**
* Set display mode
*/
function setMode(mode: 'idle' | 'voting' | 'draw' | 'results') {
currentMode.value = mode;
}
/**
* Get socket instance (for advanced usage)
*/
function getSocket(): GalaSocket | null {
return socket.value;
}
return {
// State (excluding socket to avoid type inference issues)
isConnected,
isConnecting,
onlineUsers,
currentMode,
isDrawing,
currentPrize,
currentWinner,
// Computed
connectionStatus,
// Actions
connect,
disconnect,
setMode,
getSocket,
};
});