import { defineStore } from 'pinia'; import { ref, computed, shallowRef, watch } from 'vue'; import { io, Socket } from 'socket.io-client'; import type { ServerToClientEvents, ClientToServerEvents, DrawStartPayload, DrawWinnerPayload, VoteUpdatePayload, AdminState, SystemPhase, } from '@gala/shared/types'; import { SOCKET_EVENTS } from '@gala/shared/constants'; type GalaSocket = Socket; // Audio player singleton let audioPlayer: HTMLAudioElement | null = null; // Audio file paths - place audio files in public/audio/ const AUDIO_TRACKS: Record = { bgm: '/screen/audio/bgm.mp3', lottery: '/screen/audio/lottery.mp3', fanfare: '/screen/audio/fanfare.mp3', award: '/screen/audio/award.mp3', horse: '/screen/audio/horse.mp3', // 第四轮抽奖"属马"专属音效 }; export const useDisplayStore = defineStore('display', () => { // State - use shallowRef for socket to avoid deep reactivity issues const socket = shallowRef(null); const isConnected = ref(false); const isConnecting = ref(false); const onlineUsers = ref(0); const currentMode = ref<'idle' | 'voting' | 'draw' | 'results' | 'lottery_results' | 'checkin_wall'>('idle'); const eventTitle = ref('公司2026年会'); // Draw state const isDrawing = ref(false); const currentPrize = ref(null); const currentWinner = ref<{ userId: string; userName: string; department: string; avatar?: string; } | null>(null); // QR Code display state (controlled by admin) const showEntryQR = ref(false); // Music state const musicPlaying = ref(false); const musicTrack = ref('none'); // 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 })); }); // Admin state sync - listen for phase changes from director console socketInstance.on(SOCKET_EVENTS.ADMIN_STATE_SYNC as any, (state: AdminState) => { console.log('[Screen] Admin state sync received:', state.systemPhase); // Update event title if changed if (state.eventTitle && state.eventTitle !== eventTitle.value) { eventTitle.value = state.eventTitle; } // Map SystemPhase to display mode const phaseToMode: Record = { 'IDLE': 'idle', 'VOTING': 'voting', 'LOTTERY': 'draw', 'RESULTS': 'results', 'LOTTERY_RESULTS': 'lottery_results', 'CHECKIN_WALL': 'checkin_wall', }; const newMode = phaseToMode[state.systemPhase] || 'idle'; if (currentMode.value !== newMode) { currentMode.value = newMode; // Emit custom event for App.vue to handle route navigation window.dispatchEvent(new CustomEvent('screen:mode_change', { detail: { mode: newMode, phase: state.systemPhase } })); } // Handle music state changes if (state.music) { const { isPlaying, track } = state.music; if (isPlaying && track && track !== 'none') { // Only play if not already playing the same track if (!musicPlaying.value || musicTrack.value !== track) { const shouldLoop = track === 'bgm' || track === 'lottery'; playAudio(track, shouldLoop); } } else if (!isPlaying && musicPlaying.value) { stopAudio(); } } }); // QR Code display control events socketInstance.on('display:show_entry_qr' as any, () => { console.log('[Screen] Show entry QR code'); showEntryQR.value = true; }); socketInstance.on('display:hide_qr' as any, () => { console.log('[Screen] Hide QR code'); showEntryQR.value = false; }); 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; } /** * Play audio track */ function playAudio(track: string, loop: boolean = false) { stopAudio(); const src = AUDIO_TRACKS[track]; if (!src) { console.warn(`[Screen] Unknown audio track: ${track}`); return; } audioPlayer = new Audio(src); audioPlayer.loop = loop; audioPlayer.volume = 0.7; audioPlayer.play().catch((err) => { console.error('[Screen] Failed to play audio:', err); }); musicPlaying.value = true; musicTrack.value = track; console.log(`[Screen] Playing audio: ${track}`); } /** * Stop audio playback */ function stopAudio() { if (audioPlayer) { audioPlayer.pause(); audioPlayer.currentTime = 0; audioPlayer = null; } musicPlaying.value = false; musicTrack.value = 'none'; console.log('[Screen] Audio stopped'); } return { // State (excluding socket to avoid type inference issues) isConnected, isConnecting, onlineUsers, currentMode, eventTitle, isDrawing, currentPrize, currentWinner, showEntryQR, musicPlaying, musicTrack, // Computed connectionStatus, // Actions connect, disconnect, setMode, getSocket, playAudio, stopAudio, }; });