- Add PostcardItem.vue component with Chinese postal aesthetics - Add PostcardGrid.vue container with 4x2 CSS Grid layout - Add Postmark.vue component for real-time vote stamp visualization - Update LiveVotingView.vue with cream paper theme (#FDFBF7) - Add Year of the Horse 2026 stamp image - Add responsive breakpoints for different screen sizes - Enhance admin service with program voting control - Add vote stamp accumulation for big screen display Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
467 lines
10 KiB
Vue
467 lines
10 KiB
Vue
<script setup lang="ts">
|
||
import { computed } from 'vue';
|
||
import type { VotingProgram, VoteStamp } from '@gala/shared/types';
|
||
import Postmark from './Postmark.vue';
|
||
|
||
// 票据类型名称映射
|
||
const TICKET_TYPE_NAMES: Record<string, string> = {
|
||
creative: '最佳创意',
|
||
visual: '最佳视觉',
|
||
atmosphere: '最佳氛围',
|
||
performance: '最佳表演',
|
||
teamwork: '最佳团队',
|
||
popularity: '最受欢迎',
|
||
potential: '最具潜力',
|
||
};
|
||
|
||
interface Props {
|
||
program: VotingProgram;
|
||
isFocused: boolean;
|
||
showStamps?: boolean;
|
||
compact?: boolean; // 紧凑模式用于网格布局
|
||
rotateX?: number; // 3D 旋转 X
|
||
rotateY?: number; // 3D 旋转 Y
|
||
rotateZ?: number; // 3D 旋转 Z
|
||
z?: number; // 3D 位移 Z
|
||
index?: number; // 传入索引用于生成邮编
|
||
}
|
||
|
||
const props = withDefaults(defineProps<Props>(), {
|
||
showStamps: true,
|
||
compact: false,
|
||
rotateX: 0,
|
||
rotateY: 0,
|
||
rotateZ: 0,
|
||
z: 0,
|
||
index: 0,
|
||
});
|
||
|
||
// 计算邮政编码 (模拟 202601, 202602...)
|
||
const postcodeDigits = computed(() => {
|
||
const code = (202601 + props.index).toString();
|
||
return code.split('');
|
||
});
|
||
|
||
// 在紧凑模式下不进行缩放和模糊
|
||
const scale = computed(() => props.compact ? 1 : (props.isFocused ? 1 : 0.65));
|
||
const blur = computed(() => props.compact ? 0 : (props.isFocused ? 0 : 4));
|
||
const opacity = computed(() => props.compact ? 1 : (props.isFocused ? 1 : 0.6));
|
||
|
||
// 3D 变换样式
|
||
const transformStyle = computed(() => {
|
||
if (props.compact) {
|
||
return `rotateX(${props.rotateX}deg) rotateY(${props.rotateY}deg) rotateZ(${props.rotateZ}deg) translateZ(${props.z}px)`;
|
||
}
|
||
return `scale(${scale.value})`;
|
||
});
|
||
|
||
// 格式化印章位置样式
|
||
function getStampStyle(stamp: VoteStamp) {
|
||
return {
|
||
left: `${stamp.x}%`,
|
||
top: `${stamp.y}%`,
|
||
transform: `translate(-50%, -50%) rotate(${stamp.rotation}deg)`,
|
||
};
|
||
}
|
||
|
||
// 根据 ticketType 获取颜色
|
||
function getStampColor(ticketType: string): string {
|
||
const colors: Record<string, string> = {
|
||
'best_performance': '#e8313f',
|
||
'best_creativity': '#f97316',
|
||
'best_visual': '#10b981',
|
||
'best_humor': '#eab308',
|
||
'most_touching': '#ec4899',
|
||
'best_teamwork': '#3b82f6',
|
||
'audience_favorite': '#d4af37',
|
||
};
|
||
return colors[ticketType] || '#e8313f';
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<div
|
||
class="postcard-display"
|
||
:class="{ 'is-focused': isFocused, 'is-blurred': !isFocused }"
|
||
:style="{
|
||
'--blur': `${blur}px`,
|
||
'--opacity': opacity,
|
||
'transform': transformStyle,
|
||
}"
|
||
>
|
||
<!-- 明信片主体 -->
|
||
<div class="postcard-body">
|
||
<!-- 邮票 (右上角) -->
|
||
<div class="postage-stamp">
|
||
<img src="/Users/yuanjiantsui/.gemini/antigravity/brain/0695d84c-9b47-48d6-aefb-6d219a9324c4/year_of_horse_stamp_1768535779377.png" alt="Stamp" />
|
||
</div>
|
||
|
||
<!-- 邮编框 (左上角) -->
|
||
<div class="postcode-container">
|
||
<div v-for="(digit, i) in postcodeDigits" :key="i" class="postcode-box">
|
||
{{ digit }}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 主内容区域 -->
|
||
<div class="main-layout">
|
||
<!-- 左侧:节目名与祝福语 -->
|
||
<div class="left-section">
|
||
<div class="program-name-container">
|
||
<h2 class="program-title">{{ program.name }}</h2>
|
||
</div>
|
||
|
||
<div class="blessings-outer">
|
||
<div class="blessings-box">
|
||
<span class="blessings-text handwritten">With all our passion 倾情呈现</span>
|
||
</div>
|
||
<span class="blessings-label">祝福语</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 右侧:地址栏 -->
|
||
<div class="right-section address-layout">
|
||
<div class="address-line">
|
||
<span class="address-prefix">TO:</span>
|
||
<span class="address-content handwritten">全体同事</span>
|
||
<div class="line"></div>
|
||
</div>
|
||
<div class="address-line">
|
||
<span class="address-prefix">FROM:</span>
|
||
<span class="address-content handwritten">{{ program.teamName }}</span>
|
||
<div class="line"></div>
|
||
</div>
|
||
<div class="address-line name-line">
|
||
<span class="address-content handwritten">The Gala Family</span>
|
||
<div class="line"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 印刷单位 (左下角) -->
|
||
<div class="manufacturer-mark">
|
||
四川省邮电印制有限责任公司
|
||
</div>
|
||
|
||
<!-- 票数统计 (可选,作为年会互动元素保留) -->
|
||
<div class="vote-tag">
|
||
<span class="vote-number">{{ program.votes }}</span>
|
||
<span class="vote-unit">票</span>
|
||
</div>
|
||
|
||
<!-- 盖章区域 (置于最顶层以修复 Bug) -->
|
||
<div v-if="showStamps" class="stamps-overlay">
|
||
<div
|
||
v-for="stamp in program.stamps.slice(-20)"
|
||
:key="stamp.id"
|
||
class="stamp-mark"
|
||
:style="getStampStyle(stamp)"
|
||
>
|
||
<Postmark
|
||
:award-name="TICKET_TYPE_NAMES[stamp.ticketType] || stamp.ticketType"
|
||
:user-name="stamp.userName"
|
||
color="red"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 聚焦时的发光边框 -->
|
||
<div v-if="isFocused" class="focus-glow"></div>
|
||
</div>
|
||
</template>
|
||
|
||
<style lang="scss" scoped>
|
||
@use 'sass:color';
|
||
|
||
// 核心变量
|
||
$paper-cream: #f9f6ef;
|
||
$ink-blue: #1b3a7a;
|
||
$ink-charcoal: #2c3e50;
|
||
$stamp-red: #c0392b;
|
||
$color-gold: #d4af37;
|
||
|
||
.postcard-display {
|
||
width: 100%;
|
||
height: 100%;
|
||
max-width: 600px;
|
||
max-height: 380px;
|
||
position: relative;
|
||
transition: all 0.8s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||
filter: blur(var(--blur));
|
||
opacity: var(--opacity);
|
||
transform-style: preserve-3d;
|
||
|
||
&.is-focused {
|
||
z-index: 10;
|
||
}
|
||
}
|
||
|
||
.postcard-body {
|
||
width: 100%;
|
||
height: 100%;
|
||
position: relative;
|
||
background: $paper-cream;
|
||
border-radius: 4px;
|
||
box-shadow:
|
||
0 10px 30px rgba(0, 0, 0, 0.2),
|
||
0 1px 2px rgba(0,0,0,0.1);
|
||
overflow: hidden;
|
||
display: flex;
|
||
flex-direction: column;
|
||
padding: 40px;
|
||
box-sizing: border-box;
|
||
|
||
// 更加真实的纸张纹理
|
||
background-image:
|
||
linear-gradient(rgba(255,255,255,0.5), rgba(255,255,255,0.5)),
|
||
url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.08'/%3E%3C/svg%3E");
|
||
}
|
||
|
||
// 邮编框
|
||
.postcode-container {
|
||
position: absolute;
|
||
top: 25px;
|
||
left: 25px;
|
||
display: flex;
|
||
gap: 5px;
|
||
}
|
||
|
||
.postcode-box {
|
||
width: 28px;
|
||
height: 36px;
|
||
border: 1px solid $stamp-red;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 20px;
|
||
font-weight: bold;
|
||
color: $stamp-red;
|
||
font-family: 'Courier New', Courier, monospace;
|
||
}
|
||
|
||
// 邮票
|
||
.postage-stamp {
|
||
position: absolute;
|
||
top: 15px;
|
||
right: 15px;
|
||
width: 85px;
|
||
height: 100px;
|
||
z-index: 5; // 普通层级
|
||
|
||
img {
|
||
width: 100%;
|
||
height: 100%;
|
||
object-fit: contain;
|
||
filter: drop-shadow(2px 2px 5px rgba(0,0,0,0.2));
|
||
}
|
||
}
|
||
|
||
// 主布局
|
||
.main-layout {
|
||
flex: 1;
|
||
display: flex;
|
||
margin-top: 40px;
|
||
gap: 20px;
|
||
}
|
||
|
||
.left-section {
|
||
flex: 1.2;
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: center;
|
||
padding-right: 20px;
|
||
}
|
||
|
||
.program-name-container {
|
||
margin-bottom: 30px;
|
||
border-bottom: 1px solid rgba(0,0,0,0.05);
|
||
}
|
||
|
||
.program-title {
|
||
font-size: 38px;
|
||
margin: 0;
|
||
color: $ink-charcoal;
|
||
font-family: 'Noto Serif SC', serif;
|
||
font-weight: 900;
|
||
letter-spacing: 2px;
|
||
}
|
||
|
||
.blessings-outer {
|
||
position: relative;
|
||
width: 100%;
|
||
max-width: 260px;
|
||
}
|
||
|
||
.blessings-box {
|
||
border: 1px solid #999;
|
||
height: 100px;
|
||
width: 100%;
|
||
padding: 15px;
|
||
box-sizing: border-box;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.blessings-label {
|
||
position: absolute;
|
||
top: 50%;
|
||
left: 50%;
|
||
transform: translate(-50%, -50%);
|
||
font-size: 24px;
|
||
color: rgba(0,0,0,0.05);
|
||
font-family: 'Noto Serif SC', serif;
|
||
pointer-events: none;
|
||
}
|
||
|
||
.right-section {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: center;
|
||
gap: 25px;
|
||
padding-top: 20px;
|
||
}
|
||
|
||
.address-line {
|
||
position: relative;
|
||
display: flex;
|
||
align-items: flex-end;
|
||
gap: 10px;
|
||
height: 40px;
|
||
|
||
.address-prefix {
|
||
font-size: 14px;
|
||
font-weight: bold;
|
||
color: #555;
|
||
min-width: 50px;
|
||
}
|
||
|
||
.address-content {
|
||
font-size: 22px;
|
||
color: $ink-blue;
|
||
padding-bottom: 2px;
|
||
}
|
||
|
||
.line {
|
||
position: absolute;
|
||
bottom: 5px;
|
||
left: 55px;
|
||
right: 0;
|
||
height: 1px;
|
||
background: #bbb;
|
||
}
|
||
|
||
&.name-line {
|
||
.address-content {
|
||
padding-left: 55px;
|
||
}
|
||
.line {
|
||
left: 0;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 手写体
|
||
.handwritten {
|
||
font-family: 'Ma Shan Zheng', 'Kaiti', cursive;
|
||
}
|
||
|
||
// 印刷厂标记
|
||
.manufacturer-mark {
|
||
position: absolute;
|
||
bottom: 20px;
|
||
left: 30px;
|
||
font-size: 12px;
|
||
color: #888;
|
||
letter-spacing: 1px;
|
||
}
|
||
|
||
// 投票标签
|
||
.vote-tag {
|
||
position: absolute;
|
||
bottom: 20px;
|
||
right: 30px;
|
||
display: flex;
|
||
align-items: baseline;
|
||
gap: 4px;
|
||
background: rgba($color-gold, 0.1);
|
||
padding: 5px 15px;
|
||
border-radius: 20px;
|
||
border: 1px solid rgba($color-gold, 0.3);
|
||
|
||
.vote-number {
|
||
font-size: 28px;
|
||
font-weight: 900;
|
||
color: darken($color-gold, 15%);
|
||
}
|
||
|
||
.vote-unit {
|
||
font-size: 12px;
|
||
color: $color-gold;
|
||
}
|
||
}
|
||
|
||
// 盖章区域 (置于最顶层)
|
||
.stamps-overlay {
|
||
position: absolute;
|
||
inset: 0;
|
||
pointer-events: none;
|
||
z-index: 100; // 确保在邮票和所有其他内容上方
|
||
}
|
||
|
||
.stamp-mark {
|
||
position: absolute;
|
||
animation: stamp-appear 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||
}
|
||
|
||
.stamp-circle {
|
||
width: 50px;
|
||
height: 50px;
|
||
border-radius: 50%;
|
||
border: 3px solid var(--stamp-color);
|
||
background: rgba(255, 255, 255, 0.1);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
mix-blend-mode: multiply;
|
||
box-shadow: inset 0 0 10px var(--stamp-color);
|
||
|
||
.stamp-user {
|
||
font-size: 18px;
|
||
font-weight: bold;
|
||
color: var(--stamp-color);
|
||
text-shadow: 0 0 1px white;
|
||
}
|
||
}
|
||
|
||
.focus-glow {
|
||
position: absolute;
|
||
inset: -4px;
|
||
border: 3px solid $color-gold;
|
||
border-radius: 8px;
|
||
box-shadow: 0 0 40px rgba($color-gold, 0.6);
|
||
pointer-events: none;
|
||
animation: glow-pulse 2s ease-in-out infinite;
|
||
z-index: 5;
|
||
}
|
||
|
||
@keyframes stamp-appear {
|
||
from {
|
||
transform: translate(-50%, -50%) scale(2);
|
||
opacity: 0;
|
||
}
|
||
to {
|
||
transform: translate(-50%, -50%) scale(1);
|
||
opacity: 1;
|
||
}
|
||
}
|
||
|
||
@keyframes glow-pulse {
|
||
0%, 100% { opacity: 0.6; }
|
||
50% { opacity: 1; }
|
||
}
|
||
</style>
|
||
|