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>
202 lines
4.1 KiB
Vue
202 lines
4.1 KiB
Vue
<script setup lang="ts">
|
|
import { useRouter } from 'vue-router';
|
|
|
|
const router = useRouter();
|
|
|
|
function goBack() {
|
|
router.push('/');
|
|
}
|
|
|
|
// Mock vote results
|
|
const categories = [
|
|
{
|
|
name: '最佳员工',
|
|
results: [
|
|
{ name: '张三', votes: 45, percentage: 30 },
|
|
{ name: '李四', votes: 38, percentage: 25 },
|
|
{ name: '王五', votes: 32, percentage: 21 },
|
|
],
|
|
},
|
|
{
|
|
name: '最佳团队',
|
|
results: [
|
|
{ name: '技术一组', votes: 52, percentage: 35 },
|
|
{ name: '产品组', votes: 41, percentage: 27 },
|
|
{ name: '设计组', votes: 35, percentage: 23 },
|
|
],
|
|
},
|
|
];
|
|
</script>
|
|
|
|
<template>
|
|
<div class="vote-results-view">
|
|
<!-- Header -->
|
|
<header class="header">
|
|
<button class="back-btn" @click="goBack">← 返回</button>
|
|
<h1 class="title gold-text">投票结果</h1>
|
|
<div class="placeholder"></div>
|
|
</header>
|
|
|
|
<!-- Results grid -->
|
|
<main class="results-grid">
|
|
<div v-for="category in categories" :key="category.name" class="category-card">
|
|
<h2 class="category-name">{{ category.name }}</h2>
|
|
<div class="results-list">
|
|
<div
|
|
v-for="(result, index) in category.results"
|
|
:key="result.name"
|
|
class="result-item"
|
|
:class="{ winner: index === 0 }"
|
|
>
|
|
<span class="rank">{{ index + 1 }}</span>
|
|
<span class="name">{{ result.name }}</span>
|
|
<div class="bar-container">
|
|
<div class="bar" :style="{ width: result.percentage + '%' }"></div>
|
|
</div>
|
|
<span class="votes">{{ result.votes }}票</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
</div>
|
|
</template>
|
|
|
|
<style lang="scss" scoped>
|
|
@use '../assets/styles/variables.scss' as *;
|
|
|
|
.vote-results-view {
|
|
width: 100%;
|
|
height: 100%;
|
|
background: $color-bg-gradient;
|
|
display: flex;
|
|
flex-direction: column;
|
|
overflow: auto;
|
|
}
|
|
|
|
.header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 30px 50px;
|
|
|
|
.back-btn {
|
|
background: none;
|
|
border: 1px solid $color-gold;
|
|
color: $color-gold;
|
|
padding: 10px 20px;
|
|
border-radius: 8px;
|
|
cursor: pointer;
|
|
font-size: 16px;
|
|
transition: all $transition-fast;
|
|
|
|
&:hover {
|
|
background: rgba($color-gold, 0.1);
|
|
}
|
|
}
|
|
|
|
.title {
|
|
font-size: 48px;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.placeholder {
|
|
width: 100px;
|
|
}
|
|
}
|
|
|
|
.results-grid {
|
|
flex: 1;
|
|
display: grid;
|
|
grid-template-columns: repeat(2, 1fr);
|
|
gap: 40px;
|
|
padding: 40px 50px;
|
|
}
|
|
|
|
.category-card {
|
|
background: rgba(255, 255, 255, 0.05);
|
|
border: 1px solid rgba($color-gold, 0.3);
|
|
border-radius: 16px;
|
|
padding: 30px;
|
|
|
|
.category-name {
|
|
font-size: 28px;
|
|
color: $color-gold;
|
|
margin-bottom: 24px;
|
|
text-align: center;
|
|
}
|
|
|
|
.results-list {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 16px;
|
|
}
|
|
|
|
.result-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 16px;
|
|
padding: 16px;
|
|
background: rgba(255, 255, 255, 0.03);
|
|
border-radius: 8px;
|
|
|
|
&.winner {
|
|
background: rgba($color-gold, 0.1);
|
|
border: 1px solid $color-gold;
|
|
|
|
.rank {
|
|
background: $color-gold;
|
|
color: #000;
|
|
}
|
|
|
|
.name {
|
|
color: $color-gold;
|
|
}
|
|
|
|
.bar {
|
|
background: linear-gradient(90deg, $color-gold-dark, $color-gold);
|
|
}
|
|
}
|
|
|
|
.rank {
|
|
width: 32px;
|
|
height: 32px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
background: rgba(255, 255, 255, 0.1);
|
|
border-radius: 50%;
|
|
font-size: 16px;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.name {
|
|
width: 120px;
|
|
font-size: 20px;
|
|
color: $color-text-light;
|
|
}
|
|
|
|
.bar-container {
|
|
flex: 1;
|
|
height: 24px;
|
|
background: rgba(255, 255, 255, 0.1);
|
|
border-radius: 12px;
|
|
overflow: hidden;
|
|
|
|
.bar {
|
|
height: 100%;
|
|
background: linear-gradient(90deg, $color-primary-dark, $color-primary);
|
|
border-radius: 12px;
|
|
transition: width 1s ease;
|
|
}
|
|
}
|
|
|
|
.votes {
|
|
width: 60px;
|
|
text-align: right;
|
|
font-size: 18px;
|
|
color: $color-text-muted;
|
|
}
|
|
}
|
|
}
|
|
</style>
|