ai-c/pages/activity-detail/activity-detail.js
2026-02-02 18:21:32 +08:00

768 lines
22 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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 = /<img[^>]+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(
/<img([^>]*?)(?:\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 = `<img${cleanBefore}${cleanAfter} data-index="${imageIndex}" style="max-width:100%;width:100%;height:auto;display:block;">`
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 = []
// 如果有detailContentHTML格式使用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'
})
}
}
}
})