活动详细动态页面

This commit is contained in:
minimaxagent1 2025-08-04 11:47:19 +08:00
parent 0ef5aad9d4
commit 45f5884119
6 changed files with 574 additions and 35 deletions

View File

@ -30,8 +30,11 @@ export default {
*/
getActivityDetail(id) {
return request({
url: `/app/activitie/detail/${id}`,
method: 'GET'
url: '/app/activitie',
method: 'GET',
params: {
actId: id
}
})
},

View File

@ -14,8 +14,8 @@
<view class="card-content">
<!-- 活动主题 -->
<view class="activity-theme" v-if="activity.theme">
<text class="theme-text">{{ activity.theme }}</text>
<view class="activity-theme">
<text class="theme-text">活动主题{{ activity.name }}</text>
</view>
<!-- 活动名称 -->
@ -114,7 +114,7 @@ export default {
//
handleRegister() {
this.$emit('register', this.activity)
}
},
}
}
</script>
@ -180,42 +180,33 @@ export default {
.activity-theme {
.theme-text {
font-weight: 400;
font-size: 28rpx;
color: #fff;
line-height: 1.4;
color: #FFFFFF;
line-height: 38rpx;
}
}
.activity-name {
.name-text {
font-size: 36rpx;
font-weight: bold;
color: #fff;
margin: 10rpx 0;
font-weight: 500;
font-size: 32rpx;
color: #FFFFFF;
line-height: 44rpx;
}
}
.activity-status {
.status-text {
font-size: 24rpx;
padding: 8rpx 16rpx;
padding: 8rpx 0rpx;
border-radius: 20rpx;
display: inline-block;
font-weight: 500;
font-size: 32rpx;
color: #FFFFFF;
line-height: 44rpx;
&.status-registering {
color: #FFD700;
background: rgba(255, 215, 0, 0.2);
}
&.status-ongoing {
color: #4CAF50;
background: rgba(76, 175, 80, 0.2);
}
&.status-finished {
color: #9E9E9E;
background: rgba(158, 158, 158, 0.2);
}
}
}

View File

@ -127,6 +127,15 @@
"enablePullDownRefresh": false,
"navigationStyle": "custom"
}
},
{
"path" : "pages/activity/activityDetail",
"style" :
{
"navigationBarTitleText": "",
"enablePullDownRefresh": false,
"navigationStyle": "custom"
}
}
],

View File

@ -147,10 +147,9 @@ export default {
//
handleCardClick(activity) {
console.log('点击活动卡片:', activity)
// TODO:
uni.showToast({
title: '活动详情功能开发中',
icon: 'none'
//
uni.navigateTo({
url: `/pages/activity/activityDetail?actId=${activity.id}`
})
},

View File

@ -0,0 +1,464 @@
<template>
<view class="page">
<custom-navbar title="活动详情" />
<view class="header" :style="{ backgroundColor: CommonEnum.BASE_COLOR }">
<!-- 状态展示 -->
<status-display
v-if="loading"
type="loading"
loading-text="加载中..."
/>
<!-- 错误状态 -->
<status-display
v-if="error"
type="error"
:error-text="error"
@retry="loadPageData"
/>
<!-- 活动详情内容 -->
<view v-if="!loading && !error && activityData" class="activity-detail">
<!-- 活动标题区域 -->
<view class="activity-header">
<view class="title-section">
<text class="main-title">{{ activityData.title }}</text>
<view class="meta-info">
<text class="date">{{ formatDate(activityData.createTime) }}</text>
<text class="read-count">阅读{{ activityData.readNum || 0 }}</text>
</view>
</view>
</view>
<!-- 活动信息卡片 -->
<activity-card
:activity="formattedActivity"
:show-title="false"
@card-click="handleCardClick"
@register="handleRegister"
/>
<!-- 参与统计 -->
<view class="participation-stats">
<view class="stat-item">
<text class="stat-label">活动参与人数:</text>
<text class="stat-value">{{ activityData.attendance || 0 }}</text>
</view>
<view class="stat-item">
<text class="stat-label">已报名:</text>
<text class="stat-value">{{ activityData.currentBooking || 0 }}</text>
</view>
</view>
<!-- 活动描述 -->
<view class="activity-description" v-if="activityData.content">
<view class="description-title">
<text class="title-text">活动详情</text>
</view>
<view class="description-content">
<rich-text :nodes="activityData.content" class="content-html"></rich-text>
</view>
</view>
<!-- 底部操作按钮 -->
<view class="bottom-actions">
<view class="action-button phone-button" @click="handlePhoneCall">
<text class="button-icon">📞</text>
<text class="button-text">电话</text>
</view>
<view class="action-button location-button" @click="handleLocation">
<text class="button-icon">📍</text>
<text class="button-text">地址</text>
</view>
<view class="action-button register-button" @click="handleRegister">
<text class="button-text">立即报名</text>
</view>
</view>
</view>
</view>
</view>
</template>
<script>
import CustomNavbar from "../../components/custom-navbar/custom-navbar.vue";
import CommonEnum from "../../enum/common";
import StatusDisplay from "../../components/status-display/status-display.vue";
import ActivityCard from "../../components/activity-card/activity-card.vue";
import activityApi from "../../api/activity/activity.js";
import activityFormatter from "../../utils/activity-data-formatter.js";
export default {
components: {
CustomNavbar,
StatusDisplay,
ActivityCard
},
data() {
return {
CommonEnum,
loading: false,
error: '',
activityData: null,
actId: ''
}
},
computed: {
//
formattedActivity() {
if (!this.activityData) return null
return activityFormatter.formatActivityDetail(this.activityData)
}
},
onLoad(options) {
// ID
if (options.actId) {
this.actId = options.actId
this.loadPageData()
} else {
this.error = '缺少活动ID参数'
}
},
methods: {
//
async loadPageData() {
if (!this.actId) return
this.loading = true
this.error = ''
try {
const response = await activityApi.getActivityDetail(this.actId)
if (response.code === 200 && response.data) {
this.activityData = response.data
} else {
this.error = response.msg || '获取活动详情失败'
}
} catch (error) {
console.error('获取活动详情失败:', error)
this.error = '网络错误,请稍后重试'
} finally {
this.loading = false
}
},
//
formatDate(dateString) {
if (!dateString) return ''
try {
// "yyyy-MM-dd HH:mm:ss" "yyyy-MM-ddTHH:mm:ss" iOS
const iosCompatibleDate = dateString.replace(' ', 'T')
const date = new Date(iosCompatibleDate)
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
return `${year}-${month}-${day} ${hours}:${minutes}`
} catch (error) {
console.error('格式化日期失败:', error)
return dateString
}
},
//
handleCardClick(activity) {
console.log('点击活动卡片:', activity)
//
},
//
async handleRegister() {
if (!this.activityData) return
console.log('报名活动:', this.activityData)
//
const userInfo = uni.getStorageSync('userInfo')
if (!userInfo) {
uni.showModal({
title: '提示',
content: '请先登录后再报名活动',
confirmText: '去登录',
success: (res) => {
if (res.confirm) {
//
uni.navigateTo({
url: '/pages/login/login'
})
}
}
})
return
}
//
uni.showModal({
title: '确认报名',
content: `确定要报名参加"${this.activityData.title}"活动吗?`,
success: async (res) => {
if (res.confirm) {
try {
const response = await activityApi.registerActivity({
activityId: this.activityData.id,
userId: userInfo.id,
userName: userInfo.userName || userInfo.nickName,
phone: userInfo.phone
})
if (response.code === 200) {
uni.showToast({
title: '报名成功',
icon: 'success'
})
//
this.loadPageData()
} else {
uni.showToast({
title: response.msg || '报名失败',
icon: 'none'
})
}
} catch (error) {
console.error('报名失败:', error)
uni.showToast({
title: '报名失败,请稍后重试',
icon: 'none'
})
}
}
}
})
},
//
handlePhoneCall() {
if (!this.activityData || !this.activityData.phone) {
uni.showToast({
title: '暂无联系电话',
icon: 'none'
})
return
}
uni.showModal({
title: '拨打电话',
content: `确定要拨打 ${this.activityData.phone} 吗?`,
success: (res) => {
if (res.confirm) {
uni.makePhoneCall({
phoneNumber: this.activityData.phone,
fail: (err) => {
console.error('拨打电话失败:', err)
uni.showToast({
title: '拨打电话失败',
icon: 'none'
})
}
})
}
}
})
},
//
handleLocation() {
if (!this.activityData || !this.activityData.address) {
uni.showToast({
title: '暂无地址信息',
icon: 'none'
})
return
}
//
if (this.activityData.lon && this.activityData.lat) {
uni.openLocation({
latitude: this.activityData.lat,
longitude: this.activityData.lon,
name: this.activityData.title,
address: this.activityData.address,
fail: (err) => {
console.error('打开地图失败:', err)
//
this.showAddressInfo()
}
})
} else {
//
this.showAddressInfo()
}
},
//
showAddressInfo() {
uni.showModal({
title: '活动地址',
content: this.activityData.address,
showCancel: false,
confirmText: '知道了'
})
}
}
}
</script>
<style lang="scss">
page {
background: #FAF8F3;
}
.header {
width: 100%;
min-height: 100vh;
display: flex;
align-items: flex-start;
flex-direction: column;
padding: 0 15rpx;
padding-bottom: 40rpx;
}
.activity-detail {
width: 100%;
}
.activity-header {
width: 100%;
padding: 30rpx 0 20rpx 24rpx;
.title-section {
.main-title {
font-size: 44rpx;
font-weight: 500;
color: #3D3D3D;
line-height: 60rpx;
margin-bottom: 20rpx;
}
.meta-info {
display: flex;
align-items: center;
gap: 30rpx;
.date {
font-weight: 400;
font-size: 24rpx;
color: #808080;
}
.read-count {
font-weight: 400;
font-size: 24rpx;
color: #808080;
}
}
}
}
.participation-stats {
width: 100%;
display: flex;
border-radius: 16rpx;
padding: 30rpx;
margin-bottom: 30rpx;
.stat-item {
display: flex;
align-items: center;
margin-bottom: 0;
&:last-child {
margin-left: 50rpx;
}
.stat-label {
font-size: 30rpx;
color: #333;
font-weight: 500;
}
.stat-value {
font-size: 30rpx;
color: #E74C3C;
font-weight: bold;
}
}
}
.activity-description {
width: 100%;
background: #fff;
border-radius: 16rpx;
padding: 30rpx;
margin-bottom: 30rpx;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05);
.description-title {
margin-bottom: 20rpx;
.title-text {
font-size: 32rpx;
font-weight: bold;
color: #333;
}
}
.description-content {
.content-html {
font-size: 28rpx;
color: #666;
line-height: 1.6;
text-align: justify;
}
}
}
.bottom-actions {
width: 100%;
display: flex;
align-items: center;
gap: 20rpx;
padding: 20rpx 0;
.action-button {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 20rpx;
border-radius: 12rpx;
background: #fff;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05);
&.phone-button, &.location-button {
flex: 1;
min-height: 120rpx;
.button-icon {
font-size: 40rpx;
margin-bottom: 10rpx;
}
.button-text {
font-size: 24rpx;
color: #666;
}
}
&.register-button {
flex: 2;
min-height: 120rpx;
background: #E74C3C;
.button-text {
font-size: 32rpx;
color: #fff;
font-weight: bold;
}
}
}
}
</style>

View File

@ -45,6 +45,61 @@ export default {
}
},
/**
* 格式化活动详情数据
* @param {Object} apiData - API返回的活动详情数据
* @returns {Object} 格式化后的活动详情数据
*/
formatActivityDetail(apiData) {
if (!apiData) return null
return {
id: apiData.id || '',
name: apiData.title || '未命名活动',
type: this.getActivityType(apiData.type),
status: this.getActivityStatus(apiData.status, apiData.actStartTime, apiData.actEndTime),
time: this.formatActivityTime(apiData.actStartTime, apiData.actEndTime),
location: apiData.address || '',
backgroundImage: apiData.imgUrl || this.getDefaultImageByType(apiData.type),
// 详情页额外信息
templeId: apiData.templeId,
currentBooking: apiData.currentBooking || 0,
attendance: apiData.attendance || 0,
readNum: apiData.readNum || 0,
phone: apiData.phone,
lon: apiData.lon,
lat: apiData.lat,
content: apiData.content,
createTime: apiData.createTime,
updateTime: apiData.updateTime
}
},
/**
* 从HTML内容中提取纯文本
* @param {string} htmlContent - HTML内容
* @returns {string} 纯文本内容
*/
extractTextFromHtml(htmlContent) {
if (!htmlContent) return ''
try {
// 简单的HTML标签移除
return htmlContent
.replace(/<[^>]*>/g, '') // 移除HTML标签
.replace(/&nbsp;/g, ' ') // 替换空格实体
.replace(/&amp;/g, '&') // 替换&实体
.replace(/&lt;/g, '<') // 替换<实体
.replace(/&gt;/g, '>') // 替换>实体
.replace(/&quot;/g, '"') // 替换"实体
.trim()
} catch (error) {
console.error('提取HTML文本失败:', error)
return htmlContent
}
},
/**
* 获取活动类型
* @param {string} type - API返回的类型
@ -79,8 +134,8 @@ export default {
// 如果有时间信息,根据时间判断状态
if (startTime && endTime) {
const now = new Date()
const start = new Date(startTime)
const end = new Date(endTime)
const start = this.parseDateForIOS(startTime)
const end = this.parseDateForIOS(endTime)
if (now < start) {
return ACTIVITY_STATUS.REGISTERING
@ -101,6 +156,24 @@ export default {
return statusMap[status] || ACTIVITY_STATUS.REGISTERING
},
/**
* 兼容iOS的日期解析函数
* @param {string} dateString - 日期字符串
* @returns {Date} 解析后的日期对象
*/
parseDateForIOS(dateString) {
if (!dateString) return new Date()
try {
// 将 "yyyy-MM-dd HH:mm:ss" 格式转换为 "yyyy-MM-ddTHH:mm:ss" 格式以兼容iOS
const iosCompatibleDate = dateString.replace(' ', 'T')
return new Date(iosCompatibleDate)
} catch (error) {
console.error('解析日期失败:', error)
return new Date()
}
},
/**
* 格式化活动时间
* @param {string} startTime - 开始时间
@ -111,8 +184,8 @@ export default {
if (!startTime) return ''
try {
const start = new Date(startTime)
const end = endTime ? new Date(endTime) : null
const start = this.parseDateForIOS(startTime)
const end = endTime ? this.parseDateForIOS(endTime) : null
// 格式化日期
const formatDate = (date) => {