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:
empty
2026-01-15 01:19:36 +08:00
commit e7397d22a9
74 changed files with 14088 additions and 0 deletions

View File

@@ -0,0 +1,275 @@
<script setup lang="ts">
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import { useConnectionStore } from '../stores/connection';
import { showToast } from 'vant';
const router = useRouter();
const connectionStore = useConnectionStore();
const userName = ref('');
const isLoading = ref(false);
async function handleEnter() {
if (!userName.value.trim()) {
showToast('请输入您的姓名');
return;
}
isLoading.value = true;
// Generate a simple user ID (in production, this would come from auth)
const odrawId = `user_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
connectionStore.setUser(odrawId, userName.value.trim());
// Wait for connection
await new Promise((resolve) => setTimeout(resolve, 500));
isLoading.value = false;
router.push('/vote');
}
</script>
<template>
<div class="home-view safe-area-top safe-area-bottom">
<!-- Header decoration -->
<div class="header-decoration">
<div class="lantern left"></div>
<div class="lantern right"></div>
</div>
<!-- Main content -->
<div class="content">
<div class="logo-section">
<div class="year-badge">
<span class="year">2026</span>
<span class="zodiac">马年</span>
</div>
<h1 class="title gold-text">年会互动系统</h1>
<p class="subtitle">投票 · 抽奖 · 互动</p>
</div>
<div class="form-section">
<div class="input-wrapper guochao-border">
<van-field
v-model="userName"
placeholder="请输入您的姓名"
:border="false"
clearable
maxlength="20"
@keyup.enter="handleEnter"
/>
</div>
<van-button
class="enter-btn"
type="primary"
block
round
:loading="isLoading"
loading-text="进入中..."
@click="handleEnter"
>
进入年会
</van-button>
</div>
<!-- Features -->
<div class="features">
<div class="feature-item">
<van-icon name="like-o" size="24" color="#c41230" />
<span>投票评选</span>
</div>
<div class="feature-item">
<van-icon name="gift-o" size="24" color="#d4a84b" />
<span>幸运抽奖</span>
</div>
<div class="feature-item">
<van-icon name="chart-trending-o" size="24" color="#c41230" />
<span>实时结果</span>
</div>
</div>
</div>
<!-- Footer -->
<div class="footer">
<p>© 2026 公司年会</p>
</div>
</div>
</template>
<style lang="scss" scoped>
@import '../assets/styles/variables.scss';
.home-view {
min-height: 100vh;
display: flex;
flex-direction: column;
background: linear-gradient(180deg, #fff5f5 0%, #fef8f0 50%, #ffffff 100%);
position: relative;
overflow: hidden;
}
.header-decoration {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 120px;
pointer-events: none;
.lantern {
position: absolute;
top: -20px;
width: 40px;
height: 60px;
background: linear-gradient(180deg, $color-primary 0%, $color-primary-dark 100%);
border-radius: 50% 50% 45% 45%;
box-shadow: 0 4px 12px rgba($color-primary, 0.3);
&::before {
content: '';
position: absolute;
top: -8px;
left: 50%;
transform: translateX(-50%);
width: 16px;
height: 12px;
background: $color-gold;
border-radius: 2px 2px 0 0;
}
&::after {
content: '';
position: absolute;
bottom: -15px;
left: 50%;
transform: translateX(-50%);
width: 2px;
height: 15px;
background: $color-gold;
}
&.left {
left: 20px;
animation: swing 3s ease-in-out infinite;
}
&.right {
right: 20px;
animation: swing 3s ease-in-out infinite 0.5s;
}
}
}
@keyframes swing {
0%,
100% {
transform: rotate(-5deg);
}
50% {
transform: rotate(5deg);
}
}
.content {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
padding: $spacing-xl $spacing-lg;
padding-top: 100px;
}
.logo-section {
text-align: center;
margin-bottom: $spacing-xl;
.year-badge {
display: inline-flex;
flex-direction: column;
align-items: center;
background: linear-gradient(135deg, $color-primary 0%, $color-primary-dark 100%);
color: $color-text-inverse;
padding: $spacing-sm $spacing-lg;
border-radius: $radius-lg;
margin-bottom: $spacing-md;
box-shadow: $shadow-md;
.year {
font-size: $font-size-2xl;
font-weight: bold;
}
.zodiac {
font-size: $font-size-sm;
opacity: 0.9;
}
}
.title {
font-size: 32px;
font-weight: bold;
margin-bottom: $spacing-sm;
}
.subtitle {
color: $color-text-secondary;
font-size: $font-size-md;
}
}
.form-section {
margin-bottom: $spacing-xl;
.input-wrapper {
background: $color-bg-card;
margin-bottom: $spacing-md;
padding: $spacing-xs;
:deep(.van-field) {
background: transparent;
.van-field__control {
text-align: center;
font-size: $font-size-lg;
}
}
}
.enter-btn {
background: linear-gradient(135deg, $color-primary-light 0%, $color-primary 100%);
border: none;
height: 48px;
font-size: $font-size-lg;
font-weight: 500;
box-shadow: $shadow-md;
}
}
.features {
display: flex;
justify-content: space-around;
padding: $spacing-md 0;
.feature-item {
display: flex;
flex-direction: column;
align-items: center;
gap: $spacing-xs;
span {
font-size: $font-size-sm;
color: $color-text-secondary;
}
}
}
.footer {
text-align: center;
padding: $spacing-md;
color: $color-text-muted;
font-size: $font-size-xs;
}
</style>