feat: redesign Big Screen voting view with philatelic postcard UI
- Add PostcardItem.vue component with Chinese postal aesthetics - Add PostcardGrid.vue container with 4x2 CSS Grid layout - Add Postmark.vue component for real-time vote stamp visualization - Update LiveVotingView.vue with cream paper theme (#FDFBF7) - Add Year of the Horse 2026 stamp image - Add responsive breakpoints for different screen sizes - Enhance admin service with program voting control - Add vote stamp accumulation for big screen display Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,7 @@ import type {
|
||||
ConnectionAckPayload,
|
||||
VoteCategory,
|
||||
SyncStatePayload,
|
||||
AdminState,
|
||||
} from '@gala/shared/types';
|
||||
import { SOCKET_EVENTS } from '@gala/shared/constants';
|
||||
import { CONFIG } from '@gala/shared/constants';
|
||||
@@ -24,6 +25,7 @@ export const useConnectionStore = defineStore('connection', () => {
|
||||
const reconnectAttempts = ref(0);
|
||||
const userId = ref<string | null>(null);
|
||||
const userName = ref<string | null>(null);
|
||||
const department = ref<string | null>(null);
|
||||
const votedCategories = ref<VoteCategory[]>([]);
|
||||
|
||||
// Computed
|
||||
@@ -116,6 +118,29 @@ export const useConnectionStore = defineStore('connection', () => {
|
||||
votedCategories.value = data.userVotedCategories;
|
||||
}
|
||||
});
|
||||
// Admin state sync - update voting state
|
||||
socketInstance.on(SOCKET_EVENTS.ADMIN_STATE_SYNC as any, (state: AdminState) => {
|
||||
console.log('[Socket] Admin state sync received');
|
||||
// Dynamically import voting store to avoid circular dependency
|
||||
import('./voting').then(({ useVotingStore }) => {
|
||||
const votingStore = useVotingStore();
|
||||
votingStore.syncVotingState({
|
||||
votingOpen: state.voting.subPhase === 'OPEN',
|
||||
votingPaused: state.voting.subPhase === 'PAUSED',
|
||||
programs: state.voting.programs,
|
||||
currentProgramId: state.voting.currentProgramId,
|
||||
allowLateCatch: state.voting.allowLateCatch,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Remote vote update (from other users)
|
||||
socketInstance.on(SOCKET_EVENTS.VOTE_UPDATED as any, (data: any) => {
|
||||
import('./voting').then(({ useVotingStore }) => {
|
||||
const votingStore = useVotingStore();
|
||||
votingStore.handleVoteUpdate(data);
|
||||
});
|
||||
});
|
||||
|
||||
socket.value = socketInstance as GalaSocket;
|
||||
}
|
||||
@@ -131,6 +156,7 @@ export const useConnectionStore = defineStore('connection', () => {
|
||||
{
|
||||
userId: userId.value,
|
||||
userName: userName.value || 'Guest',
|
||||
department: department.value || '未知部门',
|
||||
role: 'user',
|
||||
},
|
||||
(response: any) => {
|
||||
@@ -193,9 +219,10 @@ export const useConnectionStore = defineStore('connection', () => {
|
||||
/**
|
||||
* Set user info
|
||||
*/
|
||||
function setUser(id: string, name: string) {
|
||||
function setUser(id: string, name: string, dept: string) {
|
||||
userId.value = id;
|
||||
userName.value = name;
|
||||
department.value = dept;
|
||||
|
||||
// Rejoin if already connected
|
||||
if (socket.value?.connected) {
|
||||
@@ -237,6 +264,7 @@ export const useConnectionStore = defineStore('connection', () => {
|
||||
reconnectAttempts,
|
||||
userId,
|
||||
userName,
|
||||
department,
|
||||
votedCategories,
|
||||
|
||||
// Computed
|
||||
|
||||
@@ -3,6 +3,7 @@ import { ref, computed } from 'vue';
|
||||
import { useConnectionStore } from './connection';
|
||||
import { TICKET_TYPES, type TicketType } from '@gala/shared/constants';
|
||||
import { showToast } from 'vant';
|
||||
import type { VoteStamp } from '@gala/shared/types';
|
||||
|
||||
// Ticket display info
|
||||
export const TICKET_INFO: Record<TicketType, { name: string; icon: string }> = {
|
||||
@@ -21,6 +22,15 @@ interface PendingVote {
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
interface VotingProgram {
|
||||
id: string;
|
||||
name: string;
|
||||
teamName: string;
|
||||
order: number;
|
||||
status: 'pending' | 'voting' | 'completed';
|
||||
votes: number;
|
||||
}
|
||||
|
||||
export const useVotingStore = defineStore('voting', () => {
|
||||
const connectionStore = useConnectionStore();
|
||||
|
||||
@@ -35,6 +45,13 @@ export const useVotingStore = defineStore('voting', () => {
|
||||
potential: null,
|
||||
});
|
||||
|
||||
// Server-synced voting state
|
||||
const votingOpen = ref(false);
|
||||
const votingPaused = ref(false);
|
||||
const programs = ref<VotingProgram[]>([]);
|
||||
const currentProgramId = ref<string | null>(null);
|
||||
const allowLateCatch = ref(true);
|
||||
|
||||
// Currently selected stamp in dock
|
||||
const selectedStamp = ref<TicketType | null>(null);
|
||||
|
||||
@@ -211,6 +228,63 @@ export const useVotingStore = defineStore('voting', () => {
|
||||
tickets.value = { ...serverTickets };
|
||||
}
|
||||
|
||||
// Sync voting state from AdminState
|
||||
function syncVotingState(state: {
|
||||
votingOpen?: boolean;
|
||||
votingPaused?: boolean;
|
||||
programs?: VotingProgram[];
|
||||
currentProgramId?: string | null;
|
||||
allowLateCatch?: boolean;
|
||||
}) {
|
||||
if (state.votingOpen !== undefined) votingOpen.value = state.votingOpen;
|
||||
if (state.votingPaused !== undefined) votingPaused.value = state.votingPaused;
|
||||
if (state.programs !== undefined) programs.value = state.programs;
|
||||
if (state.currentProgramId !== undefined) currentProgramId.value = state.currentProgramId;
|
||||
if (state.allowLateCatch !== undefined) allowLateCatch.value = state.allowLateCatch;
|
||||
}
|
||||
|
||||
// Check if voting is allowed for a specific program
|
||||
// In unified voting mode, all programs can be voted when voting is open
|
||||
function canVoteForProgram(programId: string): { allowed: boolean; reason?: string } {
|
||||
// Check if voting is open
|
||||
if (!votingOpen.value) {
|
||||
return { allowed: false, reason: '投票尚未开始' };
|
||||
}
|
||||
if (votingPaused.value) {
|
||||
return { allowed: false, reason: '投票已暂停' };
|
||||
}
|
||||
|
||||
// In unified voting mode, all programs are votable when voting is open
|
||||
return { allowed: true };
|
||||
}
|
||||
|
||||
// Handle real-time vote updates
|
||||
function handleVoteUpdate(data: { candidateId: string; newCount: number; totalVotes: number }) {
|
||||
const program = programs.value.find(p => p.id === data.candidateId);
|
||||
if (program) {
|
||||
program.votes = data.newCount;
|
||||
}
|
||||
// Update global total votes if provided
|
||||
if (data.totalVotes !== undefined) {
|
||||
// We don't have a totalVotes ref in the store yet, but we could add it or just ignore
|
||||
}
|
||||
}
|
||||
|
||||
// Get current voting program
|
||||
const currentProgram = computed(() => {
|
||||
if (!currentProgramId.value) return null;
|
||||
return programs.value.find(p => p.id === currentProgramId.value) || null;
|
||||
});
|
||||
|
||||
// Get votable programs (voting or completed with late catch)
|
||||
const votablePrograms = computed(() => {
|
||||
return programs.value.filter(p => {
|
||||
if (p.status === 'voting') return true;
|
||||
if (p.status === 'completed' && allowLateCatch.value) return true;
|
||||
return false;
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
tickets,
|
||||
selectedStamp,
|
||||
@@ -225,5 +299,17 @@ export const useVotingStore = defineStore('voting', () => {
|
||||
castVote,
|
||||
revokeVote,
|
||||
syncFromServer,
|
||||
// New exports
|
||||
votingOpen,
|
||||
votingPaused,
|
||||
programs,
|
||||
currentProgramId,
|
||||
allowLateCatch,
|
||||
currentProgram,
|
||||
votablePrograms,
|
||||
syncVotingState,
|
||||
canVoteForProgram,
|
||||
handleVoteUpdate,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user