优化验证报名信息对话框车牌号输入布局,使标签和输入框在同一行显示
This commit is contained in:
@@ -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 |
@@ -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}`)
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
@@ -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>
|
||||
`
|
||||
}
|
||||
];
|
||||
Reference in New Issue
Block a user