// 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' }) } } } })