Files
company-celebration/packages/client-screen/src/views/AdminLogin.vue
empty 30cd29d45d 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>
2026-01-15 15:34:37 +08:00

182 lines
3.3 KiB
Vue

<script setup lang="ts">
import { ref } from 'vue';
import { useRouter, useRoute } from 'vue-router';
import { ADMIN_TOKEN_KEY, ADMIN_ACCESS_CODE, generateToken } from '../router';
const router = useRouter();
const route = useRoute();
const accessCode = ref('');
const error = ref('');
const isLoading = ref(false);
async function handleLogin() {
error.value = '';
if (!accessCode.value.trim()) {
error.value = '请输入访问码';
return;
}
isLoading.value = true;
// Simulate network delay for UX
await new Promise(resolve => setTimeout(resolve, 500));
if (accessCode.value === ADMIN_ACCESS_CODE) {
// Save token to localStorage
localStorage.setItem(ADMIN_TOKEN_KEY, generateToken(ADMIN_ACCESS_CODE));
// Redirect to console or original destination
const redirect = route.query.redirect as string || '/admin/director-console';
router.push(redirect);
} else {
error.value = '访问码错误';
accessCode.value = '';
}
isLoading.value = false;
}
function handleKeydown(e: KeyboardEvent) {
if (e.key === 'Enter') {
handleLogin();
}
}
</script>
<template>
<div class="admin-login">
<div class="login-box">
<div class="lock-icon">🔒</div>
<h1 class="title">导演控制台</h1>
<p class="subtitle">请输入访问码</p>
<div class="input-group">
<input
v-model="accessCode"
type="password"
placeholder="访问码"
autocomplete="off"
:disabled="isLoading"
@keydown="handleKeydown"
/>
</div>
<div v-if="error" class="error-message">
{{ error }}
</div>
<button
class="login-btn"
:disabled="isLoading"
@click="handleLogin"
>
{{ isLoading ? '验证中...' : '进入控制台' }}
</button>
<p class="hint">仅限活动导演使用</p>
</div>
</div>
</template>
<style lang="scss" scoped>
.admin-login {
width: 100%;
height: 100%;
background: #0a0a0a;
display: flex;
align-items: center;
justify-content: center;
}
.login-box {
width: 360px;
padding: 40px;
text-align: center;
}
.lock-icon {
font-size: 48px;
margin-bottom: 20px;
opacity: 0.6;
}
.title {
font-size: 24px;
font-weight: 600;
color: #e0e0e0;
margin: 0 0 8px 0;
}
.subtitle {
font-size: 14px;
color: #666;
margin: 0 0 32px 0;
}
.input-group {
margin-bottom: 16px;
input {
width: 100%;
padding: 14px 16px;
font-size: 16px;
background: #1a1a1a;
border: 1px solid #333;
border-radius: 8px;
color: #e0e0e0;
text-align: center;
letter-spacing: 4px;
&:focus {
outline: none;
border-color: #3b82f6;
}
&:disabled {
opacity: 0.5;
}
&::placeholder {
letter-spacing: normal;
color: #555;
}
}
}
.error-message {
color: #ef4444;
font-size: 14px;
margin-bottom: 16px;
}
.login-btn {
width: 100%;
padding: 14px;
font-size: 16px;
font-weight: 500;
background: #3b82f6;
border: none;
border-radius: 8px;
color: white;
cursor: pointer;
transition: background 0.2s;
&:hover:not(:disabled) {
background: #2563eb;
}
&:disabled {
opacity: 0.6;
cursor: not-allowed;
}
}
.hint {
font-size: 12px;
color: #444;
margin: 24px 0 0 0;
}
</style>