Merge pull request 'fix/deployment-and-database-integration' (#2) from fix/deployment-and-database-integration into main
Reviewed-on: #2
This commit was merged in pull request #2.
This commit is contained in:
@@ -1,6 +1,25 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
mysql:
|
||||
image: mysql:8.0
|
||||
container_name: gala-mysql
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD:-gala_root_pass}
|
||||
- MYSQL_DATABASE=${MYSQL_DATABASE:-gala_db}
|
||||
- MYSQL_USER=${MYSQL_USER:-gala_user}
|
||||
- MYSQL_PASSWORD=${MYSQL_PASSWORD:-gala_pass}
|
||||
volumes:
|
||||
- mysql_data:/var/lib/mysql
|
||||
networks:
|
||||
- gala-network
|
||||
healthcheck:
|
||||
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
container_name: gala-redis
|
||||
@@ -22,8 +41,12 @@ services:
|
||||
- REDIS_HOST=redis
|
||||
- REDIS_PORT=6379
|
||||
- CORS_ORIGINS=${CORS_ORIGINS:-*}
|
||||
- DATABASE_URL=mysql://${MYSQL_USER:-gala_user}:${MYSQL_PASSWORD:-gala_pass}@mysql:3306/${MYSQL_DATABASE:-gala_db}
|
||||
depends_on:
|
||||
- redis
|
||||
mysql:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_started
|
||||
networks:
|
||||
- gala-network
|
||||
|
||||
@@ -49,6 +72,7 @@ services:
|
||||
- gala-network
|
||||
|
||||
volumes:
|
||||
mysql_data:
|
||||
redis_data:
|
||||
caddy_data:
|
||||
caddy_config:
|
||||
|
||||
@@ -1,295 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { useVotingStore, TICKET_INFO } from '../stores/voting';
|
||||
import { TICKET_TYPES, type TicketType } from '@gala/shared/constants';
|
||||
|
||||
const votingStore = useVotingStore();
|
||||
|
||||
const stamps = computed(() => {
|
||||
return TICKET_TYPES.map((type) => ({
|
||||
type,
|
||||
...TICKET_INFO[type],
|
||||
isUsed: votingStore.tickets[type] !== null,
|
||||
isSelected: votingStore.selectedStamp === type,
|
||||
}));
|
||||
});
|
||||
|
||||
function handleStampClick(type: TicketType) {
|
||||
if (votingStore.tickets[type] !== null) return; // Already used
|
||||
|
||||
if (votingStore.selectedStamp === type) {
|
||||
votingStore.deselectStamp();
|
||||
} else {
|
||||
votingStore.selectStamp(type);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="stamp-dock">
|
||||
<!-- Wooden Tray -->
|
||||
<div class="wooden-tray">
|
||||
<!-- Tray Edge (Top) -->
|
||||
<div class="tray-edge tray-edge-top"></div>
|
||||
|
||||
<!-- Stamp Handles -->
|
||||
<div class="stamp-tray">
|
||||
<div
|
||||
v-for="stamp in stamps"
|
||||
:key="stamp.type"
|
||||
class="stamp-handle"
|
||||
:class="{
|
||||
'is-used': stamp.isUsed,
|
||||
'is-selected': stamp.isSelected,
|
||||
}"
|
||||
@click="handleStampClick(stamp.type)"
|
||||
>
|
||||
<!-- Handle Body (Wood) -->
|
||||
<div class="handle-body">
|
||||
<div class="handle-grip"></div>
|
||||
</div>
|
||||
|
||||
<!-- Rubber Base (Shows ink design) -->
|
||||
<div class="rubber-base">
|
||||
<span class="rubber-icon">{{ stamp.icon }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Label -->
|
||||
<span class="stamp-label">{{ stamp.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tray Edge (Bottom) -->
|
||||
<div class="tray-edge tray-edge-bottom"></div>
|
||||
</div>
|
||||
|
||||
<!-- Hint Text -->
|
||||
<div v-if="votingStore.isStampSelected" class="dock-hint">
|
||||
<span class="hint-icon">👆</span>
|
||||
<span class="hint-text">点击节目卡片盖章</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@use 'sass:color';
|
||||
@use '../assets/styles/variables.scss' as *;
|
||||
|
||||
// Wood colors
|
||||
$wood-light: #d4a574;
|
||||
$wood-medium: #b8864e;
|
||||
$wood-dark: #8b5a2b;
|
||||
$wood-grain: #a0693a;
|
||||
|
||||
.stamp-dock {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: $z-index-fixed;
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
|
||||
.wooden-tray {
|
||||
background: linear-gradient(180deg, $wood-light 0%, $wood-medium 50%, $wood-dark 100%);
|
||||
border-top: 3px solid $wood-dark;
|
||||
position: relative;
|
||||
|
||||
// Wood grain texture
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-image: repeating-linear-gradient(
|
||||
90deg,
|
||||
transparent,
|
||||
transparent 2px,
|
||||
rgba($wood-grain, 0.1) 2px,
|
||||
rgba($wood-grain, 0.1) 4px
|
||||
);
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.tray-edge {
|
||||
height: 8px;
|
||||
background: linear-gradient(90deg, $wood-dark, $wood-medium, $wood-dark);
|
||||
|
||||
&.tray-edge-top {
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
&.tray-edge-bottom {
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.stamp-tray {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
padding: $spacing-sm $spacing-xs;
|
||||
gap: 4px;
|
||||
overflow-x: auto;
|
||||
scrollbar-width: none;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.stamp-handle {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
cursor: pointer;
|
||||
transition: transform 0.15s ease-out;
|
||||
|
||||
&:active:not(.is-used) {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
&.is-used {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
|
||||
.handle-body {
|
||||
filter: grayscale(0.5);
|
||||
}
|
||||
|
||||
.rubber-base {
|
||||
filter: grayscale(1);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-selected {
|
||||
transform: translateY(-10px);
|
||||
|
||||
.handle-body {
|
||||
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.rubber-base {
|
||||
box-shadow: 0 4px 12px rgba($color-primary, 0.5);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.handle-body {
|
||||
width: 40px;
|
||||
height: 28px;
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
#654321 0%,
|
||||
#8b4513 30%,
|
||||
#a0522d 50%,
|
||||
#8b4513 70%,
|
||||
#654321 100%
|
||||
);
|
||||
border-radius: 6px 6px 2px 2px;
|
||||
position: relative;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
|
||||
transition: box-shadow 0.15s, transform 0.15s;
|
||||
}
|
||||
|
||||
.handle-grip {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 24px;
|
||||
height: 4px;
|
||||
background: linear-gradient(90deg, #5a3a1a, #7a5a3a, #5a3a1a);
|
||||
border-radius: 2px;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 20px;
|
||||
height: 3px;
|
||||
background: linear-gradient(90deg, #5a3a1a, #7a5a3a, #5a3a1a);
|
||||
border-radius: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.rubber-base {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
background: $color-primary;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
|
||||
transition: box-shadow 0.15s;
|
||||
|
||||
// Rubber texture
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: radial-gradient(
|
||||
circle at 30% 30%,
|
||||
rgba(255, 255, 255, 0.1) 0%,
|
||||
transparent 50%
|
||||
);
|
||||
border-radius: 4px;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.rubber-icon {
|
||||
font-size: 20px;
|
||||
filter: brightness(0) invert(1); // White icon (reverse of stamp)
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.stamp-label {
|
||||
font-size: 10px;
|
||||
color: #f5f0e6;
|
||||
font-family: 'Courier New', monospace;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
|
||||
white-space: nowrap;
|
||||
max-width: 48px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.dock-hint {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: $spacing-xs;
|
||||
padding: $spacing-xs;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
animation: hint-pulse 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.hint-icon {
|
||||
font-size: 14px;
|
||||
animation: bounce 1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.hint-text {
|
||||
font-size: $font-size-sm;
|
||||
color: $color-gold;
|
||||
font-family: 'Courier New', monospace;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
@keyframes hint-pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.7; }
|
||||
}
|
||||
|
||||
@keyframes bounce {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-4px); }
|
||||
}
|
||||
</style>
|
||||
@@ -150,7 +150,7 @@ export const useConnectionStore = defineStore('connection', () => {
|
||||
if (data.userTickets) {
|
||||
import('./voting').then(({ useVotingStore }) => {
|
||||
const votingStore = useVotingStore();
|
||||
votingStore.syncFromServer(data.userTickets);
|
||||
votingStore.syncFromServer(data.userTickets as Record<string, string | null>);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -9,6 +9,7 @@ export interface AwardConfig {
|
||||
name: string;
|
||||
icon: string;
|
||||
order: number;
|
||||
remark?: string;
|
||||
}
|
||||
|
||||
// 节目接口
|
||||
|
||||
@@ -37,7 +37,7 @@ onMounted(async () => {
|
||||
<header class="page-header">
|
||||
<h1 class="page-title">节目投票</h1>
|
||||
<p class="page-subtitle">
|
||||
已使用 {{ votingStore.usedTickets.length }}/7 枚印章
|
||||
已使用 {{ votingStore.usedTicketCount }}/{{ votingStore.totalTicketCount }} 枚印章
|
||||
</p>
|
||||
<ConnectionStatus />
|
||||
</header>
|
||||
|
||||
@@ -23,6 +23,9 @@ RUN pnpm build
|
||||
WORKDIR /app/packages/server
|
||||
RUN pnpm build
|
||||
|
||||
# Generate Prisma Client in builder stage
|
||||
RUN npx prisma generate
|
||||
|
||||
# Production stage
|
||||
FROM node:20-alpine AS production
|
||||
|
||||
@@ -39,12 +42,19 @@ COPY --from=builder /app/pnpm-lock.yaml ./
|
||||
COPY --from=builder /app/packages/server/package.json ./packages/server/
|
||||
COPY --from=builder /app/packages/shared ./packages/shared
|
||||
|
||||
# Copy Prisma schema
|
||||
COPY --from=builder /app/packages/server/prisma ./packages/server/prisma
|
||||
|
||||
# Install production dependencies only
|
||||
RUN pnpm install --prod --frozen-lockfile
|
||||
|
||||
# Copy generated Prisma Client from builder stage (pnpm workspace location)
|
||||
COPY --from=builder /app/node_modules/.pnpm ./node_modules/.pnpm
|
||||
|
||||
# Copy built files
|
||||
COPY --from=builder /app/packages/server/dist ./packages/server/dist
|
||||
COPY --from=builder /app/packages/server/src/lua ./packages/server/lua
|
||||
COPY --from=builder /app/packages/server/config ./packages/config
|
||||
|
||||
ENV NODE_ENV=production
|
||||
ENV PORT=3000
|
||||
|
||||
@@ -2,59 +2,59 @@
|
||||
"programs": [
|
||||
{
|
||||
"id": "p1",
|
||||
"name": "龙腾四海",
|
||||
"teamName": "市场部",
|
||||
"performer": "张三、李四",
|
||||
"name": "青苹果乐园",
|
||||
"teamName": "",
|
||||
"performer": "",
|
||||
"order": 1,
|
||||
"remark": "大型民族舞表演,融合了古典与现代元素,展现龙的精神。"
|
||||
"remark": ""
|
||||
},
|
||||
{
|
||||
"id": "p2",
|
||||
"name": "金马奔腾",
|
||||
"teamName": "技术部",
|
||||
"performer": "王五、赵六",
|
||||
"name": "五百年桑田沧海",
|
||||
"teamName": "",
|
||||
"performer": "",
|
||||
"order": 2,
|
||||
"remark": "动感的现代舞,充满力量与节奏感。"
|
||||
"remark": ""
|
||||
},
|
||||
{
|
||||
"id": "p3",
|
||||
"name": "春风得意",
|
||||
"teamName": "人力资源部",
|
||||
"performer": "刘七、陈八",
|
||||
"name": "我的中国心",
|
||||
"teamName": "",
|
||||
"performer": "",
|
||||
"order": 3,
|
||||
"remark": "温馨的情景剧,讲述了职场中的温暖瞬间。"
|
||||
"remark": ""
|
||||
},
|
||||
{
|
||||
"id": "p4",
|
||||
"name": "鸿运当头",
|
||||
"teamName": "财务部",
|
||||
"performer": "周九、吴十",
|
||||
"name": "萍聚",
|
||||
"teamName": "",
|
||||
"performer": "",
|
||||
"order": 4,
|
||||
"remark": "精彩的杂技表演,寓意新年鸿运连连。"
|
||||
"remark": ""
|
||||
},
|
||||
{
|
||||
"id": "p5",
|
||||
"name": "马到成功",
|
||||
"teamName": "运营部",
|
||||
"performer": "郑十一、冯十二",
|
||||
"name": "追光而行,共赴新程",
|
||||
"teamName": "",
|
||||
"performer": "",
|
||||
"order": 5,
|
||||
"remark": "热血沸腾的多人合唱,充满了前进的动力。"
|
||||
"remark": ""
|
||||
},
|
||||
{
|
||||
"id": "p6",
|
||||
"name": "一马当先",
|
||||
"teamName": "产品部",
|
||||
"performer": "孙十三、杨十四",
|
||||
"name": "粉红色的回忆",
|
||||
"teamName": "",
|
||||
"performer": "",
|
||||
"order": 6,
|
||||
"remark": "极具创意的光影秀,探索未来科技的可能。"
|
||||
"remark": ""
|
||||
},
|
||||
{
|
||||
"id": "p7",
|
||||
"name": "万马奔腾",
|
||||
"teamName": "设计部",
|
||||
"performer": "何十五、林十六",
|
||||
"name": "敬业狂想曲",
|
||||
"teamName": "",
|
||||
"performer": "",
|
||||
"order": 7,
|
||||
"remark": "大合唱,展现团队的凝聚力和向心力。"
|
||||
"remark": ""
|
||||
}
|
||||
],
|
||||
"awards": [
|
||||
|
||||
Reference in New Issue
Block a user