// pages/activity-detail/activity-detail.js - 活动详情页面
const api = require('../../utils/api')
const util = require('../../utils/util')
const imageUrl = require('../../utils/imageUrl')
const app = getApp()
Page({
data: {
statusBarHeight: 44,
navBarHeight: 44,
totalNavHeight: 88,
loading: false,
// 活动ID
activityId: '',
// 活动详情
activity: {
id: '',
title: '',
cover_image: '',
description: '',
start_date: '',
start_time: '',
end_date: '',
end_time: '',
address: '',
venue: '',
province: '',
city: '',
district: '',
organizer: '',
contact_phone: '',
price: 0,
is_free: true,
price_text: '',
participants_count: 0,
max_participants: 0,
status: 'upcoming', // upcoming/ongoing/ended/full
is_favorited: false,
images: []
},
// 状态文字
statusText: '即将开始',
signupButtonText: '立即报名',
// 参与者列表(显示前6个)
participants: [],
// 所有参与者(用于弹窗)
allParticipants: [],
// 显示参与者弹窗
showParticipantsModal: false,
// 二维码引导弹窗
showQrcodeModal: false,
qrcodeImageUrl: '',
// 推荐活动列表
recommendList: []
},
onLoad(options) {
// 计算导航栏高度
const systemInfo = wx.getSystemInfoSync()
const statusBarHeight = systemInfo.statusBarHeight || 44
const menuButton = wx.getMenuButtonBoundingClientRect()
const navBarHeight = menuButton.height + (menuButton.top - statusBarHeight) * 2
const totalNavHeight = statusBarHeight + navBarHeight
this.setData({
statusBarHeight,
navBarHeight,
totalNavHeight
})
// 获取活动ID
if (options.id) {
this.setData({ activityId: options.id })
this.loadActivityDetail()
this.loadParticipants()
this.loadRecommendList()
} else {
wx.showToast({
title: '活动不存在',
icon: 'none'
})
setTimeout(() => {
wx.navigateBack()
}, 1500)
}
},
/**
* 提取HTML中的所有图片URL
*/
extractImageUrls(html) {
if (!html) return []
const urls = []
const imgRegex = /
]+src="([^"]+)"[^>]*>/gi
let match
while ((match = imgRegex.exec(html)) !== null) {
urls.push(match[1])
}
return urls
},
/**
* 处理HTML中的图片样式
*/
processHtmlImages(html) {
if (!html) return ''
let imageIndex = 0
// 给所有img标签添加样式,确保图片不超出屏幕宽度
// 注意:rich-text中需要使用px单位,不能使用rpx
return html.replace(
/
]*?)(?:\s+style="[^"]*")?([^>]*)>/gi,
(match, before, after) => {
// 移除原有的style属性,添加新的样式和data-index
const cleanBefore = before.replace(/\s+style="[^"]*"/gi, '')
const cleanAfter = after.replace(/\s+style="[^"]*"/gi, '')
const result = `
`
imageIndex++
return result
}
)
},
/**
* 返回上一页
*/
onBack() {
wx.navigateBack()
},
/**
* 分享
*/
onShare() {
wx.showShareMenu({
withShareTicket: true,
menus: ['shareAppMessage', 'shareTimeline']
})
},
/**
* 分享给好友
*/
onShareAppMessage() {
const { activity } = this.data
const referralCode = wx.getStorageSync('referralCode') || ''
const referralCodeParam = referralCode ? `&referralCode=${referralCode}` : ''
return {
title: activity.title,
path: `/pages/activity-detail/activity-detail?id=${activity.id}${referralCodeParam}`,
imageUrl: activity.cover_image
}
},
/**
* 分享到朋友圈
*/
onShareTimeline() {
const { activity } = this.data
const referralCode = wx.getStorageSync('referralCode') || ''
const query = `id=${activity.id}${referralCode ? `&referralCode=${referralCode}` : ''}`
return {
title: activity.title,
query: query,
imageUrl: activity.cover_image
}
},
/**
* 加载活动详情
*/
async loadActivityDetail() {
this.setData({ loading: true })
try {
const res = await api.activity.getDetail(this.data.activityId)
if (res.success && res.data) {
const activity = {
...res.data,
is_favorited: res.data.isLiked || res.data.is_favorited // 兼容后端字段
}
console.log('[activity-detail] 活动详情数据:', activity)
// 处理图片数组
let images = []
if (activity.images) {
images = typeof activity.images === 'string'
? JSON.parse(activity.images)
: activity.images
}
// 格式化日期时间(兼容多种字段名)
const startDate = this.formatDate(
activity.startDate || activity.start_date || activity.activityDate
)
const startTime = this.formatTime(activity.startTime || activity.start_time || '')
const endDate = activity.endDate || activity.end_date || activity.activityDate || ''
const endTime = activity.endTime || activity.end_time || ''
// 处理封面图片URL - 后端已返回完整URL,前端只需兜底处理
const coverImage = imageUrl.getActivityCoverUrl(activity.coverImage || activity.cover_image)
// 处理地址信息
const province = activity.province || ''
const city = activity.city || ''
const district = activity.district || ''
const venue = activity.venue || ''
const address = activity.address || `${province}${city}${district}${venue}`
// 处理主办方信息(兼容对象和字符串格式)
let organizer = '主办方'
if (activity.organizer) {
if (typeof activity.organizer === 'object' && activity.organizer.nickname) {
organizer = activity.organizer.nickname
} else if (typeof activity.organizer === 'string') {
organizer = activity.organizer
}
}
// 处理活动详情内容
let description = activity.description || '暂无活动详情'
let detailHtml = ''
let detailImageUrls = []
// 如果有detailContent(HTML格式),使用rich-text渲染
if (activity.detailContent) {
// 提取HTML中的所有图片URL
detailImageUrls = this.extractImageUrls(activity.detailContent)
// 处理HTML,添加图片样式和点击事件标记
detailHtml = this.processHtmlImages(activity.detailContent)
} else if (activity.description) {
description = activity.description
}
// 判断活动状态(兼容多种字段名)
const status = this.getActivityStatus({
...activity,
start_date: activity.startDate || activity.start_date || activity.activityDate,
start_time: activity.startTime || activity.start_time || '00:00',
end_date: activity.endDate || activity.end_date || activity.activityDate,
end_time: activity.endTime || activity.end_time || '23:59',
max_participants: activity.maxParticipants || activity.max_participants || 0,
participants_count: activity.currentParticipants || activity.participants_count || 0
})
const statusText = this.getStatusText(status)
const signupButtonText = this.getSignupButtonText(status)
this.setData({
activity: {
id: activity.id,
title: activity.title || '活动标题',
cover_image: coverImage,
description: description,
detailHtml: detailHtml,
detailImageUrls: detailImageUrls,
start_date: startDate,
start_time: startTime,
end_date: endDate,
end_time: endTime,
address: address,
venue: venue,
province: province,
city: city,
district: district,
organizer: organizer,
contact_phone: activity.contactInfo || activity.contact_phone || '',
price: activity.price || 0,
is_free: activity.isFree !== undefined ? activity.isFree : activity.is_free,
price_text: activity.priceText || activity.price_text || '免费',
participants_count: activity.currentParticipants || activity.participants_count || 0,
max_participants: activity.maxParticipants || activity.max_participants || 0,
is_favorited: activity.isLiked || activity.is_favorited || false,
images: images,
status: status,
activity_guide_qrcode: activity.activity_guide_qrcode || activity.activityGuideQrcode || ''
},
statusText,
signupButtonText,
qrcodeImageUrl: activity.activity_guide_qrcode || activity.activityGuideQrcode || ''
})
console.log('[activity-detail] 封面图片URL:', coverImage)
console.log('[activity-detail] 活动地址:', address)
console.log('[activity-detail] 主办方:', organizer)
console.log('[activity-detail] 详情HTML:', detailHtml ? '有HTML内容' : '无HTML内容')
}
} catch (err) {
console.error('加载活动详情失败', err)
wx.showToast({
title: '加载失败',
icon: 'none'
})
} finally {
this.setData({ loading: false })
}
},
/**
* 加载参与者列表
*/
async loadParticipants() {
try {
const res = await api.activity.getParticipants(this.data.activityId, {
page: 1,
limit: 50
})
if (res.success && res.data) {
const allParticipants = res.data.list.map(item => ({
id: item.id,
name: item.real_name || item.nickname || '匿名用户',
avatar: item.avatar || '/images/default-avatar.png',
join_time: this.formatJoinTime(item.created_at)
}))
// 前6个用于列表显示
const participants = allParticipants.slice(0, 6)
this.setData({
participants,
allParticipants
})
}
} catch (err) {
console.log('加载参与者列表失败', err)
}
},
/**
* 加载推荐活动
*/
async loadRecommendList() {
try {
const res = await api.activity.getList({
limit: 10, // 获取10个,前端只显示8个
sortBy: 'date'
})
console.log('[activity-detail] 推荐活动API响应:', res)
if (res.success && res.data && res.data.list) {
const recommendList = res.data.list
.filter(item => item.id !== this.data.activityId) // 排除当前活动
.slice(0, 8) // 只取前8个
.map(item => {
// 处理封面图片URL - 后端已返回完整URL,前端只需兜底处理
const coverImage = imageUrl.getActivityCoverUrl(item.coverImage || item.cover_image)
// 处理日期字段(兼容多种命名)
const startDate = item.startDate || item.start_date || item.activityDate || ''
console.log('[activity-detail] 推荐活动:', item.title, '封面:', coverImage, '日期:', startDate)
return {
id: item.id,
title: item.title || '活动标题',
cover_image: coverImage,
start_date: this.formatDate(startDate)
}
})
console.log('[activity-detail] 推荐活动列表:', recommendList)
this.setData({ recommendList })
}
} catch (err) {
console.error('加载推荐活动失败', err)
}
},
/**
* 判断活动状态
*/
getActivityStatus(activity) {
const now = new Date()
const startDate = new Date(activity.start_date + ' ' + activity.start_time)
const endDate = new Date(activity.end_date + ' ' + activity.end_time)
// 已满员
if (activity.max_participants > 0 &&
activity.participants_count >= activity.max_participants) {
return 'full'
}
// 已结束
if (now > endDate) {
return 'ended'
}
// 进行中
if (now >= startDate && now <= endDate) {
return 'ongoing'
}
// 即将开始
return 'upcoming'
},
/**
* 获取状态文字
*/
getStatusText(status) {
const statusMap = {
upcoming: '即将开始',
ongoing: '进行中',
ended: '已结束',
full: '已满员'
}
return statusMap[status] || '即将开始'
},
/**
* 获取报名按钮文字
*/
getSignupButtonText(status) {
const buttonTextMap = {
upcoming: '立即报名',
ongoing: '立即报名',
ended: '活动已结束',
full: '已满员'
}
return buttonTextMap[status] || '立即报名'
},
/**
* 格式化日期
*/
formatDate(dateStr) {
if (!dateStr) return ''
const date = new Date(dateStr)
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
return `${year}年${month}月${day}日`
},
/**
* 格式化时间
*/
formatTime(timeStr) {
if (!timeStr) return ''
return timeStr.substring(0, 5) // HH:mm
},
/**
* 格式化加入时间
*/
formatJoinTime(dateStr) {
if (!dateStr) return ''
return util.formatTime(dateStr, 'MM-DD HH:mm')
},
/**
* 拨打电话
*/
onCallPhone() {
const phone = this.data.activity.contact_phone
if (!phone) return
wx.makePhoneCall({
phoneNumber: phone
})
},
/**
* 预览图片
*/
onPreviewImage(e) {
const url = e.currentTarget.dataset.url
const urls = this.data.activity.images
wx.previewImage({
current: url,
urls: urls
})
},
/**
* 点击详情区域的图片(rich-text中的图片)
*/
onDetailImageTap(e) {
// 获取点击位置
const { clientX, clientY } = e.detail || e.touches[0] || {}
// 通过createSelectorQuery获取所有图片的位置信息
const query = wx.createSelectorQuery()
query.selectAll('.detail-rich-text-wrapper >>> img').boundingClientRect()
query.exec((res) => {
if (!res || !res[0] || res[0].length === 0) {
console.log('[activity-detail] 未找到图片元素')
return
}
const imageRects = res[0]
const detailImageUrls = this.data.activity.detailImageUrls || []
// 判断点击位置是否在某个图片上
for (let i = 0; i < imageRects.length; i++) {
const rect = imageRects[i]
if (clientX >= rect.left && clientX <= rect.right &&
clientY >= rect.top && clientY <= rect.bottom) {
// 点击了第i张图片
console.log('[activity-detail] 点击了第', i, '张图片')
if (detailImageUrls.length > 0) {
wx.previewImage({
current: detailImageUrls[i],
urls: detailImageUrls
})
}
break
}
}
})
},
/**
* 查看全部参与者
*/
onViewAllParticipants() {
this.setData({ showParticipantsModal: true })
},
/**
* 关闭参与者弹窗
*/
onCloseParticipantsModal() {
this.setData({ showParticipantsModal: false })
},
/**
* 关闭二维码弹窗
*/
onCloseQrcodeModal() {
this.setData({ showQrcodeModal: false })
},
/**
* 保存二维码
*/
async onSaveQrcode() {
try {
const qrcodeUrl = this.data.qrcodeImageUrl || this.data.activity.activity_guide_qrcode
if (!qrcodeUrl) {
wx.showToast({ title: '二维码不存在', icon: 'none' })
return
}
wx.showLoading({ title: '正在保存...' })
const downloadRes = await new Promise((resolve, reject) => {
wx.downloadFile({
url: qrcodeUrl,
success: resolve,
fail: reject
})
})
if (downloadRes.statusCode !== 200) throw new Error('下载失败')
await new Promise((resolve, reject) => {
wx.saveImageToPhotosAlbum({
filePath: downloadRes.tempFilePath,
success: resolve,
fail: reject
})
})
wx.hideLoading()
wx.showToast({ title: '保存成功', icon: 'success' })
this.onCloseQrcodeModal()
} catch (err) {
wx.hideLoading()
console.error('保存失败', err)
wx.showToast({ title: '保存失败', icon: 'none' })
}
},
/**
* 阻止冒泡
*/
preventBubble() {
return
},
/**
* 点击推荐活动
*/
onRecommendTap(e) {
const id = e.currentTarget.dataset.id
wx.redirectTo({
url: `/pages/activity-detail/activity-detail?id=${id}`
})
},
/**
* 收藏/取消收藏
*/
async onToggleFavorite() {
if (!app.globalData.isLoggedIn) {
wx.navigateTo({
url: '/pages/login/login'
})
return
}
const { activity } = this.data
const isFavorited = activity.is_favorited
try {
if (isFavorited) {
// 取消收藏
await api.activity.unfavorite(activity.id)
wx.showToast({
title: '已取消收藏',
icon: 'success'
})
} else {
// 收藏
await api.activity.favorite(activity.id)
wx.showToast({
title: '收藏成功',
icon: 'success'
})
}
// 更新状态
this.setData({
'activity.is_favorited': !isFavorited
})
} catch (err) {
wx.showToast({
title: err.error || err.message || '操作失败',
icon: 'none'
})
}
},
/**
* 报名活动
*/
async onSignUp() {
const { activity } = this.data
// 检查活动状态
if (activity.status === 'ended' || activity.status === 'full') {
// 如果活动已结束或已满员,弹出二维码引导进群
const qrCode = activity.activity_guide_qrcode || activity.activityGuideQrcode || this.data.qrcodeImageUrl || 'https://ai-c.maimanji.com/api/common/qrcode?type=group'
this.setData({
qrcodeImageUrl: qrCode,
showQrcodeModal: true
})
return
}
// 检查登录
if (!app.globalData.isLoggedIn) {
wx.navigateTo({
url: '/pages/login/login'
})
return
}
// 显示报名确认
wx.showModal({
title: '确认报名',
content: activity.is_free
? '确定要报名参加这个活动吗?'
: `确定要报名参加这个活动吗?需支付 ¥${activity.price}`,
success: (res) => {
if (res.confirm) {
this.handleSignUp()
}
}
})
},
/**
* 处理报名
*/
async handleSignUp() {
try {
wx.showLoading({ title: '报名中...' })
const userInfo = app.globalData.userInfo || {}
const res = await api.activity.signup(this.data.activityId, {
real_name: userInfo.nickname || '',
phone: userInfo.phone || '',
notes: ''
})
wx.hideLoading()
if (res.success) {
wx.showToast({
title: '报名成功',
icon: 'success'
})
// 刷新数据
this.loadActivityDetail()
this.loadParticipants()
} else {
// 检查是否需要显示二维码(后端开关关闭、活动已结束或已满员)
if (res.code === 'QR_CODE_REQUIRED' || res.error === 'QR_CODE_REQUIRED' || res.code === 'ACTIVITY_ENDED' || res.error === '活动已结束' || res.code === 'ACTIVITY_FULL' || res.error === '活动已满员') {
const { activity } = this.data
if (activity.activity_guide_qrcode || activity.activityGuideQrcode) {
this.setData({
qrcodeImageUrl: activity.activity_guide_qrcode || activity.activityGuideQrcode,
showQrcodeModal: true
})
}
if (res.code === 'ACTIVITY_ENDED' || res.error === '活动已结束' || res.code === 'ACTIVITY_FULL' || res.error === '活动已满员') {
const tip = (res.code === 'ACTIVITY_FULL' || res.error === '活动已满员') ? '活动已满员,进群查看更多' : '活动已结束,进群查看更多'
wx.showToast({ title: tip, icon: 'none' })
}
} else {
wx.showToast({
title: res.error || '报名失败',
icon: 'none'
})
}
}
} catch (err) {
wx.hideLoading()
console.error('报名失败', err)
// 捕获特定错误码以显示二维码
const isQrRequired = err && (err.code === 'QR_CODE_REQUIRED' || (err.data && err.data.code === 'QR_CODE_REQUIRED'))
const isActivityEnded = err && (err.code === 'ACTIVITY_ENDED' || (err.data && err.data.code === 'ACTIVITY_ENDED') || err.error === '活动已结束')
const isActivityFull = err && (err.code === 'ACTIVITY_FULL' || (err.data && err.data.code === 'ACTIVITY_FULL') || err.error === '活动已满员')
if (isQrRequired || isActivityEnded || isActivityFull) {
const { activity } = this.data
if (activity.activity_guide_qrcode || activity.activityGuideQrcode) {
this.setData({
qrcodeImageUrl: activity.activity_guide_qrcode || activity.activityGuideQrcode,
showQrcodeModal: true
})
}
if (isActivityEnded || isActivityFull) {
const tip = isActivityFull ? '活动已满员,进群查看更多' : '活动已结束,进群查看更多'
wx.showToast({ title: tip, icon: 'none' })
}
} else {
wx.showToast({
title: err.error || err.message || '报名失败',
icon: 'none'
})
}
}
}
})