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:
275
packages/client-mobile/src/views/HomeView.vue
Normal file
275
packages/client-mobile/src/views/HomeView.vue
Normal 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>
|
||||
Reference in New Issue
Block a user