feat: add Admin Control Panel, voting status check, and router security

Admin Control Panel:
- Add full AdminControl.vue with 3 sections (Voting, Lottery, Global)
- Add AdminLogin.vue with access code gate (20268888)
- Add admin.ts store with state persistence
- Add admin.types.ts with state machine types
- Add router guards for /admin/director-console

Voting System Fixes:
- Add voting status check before accepting votes (VOTING_CLOSED error)
- Fix client to display server error messages
- Fix button disabled logic to prevent ambiguity in paused state
- Auto-generate userId on connect to fix UNAUTHORIZED error

Big Screen Enhancements:
- Add LiveVotingView.vue with particle system
- Add LotteryMachine.ts with 3-stage animation (Galaxy/Storm/Reveal)
- Add useSocketClient.ts composable
- Fix MainDisplay.vue SCSS syntax error
- Add admin state sync listener in display store

Server Updates:
- Add admin.service.ts for state management
- Add isVotingOpen() and getVotingStatus() methods
- Add admin socket event handlers

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
empty
2026-01-15 15:34:37 +08:00
parent e7397d22a9
commit 30cd29d45d
45 changed files with 7791 additions and 715 deletions

View File

@@ -0,0 +1,100 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { useVotingStore } from '../stores/voting';
import { useConnectionStore } from '../stores/connection';
import VotingDock from '../components/VotingDock.vue';
import ProgramCard from '../components/ProgramCard.vue';
import ConnectionStatus from '../components/ConnectionStatus.vue';
const votingStore = useVotingStore();
const connectionStore = useConnectionStore();
// Mock programs data (replace with API call)
const programs = ref([
{ id: 'p1', name: '龙腾四海', team: '市场部' },
{ id: 'p2', name: '金马奔腾', team: '技术部' },
{ id: 'p3', name: '春风得意', team: '人力资源部' },
{ id: 'p4', name: '鸿运当头', team: '财务部' },
{ id: 'p5', name: '马到成功', team: '运营部' },
{ id: 'p6', name: '一马当先', team: '产品部' },
{ id: 'p7', name: '万马奔腾', team: '设计部' },
{ id: 'p8', name: '龙马精神', team: '销售部' },
]);
const isLoading = ref(false);
onMounted(async () => {
// Connect if not connected
if (!connectionStore.isConnected) {
connectionStore.connect();
}
});
</script>
<template>
<div class="voting-page safe-area-top">
<!-- Header -->
<header class="page-header">
<h1 class="page-title">节目投票</h1>
<p class="page-subtitle">
已使用 {{ votingStore.usedTickets.length }}/7 枚印章
</p>
<ConnectionStatus />
</header>
<!-- Program List (Postcards fade in with stagger) -->
<main class="program-list">
<ProgramCard
v-for="(program, index) in programs"
:key="program.id"
:program-id="program.id"
:program-name="program.name"
:team-name="program.team"
:index="index"
/>
</main>
<!-- Stamp Dock -->
<VotingDock />
</div>
</template>
<style lang="scss" scoped>
@use '../assets/styles/variables.scss' as *;
.voting-page {
min-height: 100vh;
background: $color-bg-primary;
padding-bottom: 120px; // Space for dock
}
.page-header {
background: $color-surface-glass;
backdrop-filter: $backdrop-blur;
-webkit-backdrop-filter: $backdrop-blur;
padding: $spacing-lg;
padding-top: calc(env(safe-area-inset-top) + #{$spacing-lg});
color: $color-text-inverse;
position: sticky;
top: 0;
z-index: $z-index-sticky;
}
.page-title {
font-size: $font-size-2xl;
font-weight: bold;
margin-bottom: $spacing-xs;
}
.page-subtitle {
font-size: $font-size-sm;
opacity: 0.9;
}
.program-list {
padding: $spacing-md;
display: flex;
flex-direction: column;
gap: $spacing-sm;
}
</style>