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:
181
packages/client-screen/src/views/AdminLogin.vue
Normal file
181
packages/client-screen/src/views/AdminLogin.vue
Normal file
@@ -0,0 +1,181 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user