This commit is contained in:
那些花儿
2025-04-25 08:48:11 +08:00
parent 94656c9449
commit 89c2b07ca9
19 changed files with 1702 additions and 1604 deletions
+109 -774
View File
@@ -2,36 +2,26 @@
import {ref, onMounted, reactive} from 'vue';
import {onLoad} from '@dcloudio/uni-app';
import {Toast} from 'tdesign-mobile-vue';
import {useRequest} from "@/utils/request";
const request = useRequest()
// 项目ID
const itemId = ref(null);
const actId = ref(null);
// 详情数据
const detailData = ref(null);
// 加载状态
const loading = ref(true);
// 当前活动选项卡
const activeTab = ref('detail');
// 是否显示PDF预览
const showPdfPreview = ref(false);
// 当前预览的文件
const currentFile = ref({});
// 是否显示报名表单
const showRegistrationForm = ref(false);
// 相关推荐列表
const relatedList = ref([]);
// 相关推荐是否加载中
const relatedLoading = ref(null);
// 是否有更多相关推荐
const hasMoreRelated = ref(true);
// 相关推荐页码
const relatedPage = ref(1);
// 获取路由参数
onLoad((option) => {
if (option.id) {
itemId.value = option.id;
actId.value = option.id;
getDetailData(option.id);
getRelatedList();
} else {
Toast({message: 'ID不存在', theme: 'error'});
}
@@ -41,96 +31,104 @@ onLoad((option) => {
const getDetailData = async (id) => {
loading.value = true;
try {
// 模拟网络请求延迟
await new Promise(resolve => setTimeout(resolve, 1000));
// 随机生成地点
const locations = [
'南京奥体中心',
'苏州国际博览中心',
'无锡太湖国际博览中心',
'常州体育中心',
'镇江国际会展中心'
];
const {code, data} = await request.get('/client/activity/get/' + id)
if (code === 0) {
detailData.value = data
} else {
Toast({message: '获取详情失败', theme: 'error'});
}
// 模拟详情数据
detailData.value = {
id,
title: '奋进新征程 运动促健康 2024晴江第六届"牧城之夜"荧光跑',
status: 1, // 1-报名中 2-已结束 3-即将开始
banner: 'https://img0.baidu.com/it/u=1693652779,2163403752&fm=253&fmt=auto&app=120&f=JPEG?w=1200&h=800',
price: 200,
participants: 500,
maxParticipants: 800,
registrationPeriod: '2025.04.03 00:00:00\n2025.04.08 20:00:00',
date: '2024.04.19 09:00:00\n2024.04.19 19:00:00',
location: '武汉市江夏区梁子湖龙湾半岛景区',
organizer: '靖江市文化广电和旅游局,靖江市体育总会',
coorganizer: '靖江市疾病预防控制中心',
desc: '本次活动是为了推动全民健身,促进城市体育文化建设的重要赛事。活动全程约5公里,沿途设有多个补给站和打卡点,参与者完成全程将获得完赛奖牌和证书。\n\n参赛者需提前30分钟到达集合地点,并携带有效身份证件。组委会将提供统一的参赛装备,包括荧光手环、荧光贴纸等。\n\n活动期间,参与者需遵守赛事规则,服从工作人员指挥,确保安全有序参与。',
competitions: [
{
name: '同心接力 聚力前行——飓风跑',
format: '6名参赛选手,横队站于起跑线后,双手握住长杆,共同向前跑,赛程为25米绕桩跑',
rules: '以各小组完成赛程所用时间作为评分依据,用时越短,得分越高'
},
{
name: '鼓韵凝心 协作共赢——击鼓颠球',
format: '10名参赛选手,共同牵拉一面四周绑有若干绳索的圆形鼓。通过协作牵拉绳索,让鼓面有节奏地上下颠动,使排球在鼓面上持续跳动。',
rules: '以3分钟内各小组有效击球次数作为评分依据,击球次数越多,得分越高'
},
{
name: '慧眼寻同 携手并进——连连看',
format: '一次派2位选手进入挑战区域,每位选手各自掀开一顶斗笠,核实斗笠内物品是否一致,一致可将物品带回,斗笠掀开;如物品不同则原位盖好',
rules: '结合参赛小组的物品获得数量进行纵向排名'
},
{
name: '方块解码 共绘家韵——智俄罗斯方块',
format: '一次派2位选手携带一块拼图进场,进场途中跟守关裁判猜拳"输"返回起点线重新进入,"赢"携带道具进场拼图',
rules: '结合参赛小组的用时长短进行纵向排名'
},
{
name: '极速接力 暖心传递——小轮车',
format: '6位参赛选手折返接力,直至6位选手互换场地',
rules: '结合参赛小组的用时长短进行纵向排名'
},
{
name: '众志筑塔 逐梦远航——通天塔',
format: '小组成员团队合作将8块立方体进行纵向叠高垒起,全部拼装完成后5秒倒计时,读秒结束高塔不倒即为完成',
rules: '结合参赛小组的用时长短进行纵向排名'
}
],
files: [
{
id: 1,
name: '活动规则及参赛须知.pdf',
url: 'https://www.africau.edu/images/default/sample.pdf',
size: '1.2MB',
date: '2024-03-15'
},
{
id: 2,
name: '参赛安全责任书.pdf',
url: 'https://www.africau.edu/images/default/sample.pdf',
size: '580KB',
date: '2024-03-15'
},
{
id: 3,
name: '活动流程安排.pdf',
url: 'https://www.africau.edu/images/default/sample.pdf',
size: '1.5MB',
date: '2024-03-16'
},
{
id: 4,
name: '比赛评分标准.pdf',
url: 'https://www.africau.edu/images/default/sample.pdf',
size: '890KB',
date: '2024-03-16'
}
]
};
// // 模拟网络请求延迟
// await new Promise(resolve => setTimeout(resolve, 1000));
//
// // 随机生成地点
// const locations = [
// '南京奥体中心',
// '苏州国际博览中心',
// '无锡太湖国际博览中心',
// '常州体育中心',
// '镇江国际会展中心'
// ];
//
// // 模拟详情数据
// detailData.value = {
// id,
// title: '奋进新征程 运动促健康 2024晴江第六届"牧城之夜"荧光跑',
// status: 1, // 1-报名中 2-已结束 3-即将开始
// banner: 'https://img0.baidu.com/it/u=1693652779,2163403752&fm=253&fmt=auto&app=120&f=JPEG?w=1200&h=800',
// price: 200,
// participants: 500,
// maxParticipants: 800,
// registrationPeriod: '2025.04.03 00:00:00\n2025.04.08 20:00:00',
// date: '2024.04.19 09:00:00\n2024.04.19 19:00:00',
// location: '武汉市江夏区梁子湖龙湾半岛景区',
// organizer: '靖江市文化广电和旅游局,靖江市体育总会',
// coorganizer: '靖江市疾病预防控制中心',
// desc: '本次活动是为了推动全民健身,促进城市体育文化建设的重要赛事。活动全程约5公里,沿途设有多个补给站和打卡点,参与者完成全程将获得完赛奖牌和证书。\n\n参赛者需提前30分钟到达集合地点,并携带有效身份证件。组委会将提供统一的参赛装备,包括荧光手环、荧光贴纸等。\n\n活动期间,参与者需遵守赛事规则,服从工作人员指挥,确保安全有序参与。',
// competitions: [
// {
// name: '同心接力 聚力前行——飓风跑',
// format: '6名参赛选手,横队站于起跑线后,双手握住长杆,共同向前跑,赛程为25米绕桩跑',
// rules: '以各小组完成赛程所用时间作为评分依据,用时越短,得分越高'
// },
// {
// name: '鼓韵凝心 协作共赢——击鼓颠球',
// format: '10名参赛选手,共同牵拉一面四周绑有若干绳索的圆形鼓。通过协作牵拉绳索,让鼓面有节奏地上下颠动,使排球在鼓面上持续跳动。',
// rules: '以3分钟内各小组有效击球次数作为评分依据,击球次数越多,得分越高'
// },
// {
// name: '慧眼寻同 携手并进——连连看',
// format: '一次派2位选手进入挑战区域,每位选手各自掀开一顶斗笠,核实斗笠内物品是否一致,一致可将物品带回,斗笠掀开;如物品不同则原位盖好',
// rules: '结合参赛小组的物品获得数量进行纵向排名'
// },
// {
// name: '方块解码 共绘家韵——智俄罗斯方块',
// format: '一次派2位选手携带一块拼图进场,进场途中跟守关裁判猜拳"输"返回起点线重新进入,"赢"携带道具进场拼图',
// rules: '结合参赛小组的用时长短进行纵向排名'
// },
// {
// name: '极速接力 暖心传递——小轮车',
// format: '6位参赛选手折返接力,直至6位选手互换场地',
// rules: '结合参赛小组的用时长短进行纵向排名'
// },
// {
// name: '众志筑塔 逐梦远航——通天塔',
// format: '小组成员团队合作将8块立方体进行纵向叠高垒起,全部拼装完成后5秒倒计时,读秒结束高塔不倒即为完成',
// rules: '结合参赛小组的用时长短进行纵向排名'
// }
// ],
// files: [
// {
// id: 1,
// name: '活动规则及参赛须知.pdf',
// url: 'https://www.africau.edu/images/default/sample.pdf',
// size: '1.2MB',
// date: '2024-03-15'
// },
// {
// id: 2,
// name: '参赛安全责任书.pdf',
// url: 'https://www.africau.edu/images/default/sample.pdf',
// size: '580KB',
// date: '2024-03-15'
// },
// {
// id: 3,
// name: '活动流程安排.pdf',
// url: 'https://www.africau.edu/images/default/sample.pdf',
// size: '1.5MB',
// date: '2024-03-16'
// },
// {
// id: 4,
// name: '比赛评分标准.pdf',
// url: 'https://www.africau.edu/images/default/sample.pdf',
// size: '890KB',
// date: '2024-03-16'
// }
// ]
// };
} catch (error) {
console.error('获取详情失败', error);
Toast({message: '获取详情失败', theme: 'error'});
@@ -139,85 +137,6 @@ const getDetailData = async (id) => {
}
};
// 报名表单数据
const registration = reactive({
organization: '',
contactName: '',
contactPhone: '',
agreementChecked: false,
participants: [
{
name: '',
gender: '男',
position: '',
idNumber: '',
phone: '',
accommodation: false
}
]
});
// 获取相关推荐列表
const getRelatedList = async () => {
relatedLoading.value = 'loading';
try {
// 模拟网络请求延迟
await new Promise(resolve => setTimeout(resolve, 1000));
// 随机生成地点
const locations = [
'南京奥体中心',
'苏州国际博览中心',
'无锡太湖国际博览中心',
'常州体育中心',
'镇江国际会展中心'
];
// 模拟数据
const mockData = Array.from({length: 5}, (_, index) => {
const id = relatedPage.value * 5 + index;
const randomPrice = Math.floor(Math.random() * 150) + 30;
const randomParticipants = Math.floor(Math.random() * 500) + 50;
return {
id,
title: index % 2 === 0 ? '2023江苏省青少年体育锦标赛' : '2023年冬季室内马拉松挑战赛',
status: Math.floor(Math.random() * 3) + 1,
date: `2023.${Math.floor(Math.random() * 12) + 1}.${Math.floor(Math.random() * 28) + 1}`,
endDate: `2023.${Math.floor(Math.random() * 12) + 1}.${Math.floor(Math.random() * 28) + 1}`,
image: `https://picsum.photos/800/450?random=${id + 100}`,
price: randomPrice,
participants: randomParticipants,
location: locations[Math.floor(Math.random() * locations.length)]
};
});
relatedList.value = [...relatedList.value, ...mockData];
// 模拟无更多数据
if (relatedPage.value >= 2) {
hasMoreRelated.value = false;
} else {
relatedPage.value++;
}
} catch (error) {
console.error('获取相关推荐失败', error);
} finally {
relatedLoading.value = null;
}
};
// 加载更多相关推荐
const loadMoreRelated = () => {
if (relatedLoading.value === 'loading' || !hasMoreRelated.value) return;
getRelatedList();
};
// 切换选项卡
const switchTab = (tab) => {
activeTab.value = tab;
};
// 打开PDF预览
const previewPdf = (file) => {
currentFile.value = file;
@@ -229,101 +148,13 @@ const closePdfPreview = () => {
showPdfPreview.value = false;
};
// 打开报名表单
const openRegistrationForm = () => {
// showRegistrationForm.value = true;
function openRegistrationForm() {
uni.navigateTo({
url: '/pages/activity/registration-form/index'
url: '/pages/activity/registration-form/index?id=' + actId.value
})
};
}
// 关闭报名表单
const closeRegistrationForm = () => {
showRegistrationForm.value = false;
};
// 添加参赛人员
const addParticipant = () => {
registration.participants.push({
name: '',
gender: '男',
position: '',
idNumber: '',
phone: '',
accommodation: false
});
};
// 移除参赛人员
const removeParticipant = (index) => {
if (registration.participants.length > 1) {
registration.participants.splice(index, 1);
} else {
Toast({message: '至少需要一名参赛人员', theme: 'warning'});
}
};
// 提交报名信息
const submitRegistration = () => {
// 检查是否同意承诺书
if (!registration.agreementChecked) {
Toast({message: '请阅读并同意参赛承诺书', theme: 'warning'});
return;
}
// 这里可以添加表单验证逻辑
// 模拟提交成功
Toast({message: '报名成功!', theme: 'success'});
closeRegistrationForm();
};
// 分享
const handleShare = () => {
Toast({message: '分享功能开发中', theme: 'warning'});
};
// 切换到首页
const navigateToHome = () => {
uni.switchTab({
url: '/pages/index/index'
});
};
// 状态标签文字
const getStatusText = (status) => {
switch (status) {
case 1:
return '报名中';
case 2:
return '已结束';
case 3:
return '即将开始';
default:
return '未知状态';
}
};
// 报名或预约
const handleSignup = () => {
if (detailData.value.status === 2) {
Toast({message: '活动已结束', theme: 'warning'});
return;
}
if (detailData.value.status === 1) {
Toast({message: '报名成功', theme: 'success'});
} else {
Toast({message: '预约提醒成功', theme: 'success'});
}
};
// 跳转到详情页
const navigateToDetail = (id) => {
uni.navigateTo({
url: `/pages/activity/detail/index?id=${id}`
});
};
</script>
<template>
@@ -332,7 +163,7 @@ const navigateToDetail = (id) => {
<template v-if="detailData">
<!-- 顶部图片 -->
<view class="event-header">
<image :src="detailData.banner" mode="aspectFill" class="event-cover"/>
<image :src="detailData.cover" mode="aspectFill" class="event-cover"/>
</view>
<!-- 活动标题卡片 -->
@@ -403,13 +234,13 @@ const navigateToDetail = (id) => {
<t-tabs default-value="detail">
<t-tab-panel value="detail" label="详细信息">
<view class="detail-text">{{ detailData.desc }}</view>
<view class="detail-text" v-html="detailData.content"></view>
</t-tab-panel>
<t-tab-panel value="events" label="比赛项目">
<view class="event-table-mobile">
<view
class="project-card"
v-for="(item, index) in detailData.competitions"
v-for="(item, index) in detailData.items"
:key="index"
>
<view class="project-header">
@@ -419,11 +250,11 @@ const navigateToDetail = (id) => {
<view class="project-details">
<view class="detail-item">
<view class="detail-label">活动形式</view>
<view class="detail-content">{{ item.format }}</view>
<view class="detail-content">{{ item.form }}</view>
</view>
<view class="detail-item">
<view class="detail-label">评分规则</view>
<view class="detail-content">{{ item.rules }}</view>
<view class="detail-content">{{ item.rule }}</view>
</view>
</view>
</view>
@@ -459,238 +290,6 @@ const navigateToDetail = (id) => {
立即报名
</view>
</view>
<!-- 报名表单 -->
<t-popup
v-model:visible="showRegistrationForm"
placement="center"
:close-on-overlay-click="true"
:prevent-scroll-through="true"
:show-overlay="true"
class="registration-dialog"
style="width: 100%"
>
<view class="registration-container">
<view class="registration-header">
<view class="registration-title">活动报名</view>
<view class="registration-close" @click="closeRegistrationForm">
<t-icon name="close"/>
</view>
</view>
<scroll-view class="registration-content" :scroll-y="true">
<view class="form-block">
<view class="form-block-title">
<t-icon name="user-circle"/>
单位信息
</view>
<view class="form-field">
<text class="field-label">单位名称
<text class="required-mark">*</text>
</text>
<input
type="text"
class="field-input"
placeholder="请输入单位名称"
v-model="registration.organization"
/>
</view>
<view class="form-row">
<view class="field-col">
<view class="form-field">
<text class="field-label">联系人
<text class="required-mark">*</text>
</text>
<input
type="text"
class="field-input"
placeholder="请输入姓名"
v-model="registration.contactName"
/>
</view>
</view>
<view class="field-col">
<view class="form-field">
<text class="field-label">联系电话
<text class="required-mark">*</text>
</text>
<input
type="text"
class="field-input"
placeholder="请输入手机号"
v-model="registration.contactPhone"
/>
</view>
</view>
</view>
</view>
<view class="form-block">
<view class="form-block-title">
<t-icon name="usergroup"/>
参赛人员
</view>
<view
class="participant-card"
v-for="(participant, index) in registration.participants"
:key="index"
>
<view class="participant-header">
<view class="participant-title">
<text>{{ index + 1 }}</text>
参赛人员信息
</view>
<view
class="remove-participant"
v-if="registration.participants.length > 1"
@click="removeParticipant(index)"
>
删除
<t-icon name="close-circle" size="16px"/>
</view>
</view>
<view class="form-field">
<text class="field-label">姓名
<text class="required-mark">*</text>
</text>
<input
type="text"
class="field-input"
placeholder="请输入姓名"
v-model="participant.name"
/>
</view>
<view class="form-field-horizontal">
<text class="field-label-horizontal">性别
<text class="required-mark">*</text>
</text>
<view class="field-content">
<view class="radio-buttons">
<view
class="radio-button"
:class="{ active: participant.gender === '男' }"
@click="participant.gender = '男'"
>
<t-icon name="check-circle" v-if="participant.gender === '男'"/>
<t-icon name="circle" v-else/>
<text>男</text>
</view>
<view
class="radio-button"
:class="{ active: participant.gender === '女' }"
@click="participant.gender = '女'"
>
<t-icon name="check-circle" v-if="participant.gender === '女'"/>
<t-icon name="circle" v-else/>
<text>女</text>
</view>
</view>
</view>
</view>
<view class="form-field">
<text class="field-label">职务</text>
<input
type="text"
class="field-input"
placeholder="请输入职务"
v-model="participant.position"
/>
</view>
<view class="form-row">
<view class="field-col">
<view class="form-field">
<text class="field-label">身份证号
<text class="required-mark">*</text>
</text>
<input
type="text"
class="field-input"
placeholder="请输入身份证号"
v-model="participant.idNumber"
/>
</view>
</view>
<view class="field-col">
<view class="form-field">
<text class="field-label">联系电话
<text class="required-mark">*</text>
</text>
<input
type="text"
class="field-input"
placeholder="请输入手机号"
v-model="participant.phone"
/>
</view>
</view>
</view>
<view class="form-field-horizontal">
<text class="field-label-horizontal">是否住宿</text>
<view class="field-content">
<view class="radio-buttons">
<view
class="radio-button"
:class="{ active: participant.accommodation }"
@click="participant.accommodation = true"
>
<t-icon name="check-circle" v-if="participant.accommodation"/>
<t-icon name="circle" v-else/>
<text>是</text>
</view>
<view
class="radio-button"
:class="{ active: !participant.accommodation }"
@click="participant.accommodation = false"
>
<t-icon name="check-circle" v-if="!participant.accommodation"/>
<t-icon name="circle" v-else/>
<text>否</text>
</view>
</view>
</view>
</view>
</view>
<view class="add-participant" @click="addParticipant">
<t-icon name="add-circle"/>
添加参赛人员
</view>
</view>
<t-input label="标签文字" layout="vertical" placeholder="请输入姓名"></t-input>
</scroll-view>
<view class="registration-footer">
<view class="agreement-checkbox">
<input
type="checkbox"
v-model="registration.agreementChecked"
/>
<label>我已阅读并同意
<text class="agreement-link">《参赛承诺书》</text>
</label>
</view>
<t-button
block
theme="primary"
size="large"
shape="round"
@click="submitRegistration"
>
提交报名
</t-button>
</view>
</view>
</t-popup>
</template>
</view>
</template>
@@ -1113,268 +712,4 @@ const navigateToDetail = (id) => {
box-shadow: 0 2px 8px rgba(231, 76, 60, 0.3);
}
/* 报名表单样式 */
.registration-dialog {
width: 100%;
height: 100%;
}
.registration-container {
background: white;
width: 100%;
height: 100%;
max-height: 100vh;
display: flex;
flex-direction: column;
}
.registration-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px;
background-color: #e74c3c;
color: white;
}
.registration-title {
font-size: 18px;
font-weight: 500;
}
.registration-close {
width: 32px;
height: 32px;
display: flex;
justify-content: center;
align-items: center;
border-radius: 50%;
background-color: rgba(255, 255, 255, 0.2);
color: white;
}
.registration-content {
flex: 1;
overflow-y: auto;
}
.form-block {
padding: 20px 16px;
border-bottom: 10px solid #f5f5f5;
}
.form-block-title {
font-size: 16px;
font-weight: 500;
margin-bottom: 16px;
color: #333;
display: flex;
align-items: center;
}
.form-block-title :deep(i) {
margin-right: 8px;
color: #e74c3c;
}
.form-field {
margin-bottom: 16px;
}
.form-field:last-child {
margin-bottom: 0;
}
/* 左右布局表单 */
.form-field-horizontal {
display: flex;
align-items: center;
margin-bottom: 16px;
}
.form-field-horizontal:last-child {
margin-bottom: 0;
}
.field-label-horizontal {
width: 80px;
font-size: 14px;
color: #666;
font-weight: 500;
flex-shrink: 0;
}
.field-content {
flex: 1;
}
.field-label {
display: block;
font-size: 14px;
color: #666;
margin-bottom: 8px;
font-weight: 500;
}
.field-input {
width: 100%;
height: 48px;
border: none;
border-bottom: 1px solid #ddd;
font-size: 15px;
color: #333;
background: transparent;
transition: border-color 0.2s;
}
.field-input:focus {
border-color: #e74c3c;
outline: none;
}
.field-input::placeholder {
color: #bbb;
}
.form-row {
display: flex;
gap: 15px;
}
.field-col {
flex: 1;
}
.radio-buttons {
display: flex;
gap: 10px;
}
.radio-button {
display: flex;
align-items: center;
border: 1px solid #ddd;
padding: 8px 12px;
border-radius: 6px;
flex: none;
min-width: 60px;
}
.radio-button.active {
border-color: #e74c3c;
background-color: rgba(231, 76, 60, 0.05);
}
.radio-button :deep(i) {
font-size: 18px;
margin-right: 8px;
color: #999;
}
.radio-button.active :deep(i) {
color: #e74c3c;
}
.participant-card {
background: white;
margin-bottom: 20px;
position: relative;
border-left: 3px solid #e74c3c;
padding-left: 12px;
}
.participant-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.participant-title {
font-weight: 500;
color: #333;
font-size: 15px;
display: flex;
align-items: center;
}
.participant-title text {
display: flex;
justify-content: center;
align-items: center;
width: 24px;
height: 24px;
background-color: #e74c3c;
color: white;
border-radius: 50%;
font-size: 14px;
margin-right: 8px;
}
.remove-participant {
color: #999;
font-size: 14px;
display: flex;
align-items: center;
background: none;
border: none;
}
.add-participant {
padding: 15px;
text-align: center;
color: #e74c3c;
font-weight: 500;
display: flex;
justify-content: center;
align-items: center;
border-top: 1px solid #f0f0f0;
}
.add-participant :deep(i) {
margin-right: 8px;
font-size: 18px;
}
.registration-footer {
padding: 16px;
background-color: white;
box-shadow: 0 -1px 5px rgba(0, 0, 0, 0.1);
}
.required-mark {
color: #e74c3c;
margin-left: 3px;
}
.agreement-checkbox {
display: flex;
align-items: center;
margin-bottom: 16px;
padding: 0 4px;
}
.agreement-checkbox input[type="checkbox"] {
width: 18px;
height: 18px;
margin-right: 8px;
accent-color: #e74c3c;
}
.agreement-checkbox label {
font-size: 14px;
color: #666;
}
@media (min-width: 768px) {
.registration-container {
max-width: 500px;
max-height: 90vh;
border-radius: 12px;
}
.registration-header {
border-radius: 12px 12px 0 0;
}
}
</style>