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>
182 lines
3.3 KiB
Vue
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>
|