feat: initialize Annual Gala Interactive System monorepo
- Set up pnpm workspace with 4 packages: shared, server, client-mobile, client-screen - Implement Redis atomic voting with Lua scripts (HINCRBY + distributed lock) - Add optimistic UI with IndexedDB queue for offline resilience - Configure Socket.io with auto-reconnection (infinite retries) - Separate mobile (Vant) and big screen (Pixi.js) dependencies Tech stack: - Frontend Mobile: Vue 3 + Vant + Socket.io-client - Frontend Screen: Vue 3 + Pixi.js + GSAP - Backend: Express + Socket.io + Redis + Prisma/MySQL Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
121
packages/client-screen/src/views/AdminControl.vue
Normal file
121
packages/client-screen/src/views/AdminControl.vue
Normal file
@@ -0,0 +1,121 @@
|
||||
<script setup lang="ts">
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
function goBack() {
|
||||
router.push('/');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="admin-control">
|
||||
<header class="header">
|
||||
<button class="back-btn" @click="goBack">← 返回</button>
|
||||
<h1 class="title">管理控制台</h1>
|
||||
</header>
|
||||
|
||||
<main class="content">
|
||||
<div class="control-panel">
|
||||
<h2>抽奖控制</h2>
|
||||
<div class="controls">
|
||||
<button class="control-btn">开始抽奖</button>
|
||||
<button class="control-btn danger">停止抽奖</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-panel">
|
||||
<h2>显示模式</h2>
|
||||
<div class="controls">
|
||||
<button class="control-btn">待机画面</button>
|
||||
<button class="control-btn">投票结果</button>
|
||||
<button class="control-btn">抽奖画面</button>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '../assets/styles/variables.scss';
|
||||
|
||||
.admin-control {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #1a1a1a;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
padding: 20px 30px;
|
||||
border-bottom: 1px solid #333;
|
||||
|
||||
.back-btn {
|
||||
background: none;
|
||||
border: 1px solid #666;
|
||||
color: #999;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
border-color: #999;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 30px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 30px;
|
||||
}
|
||||
|
||||
.control-panel {
|
||||
background: #222;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
|
||||
h2 {
|
||||
font-size: 18px;
|
||||
margin-bottom: 16px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.control-btn {
|
||||
padding: 12px 24px;
|
||||
background: #333;
|
||||
border: 1px solid #444;
|
||||
color: white;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: #444;
|
||||
}
|
||||
|
||||
&.danger {
|
||||
border-color: #ff4d4f;
|
||||
color: #ff4d4f;
|
||||
|
||||
&:hover {
|
||||
background: rgba(#ff4d4f, 0.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
326
packages/client-screen/src/views/LuckyDrawView.vue
Normal file
326
packages/client-screen/src/views/LuckyDrawView.vue
Normal file
@@ -0,0 +1,326 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useDisplayStore } from '../stores/display';
|
||||
|
||||
const router = useRouter();
|
||||
const displayStore = useDisplayStore();
|
||||
|
||||
// Draw state
|
||||
const isSpinning = ref(false);
|
||||
const spinningNames = ref<string[]>([]);
|
||||
const winner = ref<{ name: string; department: string } | null>(null);
|
||||
|
||||
// Mock participants for demo
|
||||
const mockParticipants = [
|
||||
{ name: '张三', department: '技术部' },
|
||||
{ name: '李四', department: '产品部' },
|
||||
{ name: '王五', department: '设计部' },
|
||||
{ name: '赵六', department: '市场部' },
|
||||
{ name: '钱七', department: '运营部' },
|
||||
{ name: '孙八', department: '人事部' },
|
||||
{ name: '周九', department: '财务部' },
|
||||
{ name: '吴十', department: '销售部' },
|
||||
];
|
||||
|
||||
let spinInterval: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
function startDraw() {
|
||||
if (isSpinning.value) return;
|
||||
|
||||
isSpinning.value = true;
|
||||
winner.value = null;
|
||||
|
||||
// Start spinning animation
|
||||
let speed = 50;
|
||||
let index = 0;
|
||||
|
||||
spinInterval = setInterval(() => {
|
||||
spinningNames.value = [
|
||||
mockParticipants[(index) % mockParticipants.length].name,
|
||||
mockParticipants[(index + 1) % mockParticipants.length].name,
|
||||
mockParticipants[(index + 2) % mockParticipants.length].name,
|
||||
];
|
||||
index++;
|
||||
}, speed);
|
||||
|
||||
// Slow down and stop after 5 seconds
|
||||
setTimeout(() => {
|
||||
slowDownAndStop();
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
function slowDownAndStop() {
|
||||
if (spinInterval) {
|
||||
clearInterval(spinInterval);
|
||||
}
|
||||
|
||||
// Pick random winner
|
||||
const winnerIndex = Math.floor(Math.random() * mockParticipants.length);
|
||||
winner.value = mockParticipants[winnerIndex];
|
||||
isSpinning.value = false;
|
||||
spinningNames.value = [];
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
router.push('/');
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
if (spinInterval) {
|
||||
clearInterval(spinInterval);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="lucky-draw-view">
|
||||
<!-- Header -->
|
||||
<header class="header">
|
||||
<button class="back-btn" @click="goBack">← 返回</button>
|
||||
<h1 class="title gold-text">幸运抽奖</h1>
|
||||
<div class="online">{{ displayStore.onlineUsers }} 人在线</div>
|
||||
</header>
|
||||
|
||||
<!-- Main draw area -->
|
||||
<main class="draw-area">
|
||||
<!-- Prize display -->
|
||||
<div class="prize-section">
|
||||
<div class="prize-badge">
|
||||
<span class="level">特等奖</span>
|
||||
<span class="name">iPhone 16 Pro Max</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Spinning names -->
|
||||
<div class="spin-section">
|
||||
<div v-if="isSpinning" class="spinning-names">
|
||||
<div v-for="(name, i) in spinningNames" :key="i" class="name-item">
|
||||
{{ name }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Winner display -->
|
||||
<div v-else-if="winner" class="winner-display">
|
||||
<div class="winner-card">
|
||||
<div class="avatar">🎉</div>
|
||||
<div class="info">
|
||||
<h2 class="name gold-text">{{ winner.name }}</h2>
|
||||
<p class="department">{{ winner.department }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<p class="congrats">恭喜中奖!</p>
|
||||
</div>
|
||||
|
||||
<!-- Idle state -->
|
||||
<div v-else class="idle-state">
|
||||
<p>点击下方按钮开始抽奖</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Draw button -->
|
||||
<div class="action-section">
|
||||
<button
|
||||
class="draw-btn"
|
||||
:class="{ spinning: isSpinning }"
|
||||
:disabled="isSpinning"
|
||||
@click="startDraw"
|
||||
>
|
||||
{{ isSpinning ? '抽奖中...' : '开始抽奖' }}
|
||||
</button>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '../assets/styles/variables.scss';
|
||||
|
||||
.lucky-draw-view {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: $color-bg-gradient;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.online {
|
||||
color: $color-text-muted;
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.draw-area {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 50px;
|
||||
}
|
||||
|
||||
.prize-section {
|
||||
margin-bottom: 60px;
|
||||
|
||||
.prize-badge {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 30px 60px;
|
||||
border: 2px solid $color-gold;
|
||||
border-radius: 16px;
|
||||
background: rgba($color-gold, 0.1);
|
||||
|
||||
.level {
|
||||
font-size: 24px;
|
||||
color: $color-gold;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-size: 36px;
|
||||
color: $color-text-light;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.spin-section {
|
||||
min-height: 300px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-bottom: 60px;
|
||||
|
||||
.spinning-names {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
|
||||
.name-item {
|
||||
font-size: 72px;
|
||||
color: $color-text-light;
|
||||
text-align: center;
|
||||
animation: flash 0.1s infinite;
|
||||
|
||||
&:nth-child(2) {
|
||||
font-size: 96px;
|
||||
color: $color-gold;
|
||||
text-shadow: $glow-gold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.winner-display {
|
||||
text-align: center;
|
||||
|
||||
.winner-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
padding: 40px 80px;
|
||||
border: 3px solid $color-gold;
|
||||
border-radius: 20px;
|
||||
background: rgba($color-gold, 0.1);
|
||||
animation: scale-pulse 1s ease-in-out infinite;
|
||||
|
||||
.avatar {
|
||||
font-size: 80px;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-size: 64px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.department {
|
||||
font-size: 24px;
|
||||
color: $color-text-muted;
|
||||
}
|
||||
}
|
||||
|
||||
.congrats {
|
||||
margin-top: 30px;
|
||||
font-size: 36px;
|
||||
color: $color-primary-light;
|
||||
text-shadow: $glow-red;
|
||||
}
|
||||
}
|
||||
|
||||
.idle-state {
|
||||
font-size: 24px;
|
||||
color: $color-text-muted;
|
||||
}
|
||||
}
|
||||
|
||||
.action-section {
|
||||
.draw-btn {
|
||||
padding: 20px 80px;
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
color: $color-text-light;
|
||||
background: linear-gradient(135deg, $color-primary-light 0%, $color-primary 100%);
|
||||
border: none;
|
||||
border-radius: 50px;
|
||||
cursor: pointer;
|
||||
transition: all $transition-normal;
|
||||
box-shadow: $glow-red;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.7;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&.spinning {
|
||||
animation: glow-pulse 0.5s infinite;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes flash {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.7; }
|
||||
}
|
||||
|
||||
@keyframes scale-pulse {
|
||||
0%, 100% { transform: scale(1); }
|
||||
50% { transform: scale(1.02); }
|
||||
}
|
||||
|
||||
@keyframes glow-pulse {
|
||||
0%, 100% { box-shadow: $glow-red; }
|
||||
50% { box-shadow: 0 0 40px rgba($color-primary, 0.8), 0 0 80px rgba($color-primary, 0.5); }
|
||||
}
|
||||
</style>
|
||||
248
packages/client-screen/src/views/MainDisplay.vue
Normal file
248
packages/client-screen/src/views/MainDisplay.vue
Normal file
@@ -0,0 +1,248 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useDisplayStore } from '../stores/display';
|
||||
|
||||
const router = useRouter();
|
||||
const displayStore = useDisplayStore();
|
||||
|
||||
const currentTime = ref(new Date().toLocaleTimeString('zh-CN'));
|
||||
|
||||
// Update time every second
|
||||
onMounted(() => {
|
||||
setInterval(() => {
|
||||
currentTime.value = new Date().toLocaleTimeString('zh-CN');
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
function goToDraw() {
|
||||
router.push('/draw');
|
||||
}
|
||||
|
||||
function goToResults() {
|
||||
router.push('/results');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="main-display">
|
||||
<!-- Background particles will be added via Pixi.js -->
|
||||
<div class="background-overlay"></div>
|
||||
|
||||
<!-- Header -->
|
||||
<header class="header">
|
||||
<div class="logo">
|
||||
<span class="year gold-text">2026</span>
|
||||
<span class="title">年会盛典</span>
|
||||
</div>
|
||||
<div class="status">
|
||||
<span class="online-count">
|
||||
<span class="dot"></span>
|
||||
{{ displayStore.onlineUsers }} 人在线
|
||||
</span>
|
||||
<span class="time">{{ currentTime }}</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Main content -->
|
||||
<main class="content">
|
||||
<div class="welcome-section">
|
||||
<h1 class="main-title">
|
||||
<span class="gold-text">马到成功</span>
|
||||
</h1>
|
||||
<p class="subtitle">2026 年度盛典</p>
|
||||
</div>
|
||||
|
||||
<!-- Action buttons -->
|
||||
<div class="actions">
|
||||
<button class="action-btn draw-btn" @click="goToDraw">
|
||||
<span class="icon">🎁</span>
|
||||
<span class="text">幸运抽奖</span>
|
||||
</button>
|
||||
<button class="action-btn results-btn" @click="goToResults">
|
||||
<span class="icon">📊</span>
|
||||
<span class="text">投票结果</span>
|
||||
</button>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Decorative elements -->
|
||||
<div class="decoration left-lantern"></div>
|
||||
<div class="decoration right-lantern"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '../assets/styles/variables.scss';
|
||||
|
||||
.main-display {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: $color-bg-gradient;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.background-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: radial-gradient(ellipse at center, transparent 0%, rgba(0, 0, 0, 0.5) 100%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 30px 50px;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
|
||||
.logo {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 16px;
|
||||
|
||||
.year {
|
||||
font-size: 48px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 24px;
|
||||
color: $color-text-light;
|
||||
letter-spacing: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 30px;
|
||||
font-size: 18px;
|
||||
color: $color-text-muted;
|
||||
|
||||
.online-count {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background: #52c41a;
|
||||
border-radius: 50%;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.welcome-section {
|
||||
text-align: center;
|
||||
margin-bottom: 80px;
|
||||
|
||||
.main-title {
|
||||
font-size: 120px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 20px;
|
||||
text-shadow: $glow-gold;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 36px;
|
||||
color: $color-text-muted;
|
||||
letter-spacing: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 60px;
|
||||
|
||||
.action-btn {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 40px 60px;
|
||||
border: 2px solid $color-gold;
|
||||
border-radius: 20px;
|
||||
background: rgba($color-gold, 0.1);
|
||||
cursor: pointer;
|
||||
transition: all $transition-normal;
|
||||
|
||||
&:hover {
|
||||
background: rgba($color-gold, 0.2);
|
||||
transform: translateY(-5px);
|
||||
box-shadow: $glow-gold;
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 64px;
|
||||
}
|
||||
|
||||
.text {
|
||||
font-size: 24px;
|
||||
color: $color-gold;
|
||||
letter-spacing: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.draw-btn {
|
||||
border-color: $color-primary;
|
||||
background: rgba($color-primary, 0.1);
|
||||
|
||||
&:hover {
|
||||
background: rgba($color-primary, 0.2);
|
||||
box-shadow: $glow-red;
|
||||
}
|
||||
|
||||
.text {
|
||||
color: $color-primary-light;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.decoration {
|
||||
position: absolute;
|
||||
width: 80px;
|
||||
height: 120px;
|
||||
background: linear-gradient(180deg, $color-primary 0%, $color-primary-dark 100%);
|
||||
border-radius: 50% 50% 45% 45%;
|
||||
opacity: 0.6;
|
||||
|
||||
&.left-lantern {
|
||||
top: 100px;
|
||||
left: 50px;
|
||||
animation: float 4s ease-in-out infinite;
|
||||
}
|
||||
|
||||
&.right-lantern {
|
||||
top: 100px;
|
||||
right: 50px;
|
||||
animation: float 4s ease-in-out infinite 1s;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%, 100% { transform: translateY(0) rotate(-3deg); }
|
||||
50% { transform: translateY(-15px) rotate(3deg); }
|
||||
}
|
||||
</style>
|
||||
201
packages/client-screen/src/views/VoteResultsView.vue
Normal file
201
packages/client-screen/src/views/VoteResultsView.vue
Normal file
@@ -0,0 +1,201 @@
|
||||
<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>
|
||||
@import '../assets/styles/variables.scss';
|
||||
|
||||
.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>
|
||||
Reference in New Issue
Block a user