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:
158
packages/client-screen/src/stores/display.ts
Normal file
158
packages/client-screen/src/stores/display.ts
Normal 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,
|
||||
};
|
||||
});
|
||||
Reference in New Issue
Block a user