优化验证报名信息对话框车牌号输入布局,使标签和输入框在同一行显示

This commit is contained in:
Yvan
2026-04-09 15:37:40 +08:00
parent 89960b1cb8
commit 4920620d2c
48 changed files with 4804 additions and 3916 deletions
-201
View File
@@ -1,201 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>高德地图搜索测试</title>
<style>
body {
font-family: Arial, sans-serif;
padding: 20px;
max-width: 800px;
margin: 0 auto;
}
#container {
width: 100%;
height: 400px;
margin: 20px 0;
border: 1px solid #ccc;
}
.search-box {
margin: 20px 0;
}
input {
padding: 10px;
width: 300px;
font-size: 14px;
}
button {
padding: 10px 20px;
font-size: 14px;
cursor: pointer;
}
.results {
margin-top: 20px;
}
.result-item {
padding: 10px;
border-bottom: 1px solid #eee;
cursor: pointer;
}
.result-item:hover {
background: #f5f5f5;
}
.log {
background: #f5f5f5;
padding: 10px;
margin-top: 20px;
max-height: 300px;
overflow-y: auto;
font-family: monospace;
font-size: 12px;
}
</style>
</head>
<body>
<h1>高德地图搜索功能测试</h1>
<div class="search-box">
<input type="text" id="keyword" placeholder="输入地点名称,如:泉州市政府" />
<button onclick="searchPlace()">搜索</button>
</div>
<div id="container"></div>
<div class="results" id="results"></div>
<div class="log" id="log"></div>
<script>
// 替换为你的高德地图Key和安全密钥
const AMAP_KEY = 'fd5b77136ba34fe6b4f0f86dca7782d9';
const SECURITY_CODE = 'b2e67bb4d5eed8848f0b8aae6ca667fc';
let map = null;
let placeSearch = null;
let marker = null;
function log(message) {
const logDiv = document.getElementById('log');
const time = new Date().toLocaleTimeString();
logDiv.innerHTML += `[${time}] ${message}<br>`;
logDiv.scrollTop = logDiv.scrollHeight;
console.log(message);
}
function initMap() {
log('开始初始化地图...');
map = new AMap.Map('container', {
zoom: 13,
center: [118.6762, 24.8742] // 泉州市中心
});
log('地图初始化成功');
// 初始化PlaceSearch
AMap.plugin(['AMap.PlaceSearch'], function() {
placeSearch = new AMap.PlaceSearch({
pageSize: 10,
pageIndex: 1,
city: '泉州',
citylimit: false,
extensions: 'base'
});
log('PlaceSearch插件加载成功');
log('配置信息:city=泉州, citylimit=false, extensions=base');
});
}
function searchPlace() {
const keyword = document.getElementById('keyword').value.trim();
if (!keyword) {
alert('请输入搜索关键词');
return;
}
if (!placeSearch) {
log('错误:PlaceSearch未初始化');
alert('地图搜索功能未就绪,请刷新页面重试');
return;
}
log(`开始搜索:${keyword}`);
placeSearch.search(keyword, function(status, result) {
log(`搜索状态:${status}`);
const resultsDiv = document.getElementById('results');
resultsDiv.innerHTML = '';
if (status === 'complete' && result.info === 'OK') {
if (result.poiList && result.poiList.pois && result.poiList.pois.length > 0) {
log(`找到 ${result.poiList.pois.length} 个结果`);
result.poiList.pois.forEach((poi, index) => {
const div = document.createElement('div');
div.className = 'result-item';
div.innerHTML = `
<strong>${index + 1}. ${poi.name}</strong><br>
地址:${poi.address || '无'}<br>
坐标:${poi.location.lng}, ${poi.location.lat}
`;
div.onclick = () => showOnMap(poi);
resultsDiv.appendChild(div);
});
} else {
log('搜索结果为空');
resultsDiv.innerHTML = '<p>未找到相关地点</p>';
}
} else if (status === 'no_data') {
log('无搜索结果');
resultsDiv.innerHTML = '<p>未找到相关地点</p>';
} else {
log(`搜索失败:${status} - ${result.info || '未知错误'}`);
log(`完整结果:${JSON.stringify(result)}`);
resultsDiv.innerHTML = `<p style="color: red;">搜索失败:${result.info || status}</p>`;
}
});
}
function showOnMap(poi) {
const position = [poi.location.lng, poi.location.lat];
if (marker) {
marker.setPosition(position);
} else {
marker = new AMap.Marker({
position: position,
map: map
});
}
map.setCenter(position);
map.setZoom(16);
log(`在地图上显示:${poi.name}`);
}
// 配置安全密钥(必须在加载地图前配置)
window._AMapSecurityConfig = {
securityJsCode: SECURITY_CODE
};
log(`配置安全密钥:${SECURITY_CODE}`);
// 加载高德地图脚本
const script = document.createElement('script');
script.src = `https://webapi.amap.com/maps?v=2.0&key=${AMAP_KEY}`;
script.onload = () => {
log('高德地图脚本加载成功');
initMap();
};
script.onerror = () => {
log('错误:高德地图脚本加载失败');
alert('地图加载失败,请检查网络连接或API Key配置');
};
document.head.appendChild(script);
</script>
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 218 KiB

+34 -2
View File
@@ -1,7 +1,7 @@
import axios from 'axios';
const api = axios.create({
baseURL: '/api',
baseURL: 'https://mi.xyvan.cn/api',
timeout: 10000
});
@@ -13,6 +13,29 @@ api.interceptors.request.use(config => {
return config;
});
// 响应拦截器:确保所有错误都能被正确捕获
api.interceptors.response.use(
response => response,
error => {
// 统一错误处理
console.error('API请求错误:', error);
return Promise.reject(error);
}
);
// 获取完整的图片URL
export const getImageUrl = (path) => {
if (!path) return '';
if (path.startsWith('http://') || path.startsWith('https://')) {
return path;
}
// 开发环境使用代理,生产环境使用完整域名
if (import.meta.env.DEV) {
return `http://localhost:3000${path}`;
}
return `https://mi.xyvan.cn${path}`;
};
export default {
// 活动相关
getActivities: (params) => api.get('/activities', { params }),
@@ -65,5 +88,14 @@ export default {
headers: { 'Content-Type': 'multipart/form-data' },
timeout: 60000
}),
deleteSponsor: (id) => api.delete(`/admin/sponsors/${id}`)
deleteSponsor: (id) => api.delete(`/admin/sponsors/${id}`),
addPrize: (data) => api.post('/admin/prizes', data, {
headers: { 'Content-Type': 'multipart/form-data' },
timeout: 60000
}),
updatePrize: (id, data) => api.put(`/admin/prizes/${id}`, data, {
headers: { 'Content-Type': 'multipart/form-data' },
timeout: 60000
}),
deletePrize: (id) => api.delete(`/admin/prizes/${id}`)
};
+6 -1
View File
@@ -14,7 +14,12 @@ const routes = [
{
path: '/activity/:id',
name: 'ActivityDetail',
component: () => import('../views/ActivityDetail.vue')
component: () => import('../views/ActivityDetail/index.vue')
},
{
path: '/danmaku-demo',
name: 'DanmakuDemo',
component: () => import('../views/ActivityDetail/DanmakuDemo.vue')
},
{
path: '/register/:id',
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,292 @@
<template>
<div class="danmaku-demo">
<h2>弹幕效果演示</h2>
<p class="demo-tip">鼠标悬停可暂停弹幕</p>
<div class="danmaku-container">
<div
v-for="(item, index) in activeDanmaku"
:key="item.id"
class="danmaku-item"
:class="[`track-${item.track}`, `speed-${item.speed}`, item.type]"
:style="{ animationDelay: `${item.delay}s` }"
>
<div class="danmaku-card">
<div class="danmaku-logo">
<img v-if="item.logo" :src="item.logo" :alt="item.name" />
<span v-else class="logo-placeholder">{{ item.name.charAt(0) }}</span>
</div>
<div class="danmaku-content">
<span class="sponsor-name">{{ item.name }}</span>
<span class="sponsor-action">{{ item.type === 'title' ? '冠名赞助' : '赞助了' }}</span>
<span class="sponsor-item">{{ item.item }}</span>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
// 模拟赞助数据
const mockSponsors = [
{ id: 1, name: '小米汽车', logo: '', type: 'title', item: '本次活动' },
{ id: 2, name: '泉州欧乐堡', logo: '', type: 'sponsor', item: '游乐园门票' },
{ id: 3, name: '德劲新能源', logo: '', type: 'sponsor', item: '充电服务' },
{ id: 4, name: '泉友米车友会', logo: '', type: 'sponsor', item: '活动组织' },
{ id: 5, name: '本地商家A', logo: '', type: 'sponsor', item: '饮料零食' },
{ id: 6, name: '本地商家B', logo: '', type: 'sponsor', item: '精美礼品' },
{ id: 7, name: '本地商家C', logo: '', type: 'title', item: '特别赞助' },
{ id: 8, name: '本地商家D', logo: '', type: 'sponsor', item: '活动道具' },
];
const activeDanmaku = ref([]);
const speeds = ['slow', 'medium', 'fast'];
const tracks = [0, 1, 2, 3];
let intervalId = null;
const generateDanmaku = () => {
const sponsor = mockSponsors[Math.floor(Math.random() * mockSponsors.length)];
const track = tracks[Math.floor(Math.random() * tracks.length)];
const speed = speeds[Math.floor(Math.random() * speeds.length)];
return {
...sponsor,
id: Date.now() + Math.random(),
track,
speed,
delay: 0
};
};
const addDanmaku = () => {
if (activeDanmaku.value.length < 12) {
activeDanmaku.value.push(generateDanmaku());
}
// 清理已完成的弹幕
if (activeDanmaku.value.length > 15) {
activeDanmaku.value = activeDanmaku.value.slice(-12);
}
};
onMounted(() => {
// 初始添加几条弹幕
for (let i = 0; i < 6; i++) {
setTimeout(() => addDanmaku(), i * 800);
}
// 定期添加新弹幕
intervalId = setInterval(addDanmaku, 2000);
});
onUnmounted(() => {
if (intervalId) clearInterval(intervalId);
});
</script>
<style scoped>
.danmaku-demo {
min-height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 20px;
}
h2 {
text-align: center;
color: white;
font-size: 32px;
margin: 40px 0 10px;
text-shadow: 0 2px 10px rgba(0,0,0,0.3);
}
.demo-tip {
text-align: center;
color: rgba(255,255,255,0.9);
font-size: 14px;
margin-bottom: 40px;
}
.danmaku-container {
position: relative;
width: 100%;
height: 400px;
overflow: hidden;
background: rgba(0,0,0,0.1);
border-radius: 16px;
backdrop-filter: blur(10px);
}
.danmaku-item {
position: absolute;
right: -100%;
animation-timing-function: linear;
animation-fill-mode: forwards;
will-change: transform;
}
.danmaku-item:hover {
animation-play-state: paused;
}
/* 轨道位置 */
.track-0 { top: 20px; }
.track-1 { top: 110px; }
.track-2 { top: 200px; }
.track-3 { top: 290px; }
/* 速度变化 */
.speed-slow {
animation: danmaku-move 20s linear infinite;
}
.speed-medium {
animation: danmaku-move 15s linear infinite;
}
.speed-fast {
animation: danmaku-move 10s linear infinite;
}
@keyframes danmaku-move {
0% {
right: -100%;
opacity: 0;
}
5% {
opacity: 1;
}
95% {
opacity: 1;
}
100% {
right: 100%;
opacity: 0;
}
}
.danmaku-card {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 20px;
border-radius: 50px;
backdrop-filter: blur(10px);
box-shadow: 0 4px 20px rgba(0,0,0,0.2);
transition: transform 0.3s, box-shadow 0.3s;
white-space: nowrap;
}
.danmaku-item:hover .danmaku-card {
transform: scale(1.05);
box-shadow: 0 6px 30px rgba(0,0,0,0.3);
}
/* 冠名赞助样式 */
.danmaku-item.title .danmaku-card {
background: linear-gradient(135deg, rgba(255, 215, 0, 0.9) 0%, rgba(255, 165, 0, 0.9) 100%);
border: 2px solid rgba(255, 255, 255, 0.5);
}
.danmaku-item.title .danmaku-content {
color: #fff;
text-shadow: 0 2px 4px rgba(0,0,0,0.3);
}
/* 普通赞助样式 */
.danmaku-item.sponsor .danmaku-card {
background: linear-gradient(135deg, rgba(74, 144, 226, 0.9) 0%, rgba(53, 122, 189, 0.9) 100%);
border: 2px solid rgba(255, 255, 255, 0.3);
}
.danmaku-item.sponsor .danmaku-content {
color: #fff;
}
.danmaku-logo {
width: 40px;
height: 40px;
border-radius: 50%;
overflow: hidden;
background: rgba(255,255,255,0.3);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.danmaku-logo img {
width: 100%;
height: 100%;
object-fit: cover;
}
.logo-placeholder {
font-size: 20px;
font-weight: bold;
color: white;
}
.danmaku-content {
display: flex;
align-items: center;
gap: 6px;
font-size: 15px;
font-weight: 500;
}
.sponsor-name {
font-weight: 600;
}
.sponsor-action {
opacity: 0.9;
font-size: 13px;
}
.sponsor-item {
font-weight: 600;
padding: 2px 8px;
background: rgba(255,255,255,0.2);
border-radius: 12px;
font-size: 13px;
}
/* 移动端适配 */
@media (max-width: 768px) {
.danmaku-container {
height: 300px;
}
.track-0 { top: 15px; }
.track-1 { top: 85px; }
.track-2 { top: 155px; }
.track-3 { top: 225px; }
.danmaku-card {
padding: 8px 16px;
gap: 10px;
}
.danmaku-logo {
width: 32px;
height: 32px;
}
.danmaku-content {
font-size: 13px;
gap: 4px;
}
.sponsor-action {
font-size: 11px;
}
.sponsor-item {
font-size: 11px;
padding: 2px 6px;
}
}
</style>
@@ -0,0 +1,266 @@
<template>
<div class="sponsor-danmaku">
<div class="danmaku-item">
<div class="danmaku-card">
<div class="danmaku-avatar">
<img src="/avatars/wang.png" alt="王总" @error="handleImageError" />
</div>
<div class="danmaku-content">
<span class="thank-text">感谢</span>
<span class="sponsor-name">王总</span>
<span class="sponsor-action">赞助的</span>
<span class="sponsor-item">小米商城价值3990积分的物料</span>
</div>
</div>
</div>
</div>
</template>
<script setup>
// 单条固定弹幕,无需额外逻辑
const handleImageError = (e) => {
// 如果图片加载失败,显示文字占位
e.target.style.display = 'none';
e.target.parentElement.innerHTML = '<span class="avatar-placeholder">王</span>';
};
</script>
<style scoped>
.sponsor-danmaku {
position: fixed;
top: 80px;
left: 0;
width: 100%;
height: 70px;
overflow: hidden;
pointer-events: none;
z-index: 50;
}
.danmaku-item {
position: absolute;
top: 10px;
right: -100%;
animation: danmaku-move 15s linear infinite;
will-change: transform;
pointer-events: auto;
filter: drop-shadow(0 8px 30px rgba(212, 175, 55, 0.6));
}
.danmaku-item:hover {
animation-play-state: paused;
}
@keyframes danmaku-move {
0% {
right: -100%;
opacity: 0;
}
5% {
opacity: 1;
}
95% {
opacity: 1;
}
100% {
right: 100%;
opacity: 0;
}
}
.danmaku-card {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 24px;
border-radius: 35px;
background: linear-gradient(135deg, #1a1a1a 0%, #2d2d2d 50%, #1a1a1a 100%);
border: 2px solid #d4af37;
box-shadow:
0 0 20px rgba(212, 175, 55, 0.5),
0 0 40px rgba(212, 175, 55, 0.3),
inset 0 1px 0 rgba(255, 255, 255, 0.1),
inset 0 -1px 0 rgba(0, 0, 0, 0.5);
transition: all 0.3s ease;
white-space: nowrap;
position: relative;
overflow: hidden;
}
.danmaku-card::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(212, 175, 55, 0.3), transparent);
animation: shine 3s infinite;
}
@keyframes shine {
0% {
left: -100%;
}
50%, 100% {
left: 100%;
}
}
.danmaku-item:hover .danmaku-card {
transform: scale(1.1);
box-shadow:
0 0 30px rgba(212, 175, 55, 0.8),
0 0 60px rgba(212, 175, 55, 0.5),
inset 0 1px 0 rgba(255, 255, 255, 0.2),
inset 0 -1px 0 rgba(0, 0, 0, 0.5);
}
.danmaku-avatar {
width: 42px;
height: 42px;
border-radius: 50%;
overflow: hidden;
background: linear-gradient(135deg, #d4af37 0%, #f4e5a1 50%, #d4af37 100%);
flex-shrink: 0;
border: 3px solid #d4af37;
box-shadow:
0 0 15px rgba(212, 175, 55, 0.8),
inset 0 0 10px rgba(0, 0, 0, 0.3);
position: relative;
}
.danmaku-avatar::after {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: linear-gradient(45deg, transparent 30%, rgba(255, 255, 255, 0.3) 50%, transparent 70%);
animation: avatar-shine 4s infinite;
}
@keyframes avatar-shine {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.danmaku-avatar img {
width: 100%;
height: 100%;
object-fit: cover;
position: relative;
z-index: 1;
}
.avatar-placeholder {
font-size: 20px;
font-weight: bold;
background: linear-gradient(135deg, #d4af37 0%, #f4e5a1 50%, #d4af37 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
position: relative;
z-index: 1;
}
.danmaku-content {
display: flex;
align-items: center;
gap: 7px;
font-size: 15px;
font-weight: 600;
position: relative;
z-index: 1;
}
.thank-text {
font-size: 14px;
color: #d4af37;
font-weight: 500;
text-shadow: 0 0 10px rgba(212, 175, 55, 0.8);
}
.sponsor-name {
font-weight: 800;
font-size: 17px;
background: linear-gradient(135deg, #d4af37 0%, #f4e5a1 50%, #d4af37 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
text-shadow: 0 0 20px rgba(212, 175, 55, 0.8);
letter-spacing: 1px;
}
.sponsor-action {
color: #c0c0c0;
font-size: 14px;
font-weight: 500;
}
.sponsor-item {
font-weight: 700;
padding: 4px 12px;
background: linear-gradient(135deg, #d4af37 0%, #f4e5a1 50%, #d4af37 100%);
border-radius: 15px;
font-size: 14px;
color: #1a1a1a;
box-shadow:
0 0 15px rgba(212, 175, 55, 0.6),
inset 0 1px 0 rgba(255, 255, 255, 0.5);
letter-spacing: 0.5px;
}
@media (max-width: 768px) {
.sponsor-danmaku {
top: 60px;
height: 60px;
}
.danmaku-item {
top: 5px;
}
.danmaku-card {
padding: 8px 18px;
gap: 10px;
}
.danmaku-avatar {
width: 36px;
height: 36px;
border: 2.5px solid #d4af37;
}
.avatar-placeholder {
font-size: 16px;
}
.danmaku-content {
font-size: 13px;
gap: 6px;
}
.thank-text {
font-size: 12px;
}
.sponsor-name {
font-size: 15px;
}
.sponsor-action {
font-size: 12px;
}
.sponsor-item {
font-size: 12px;
padding: 3px 10px;
}
}
</style>
+638
View File
@@ -0,0 +1,638 @@
<template>
<div class="detail">
<div class="detail-header" :style="{ backgroundImage: `url(${activity.coverImage || 'https://s1.xiaomiev.com/activity-outer-assets/0328/images/su7_ultra_20250227/pc/5.jpg'})` }">
<div class="header-overlay"></div>
<div class="header-back" @click="$router.back()">
<van-icon name="arrow-left" />
</div>
<div class="header-content">
<h1 class="activity-title">{{ activity.title }}</h1>
</div>
</div>
<div class="content">
<div class="info-card">
<div class="info-item">
<van-icon name="clock-o" class="info-icon" />
<div class="info-text">
<div class="info-label">活动时间</div>
<div class="info-value">{{ formatDate(activity.startTime) }}</div>
</div>
</div>
<div class="info-item" @click="showMap">
<van-icon name="location-o" class="info-icon" />
<div class="info-text">
<div class="info-label">活动地点</div>
<div class="info-value">{{ activity.location }}</div>
</div>
<van-icon name="arrow" class="info-arrow" />
</div>
<div class="info-item">
<van-icon name="calendar-o" class="info-icon" />
<div class="info-text">
<div class="info-label">报名截止</div>
<div class="info-value">{{ formatDate(activity.registrationDeadline) }}</div>
</div>
</div>
<div class="info-item">
<van-icon name="gold-coin-o" class="info-icon" />
<div class="info-text">
<div class="info-label">人均费用</div>
<div class="info-value price-value">{{ activity.price > 0 ? `¥${activity.price}` : '免费' }}</div>
</div>
</div>
</div>
<div class="section">
<h3 class="section-title">活动介绍</h3>
<p class="description">{{ activity.description }}</p>
</div>
<!-- 赞助商弹幕 -->
<SponsorDanmaku v-if="activity.danmakuEnabled" />
<!-- 活动流程 -->
<div class="section">
<h3 class="section-title">活动流程</h3>
<div class="timeline">
<!-- 签到与集结 -->
<div class="timeline-item">
<div class="timeline-dot"></div>
<div class="timeline-content" @click="toggleTimeline(0)" :class="{ expanded: expandedTimelineIndex === 0 }">
<div class="timeline-time">
<span class="time-text">14:00 - 14:30</span>
<van-icon :name="expandedTimelineIndex === 0 ? 'arrow-up' : 'arrow-down'" class="expand-icon" />
</div>
<div class="timeline-title">签到与集结</div>
<div class="timeline-brief">车辆有序到场,签到领卡,开启精彩活动之旅</div>
<div class="timeline-detail" :class="{ expanded: expandedTimelineIndex === 0 }">
<div class="detail-content">
<div class="flow-steps">
<div class="step-item">🚗 车辆到场 → 指引至临时停车点</div>
<div class="step-item">✍️ 签到处签名确认参与</div>
<div class="step-item">📱 扫码入群,加入活动大家庭</div>
<div class="step-item">🎫 领取专属积分卡</div>
</div>
<div class="game-rule">
<div class="rule-title">📋 积分卡说明</div>
<div class="rule-content">
• 每人领取一张积分卡(红绿双色章)<br>
• 初始积分:0分<br>
• 游戏得分范围:+1至+5分<br>
• 游戏扣分范围:-1至-5分<br>
• 最终积分可兑换精美礼品<br>
• 积分越高,礼品越丰厚
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 观赏素材拍摄 -->
<div class="timeline-item">
<div class="timeline-dot"></div>
<div class="timeline-content" @click="toggleTimeline(1)" :class="{ expanded: expandedTimelineIndex === 1 }">
<div class="timeline-time">
<span class="time-text">14:30 - 16:30</span>
<van-icon :name="expandedTimelineIndex === 1 ? 'arrow-up' : 'arrow-down'" class="expand-icon" />
</div>
<div class="timeline-title">观赏素材拍摄</div>
<div class="timeline-brief">专业摄影团队记录精彩瞬间,留下珍贵回忆</div>
<div class="timeline-detail" :class="{ expanded: expandedTimelineIndex === 1 }">
<div class="detail-content">
<div class="flow-steps">
<div class="step-item">📸 全员车前合影 - 展示车队阵容</div>
<div class="step-item">🎯 签到墙合照 - 打卡留念</div>
<div class="step-item">🏛️ 地标合影 - 记录活动地点</div>
<div class="step-item">🎬 动态全员上车 - 拍摄车队启动瞬间</div>
<div class="step-item">🔥 点火仪式 - 引擎轰鸣</div>
<div class="step-item">✨ 拍摄"泉州"字样 - 城市印记</div>
</div>
<div class="tip-box">
💡 提示:这些素材将用于活动回顾视频制作,请积极配合拍摄
</div>
</div>
</div>
</div>
</div>
<!-- 互动游戏时间 -->
<div class="timeline-item">
<div class="timeline-dot"></div>
<div class="timeline-content" @click="toggleTimeline(2)" :class="{ expanded: expandedTimelineIndex === 2 }">
<div class="timeline-time">
<span class="time-text">16:30 - 18:00</span>
<van-icon :name="expandedTimelineIndex === 2 ? 'arrow-up' : 'arrow-down'" class="expand-icon" />
</div>
<div class="timeline-title">互动游戏时间</div>
<div class="timeline-brief">10+趣味游戏项目,赢取积分兑换精美礼品</div>
<div class="timeline-detail" :class="{ expanded: expandedTimelineIndex === 2 }">
<div class="detail-content">
<div class="game-list">
<div
v-for="(game, index) in timelineData[2].items"
:key="'game' + index"
class="game-card"
@click.stop="toggleGame('game' + index)"
:class="{ expanded: expandedGameId === 'game' + index, 'highlight-game': game.name.includes('最终复活') }"
>
<div class="game-header">
<div class="game-title">{{ game.icon }} {{ game.name }}</div>
<van-icon :name="expandedGameId === 'game' + index ? 'arrow-up' : 'arrow-down'" class="expand-icon" />
</div>
<div class="game-detail" :class="{ expanded: expandedGameId === 'game' + index }">
<div class="game-desc" v-html="game.detail"></div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 落日BBQ -->
<div class="timeline-item">
<div class="timeline-dot"></div>
<div class="timeline-content" @click="toggleTimeline(3)" :class="{ expanded: expandedTimelineIndex === 3 }">
<div class="timeline-time">
<span class="time-text">18:00 - 19:30</span>
<van-icon :name="expandedTimelineIndex === 3 ? 'arrow-up' : 'arrow-down'" class="expand-icon" />
</div>
<div class="timeline-title">落日BBQ</div>
<div class="timeline-brief">落日余晖下品尝美食,拍摄浪漫氛围大片</div>
<div class="timeline-detail" :class="{ expanded: expandedTimelineIndex === 3 }">
<div class="detail-content">
<div class="flow-steps">
<div class="step-item">🌅 落日余晖下车辆排成一排</div>
<div class="step-item">💡 齐开大灯营造氛围</div>
<div class="step-item">📷 拍摄氛围感大片</div>
<div class="step-item">🍖 边烤肉边欣赏晚霞</div>
<div class="step-item">🎵 轻松音乐伴随美食时光</div>
</div>
<div class="tip-box">
💡 提示:分享美食照片到朋友圈并@活动官方账号可获得 <span class="score-badge">+1分</span>
</div>
</div>
</div>
</div>
</div>
<!-- 篝火歌舞会 -->
<div class="timeline-item">
<div class="timeline-dot"></div>
<div class="timeline-content" @click="toggleTimeline(4)" :class="{ expanded: expandedTimelineIndex === 4 }">
<div class="timeline-time">
<span class="time-text">19:00 - 20:00</span>
<van-icon :name="expandedTimelineIndex === 4 ? 'arrow-up' : 'arrow-down'" class="expand-icon" />
</div>
<div class="timeline-title">篝火歌舞会</div>
<div class="timeline-brief">5个精彩互动游戏,点燃现场欢乐气氛</div>
<div class="timeline-detail" :class="{ expanded: expandedTimelineIndex === 4 }">
<div class="detail-content">
<div class="game-list">
<div class="game-card" @click.stop="toggleGame('bonfire1')" :class="{ expanded: expandedGameId === 'bonfire1' }">
<div class="game-header">
<div class="game-title">🥁 击鼓传花(10轮)</div>
<van-icon :name="expandedGameId === 'bonfire1' ? 'arrow-up' : 'arrow-down'" class="expand-icon" />
</div>
<div class="game-detail" :class="{ expanded: expandedGameId === 'bonfire1' }">
<div class="game-desc">
<strong>玩法:</strong>音乐响起时传递道具,音乐停止时持花者上台<br>
<strong>惩罚:</strong>吃柠檬或苦瓜(二选一)<br>
<strong>得分:</strong>勇敢完成+2分,拒绝挑战-3分<br>
<strong>轮数:</strong>共10轮,气氛逐渐升温
</div>
</div>
</div>
<div class="game-card" @click.stop="toggleGame('bonfire2')" :class="{ expanded: expandedGameId === 'bonfire2' }">
<div class="game-header">
<div class="game-title">🤝 喊数抱团(10轮)</div>
<van-icon :name="expandedGameId === 'bonfire2' ? 'arrow-up' : 'arrow-down'" class="expand-icon" />
</div>
<div class="game-detail" :class="{ expanded: expandedGameId === 'bonfire2' }">
<div class="game-desc">
<strong>玩法:</strong>主持人喊数字,参与者按数字抱团<br>
<strong>规则:</strong>落单者淘汰,上台自我介绍(1分钟)<br>
<strong>得分:</strong>成功抱团+1分,落单但完成自我介绍+2分,拒绝-2分<br>
<strong>互动:</strong>认识新朋友的好机会!
</div>
</div>
</div>
<div class="game-card" @click.stop="toggleGame('bonfire3')" :class="{ expanded: expandedGameId === 'bonfire3' }">
<div class="game-header">
<div class="game-title">🎵 大合唱</div>
<van-icon :name="expandedGameId === 'bonfire3' ? 'arrow-up' : 'arrow-down'" class="expand-icon" />
</div>
<div class="game-detail" :class="{ expanded: expandedGameId === 'bonfire3' }">
<div class="game-desc">
<strong>玩法:</strong>全员参与,齐唱经典歌曲<br>
<strong>曲目:</strong>《朋友》《真心英雄》等<br>
<strong>得分:</strong>参与即可获得+1分<br>
<strong>氛围:</strong>点燃现场,凝聚团队
</div>
</div>
</div>
<div class="game-card" @click.stop="toggleGame('bonfire4')" :class="{ expanded: expandedGameId === 'bonfire4' }">
<div class="game-header">
<div class="game-title">🎭 个人才艺表演</div>
<van-icon :name="expandedGameId === 'bonfire4' ? 'arrow-up' : 'arrow-down'" class="expand-icon" />
</div>
<div class="game-detail" :class="{ expanded: expandedGameId === 'bonfire4' }">
<div class="game-desc">
<strong>玩法:</strong>自愿上台展示才艺(唱歌/跳舞/表演)<br>
<strong>准备:</strong>需提前到后台准备BGM<br>
<strong>得分:</strong>参与表演+3分,精彩表演+5分<br>
<strong>鼓励:</strong>展现自我,收获掌声!
</div>
</div>
</div>
<div class="game-card" @click.stop="toggleGame('bonfire5')" :class="{ expanded: expandedGameId === 'bonfire5' }">
<div class="game-header">
<div class="game-title">👶 小朋友互动</div>
<van-icon :name="expandedGameId === 'bonfire5' ? 'arrow-up' : 'arrow-down'" class="expand-icon" />
</div>
<div class="game-detail" :class="{ expanded: expandedGameId === 'bonfire5' }">
<div class="game-desc">
<strong>玩法:</strong>主持人邀请小朋友上台互动<br>
<strong>内容:</strong>简单游戏/才艺展示/问答<br>
<strong>礼品:</strong>参与即有小礼物<br>
<strong>得分:</strong>家长陪同参与+2分
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 璀璨烟火秀 -->
<div class="timeline-item">
<div class="timeline-dot"></div>
<div class="timeline-content" @click="toggleTimeline(5)" :class="{ expanded: expandedTimelineIndex === 5 }">
<div class="timeline-time">
<span class="time-text">20:00 - 20:30</span>
<van-icon :name="expandedTimelineIndex === 5 ? 'arrow-up' : 'arrow-down'" class="expand-icon" />
</div>
<div class="timeline-title">璀璨烟火秀</div>
<div class="timeline-brief">三重烟花盛宴,点亮夜空绽放精彩</div>
<div class="timeline-detail" :class="{ expanded: expandedTimelineIndex === 5 }">
<div class="detail-content">
<div class="flow-steps">
<div class="step-item">🎆 米兰之夜 - 大型专业烟火表演,绚丽夺目</div>
<div class="step-item">✨ 仙女棒 - 每车配发手持烟花,拍照打卡</div>
<div class="step-item">🔥 加特林压轴 - 震撼收尾,全场欢呼</div>
<div class="step-item">📸 最佳拍摄时机 - 记录璀璨瞬间</div>
</div>
<div class="tip-box">
💡 提示:拍摄烟花视频并分享可获得 <span class="score-badge">+2分</span>
</div>
</div>
</div>
</div>
</div>
<!-- 积分兑奖 & 自由交流 -->
<div class="timeline-item timeline-item-last">
<div class="timeline-dot"></div>
<div class="timeline-content" @click="toggleTimeline(6)" :class="{ expanded: expandedTimelineIndex === 6 }">
<div class="timeline-time">
<span class="time-text">20:30 以后</span>
<van-icon :name="expandedTimelineIndex === 6 ? 'arrow-up' : 'arrow-down'" class="expand-icon" />
</div>
<div class="timeline-title">积分兑奖 & 自由交流</div>
<div class="timeline-brief">凭积分兑换精美礼品,自由合影欢乐交流</div>
<div class="timeline-detail" :class="{ expanded: expandedTimelineIndex === 6 }">
<div class="detail-content">
<div class="game-rule">
<div class="rule-title">🏆 积分兑换礼品</div>
<div class="rule-content">
<strong>积分排行榜:</strong><br>
• 第1名(20分以上):神秘大奖 🎁<br>
• 第2-3名(15-19分):精美礼品 🎀<br>
• 第4-10名(10-14分):纪念品 🎈<br>
• 参与奖(5-9分):小礼物 🎊<br>
<br>
<strong>活动收尾:</strong><br>
• 自由合影留念<br>
• 整理现场垃圾<br>
• 提醒安全驾驶返程<br>
• 期待下次相聚
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 活动经费 -->
<div v-if="expenses.length && isApproved" class="section">
<h3 class="section-title">活动经费 <span class="count">¥{{ totalExpense }}</span></h3>
<div class="expense-list">
<div v-for="expense in expenses" :key="expense.id" class="expense-item">
<div class="expense-info">
<div class="expense-name">{{ expense.item }}</div>
<div class="expense-category">{{ expense.category }}</div>
</div>
<div v-if="expense.media && expense.media.length" class="expense-media">
<div v-for="(media, index) in expense.media" :key="index" class="expense-media-item">
<van-image
v-if="media.type === 'image'"
:src="media.url"
fit="cover"
@click="previewImage(media.url)"
/>
<div v-else class="expense-video" @click="playExpenseVideo(media.url)">
<video :src="media.url"></video>
<van-icon name="play-circle-o" class="play-icon" />
</div>
</div>
</div>
<div class="expense-amount">金额:¥{{ expense.amount }}</div>
</div>
</div>
</div>
<div v-if="!isApproved && hasRegistered" class="section">
<div class="permission-tip">
<van-icon name="info-o" />
<span v-if="userRegistration.status === 'pending'">您的报名正在审核中,审核通过后可查看活动经费</span>
<span v-else-if="userRegistration.status === 'rejected'">您的报名未通过审核</span>
</div>
</div>
<!-- 赞助商 -->
<div v-if="sponsors.length" class="section">
<h3 class="section-title">赞助商</h3>
<div class="sponsor-grid">
<div v-for="sponsor in sponsors" :key="sponsor.id" class="sponsor-card">
<div class="sponsor-logo" :style="{ backgroundImage: `url(${getImageUrl(sponsor.logo)})` }"></div>
<div class="sponsor-name">{{ sponsor.name }}</div>
</div>
</div>
</div>
<!-- 奖品礼品 -->
<div v-if="prizes.length" class="section">
<h3 class="section-title">🏆 奖品礼品</h3>
<div class="prize-list">
<div v-for="prize in prizes" :key="prize.id" class="prize-card">
<div class="prize-image" :style="prize.image ? { backgroundImage: `url(${getImageUrl(prize.image)})` } : {}">
<van-icon v-if="!prize.image" name="gift-o" size="40" color="#ccc" />
</div>
<div class="prize-content">
<div class="prize-header">
<span class="prize-level" :class="`level-${prize.level}`">{{ prize.level }}</span>
<span class="prize-name">{{ prize.name }}</span>
</div>
<div v-if="prize.description" class="prize-desc">{{ prize.description }}</div>
<div class="prize-footer">
<span class="prize-quantity">数量: {{ prize.quantity }}</span>
</div>
</div>
</div>
</div>
</div>
<!-- 活动照片与视频 -->
<div v-if="photos.length || videos.length" class="section">
<h3 class="section-title">活动照片与视频</h3>
<div v-if="photos.length" class="media-section">
<div class="media-label">照片</div>
<div class="photo-grid">
<div v-for="photo in photos" :key="photo.id" class="photo-item" @click="previewImage(photo.url)">
<div class="photo-img" :style="{ backgroundImage: `url(${photo.url})` }"></div>
</div>
</div>
</div>
<div v-if="videos.length" class="media-section">
<div class="media-label">视频</div>
<div class="video-list">
<div v-for="video in videos" :key="video.id" class="video-item">
<video :src="video.url" controls class="video-player"></video>
</div>
</div>
</div>
<div v-if="hasRegistered && isApproved" class="upload-section">
<van-uploader
v-model="userMedia"
multiple
:max-count="5"
:after-read="uploadUserMedia"
accept="image/*,video/*"
>
<van-button icon="photograph" type="primary" size="small">上传照片/视频</van-button>
</van-uploader>
<div class="upload-tip">您可以上传活动照片或视频,审核后将展示给所有人</div>
</div>
<div v-if="hasRegistered && !isApproved" class="upload-section">
<div class="permission-tip">
<van-icon name="info-o" />
<span v-if="userRegistration.status === 'pending'">审核通过后可上传照片/视频</span>
<span v-else-if="userRegistration.status === 'rejected'">您的报名未通过审核</span>
</div>
</div>
</div>
<!-- 报名名单 -->
<div class="section">
<h3 class="section-title">报名名单 <span class="count">({{ registrations.length }}人)</span></h3>
<div class="member-list">
<div v-for="reg in registrations" :key="reg.id" class="member-item">
<div class="member-avatar">{{ reg.name.charAt(0) }}</div>
<div class="member-info">
<div class="member-name">{{ reg.name }}</div>
<div class="member-car">{{ reg.carModel }}</div>
</div>
</div>
</div>
</div>
</div>
<!-- 底部操作按钮 -->
<div v-if="!hasRegistered && activity.registrationEnabled" class="footer-action">
<van-button
type="primary"
block
round
size="large"
@click="$router.push(`/register/${activity.id}`)"
class="action-button"
>
<van-icon name="add-o" /> 立即报名
</van-button>
<van-button
plain
type="primary"
block
round
size="large"
@click="handleLogin"
class="action-button login-button"
>
<van-icon name="user-o" /> 已报名?点击登录
</van-button>
</div>
<div v-else-if="!hasRegistered && !activity.registrationEnabled" class="footer-action">
<van-button type="default" block round size="large" disabled class="action-button">
<van-icon name="warning-o" /> 报名已结束
</van-button>
<van-button
plain
type="primary"
block
round
size="large"
@click="handleLogin"
class="action-button login-button"
>
<van-icon name="user-o" /> 已报名?点击登录
</van-button>
</div>
<div v-else-if="!isApproved" class="footer-action">
<van-button type="default" block round size="large" disabled class="action-button">
<van-icon name="clock-o" />
<span v-if="userRegistration.status === 'pending'">等待审核中</span>
<span v-else>报名未通过</span>
</van-button>
</div>
<div v-else class="footer-action">
<van-button type="success" block round size="large" disabled class="action-button">
<van-icon name="success" /> 已报名
</van-button>
</div>
<!-- 分享按钮 -->
<div class="share-btn" @click="handleShare">
<van-icon name="share-o" />
<span>分享</span>
</div>
<!-- 地图弹窗 -->
<van-popup v-model:show="showMapPopup" position="bottom" :style="{ height: '70%' }">
<div class="map-container">
<div class="map-header">
<h3>{{ activity.location }}</h3>
<van-icon name="cross" @click="showMapPopup = false" />
</div>
<div id="amap-container" style="width: 100%; height: calc(100% - 50px);"></div>
</div>
</van-popup>
<!-- 登录弹窗 -->
<van-dialog
v-model:show="showLoginDialog"
title="验证报名信息"
show-cancel-button
@confirm="submitLogin"
>
<div class="login-form">
<van-field
v-model="loginForm.phone"
label="手机号"
placeholder="请输入报名时的手机号"
type="tel"
maxlength="11"
/>
<van-field
label="车牌号"
>
<template #input>
<div class="license-plate-input">
<span class="plate-prefix">闽</span>
<input
v-model="carLetter"
@input="handleLetterInput"
placeholder="_"
maxlength="1"
class="plate-letter"
/>
<span class="plate-dot">·</span>
<input
v-model="carDigits"
@input="handleDigitsInput"
placeholder="______"
maxlength="6"
class="plate-digits"
/>
</div>
</template>
</van-field>
</div>
</van-dialog>
<!-- 经费视频播放弹窗 -->
<van-popup v-model:show="showExpenseVideoPopup" position="center" round :style="{ width: '90%' }">
<div class="video-player-popup">
<video :src="currentExpenseVideo" controls autoplay class="popup-video"></video>
</div>
</van-popup>
</div>
</template>
<script setup>
import { useActivityDetail } from './useActivityDetail';
import SponsorDanmaku from './SponsorDanmaku.vue';
import { timelineData } from '../timeline-data.js';
// 获取完整图片URL的辅助函数
const getImageUrl = (path) => {
if (!path) return '';
if (path.startsWith('http://') || path.startsWith('https://')) {
return path;
}
return import.meta.env.DEV ? `http://localhost:3000${path}` : `https://mi.xyvan.cn${path}`;
};
const {
activity,
photos,
videos,
sponsors,
prizes,
registrations,
expenses,
userMedia,
showMapPopup,
showLoginDialog,
showExpenseVideoPopup,
currentExpenseVideo,
loginForm,
carLetter,
carDigits,
userRegistration,
expandedTimelineIndex,
expandedGameId,
totalExpense,
isApproved,
hasRegistered,
formatDate,
previewImage,
playExpenseVideo,
showMap,
uploadUserMedia,
handleLogin,
handleLetterInput,
handleDigitsInput,
submitLogin,
handleShare,
toggleTimeline,
toggleGame
} = useActivityDetail();
</script>
<style src="./styles.css" scoped></style>
@@ -0,0 +1,936 @@
.detail {
min-height: 100vh;
background: #f8f9fa;
padding-bottom: 140px;
}
.detail-header {
position: relative;
height: 280px;
background-size: cover;
background-position: center;
background-color: #e8e8e8;
}
.header-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(180deg, rgba(0,0,0,0.3) 0%, rgba(0,0,0,0.6) 100%);
}
.header-back {
position: absolute;
top: 16px;
left: 16px;
z-index: 2;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(255,255,255,0.3);
border-radius: 50%;
color: #fff;
font-size: 20px;
backdrop-filter: blur(10px);
}
.header-content {
position: absolute;
bottom: 24px;
left: 16px;
right: 16px;
z-index: 1;
}
.activity-title {
font-size: 24px;
font-weight: bold;
color: #fff;
margin: 0;
text-shadow: 0 2px 8px rgba(0,0,0,0.3);
}
.content {
padding: 16px;
margin-top: -20px;
position: relative;
z-index: 1;
}
.info-card {
background: #fff;
border-radius: 16px;
padding: 16px;
box-shadow: 0 2px 12px rgba(0,0,0,0.08);
margin-bottom: 16px;
}
.info-item {
display: flex;
align-items: center;
padding: 12px 0;
border-bottom: 1px solid #f0f0f0;
}
.info-item:last-child {
border-bottom: none;
}
.info-arrow {
font-size: 16px;
color: #999;
margin-left: 8px;
}
.info-icon {
font-size: 24px;
color: #FF6700;
margin-right: 12px;
}
.info-text {
flex: 1;
}
.info-label {
font-size: 12px;
color: #999;
margin-bottom: 4px;
}
.info-value {
font-size: 14px;
color: #333;
font-weight: 500;
}
.price-value {
color: #FF6700;
font-weight: 600;
font-size: 16px;
}
.section {
background: #fff;
border-radius: 16px;
padding: 16px;
margin-bottom: 16px;
box-shadow: 0 2px 12px rgba(0,0,0,0.08);
}
.section-title {
font-size: 16px;
font-weight: bold;
margin: 0 0 16px;
color: #333;
}
.section-title .count {
color: #FF6700;
font-size: 14px;
}
.description {
font-size: 14px;
line-height: 1.8;
color: #666;
white-space: pre-line;
}
/* Timeline样式 */
.timeline {
position: relative;
padding-left: 32px;
}
.timeline::before {
content: '';
position: absolute;
left: 8px;
top: 8px;
bottom: 8px;
width: 2px;
background: linear-gradient(180deg, #FF6700 0%, #FF8533 100%);
}
.timeline-item {
position: relative;
padding-bottom: 24px;
}
.timeline-item-last {
padding-bottom: 0;
}
.timeline-dot {
position: absolute;
left: -28px;
top: 4px;
width: 12px;
height: 12px;
border-radius: 50%;
background: #fff;
border: 3px solid #FF6700;
box-shadow: 0 0 0 3px rgba(255, 103, 0, 0.1);
z-index: 1;
}
.timeline-content {
background: #fff;
border-radius: 12px;
padding: 12px 16px;
border: 1px solid #f0f0f0;
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
transition: all 0.3s;
cursor: pointer;
}
.timeline-content:active {
transform: scale(0.98);
}
.timeline-content.expanded {
box-shadow: 0 4px 12px rgba(255, 103, 0, 0.15);
border-color: #FF6700;
}
.timeline-time {
font-size: 13px;
color: #FF6700;
font-weight: 600;
margin-bottom: 6px;
display: flex;
align-items: center;
}
.timeline-time::before {
content: '🕐';
margin-right: 6px;
}
.time-text {
margin-right: auto;
}
.expand-icon {
font-size: 16px;
color: #999;
transition: transform 0.3s;
margin-left: 8px;
}
.timeline-title {
font-size: 15px;
color: #333;
font-weight: 600;
margin-bottom: 8px;
}
.timeline-brief {
font-size: 13px;
color: #666;
line-height: 1.6;
margin-bottom: 0;
}
.timeline-detail {
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease-out;
}
.timeline-detail.expanded {
max-height: 3000px;
transition: max-height 0.5s ease-in;
}
.detail-content {
margin-top: 12px;
}
.flow-steps {
margin-bottom: 12px;
}
.step-item {
padding: 8px 12px;
margin-bottom: 8px;
background: #fff;
border-radius: 6px;
border-left: 3px solid #FF6700;
font-size: 13px;
color: #666;
line-height: 1.6;
}
.step-item:last-child {
margin-bottom: 0;
}
.tip-box {
padding: 10px 12px;
background: #fff7e6;
border-radius: 6px;
font-size: 13px;
color: #ff9800;
line-height: 1.6;
}
.game-rule {
margin-top: 12px;
padding: 12px;
background: #f8f9fa;
border-radius: 8px;
border-left: 3px solid #FF6700;
}
.rule-title {
font-size: 14px;
font-weight: 600;
color: #333;
margin-bottom: 8px;
}
.rule-content {
font-size: 13px;
color: #666;
line-height: 1.8;
}
.score-badge {
display: inline-block;
padding: 2px 8px;
background: linear-gradient(135deg, #FF6700 0%, #FF8533 100%);
color: #fff;
border-radius: 12px;
font-size: 12px;
font-weight: 600;
margin: 0 2px;
}
/* 游戏卡片样式 */
.game-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.game-card {
padding: 12px;
background: #fff;
border-radius: 8px;
border: 1px solid #e8e8e8;
cursor: pointer;
transition: all 0.2s;
}
.game-card:active {
transform: scale(0.98);
}
.game-card.expanded {
border-color: #FF6700;
background: #fff7e6;
}
.game-card.highlight-game {
background: linear-gradient(135deg, #fff7e6 0%, #fff 100%);
border-color: #FF6700;
}
.game-header {
display: flex;
align-items: center;
justify-content: space-between;
}
.game-title {
font-size: 14px;
font-weight: 600;
color: #333;
}
.game-detail {
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease-out;
margin-top: 0;
}
.game-detail.expanded {
max-height: 500px;
transition: max-height 0.5s ease-in;
margin-top: 12px;
}
.game-desc {
font-size: 13px;
color: #666;
line-height: 1.8;
padding: 10px;
background: #fff;
border-radius: 6px;
}
/* 其他组件样式 */
.sponsor-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
max-width: 600px;
margin: 0 auto;
}
.sponsor-card {
text-align: center;
padding: 12px;
background: #fff;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
transition: transform 0.2s;
}
.sponsor-card:active {
transform: scale(0.98);
}
.sponsor-logo {
width: 60px;
height: 60px;
margin: 0 auto 10px;
background-size: cover;
background-position: center;
background-color: #f5f5f5;
border-radius: 10px;
}
.sponsor-name {
font-size: 13px;
color: #333;
font-weight: 600;
line-height: 1.4;
width: 100%;
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
word-break: break-word;
}
.photo-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
}
.photo-item {
aspect-ratio: 1;
border-radius: 8px;
overflow: hidden;
}
.photo-img {
width: 100%;
height: 100%;
background-size: cover;
background-position: center;
background-color: #f5f5f5;
}
.media-section {
margin-bottom: 20px;
}
.media-label {
font-size: 14px;
font-weight: 500;
color: #666;
margin-bottom: 12px;
}
.video-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.video-item {
border-radius: 12px;
overflow: hidden;
background: #000;
}
.video-player {
width: 100%;
max-height: 300px;
display: block;
}
.member-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.member-item {
display: flex;
align-items: center;
gap: 12px;
}
.member-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
background: linear-gradient(135deg, #FF6700 0%, #FF8533 100%);
color: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
font-weight: bold;
}
.member-info {
flex: 1;
}
.member-name {
font-size: 14px;
color: #333;
font-weight: 500;
margin-bottom: 4px;
}
.member-car {
font-size: 12px;
color: #999;
}
.expense-list {
display: flex;
flex-direction: column;
gap: 16px;
}
.expense-item {
display: flex;
flex-direction: column;
padding: 16px;
background: #fff;
border-radius: 12px;
border: 1px solid #f0f0f0;
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
}
.expense-info {
flex: 1;
}
.expense-name {
font-size: 15px;
color: #333;
font-weight: 600;
margin-bottom: 6px;
}
.expense-category {
display: inline-block;
font-size: 11px;
color: #FF6700;
background: #fff7e6;
padding: 2px 8px;
border-radius: 4px;
margin-bottom: 8px;
}
.expense-media {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 8px;
margin-top: 12px;
}
.expense-media-item {
width: 100%;
aspect-ratio: 1;
border-radius: 8px;
overflow: hidden;
background: #f5f5f5;
cursor: pointer;
transition: transform 0.2s;
}
.expense-media-item:active {
transform: scale(0.95);
}
.expense-video {
position: relative;
width: 100%;
height: 100%;
cursor: pointer;
}
.expense-video video {
width: 100%;
height: 100%;
object-fit: cover;
}
.expense-video .play-icon {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 28px;
color: #fff;
filter: drop-shadow(0 2px 4px rgba(0,0,0,0.5));
}
.expense-amount {
font-size: 15px;
color: #FF6700;
font-weight: bold;
text-align: right;
margin-top: 12px;
padding-top: 12px;
border-top: 1px dashed #f0f0f0;
}
.upload-section {
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid #f0f0f0;
}
.upload-tip {
font-size: 12px;
color: #999;
margin-top: 8px;
}
/* 底部按钮和弹窗样式 */
.footer-action {
position: fixed;
bottom: 0;
left: 0;
right: 0;
padding: 12px 16px;
background: #fff;
box-shadow: 0 -2px 12px rgba(0,0,0,0.08);
z-index: 10;
display: flex;
flex-direction: column;
gap: 8px;
}
.action-button {
height: 48px;
background: linear-gradient(135deg, #FF6700 0%, #FF8533 100%);
border: none;
font-weight: 600;
}
.action-button .van-icon {
margin-right: 4px;
}
.login-button {
margin-top: 0;
background: linear-gradient(135deg, #FF6700 0%, #FF8533 100%) !important;
color: #fff !important;
}
.share-btn {
position: fixed;
top: 16px;
right: 16px;
z-index: 20;
display: flex;
align-items: center;
gap: 4px;
padding: 8px 16px;
background: rgba(255,255,255,0.95);
border-radius: 20px;
color: #FF6700;
font-size: 14px;
font-weight: 500;
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
backdrop-filter: blur(10px);
cursor: pointer;
}
.map-container {
height: 100%;
display: flex;
flex-direction: column;
}
.map-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px;
border-bottom: 1px solid #f0f0f0;
}
.map-header h3 {
margin: 0;
font-size: 16px;
}
.map-header .van-icon {
font-size: 20px;
color: #999;
}
.login-form {
padding: 16px;
}
/* 车牌号字段样式优化 - 标签和输入框同行显示 */
.login-form .van-field {
display: flex;
align-items: center;
flex-wrap: nowrap;
}
.login-form .van-field__label {
width: 60px;
flex-shrink: 0;
margin-right: 8px;
}
.login-form .van-field__control {
flex: 1;
display: flex;
align-items: center;
}
.login-form .van-field__control--custom {
padding: 0;
}
.permission-tip {
display: flex;
align-items: center;
gap: 8px;
padding: 12px;
background: #fff7e6;
border-radius: 8px;
color: #ff9800;
font-size: 14px;
}
.permission-tip .van-icon {
font-size: 18px;
}
.video-player-popup {
padding: 20px;
background: #000;
border-radius: 16px;
}
.popup-video {
width: 100%;
max-height: 60vh;
border-radius: 12px;
}
/* 车牌输入样式 */
.license-plate-input {
display: flex;
align-items: center;
flex-wrap: nowrap;
gap: 3px;
padding: 5px 8px;
background: linear-gradient(135deg, #FF6700 0%, #FF8533 100%);
border-radius: 6px;
box-shadow: 0 2px 8px rgba(255, 103, 0, 0.3);
width: fit-content;
}
.plate-prefix {
color: #fff;
font-weight: 700;
font-size: 15px;
text-shadow: 0 1px 2px rgba(0,0,0,0.2);
}
.plate-letter {
width: 20px;
height: 26px;
border: 2px solid rgba(255,255,255,0.9);
border-radius: 3px;
background: #fff;
text-align: center;
font-size: 15px;
font-weight: 700;
color: #333;
outline: none;
text-transform: uppercase;
padding: 0;
}
.plate-dot {
color: #fff;
font-size: 16px;
font-weight: 700;
margin: 0 1px;
}
.plate-digits {
width: 95px;
height: 26px;
border: 2px solid rgba(255,255,255,0.9);
border-radius: 3px;
background: #fff;
text-align: center;
font-size: 15px;
font-weight: 700;
color: #333;
outline: none;
letter-spacing: 1px;
text-transform: uppercase;
padding: 0;
}
.plate-letter::placeholder,
.plate-digits::placeholder {
color: #ddd;
font-weight: 400;
}
/* 奖品礼品样式 */
.prize-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.prize-card {
display: flex;
gap: 12px;
padding: 12px;
background: linear-gradient(135deg, #fff 0%, #fffbf5 100%);
border-radius: 12px;
border: 1px solid #ffe8cc;
box-shadow: 0 2px 8px rgba(255, 103, 0, 0.08);
transition: transform 0.2s;
}
.prize-card:active {
transform: scale(0.98);
}
.prize-image {
width: 70px;
height: 70px;
flex-shrink: 0;
border-radius: 8px;
background-size: cover;
background-position: center;
background-color: #f5f5f5;
display: flex;
align-items: center;
justify-content: center;
border: 2px solid #ffe8cc;
}
.prize-content {
flex: 1;
display: flex;
flex-direction: column;
gap: 6px;
min-width: 0;
}
.prize-header {
display: flex;
flex-direction: column;
gap: 4px;
}
.prize-level {
display: inline-block;
padding: 3px 10px;
border-radius: 12px;
font-size: 11px;
font-weight: 600;
white-space: nowrap;
width: fit-content;
}
.level-一等奖 {
background: linear-gradient(135deg, #FFD700 0%, #FFA500 100%);
color: #fff;
box-shadow: 0 2px 6px rgba(255, 215, 0, 0.4);
}
.level-二等奖 {
background: linear-gradient(135deg, #C0C0C0 0%, #A8A8A8 100%);
color: #fff;
box-shadow: 0 2px 6px rgba(192, 192, 192, 0.4);
}
.level-三等奖 {
background: linear-gradient(135deg, #CD7F32 0%, #B8733E 100%);
color: #fff;
box-shadow: 0 2px 6px rgba(205, 127, 50, 0.4);
}
.level-参与奖 {
background: linear-gradient(135deg, #FF6700 0%, #FF8533 100%);
color: #fff;
box-shadow: 0 2px 6px rgba(255, 103, 0, 0.3);
}
.level-伴手礼 {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
color: #fff;
box-shadow: 0 2px 6px rgba(240, 147, 251, 0.3);
}
.prize-name {
font-size: 14px;
font-weight: 600;
color: #333;
line-height: 1.4;
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
word-break: break-word;
}
.prize-desc {
font-size: 12px;
color: #666;
line-height: 1.5;
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
word-break: break-word;
}
.prize-footer {
display: flex;
align-items: center;
justify-content: flex-start;
margin-top: auto;
padding-top: 6px;
border-top: 1px dashed #ffe8cc;
}
.prize-quantity {
font-size: 12px;
color: #FF6700;
font-weight: 500;
}
@@ -0,0 +1,256 @@
import { ref, computed, onMounted } from 'vue';
import { useRoute } from 'vue-router';
import { showImagePreview, showToast } from 'vant';
import api from '../../api';
export function useActivityDetail() {
const route = useRoute();
const activity = ref({});
const photos = ref([]);
const videos = ref([]);
const sponsors = ref([]);
const prizes = ref([]);
const registrations = ref([]);
const expenses = ref([]);
const userMedia = ref([]);
const showMapPopup = ref(false);
const showLoginDialog = ref(false);
const showExpenseVideoPopup = ref(false);
const currentExpenseVideo = ref('');
const loginForm = ref({ phone: '', carNumber: '' });
const expandedTimelineIndex = ref(null);
const expandedGameId = ref(null);
const carLetter = ref('');
const carDigits = ref('');
const userRegistration = ref(null);
let map = null;
const handleLetterInput = (e) => {
const value = e.target.value.toUpperCase().replace(/[^A-Z]/g, '');
carLetter.value = value;
loginForm.value.carNumber = value + carDigits.value;
if (value.length === 1) {
e.target.nextElementSibling?.nextElementSibling?.focus();
}
};
const handleDigitsInput = (e) => {
const value = e.target.value.toUpperCase().replace(/[^A-Z0-9]/g, '');
carDigits.value = value;
loginForm.value.carNumber = carLetter.value + value;
};
const toggleTimeline = (index) => {
expandedTimelineIndex.value = expandedTimelineIndex.value === index ? null : index;
expandedGameId.value = null;
};
const toggleGame = (gameId) => {
expandedGameId.value = expandedGameId.value === gameId ? null : gameId;
};
const totalExpense = computed(() => {
return expenses.value.reduce((sum, e) => sum + parseFloat(e.amount), 0).toFixed(2);
});
const isApproved = computed(() => {
return userRegistration.value && userRegistration.value.status === 'approved';
});
const hasRegistered = computed(() => {
return userRegistration.value !== null;
});
const loadData = async () => {
const { data } = await api.getActivity(route.params.id);
activity.value = data.activity;
const allMedia = data.photos || [];
photos.value = allMedia.filter(m => m.type === 'image');
videos.value = allMedia.filter(m => m.type === 'video');
sponsors.value = data.sponsors;
prizes.value = data.prizes || [];
expenses.value = data.expenses || [];
const { data: regs } = await api.getActivityRegistrations(route.params.id);
registrations.value = regs;
const savedReg = localStorage.getItem(`user_reg_${route.params.id}`);
if (savedReg) {
userRegistration.value = JSON.parse(savedReg);
}
};
const formatDate = (date) => {
return new Date(date).toLocaleString('zh-CN');
};
const previewImage = (url) => {
showImagePreview([url]);
};
const playExpenseVideo = (url) => {
currentExpenseVideo.value = url;
showExpenseVideoPopup.value = true;
};
const showMap = () => {
if (!activity.value.locationLat || !activity.value.locationLng) {
showToast('暂无地图信息');
return;
}
showMapPopup.value = true;
setTimeout(() => {
initMap();
}, 300);
};
const initMap = async () => {
try {
const AMap = await window.loadAMap();
if (map) {
map.destroy();
}
const center = [activity.value.locationLng, activity.value.locationLat];
map = new AMap.Map('amap-container', { zoom: 15, center });
new AMap.Marker({ position: center, map, title: activity.value.location });
} catch (e) {
console.error('地图加载失败:', e);
showToast('地图加载失败: ' + e.message);
}
};
const uploadUserMedia = async (files) => {
if (!isApproved.value) {
showToast('请先登录并等待审核通过');
return;
}
const loadingToast = showToast({
type: 'loading',
message: '上传中...',
forbidClick: true,
duration: 0
});
try {
const formData = new FormData();
formData.append('uploadedBy', userRegistration.value.name);
if (Array.isArray(files)) {
files.forEach(file => formData.append('photos', file.file));
} else {
formData.append('photos', files.file);
}
await api.uploadUserPhotos(route.params.id, formData);
showToast('上传成功,等待审核');
userMedia.value = [];
} catch (error) {
showToast('上传失败');
} finally {
loadingToast.clear();
}
};
const handleLogin = () => {
showLoginDialog.value = true;
};
const submitLogin = async () => {
if (!loginForm.value.phone || !loginForm.value.carNumber) {
showToast('请输入手机号和车牌号');
return;
}
try {
const carNumberWithPrefix = `闽${loginForm.value.carNumber}`;
const { data } = await api.verifyUser(route.params.id, {
phone: loginForm.value.phone,
carNumber: carNumberWithPrefix
});
userRegistration.value = data.registration;
localStorage.setItem(`user_reg_${route.params.id}`, JSON.stringify(data.registration));
showLoginDialog.value = false;
if (data.registration.status === 'approved') {
showToast('登录成功');
} else if (data.registration.status === 'pending') {
showToast('您的报名正在审核中');
} else {
showToast('您的报名未通过审核');
}
} catch (error) {
showToast(error.response?.data?.error || '验证失败');
}
};
const handleShare = () => {
const shareUrl = `${window.location.origin}/share/${route.params.id}`;
if (navigator.share) {
navigator.share({
title: activity.value.title,
text: `${activity.value.title} - 泉友米车友会`,
url: shareUrl
}).catch(() => {
copyShareLink(shareUrl);
});
} else {
copyShareLink(shareUrl);
}
};
const copyShareLink = (url) => {
navigator.clipboard.writeText(url).then(() => {
showToast('分享链接已复制');
}).catch(() => {
showToast('复制失败,请手动复制:' + url);
});
};
onMounted(loadData);
return {
activity,
photos,
videos,
sponsors,
prizes,
registrations,
expenses,
userMedia,
showMapPopup,
showLoginDialog,
showExpenseVideoPopup,
currentExpenseVideo,
loginForm,
carLetter,
carDigits,
userRegistration,
expandedTimelineIndex,
expandedGameId,
totalExpense,
isApproved,
hasRegistered,
formatDate,
previewImage,
playExpenseVideo,
showMap,
uploadUserMedia,
handleLogin,
handleLetterInput,
handleDigitsInput,
submitLogin,
handleShare,
toggleTimeline,
toggleGame
};
}
File diff suppressed because it is too large Load Diff
+34 -28
View File
@@ -713,27 +713,25 @@ onUnmounted(() => {
}
.sponsors-grid {
display: flex;
justify-content: center;
align-items: stretch;
gap: 16px;
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
margin-top: 32px;
flex-wrap: wrap;
max-width: 800px;
margin-left: auto;
margin-right: auto;
}
.sponsor-card {
background: #fff;
border-radius: 12px;
padding: 12px;
padding: 16px;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
box-shadow: 0 2px 12px rgba(0,0,0,0.06);
transition: transform 0.3s, box-shadow 0.3s;
flex: 0 1 140px;
max-width: 150px;
min-width: 120px;
}
.sponsor-card:hover {
@@ -742,12 +740,12 @@ onUnmounted(() => {
}
.sponsor-logo {
width: 45px;
height: 45px;
width: 60px;
height: 60px;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 8px;
margin-bottom: 12px;
}
.sponsor-logo svg {
@@ -768,20 +766,30 @@ onUnmounted(() => {
}
.sponsor-name {
font-size: 11px;
font-size: 13px;
font-weight: 600;
color: #333;
margin: 0 0 4px;
line-height: 1.3;
word-break: break-all;
margin: 0 0 6px;
line-height: 1.4;
width: 100%;
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
word-break: break-word;
}
.sponsor-desc {
font-size: 10px;
font-size: 11px;
color: #999;
margin: 0;
line-height: 1.4;
word-break: break-all;
width: 100%;
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
word-break: break-word;
}
/* CTA区域 */
@@ -930,28 +938,26 @@ onUnmounted(() => {
}
.sponsors-grid {
gap: 12px;
grid-template-columns: 1fr;
gap: 16px;
}
.sponsor-card {
flex: 0 1 calc(20% - 10px);
min-width: 100px;
max-width: 130px;
padding: 10px;
padding: 16px;
}
.sponsor-logo {
width: 40px;
height: 40px;
margin-bottom: 6px;
width: 50px;
height: 50px;
margin-bottom: 10px;
}
.sponsor-name {
font-size: 10px;
font-size: 12px;
}
.sponsor-desc {
font-size: 9px;
font-size: 10px;
}
}
</style>
+1 -1
View File
@@ -347,7 +347,7 @@ const onSubmit = async () => {
}
.plate-digits {
width: 110px;
width: 95px;
height: 28px;
border: 2px solid rgba(255,255,255,0.9);
border-radius: 3px;
+294 -96
View File
@@ -64,6 +64,16 @@
/>
</template>
</van-cell>
<van-cell title="弹幕开关" center>
<template #right-icon>
<van-switch
v-model="activity.danmakuEnabled"
size="24px"
@change="toggleDanmaku"
active-color="#d4af37"
/>
</template>
</van-cell>
<van-cell title="活动描述" :label="activity.description" />
</van-cell-group>
</div>
@@ -139,7 +149,7 @@
<div class="sponsor-list">
<div v-for="sponsor in sponsors" :key="sponsor.id" class="sponsor-card">
<div class="sponsor-logo-wrapper">
<div class="sponsor-logo" :style="sponsor.logo ? { backgroundImage: `url(${sponsor.logo})` } : {}">
<div class="sponsor-logo" :style="sponsor.logo ? { backgroundImage: `url(${getImageUrl(sponsor.logo)})` } : {}">
<van-icon v-if="!sponsor.logo" name="shop-o" />
</div>
</div>
@@ -150,9 +160,12 @@
{{ sponsor.type === 'title' ? '冠名' : '赞助' }}
</span>
</div>
<div v-if="sponsor.link" class="sponsor-link">
<van-icon name="link-o" />
<span>{{ sponsor.link }}</span>
<div class="sponsor-meta">
<span class="sponsor-sort">排序: {{ sponsor.sortOrder || 0 }}</span>
<div v-if="sponsor.link" class="sponsor-link">
<van-icon name="link-o" />
<span>{{ sponsor.link }}</span>
</div>
</div>
</div>
<div class="sponsor-ops">
@@ -171,6 +184,42 @@
</div>
</van-tab>
<van-tab title="奖品管理" name="prizes">
<div class="tab-content">
<div class="prize-list">
<div v-for="prize in prizes" :key="prize.id" class="prize-manage-card">
<div class="prize-image-wrapper">
<div class="prize-image" :style="prize.image ? { backgroundImage: `url(${getImageUrl(prize.image)})` } : {}">
<van-icon v-if="!prize.image" name="gift-o" size="30" />
</div>
</div>
<div class="prize-details">
<div class="prize-header">
<span class="prize-level-badge" :class="`level-${prize.level}`">{{ prize.level }}</span>
<span class="prize-name">{{ prize.name }}</span>
</div>
<div v-if="prize.description" class="prize-desc-text">{{ prize.description }}</div>
<div class="prize-meta">
<span class="prize-quantity-text">数量: {{ prize.quantity }}</span>
<span class="prize-sort">排序: {{ prize.sortOrder || 0 }}</span>
</div>
</div>
<div class="prize-ops">
<van-icon name="edit" @click="editPrize(prize)" />
<van-icon name="delete-o" @click="deletePrizeItem(prize.id)" />
</div>
</div>
</div>
<van-empty v-if="!prizes.length" description="暂无奖品" />
<!-- 悬浮添加按钮 -->
<div class="float-add-btn" @click="showPrizeForm = true">
<van-icon name="plus" />
</div>
</div>
</van-tab>
<van-tab title="报名情况" name="registrations">
<div class="tab-content">
<!-- 统计栏 -->
@@ -636,6 +685,12 @@
</van-radio-group>
</template>
</van-field>
<van-field
v-model.number="sponsorForm.sortOrder"
type="number"
label="排序"
placeholder="数字越小越靠前"
/>
<van-field v-model="sponsorForm.link" label="链接" placeholder="官网链接(选填)" />
<van-field name="logo" label="Logo">
<template #input>
@@ -657,6 +712,79 @@
</div>
</van-popup>
<!-- 奖品表单弹窗 -->
<van-popup v-model:show="showPrizeForm" position="bottom" round>
<div class="form-popup">
<div class="popup-header">
<h3>{{ prizeForm.id ? '编辑奖品' : '添加奖品' }}</h3>
<van-icon name="cross" @click="showPrizeForm = false" />
</div>
<van-form @submit="submitPrize" class="popup-form">
<van-cell-group>
<van-field v-model="prizeForm.name" label="名称" placeholder="请输入奖品名称" required />
<van-field
v-model="prizeForm.level"
label="等级"
placeholder="选择等级"
is-link
readonly
@click="showLevelPicker = true"
required
/>
<van-field v-model="prizeForm.description" label="描述" type="textarea" placeholder="奖品描述(选填)" rows="2" autosize />
<van-field
v-model.number="prizeForm.quantity"
type="number"
label="数量"
placeholder="请输入数量"
required
/>
<van-field
v-model.number="prizeForm.sortOrder"
type="number"
label="排序"
placeholder="数字越小越靠前"
/>
<van-field name="image" label="图片">
<template #input>
<van-uploader
v-model="prizeImage"
:max-count="1"
:after-read="onPrizeImageRead"
accept="image/*"
>
<van-button icon="plus" type="primary" size="small">选择图片</van-button>
</van-uploader>
</template>
</van-field>
</van-cell-group>
<div class="form-actions">
<van-button round block type="primary" native-type="submit" size="large">保存</van-button>
</div>
</van-form>
</div>
</van-popup>
<!-- 等级选择器 -->
<van-popup v-model:show="showLevelPicker" position="bottom" round>
<div class="level-picker">
<div class="picker-header">
<h3>选择等级</h3>
<van-icon name="cross" @click="showLevelPicker = false" />
</div>
<div class="level-list">
<div
v-for="level in prizeLevels"
:key="level"
class="level-item"
@click="selectLevel(level)"
>
<span class="level-badge" :class="`level-${level}`">{{ level }}</span>
</div>
</div>
</div>
</van-popup>
<!-- 视频播放弹窗 -->
<van-popup v-model:show="showVideoPlayer" position="center" round :style="{ width: '90%' }">
<div class="video-player-popup">
@@ -761,13 +889,23 @@
<script setup>
import { ref, computed, onMounted, watch } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { showToast } from 'vant';
import { showToast, closeToast, showConfirmDialog, ImagePreview } from 'vant';
import api from '../../api';
import { usePrizeManage } from './usePrizeManage';
const route = useRoute();
const router = useRouter();
const activeTab = ref('info');
// 获取完整图片URL的辅助函数
const getImageUrl = (path) => {
if (!path) return '';
if (path.startsWith('http://') || path.startsWith('https://')) {
return path;
}
return import.meta.env.DEV ? `http://localhost:3000${path}` : `https://mi.xyvan.cn${path}`;
};
const activity = ref({});
const expenses = ref([]);
const sponsors = ref([]);
@@ -790,6 +928,25 @@ const activeFilter = ref('all'); // 过滤器状态
const showActivityForm = ref(false);
const showExpenseForm = ref(false);
const showSponsorForm = ref(false);
// 奖品管理
const activityId = computed(() => route.params.id);
const {
prizes,
showPrizeForm,
showLevelPicker,
prizeImage,
prizeForm,
prizeLevels,
loadPrizes,
editPrize,
onPrizeImageRead,
selectLevel,
submitPrize,
deletePrizeItem,
resetPrizeForm
} = usePrizeManage(activityId);
const showMapPicker = ref(false);
const showStartTimePicker = ref(false);
const showDeadlinePicker = ref(false);
@@ -852,7 +1009,8 @@ const sponsorForm = ref({
id: null,
name: '',
type: 'sponsor',
link: ''
link: '',
sortOrder: 0
});
const totalExpense = computed(() => {
@@ -996,6 +1154,9 @@ const loadData = async () => {
const { data: regs } = await api.getAllRegistrations({ activityId: route.params.id });
registrations.value = regs;
// 加载奖品
await loadPrizes();
} catch (error) {
showToast('加载失败');
}
@@ -1057,43 +1218,62 @@ const toggleRegistration = async (value) => {
}
};
const submitExpense = async () => {
const toggleDanmaku = async (value) => {
try {
const formData = new FormData();
formData.append('activityId', activity.value.id);
formData.append('item', expenseForm.value.item);
formData.append('amount', expenseForm.value.amount);
formData.append('category', expenseForm.value.category);
formData.append('note', expenseForm.value.note);
// 添加现有媒体
if (expenseForm.value.media && expenseForm.value.media.length > 0) {
formData.append('existingMedia', JSON.stringify(expenseForm.value.media));
}
// 添加新上传的媒体文件
expenseMediaFiles.value.forEach(item => {
if (item.file) {
formData.append('media', item.file);
}
});
if (expenseForm.value.id) {
await api.updateExpense(expenseForm.value.id, formData);
showToast('更新成功');
} else {
await api.addExpense(formData);
showToast('添加成功');
}
showExpenseForm.value = false;
expenseForm.value = { id: null, item: '', amount: '', category: '', note: '', media: [] };
expenseMediaFiles.value = [];
loadData();
await api.updateActivity(activity.value.id, { danmakuEnabled: value });
showToast(value ? '弹幕已开启' : '弹幕已关闭');
} catch (error) {
showToast(expenseForm.value.id ? '更新失败' : '添加失败');
showToast('操作失败');
activity.value.danmakuEnabled = !value;
}
};
const submitExpense = async () => {
const loadingToast = showToast({
type: 'loading',
message: '保存中...',
forbidClick: true,
duration: 0
});
try {
const formData = new FormData();
formData.append('activityId', activity.value.id);
formData.append('item', expenseForm.value.item);
formData.append('amount', expenseForm.value.amount);
formData.append('category', expenseForm.value.category);
formData.append('note', expenseForm.value.note);
// 添加现有媒体
if (expenseForm.value.media && expenseForm.value.media.length > 0) {
formData.append('existingMedia', JSON.stringify(expenseForm.value.media));
}
// 添加新上传的媒体文件
expenseMediaFiles.value.forEach(item => {
if (item.file) {
formData.append('media', item.file);
}
});
if (expenseForm.value.id) {
await api.updateExpense(expenseForm.value.id, formData);
showToast('更新成功');
} else {
await api.addExpense(formData);
showToast('添加成功');
}
showExpenseForm.value = false;
expenseForm.value = { id: null, item: '', amount: '', category: '', note: '', media: [] };
expenseMediaFiles.value = [];
loadData();
} catch (error) {
showToast(expenseForm.value.id ? '更新失败' : '添加失败');
} finally {
loadingToast.clear();
}
};
const editExpense = (expense) => {
expenseForm.value = {
...expense,
@@ -1113,18 +1293,18 @@ const removeExpenseMedia = (index) => {
const deleteExpenseItem = async (id) => {
try {
await import('vant').then(({ showConfirmDialog }) => {
showConfirmDialog({
title: '确认删除',
message: '确定要删除这条经费记录吗?',
}).then(async () => {
await api.deleteExpense(id);
showToast('删除成功');
loadData();
}).catch(() => {});
await showConfirmDialog({
title: '确认删除',
message: '确定要删除这条经费记录吗?',
});
await api.deleteExpense(id);
showToast('删除成功');
loadData();
} catch (error) {
showToast('删除失败');
// 用户取消或删除失败
if (error !== 'cancel') {
showToast('删除失败');
}
}
};
@@ -1139,6 +1319,8 @@ const submitSponsor = async () => {
console.log('表单数据:', sponsorForm.value);
console.log('Logo 文件:', sponsorLogo.value);
let loadingToast = null;
try {
const formData = new FormData();
if (!sponsorForm.value.id) {
@@ -1147,6 +1329,7 @@ const submitSponsor = async () => {
formData.append('name', sponsorForm.value.name);
formData.append('type', sponsorForm.value.type);
formData.append('link', sponsorForm.value.link || '');
formData.append('sortOrder', sponsorForm.value.sortOrder || 0);
// 检查是否有 Logo 需要上传
const hasLogo = sponsorLogo.value.length > 0 && sponsorLogo.value[0].file;
@@ -1154,7 +1337,7 @@ const submitSponsor = async () => {
if (hasLogo) {
const logoSize = (sponsorLogo.value[0].file.size / 1024 / 1024).toFixed(2);
console.log('准备上传 Logo,大小:', logoSize, 'MB');
showToast({
loadingToast = showToast({
type: 'loading',
message: `正在上传 Logo (${logoSize}MB)...`,
forbidClick: true,
@@ -1163,7 +1346,7 @@ const submitSponsor = async () => {
formData.append('logo', sponsorLogo.value[0].file);
} else {
console.log('没有 Logo,仅保存数据');
showToast({
loadingToast = showToast({
type: 'loading',
message: '正在保存...',
forbidClick: true,
@@ -1183,10 +1366,6 @@ const submitSponsor = async () => {
const endTime = Date.now();
console.log('API 调用完成,耗时:', (endTime - startTime) / 1000, '秒');
// 使用 closeToast 关闭所有 Toast
const { closeToast } = await import('vant');
closeToast();
// 显示成功提示
showToast({
type: 'success',
@@ -1196,18 +1375,14 @@ const submitSponsor = async () => {
// 关闭弹窗并重置表单
showSponsorForm.value = false;
sponsorForm.value = { id: null, name: '', type: 'sponsor', link: '' };
sponsorForm.value = { id: null, name: '', type: 'sponsor', link: '', sortOrder: 0 };
sponsorLogo.value = [];
console.log('开始重新加载数据...');
// 重新加载数据
await loadData();
// 重新加载数据(不等待,避免阻塞)
loadData();
console.log('数据加载完成');
} catch (error) {
// 使用 closeToast 关闭所有 Toast
const { closeToast } = await import('vant');
closeToast();
console.error('赞助商提交失败:', error);
console.error('错误详情:', error.response);
const errorMsg = error.response?.data?.error || error.message || '操作失败';
@@ -1216,11 +1391,23 @@ const submitSponsor = async () => {
message: errorMsg,
duration: 3000
});
} finally {
if (loadingToast) {
loadingToast.clear();
}
}
};
const editSponsor = (sponsor) => {
sponsorForm.value = { ...sponsor };
// 回显Logo - 需要使用完整URL
if (sponsor.logo) {
const logoUrl = sponsor.logo.startsWith('http') ? sponsor.logo :
(import.meta.env.DEV ? `http://localhost:3000${sponsor.logo}` : `https://mi.xyvan.cn${sponsor.logo}`);
sponsorLogo.value = [{ url: logoUrl }];
} else {
sponsorLogo.value = [];
}
showSponsorForm.value = true;
};
@@ -1234,18 +1421,18 @@ const onLogoRead = (file) => {
const deleteSponsorItem = async (id) => {
try {
await import('vant').then(({ showConfirmDialog }) => {
showConfirmDialog({
title: '确认删除',
message: '确定要删除这个赞助商吗?',
}).then(async () => {
await api.deleteSponsor(id);
showToast('删除成功');
loadData();
}).catch(() => {});
await showConfirmDialog({
title: '确认删除',
message: '确定要删除这个赞助商吗?',
});
await api.deleteSponsor(id);
showToast('删除成功');
loadData();
} catch (error) {
showToast('删除失败');
// 用户取消或删除失败
if (error !== 'cancel') {
showToast('删除失败');
}
}
};
@@ -1335,7 +1522,7 @@ const uploadPhotos = async (files) => {
const totalSizeMB = (totalSize / 1024 / 1024).toFixed(2);
const fileCount = fileArray.length;
showToast({
const loadingToast = showToast({
type: 'loading',
message: `正在上传 ${fileCount} 个文件 (${totalSizeMB}MB)...`,
forbidClick: true,
@@ -1350,10 +1537,6 @@ const uploadPhotos = async (files) => {
await api.uploadPhotos(formData);
// 使用 closeToast 关闭所有 Toast
const { closeToast } = await import('vant');
closeToast();
// 显示成功提示
showToast({
type: 'success',
@@ -1364,19 +1547,17 @@ const uploadPhotos = async (files) => {
// 清空文件列表
photoFiles.value = [];
// 重新加载数据
await loadData();
// 重新加载数据(不等待,避免阻塞)
loadData();
} catch (error) {
// 使用 closeToast 关闭所有 Toast
const { closeToast } = await import('vant');
closeToast();
console.error('上传失败:', error);
showToast({
type: 'fail',
message: error.response?.data?.error || '上传失败',
duration: 3000
});
} finally {
loadingToast.clear();
}
};
@@ -1393,26 +1574,24 @@ const approvePhoto = async (id, status) => {
const deletePhoto = async (id) => {
try {
await import('vant').then(({ showConfirmDialog }) => {
showConfirmDialog({
title: '确认删除',
message: '删除后无法恢复,确定要删除吗?',
}).then(async () => {
await api.deletePhoto(id);
showToast('删除成功');
loadData();
}).catch(() => {});
await showConfirmDialog({
title: '确认删除',
message: '删除后无法恢复,确定要删除吗?',
});
await api.deletePhoto(id);
showToast('删除成功');
loadData();
} catch (error) {
console.error('删除失败:', error);
showToast('删除失败');
// 用户取消或删除失败
if (error !== 'cancel') {
console.error('删除失败:', error);
showToast('删除失败');
}
}
};
const previewImage = (url) => {
import('vant').then(({ ImagePreview }) => {
ImagePreview([url]);
});
ImagePreview([url]);
};
const playVideo = (url) => {
@@ -1612,6 +1791,8 @@ onMounted(loadData);
</script>
<style scoped>
@import './prize-manage-styles.css';
.activity-manage {
min-height: 100vh;
background: #f8f9fa;
@@ -1934,6 +2115,22 @@ onMounted(loadData);
background: linear-gradient(135deg, #07c160 0%, #00d68f 100%);
}
.sponsor-meta {
display: flex;
align-items: center;
gap: 12px;
margin-top: 6px;
}
.sponsor-sort {
font-size: 12px;
color: #999;
padding: 2px 8px;
background: #f5f5f5;
border-radius: 4px;
white-space: nowrap;
}
.sponsor-link {
display: flex;
align-items: center;
@@ -1941,6 +2138,7 @@ onMounted(loadData);
font-size: 12px;
color: #FF6700;
overflow: hidden;
flex: 1;
}
.sponsor-link .van-icon {
@@ -0,0 +1,179 @@
/* 奖品管理样式 */
.prize-manage-card {
display: flex;
gap: 12px;
padding: 16px;
background: #fff;
border-radius: 12px;
border: 1px solid #f0f0f0;
margin-bottom: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
transition: all 0.2s;
}
.prize-manage-card:active {
transform: scale(0.98);
}
.prize-image-wrapper {
flex-shrink: 0;
}
.prize-manage-card .prize-image {
width: 70px;
height: 70px;
border-radius: 8px;
background-size: cover;
background-position: center;
background-color: #f5f5f5;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid #e8e8e8;
}
.prize-details {
flex: 1;
display: flex;
flex-direction: column;
gap: 6px;
}
.prize-details .prize-header {
display: flex;
align-items: center;
gap: 8px;
}
.prize-level-badge {
display: inline-block;
padding: 2px 8px;
border-radius: 10px;
font-size: 11px;
font-weight: 600;
white-space: nowrap;
}
.prize-level-badge.level-一等奖 {
background: linear-gradient(135deg, #FFD700 0%, #FFA500 100%);
color: #fff;
}
.prize-level-badge.level-二等奖 {
background: linear-gradient(135deg, #C0C0C0 0%, #A8A8A8 100%);
color: #fff;
}
.prize-level-badge.level-三等奖 {
background: linear-gradient(135deg, #CD7F32 0%, #B8733E 100%);
color: #fff;
}
.prize-level-badge.level-参与奖 {
background: linear-gradient(135deg, #FF6700 0%, #FF8533 100%);
color: #fff;
}
.prize-level-badge.level-伴手礼 {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
color: #fff;
}
.prize-details .prize-name {
font-size: 14px;
font-weight: 600;
color: #333;
flex: 1;
}
.prize-desc-text {
font-size: 12px;
color: #666;
line-height: 1.5;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
}
.prize-meta {
display: flex;
gap: 12px;
font-size: 12px;
color: #999;
}
.prize-quantity-text {
color: #FF6700;
font-weight: 500;
}
.prize-ops {
display: flex;
flex-direction: column;
gap: 12px;
justify-content: center;
}
.prize-ops .van-icon {
font-size: 20px;
color: #999;
cursor: pointer;
transition: color 0.2s;
}
.prize-ops .van-icon:active {
color: #FF6700;
}
/* 等级选择器 */
.level-picker {
padding: 16px;
}
.level-picker .picker-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.level-picker .picker-header h3 {
margin: 0;
font-size: 16px;
color: #333;
}
.level-picker .picker-header .van-icon {
font-size: 20px;
color: #999;
cursor: pointer;
}
.level-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.level-item {
padding: 12px 16px;
background: #f8f9fa;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
text-align: center;
}
.level-item:active {
transform: scale(0.98);
background: #fff7e6;
}
.level-item .level-badge {
display: inline-block;
padding: 6px 20px;
border-radius: 16px;
font-size: 14px;
font-weight: 600;
}
+153
View File
@@ -0,0 +1,153 @@
import { ref } from 'vue';
import { showToast, showConfirmDialog } from 'vant';
import api from '../../api';
export function usePrizeManage(activityId) {
const prizes = ref([]);
const showPrizeForm = ref(false);
const showLevelPicker = ref(false);
const prizeImage = ref([]);
const prizeForm = ref({
id: null,
activityId: activityId.value,
name: '',
level: '',
description: '',
quantity: 1,
sortOrder: 0,
image: null
});
const prizeLevels = ['一等奖', '二等奖', '三等奖', '参与奖', '伴手礼'];
const loadPrizes = async () => {
try {
const { data } = await api.getActivity(activityId.value);
prizes.value = data.prizes || [];
} catch (error) {
console.error('加载奖品失败:', error);
}
};
const editPrize = (prize) => {
prizeForm.value = {
id: prize.id,
activityId: activityId.value,
name: prize.name,
level: prize.level,
description: prize.description || '',
quantity: prize.quantity,
sortOrder: prize.sortOrder || 0,
image: prize.image
};
// 回显图片 - 需要使用完整URL
if (prize.image) {
const imageUrl = prize.image.startsWith('http') ? prize.image :
(import.meta.env.DEV ? `http://localhost:3000${prize.image}` : `https://mi.xyvan.cn${prize.image}`);
prizeImage.value = [{ url: imageUrl }];
} else {
prizeImage.value = [];
}
showPrizeForm.value = true;
};
const onPrizeImageRead = (file) => {
prizeForm.value.imageFile = file.file;
};
const selectLevel = (level) => {
prizeForm.value.level = level;
showLevelPicker.value = false;
};
const submitPrize = async () => {
if (!prizeForm.value.name || !prizeForm.value.level) {
showToast('请填写必填项');
return;
}
const loadingToast = showToast({
type: 'loading',
message: '保存中...',
forbidClick: true,
duration: 0
});
try {
const formData = new FormData();
formData.append('activityId', activityId.value);
formData.append('name', prizeForm.value.name);
formData.append('level', prizeForm.value.level);
formData.append('description', prizeForm.value.description);
formData.append('quantity', prizeForm.value.quantity);
formData.append('sortOrder', prizeForm.value.sortOrder);
if (prizeForm.value.imageFile) {
formData.append('image', prizeForm.value.imageFile);
}
if (prizeForm.value.id) {
await api.updatePrize(prizeForm.value.id, formData);
} else {
await api.addPrize(formData);
}
showToast('保存成功');
showPrizeForm.value = false;
resetPrizeForm();
loadPrizes();
} catch (error) {
showToast('保存失败');
console.error('保存奖品失败:', error);
} finally {
loadingToast.clear();
}
};
const deletePrizeItem = async (id) => {
try {
await showConfirmDialog({
title: '确认删除',
message: '确定要删除这个奖品吗?'
});
await api.deletePrize(id);
showToast('删除成功');
await loadPrizes();
} catch (error) {
if (error !== 'cancel') {
showToast('删除失败');
}
}
};
const resetPrizeForm = () => {
prizeForm.value = {
id: null,
activityId: activityId.value,
name: '',
level: '',
description: '',
quantity: 1,
sortOrder: 0,
image: null
};
prizeImage.value = [];
};
return {
prizes,
showPrizeForm,
showLevelPicker,
prizeImage,
prizeForm,
prizeLevels,
loadPrizes,
editPrize,
onPrizeImageRead,
selectLevel,
submitPrize,
deletePrizeItem,
resetPrizeForm
};
}
+417
View File
@@ -0,0 +1,417 @@
// 活动流程数据
export const timelineData = [
{
title: '签到与集结',
time: '14:00 - 14:30',
brief: '车辆到场、签到登记、领取积分卡',
overview: `
<div class="overview-text">
<p>📍 车辆到达指定地点后,工作人员将引导至临时停车点</p>
<p>✍️ 前往签到处签名确认,扫码加入活动群</p>
<p>🎫 领取专属积分卡,开启精彩旅程</p>
</div>
`,
content: `
<div class="game-rule">
<div class="rule-title">📋 积分卡说明</div>
<div class="rule-content">
• 每人领取一张积分卡(红绿双色章)<br>
• 初始积分:0分<br>
• 游戏得分范围:1~5分<br>
• 游戏扣分范围:1~5分<br>
• 最终积分可兑换礼品
</div>
</div>
`
},
{
title: '观赏素材拍摄',
time: '14:30 - 16:30',
brief: '多点位打卡拍照,记录精彩瞬间',
overview: `
<div class="overview-text">
<p>📸 专业摄影师带队,前往多个精选点位</p>
<p>🚗 全员车前合影、签到墙打卡、地标合照</p>
<p>✨ 完成所有点位拍照可获得 <span class="score-badge">+2分</span></p>
</div>
`,
items: [
{
icon: '🏛️',
name: '宝船门口',
detail: `
<div class="sub-detail">
<p><strong>拍摄内容:</strong></p>
<p>• 全员车前合影</p>
<p>• 签到墙合照</p>
<p>• 个人打卡照</p>
</div>
`
},
{
icon: '🌉',
name: '隧道三点位',
detail: `
<div class="sub-detail">
<p><strong>拍摄内容:</strong></p>
<p>• 地标合影</p>
<p>• 动态全员上车</p>
<p>• 点火瞬间</p>
</div>
`
},
{
icon: '🎨',
name: '特色造型',
detail: `
<div class="sub-detail">
<p><strong>拍摄内容:</strong></p>
<p>• 摆"泉州"两个字造型</p>
<p>• 创意合影</p>
</div>
`
}
]
},
{
title: '互动游戏时间',
time: '16:30 - 18:00',
brief: '10+趣味游戏项目,赢取积分',
overview: `
<div class="overview-text">
<p>🎮 精心准备10+款趣味游戏</p>
<p>🏆 每个游戏都有积分奖励,表现越好得分越高</p>
<p>⚡ 特别设置复活挑战赛,给你翻盘机会</p>
<p class="tip-text">💡 点击下方游戏查看详细规则和得分标准</p>
</div>
`,
items: [
{
icon: '🎯',
name: '套圈圈',
detail: `
<div class="sub-detail">
<p><strong>游戏规则:</strong>站在指定位置,用圈圈套中目标物品</p>
<p><strong>得分标准:</strong></p>
<p>• 套中1个:<span class="score-badge">+1分</span></p>
<p>• 套中3个:<span class="score-badge">+3分</span></p>
<p>• 套中5个:<span class="score-badge">+5分</span></p>
</div>
`
},
{
icon: '🏺',
name: '投壶',
detail: `
<div class="sub-detail">
<p><strong>游戏规则:</strong>将箭矢投入壶中,考验准确度</p>
<p><strong>得分标准:</strong></p>
<p>• 投中3个:<span class="score-badge">+2分</span></p>
<p>• 投中5个:<span class="score-badge">+4分</span></p>
<p>• 全部投中:<span class="score-badge">+5分</span></p>
</div>
`
},
{
icon: '🎲',
name: '摇骰子(067玩法)',
detail: `
<div class="sub-detail">
<p><strong>游戏规则:</strong>玩家与庄家对战,比拼骰子点数大小</p>
<p><strong>得分标准:</strong></p>
<p>• 玩家赢庄家:<span class="score-badge">+1分</span></p>
<p>• 玩家输庄家:<span class="score-badge">-1分</span></p>
<p>• 摇出5个正豹子:<span class="score-badge">+5分</span>(直接判赢)</p>
</div>
`
},
{
icon: '🎲',
name: '摇骰子(单骰比大小)',
detail: `
<div class="sub-detail">
<p><strong>游戏规则:</strong>单个骰子比大小,玩家与庄家对战</p>
<p><strong>得分标准:</strong></p>
<p>• 玩家赢庄家:<span class="score-badge">+1分</span></p>
<p>• 玩家输庄家:<span class="score-badge">-1分</span></p>
</div>
`
},
{
icon: '🐂',
name: '牛牛',
detail: `
<div class="sub-detail">
<p><strong>游戏规则:</strong>扑克牌牛牛游戏,与对手比拼牌型大小</p>
<p><strong>得分标准:</strong></p>
<p>• 赢一局:<span class="score-badge">+1分</span></p>
<p>• 连赢3局:<span class="score-badge">+3分</span></p>
<p>• 连赢5局:<span class="score-badge">+5分</span></p>
</div>
`
},
{
icon: '✊',
name: '闽南划拳(两拳倒)',
detail: `
<div class="sub-detail">
<p><strong>游戏规则:</strong>闽南传统划拳游戏,两拳倒玩法</p>
<p><strong>得分标准:</strong></p>
<p>• 玩家赢:<span class="score-badge">+1分</span></p>
<p>• 玩家输:<span class="score-badge">-1分</span></p>
<p>• 一人一拳投资,赢的玩家:<span class="score-badge">+2分</span></p>
<p>• 最高叠加投资到划5拳,赢的玩家:<span class="score-badge">+5分</span></p>
<p class="highlight-tip">💡 投资机制:可叠加投资,赢得越多分数越高!</p>
</div>
`
},
{
icon: '⚽',
name: '足球射门',
detail: `
<div class="sub-detail">
<p><strong>游戏规则:</strong>踢足球射门,进球数决定得分</p>
<p><strong>得分标准:</strong></p>
<p>• 进1球:<span class="score-badge">+1分</span></p>
<p>• 进3球:<span class="score-badge">+3分</span></p>
<p>• 进5球:<span class="score-badge">+5分</span></p>
</div>
`
},
{
icon: '🎮',
name: '王者荣耀',
detail: `
<div class="sub-detail">
<p><strong>游戏规则:</strong>组队进行王者荣耀对战</p>
<p><strong>5V5模式(凑齐10人):</strong></p>
<p>• 胜方队员(4人):<span class="score-badge">+3分</span></p>
<p>• 胜方MVP(评分最高):<span class="score-badge">+5分</span></p>
<p>• 败方MVP(评分最高):<span class="score-badge">+1分</span></p>
<p><strong>SOLO模式(1V1):</strong></p>
<p>• 胜者(一血或一塔):<span class="score-badge">+2分</span></p>
<p class="highlight-tip">💡 团队配合最重要,选好英雄!</p>
</div>
`
},
{
icon: '💧',
name: '抛水瓶',
detail: `
<div class="sub-detail">
<p><strong>游戏规则:</strong>抛水瓶使其立住,考验技巧</p>
<p><strong>得分标准:</strong></p>
<p>• 立住1个:<span class="score-badge">+2分</span></p>
<p>• 立住3个:<span class="score-badge">+4分</span></p>
<p>• 立住5个:<span class="score-badge">+5分</span></p>
</div>
`
},
{
icon: '🎴',
name: '扑克游戏',
detail: `
<div class="sub-detail">
<p><strong>游戏规则:</strong>多种扑克玩法,赢家得分</p>
<p><strong>得分标准:</strong></p>
<p>• 赢家:<span class="score-badge">+2分</span></p>
<p>• 输家:<span class="score-badge">-1分</span></p>
</div>
`
},
{
icon: '🐊',
name: '鳄鱼牙齿/插剑玩具',
detail: `
<div class="sub-detail">
<p><strong>游戏规则:</strong>按压鳄鱼牙齿或插剑,避免触发机关</p>
<p><strong>得分标准:</strong></p>
<p>• 按/插过半数:<span class="score-badge">+1分</span></p>
<p>• 每多按/插一个:<span class="score-badge">+1分</span>(累加)</p>
<p>• 最高封顶:<span class="score-badge">+5分</span></p>
<p>• 触发机关(爆了):<span class="score-badge">积分清0</span></p>
<p class="highlight-tip">💡 胆大心细,步步为营!</p>
</div>
`
},
{
icon: '🪑',
name: '最终复活挑战 - 抢椅子',
detail: `
<div class="sub-detail highlight-sub">
<p><strong>游戏规则:</strong>音乐停止时抢座位,未抢到者淘汰</p>
<p><strong>得分标准:</strong></p>
<p>• 冠军:<span class="score-badge">+5分</span></p>
<p>• 亚军:<span class="score-badge">+3分</span></p>
<p>• 季军:<span class="score-badge">+2分</span></p>
<p>• 淘汰:<span class="score-badge">-1分</span></p>
<p class="highlight-tip">⚡ 这是翻盘的最佳机会!</p>
</div>
`
}
]
},
{
title: '落日BBQ',
time: '18:00 - 19:30',
brief: '落日余晖下享受美食,拍摄氛围大片',
overview: `
<div class="overview-text">
<p>🌅 在落日余晖下,车辆排成一排</p>
<p>🚗 齐开大灯,营造浪漫氛围</p>
<p>🍖 边烤肉边欣赏晚霞,拍摄氛围感大片</p>
</div>
`,
content: `
<div class="game-rule">
<div class="rule-content">
💡 提示:分享美食照片到朋友圈并@活动官方账号可获得 <span class="score-badge">+1分</span>
</div>
</div>
`
},
{
title: '篝火歌舞会',
time: '19:00 - 20:00',
brief: '围炉夜话,互动游戏,才艺表演',
overview: `
<div class="overview-text">
<p>🔥 点燃篝火,围坐一圈</p>
<p>🎤 击鼓传花、喊数抱团等互动游戏</p>
<p>🎭 才艺表演,展示个人魅力</p>
<p class="tip-text">💡 点击下方查看各个游戏详细规则</p>
</div>
`,
items: [
{
icon: '🥁',
name: '击鼓传花(10轮)',
detail: `
<div class="sub-detail">
<p><strong>游戏规则:</strong>音乐停止时持花者上台,吃柠檬或苦瓜</p>
<p><strong>得分标准:</strong></p>
<p>• 勇敢完成:<span class="score-badge">+2分</span></p>
<p>• 拒绝挑战:<span class="score-badge">-3分</span></p>
</div>
`
},
{
icon: '🤝',
name: '喊数抱团(10轮)',
detail: `
<div class="sub-detail">
<p><strong>游戏规则:</strong>主持人喊数字,参与者按数字抱团,落单者淘汰</p>
<p><strong>惩罚:</strong>上台自我介绍(1分钟)</p>
<p><strong>得分标准:</strong></p>
<p>• 成功抱团:<span class="score-badge">+1分</span></p>
<p>• 落单但完成自我介绍:<span class="score-badge">+2分</span></p>
<p>• 拒绝:<span class="score-badge">-2分</span></p>
</div>
`
},
{
icon: '🎵',
name: '大合唱',
detail: `
<div class="sub-detail">
<p><strong>活动内容:</strong>全员参与,气氛热烈</p>
<p><strong>得分标准:</strong></p>
<p>• 每人:<span class="score-badge">+1分</span></p>
</div>
`
},
{
icon: '🎭',
name: '个人才艺表演',
detail: `
<div class="sub-detail">
<p><strong>活动内容:</strong>自愿上台唱歌、跳舞、表演才艺</p>
<p><strong>提示:</strong>需提前到后台准备BGM</p>
<p><strong>得分标准:</strong></p>
<p>• 参与表演:<span class="score-badge">+3分</span></p>
<p>• 精彩表演:<span class="score-badge">+5分</span></p>
</div>
`
},
{
icon: '👶',
name: '小朋友互动',
detail: `
<div class="sub-detail">
<p><strong>活动内容:</strong>主持人邀请小朋友上台互动,参与即有礼物</p>
<p><strong>得分标准:</strong></p>
<p>• 家长陪同参与:<span class="score-badge">+2分</span></p>
</div>
`
}
]
},
{
title: '璀璨烟火秀',
time: '20:00 - 20:30',
brief: '专业烟火表演,点亮夜空',
overview: `
<div class="overview-text">
<p>🎆 大型专业烟火表演</p>
<p>✨ 每车配发手持仙女棒</p>
<p>🔥 加特林压轴,震撼收尾</p>
</div>
`,
items: [
{
icon: '🌃',
name: '米兰之夜',
detail: `
<div class="sub-detail">
<p><strong>表演内容:</strong>大型专业烟火表演,点亮夜空</p>
</div>
`
},
{
icon: '✨',
name: '仙女棒',
detail: `
<div class="sub-detail">
<p><strong>活动内容:</strong>每车配发手持烟花,拍照打卡</p>
</div>
`
},
{
icon: '🔥',
name: '加特林压轴',
detail: `
<div class="sub-detail">
<p><strong>表演内容:</strong>震撼收尾,全场欢呼</p>
<p class="highlight-tip">💡 拍摄烟花视频并分享可获得 <span class="score-badge">+2分</span></p>
</div>
`
}
]
},
{
title: '积分兑奖 & 自由交流',
time: '20:30 以后',
brief: '积分兑换礼品,自由合影交流',
overview: `
<div class="overview-text">
<p>🏆 根据积分排行榜兑换礼品</p>
<p>📸 自由合影,交流心得</p>
<p>🚗 整理垃圾,安全驾驶返程</p>
</div>
`,
content: `
<div class="game-rule">
<div class="rule-title">🏆 积分兑换礼品</div>
<div class="rule-content">
<strong>积分排行榜:</strong><br>
• 第1名(20分以上):神秘大奖<br>
• 第2-3名(15-19分):精美礼品<br>
• 第4-10名(10-14分):纪念品<br>
• 参与奖(5-9分):小礼物<br>
<br>
随后自由合影、整理垃圾、提醒安全驾驶返程
</div>
</div>
`
}
];