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

@@ -1,29 +1,96 @@
import { createRouter, createWebHistory } from 'vue-router';
import { createRouter, createWebHistory, type RouteLocationNormalized } from 'vue-router';
// Admin auth constants
const ADMIN_TOKEN_KEY = 'gala_admin_token';
const ADMIN_ACCESS_CODE = '20268888';
// Auth guard for admin routes
function requireAdminAuth(to: RouteLocationNormalized) {
const token = localStorage.getItem(ADMIN_TOKEN_KEY);
if (!token || token !== generateToken(ADMIN_ACCESS_CODE)) {
return { path: '/admin/login', query: { redirect: to.fullPath } };
}
return true;
}
// Simple token generator (not cryptographically secure, but sufficient for internal event)
function generateToken(code: string): string {
return btoa(`gala2026:${code}:${code.split('').reverse().join('')}`);
}
// Export for use in login component
export { ADMIN_TOKEN_KEY, ADMIN_ACCESS_CODE, generateToken };
const router = createRouter({
history: createWebHistory(),
routes: [
// ============================================
// Big Screen Display Routes (LED PC)
// ============================================
{
path: '/',
name: 'main',
name: 'screen-main',
component: () => import('../views/MainDisplay.vue'),
meta: { title: '年会大屏 - 主页' },
},
{
path: '/draw',
name: 'draw',
path: '/screen/voting',
name: 'screen-voting',
component: () => import('../views/LiveVotingView.vue'),
meta: { title: '年会大屏 - 实时投票' },
},
{
path: '/screen/draw',
name: 'screen-draw',
component: () => import('../views/LuckyDrawView.vue'),
meta: { title: '年会大屏 - 幸运抽奖' },
},
{
path: '/results',
name: 'results',
path: '/screen/results',
name: 'screen-results',
component: () => import('../views/VoteResultsView.vue'),
meta: { title: '年会大屏 - 投票结果' },
},
// Legacy routes (redirect to new paths)
{ path: '/voting', redirect: '/screen/voting' },
{ path: '/draw', redirect: '/screen/draw' },
{ path: '/results', redirect: '/screen/results' },
// ============================================
// Admin Routes (Director Console)
// ============================================
{
path: '/admin/login',
name: 'admin-login',
component: () => import('../views/AdminLogin.vue'),
meta: { title: '管理员登录' },
},
{
path: '/admin',
name: 'admin',
path: '/admin/director-console',
name: 'admin-console',
component: () => import('../views/AdminControl.vue'),
meta: { title: '导演控制台' },
beforeEnter: requireAdminAuth,
},
// Legacy admin route (redirect)
{ path: '/admin', redirect: '/admin/director-console' },
// ============================================
// 404 Catch-all
// ============================================
{
path: '/:pathMatch(.*)*',
name: 'not-found',
redirect: '/',
},
],
});
// Update document title on navigation
router.afterEach((to) => {
document.title = (to.meta.title as string) || '年会互动系统';
});
export default router;