fjrcloud-community-app/pages/sub/activity/detail.vue

560 lines
13 KiB
Vue
Raw Normal View History

<!-- 小区活动详情页 -->
<template>
<s-layout title="小区活动">
<view class="activity-detail-page">
<!-- 顶部Banner轮播 -->
<swiper class="banner-swiper" :autoplay="true" :interval="3000" :circular="true" indicator-dots indicator-color="rgba(255,255,255,0.4)" active-color="#FFFFFF">
<swiper-item v-for="(img, index) in activityInfo.banners" :key="index">
<image class="banner-img" :src="img" mode="aspectFill" />
</swiper-item>
</swiper>
<!-- Tab 切换栏 -->
<view class="tab-bar">
<view
class="tab-item"
:class="{ active: currentTab === 'basic' }"
@tap="currentTab = 'basic'"
>
<text class="tab-text">基本信息</text>
</view>
<view
class="tab-item"
:class="{ active: currentTab === 'intro' }"
@tap="currentTab = 'intro'"
>
<text class="tab-text">活动简介</text>
</view>
</view>
<!-- 内容区域 - 滚动 -->
<scroll-view class="detail-scroll" scroll-y>
<view class="detail-content">
<!-- 基本信息 -->
<template v-if="currentTab === 'basic'">
<!-- 活动标题 -->
<view class="activity-title">{{ activityInfo.title }}</view>
<!-- 地址 -->
<view class="info-row location-row">
<image class="row-icon" src="/static/img/dw.png" mode="aspectFit" />
<text class="location-text">{{ activityInfo.location }}</text>
<image class="row-icon navigation-icon" src="/static/img/dh.png" mode="aspectFit" @tap="openNavigation" />
</view>
<!-- 服务类别 -->
<view class="info-row tag-row">
<text class="label-text">服务类别</text>
<view class="tag-list">
<text
class="tag-item"
v-for="(tag, index) in activityInfo.serviceTypes"
:key="index"
>{{ tag }}</text>
</view>
</view>
<!-- 服务对象 -->
<view class="info-row tag-row">
<text class="label-text">服务对象</text>
<view class="tag-list">
<text
class="tag-item target-tag"
v-for="(tag, index) in activityInfo.targetAudience"
:key="index"
>{{ tag }}</text>
</view>
</view>
<!-- 报名日期 -->
<view class="info-row text-row">
<text class="label-text">报名日期</text>
<text class="value-text">{{ activityInfo.registerDate }}</text>
</view>
<!-- 活动日期 -->
<view class="info-row text-row">
<text class="label-text">活动日期</text>
<text class="value-text">{{ activityInfo.activityDate }}</text>
</view>
<!-- 人数上限 -->
<view class="info-row text-row">
<image class="row-icon" src="/static/img/people.png" mode="aspectFit" />
<text class="label-text">人数上限</text>
<text class="value-text">{{ activityInfo.maxPeople }}</text>
</view>
<!-- 联系人 -->
<view class="info-row contact-row" @tap="callPhone">
<image class="row-icon" src="/static/img/phone.png" mode="aspectFit" />
<text class="contact-name">{{ activityInfo.contactName }}</text>
<text class="contact-phone">{{ activityInfo.contactPhone }}</text>
</view>
</template>
<!-- 活动简介 -->
<template v-if="currentTab === 'intro'">
<view class="intro-content">
<mp-html :content="activityInfo.introduction"></mp-html>
</view>
</template>
<!-- 底部占位防止内容被底部按钮遮挡 -->
<view class="bottom-placeholder"></view>
</view>
</scroll-view>
<!-- 底部按钮栏 -->
<view class="bottom-bar">
<!-- 未报名左侧立即报名右侧返回 -->
<template v-if="!activityInfo.hasRegistered">
<view class="register-btn" @tap="handleRegister">
<text class="register-text">立即报名</text>
</view>
<view class="back-btn" @tap="goBack">
<text class="back-text">返回</text>
</view>
</template>
<!-- 已报名左侧显示人数右侧取消报名 -->
<template v-else>
<view class="registered-btn" @tap="goBack">
<text class="registered-text">已有{{ activityInfo.registeredCount }}人报名</text>
</view>
<view class="cancel-btn" @tap="handleCancelRegister">
<text class="cancel-text">取消报名</text>
</view>
</template>
</view>
</view>
</s-layout>
</template>
<script setup>
import { ref } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
import ActivityApi from '@/sheep/api/community/activity';
import sheep from '@/sheep';
// 当前Tab
const currentTab = ref('basic');
// 活动详情数据
const activityInfo = ref({
id: null,
title: '',
banners: [],
location: '',
serviceTypes: [],
targetAudience: [],
registerDate: '',
activityDate: '',
maxPeople: 0,
contactName: '',
contactPhone: '',
registeredCount: 0,
hasRegistered: false,
introduction: '',
});
// 页面加载
onLoad((options) => {
if (options.id) {
loadActivityDetail(options.id);
}
});
// 安全解析 JSON 字符串
function safeJsonParse(str, defaultVal = []) {
if (!str) return defaultVal;
try {
return JSON.parse(str);
} catch (e) {
return defaultVal;
}
}
// 服务类别映射
const serviceCategoryMap = {
1: '社区服务',
2: '敬老服务',
3: '助残服务',
4: '关爱儿童',
5: '环保宣传',
6: '文明礼仪',
7: '文化教育',
};
// 服务对象映射
const serviceTargetMap = {
1: '儿童',
2: '孤寡老人',
3: '残障人士',
4: '优抚对象',
5: '其他',
};
// 加载活动详情
async function loadActivityDetail(id) {
const { code, data } = await ActivityApi.getDetail(id);
if (code === 0 && data) {
activityInfo.value = {
id: data.id,
title: data.title || '',
banners: safeJsonParse(data.bannerImages, [data.coverImage]).filter(Boolean),
location: data.location || '',
serviceTypes: (data.serviceCategories || []).map((id) => serviceCategoryMap[id] || id),
targetAudience: (data.serviceTargets|| []).map((id) => serviceTargetMap[id] || id),
registerDate: data.registrationStartTime && data.registrationEndTime
? `${sheep.$helper.timeFormat(data.registrationStartTime, 'yyyy/mm/dd')} - ${sheep.$helper.timeFormat(data.registrationEndTime, 'yyyy/mm/dd')}`
: '',
activityDate: data.activityStartTime && data.activityEndTime
? `${sheep.$helper.timeFormat(data.activityStartTime, 'yyyy/mm/dd hh:MM')} - ${sheep.$helper.timeFormat(data.activityEndTime, 'yyyy/mm/dd hh:MM')}`
: '',
maxPeople: data.maxParticipants || 0,
contactName: data.contactPerson || '',
contactPhone: data.contactPhone || '',
registeredCount: data.currentParticipants || 0,
hasRegistered: data.hasRegistered || false,
introduction: data.content || '',
};
}
}
// 打开导航
function openNavigation() {
if (!activityInfo.value.location) return;
uni.openLocation({
address: activityInfo.value.location,
success: () => console.log('打开导航成功'),
fail: () => uni.showToast({ title: '无法打开导航', icon: 'none' }),
});
}
// 拨打电话
function callPhone() {
if (activityInfo.value.contactPhone) {
uni.makePhoneCall({
phoneNumber: activityInfo.value.contactPhone,
});
}
}
// 立即报名
async function handleRegister() {
const { code, data } = await ActivityApi.register({
activityId: activityInfo.value.id,
});
if (code === 0 && data) {
uni.showToast({ title: '报名成功', icon: 'success' });
activityInfo.value.hasRegistered = true;
activityInfo.value.registeredCount++;
} else {
uni.showToast({ title: msg, icon: 'none' });
}
}
// 取消报名
async function handleCancelRegister() {
uni.showModal({
title: '提示',
content: '确定取消报名吗?',
success: async (res) => {
if (res.confirm) {
const { code, data } = await ActivityApi.cancelRegister(activityInfo.value.id);
if (code === 0 && data) {
uni.showToast({ title: '取消报名成功', icon: 'success' });
activityInfo.value.hasRegistered = false;
activityInfo.value.registeredCount--;
} else {
uni.showToast({ title: '取消报名失败', icon: 'none' });
}
}
},
});
}
// 返回上一页
function goBack() {
uni.navigateBack();
}
</script>
<style lang="scss" scoped>
/* 页面容器:纵向 flex 布局,减去导航栏高度 */
.activity-detail-page {
position: relative;
display: flex;
flex-direction: column;
height: calc(100vh - 176rpx);
overflow: hidden;
}
/* 顶部Banner轮播 */
.banner-swiper {
width: 100%;
height: 400rpx;
flex-shrink: 0;
}
.banner-img {
width: 100%;
height: 400rpx;
display: block;
}
/* Tab 切换栏 */
.tab-bar {
display: flex;
align-items: center;
background-color: #FFFFFF;
padding: 0 32rpx;
border-bottom: 1rpx solid #F0F0F0;
flex-shrink: 0;
.tab-item {
flex: 1;
text-align: center;
padding: 28rpx 0;
position: relative;
.tab-text {
font-size: 30rpx;
color: #999999;
transition: all 0.3s;
}
&.active {
&::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 120rpx;
height: 4rpx;
background: #FA7E49;
border-radius: 2rpx;
}
}
&.active .tab-text {
font-size: 30rpx;
font-weight: 600;
color: #333333;
}
}
}
/* 内容滚动区域flex:1 占满剩余空间height:0 是小程序 scroll-view 配合 flex 的关键 */
.detail-scroll {
position: relative;
z-index: 5;
flex: 1;
height: 0;
}
/* 详情内容 */
.detail-content {
background-color: #FFFFFF;
}
/* 活动标题 */
.activity-title {
padding: 32rpx 32rpx 16rpx;
font-size: 34rpx;
font-weight: 600;
color: #FA7E49;
line-height: 1.5;
}
/* 信息行 */
.info-row {
display: flex;
align-items: center;
padding: 20rpx 32rpx;
.row-icon {
width: 32rpx;
height: 32rpx;
margin-right: 8rpx;
flex-shrink: 0;
}
}
/* 地址行 */
.location-row {
align-items: center;
.location-text {
font-size: 28rpx;
color: #333333;
flex: 1;
}
.navigation-icon {
font-size: 36rpx;
color: #FA7E49;
flex-shrink: 0;
}
}
/* 标签行 */
.tag-row {
align-items: flex-start;
.label-text {
font-size: 28rpx;
color: #333333;
flex-shrink: 0;
line-height: 1.8;
}
}
.tag-list {
display: flex;
flex-wrap: wrap;
gap: 12rpx;
flex: 1;
}
.tag-item {
font-size: 24rpx;
color: #FA7E49;
border: 1rpx solid #FA7E49;
border-radius: 8rpx;
padding: 6rpx 16rpx;
background-color: rgba(250, 126, 73, 0.04);
line-height: 1.4;
}
.target-tag {
color: #FA7E49;
border: 1rpx solid #FA7E49;
}
/* 文本行 */
.text-row {
.label-text {
font-size: 28rpx;
color: #333333;
flex-shrink: 0;
}
.value-text {
font-size: 28rpx;
color: #333333;
}
}
/* 联系人行 */
.contact-row {
align-items: center;
&:active {
opacity: 0.7;
}
.contact-name {
font-size: 28rpx;
color: #333333;
margin-left: 8rpx;
margin-right: 24rpx;
}
.contact-phone {
font-size: 28rpx;
color: #333333;
}
}
/* 活动简介内容 */
.intro-content {
padding: 32rpx;
}
/* 底部占位 */
.bottom-placeholder {
height: 40rpx;
}
/* 底部按钮栏正常文档流flex-shrink:0 保证不被压缩 */
.bottom-bar {
flex-shrink: 0;
display: flex;
align-items: center;
gap: 24rpx;
padding: 20rpx 32rpx;
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
background-color: #FFFFFF;
box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.06);
.registered-btn,
.register-btn,
.cancel-btn {
flex: 1;
height: 88rpx;
border-radius: 44rpx;
display: flex;
align-items: center;
justify-content: center;
&:active {
opacity: 0.85;
}
}
.registered-btn {
border: 2rpx solid #FA7E49;
.registered-text {
font-size: 30rpx;
color: #FA7E49;
font-weight: 500;
}
}
.register-btn {
background-color: #FA7E49;
.register-text {
font-size: 30rpx;
color: #FFFFFF;
font-weight: 500;
}
}
.cancel-btn {
border: 2rpx solid #999999;
.cancel-text {
font-size: 30rpx;
color: #999999;
font-weight: 500;
}
}
.back-btn {
width: 240rpx;
height: 88rpx;
background-color: #FA7E49;
border-radius: 44rpx;
display: flex;
align-items: center;
justify-content: center;
&:active {
opacity: 0.85;
}
.back-text {
font-size: 30rpx;
color: #FFFFFF;
font-weight: 500;
}
}
}
</style>