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

571 lines
14 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<!-- 小区活动详情页 -->
<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 DictApi from '@/sheep/api/system/dict';
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: '',
});
// 字典映射(动态加载)
const serviceCategoryMap = ref({});
const serviceTargetMap = ref({});
// 页面加载
onLoad((options) => {
// 并行加载字典
loadDictMaps();
if (options.id) {
loadActivityDetail(options.id);
}
});
// 加载字典映射
async function loadDictMaps() {
const [categoryRes, targetRes] = await Promise.all([
DictApi.getDictDataListByType('comm_activity_service_category'),
DictApi.getDictDataListByType('comm_activity_service_target'),
]);
if (categoryRes.code === 0 && categoryRes.data) {
const map = {};
categoryRes.data.forEach((item) => {
map[item.value] = item.label;
});
serviceCategoryMap.value = map;
}
if (targetRes.code === 0 && targetRes.data) {
const map = {};
targetRes.data.forEach((item) => {
map[item.value] = item.label;
});
serviceTargetMap.value = map;
}
}
// 安全解析 JSON 字符串
function safeJsonParse(str, defaultVal = []) {
if (!str) return defaultVal;
try {
return JSON.parse(str);
} catch (e) {
return defaultVal;
}
}
// 加载活动详情
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.value[id] || id),
targetAudience: (data.serviceTargets || []).map((id) => serviceTargetMap.value[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>