560 lines
13 KiB
Vue
560 lines
13 KiB
Vue
<!-- 小区活动详情页 -->
|
||
<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>
|