// pages/index/index.js // 首页 - 角色列表展示(卡片滑动) const app = getApp() const api = require('../../utils/api') const util = require('../../utils/util') const config = require('../../config/index') const proactiveMessage = require('../../utils/proactiveMessage') // 获取静态资源基础URL(去掉/api后缀) const getStaticBaseUrl = () => { const apiUrl = config.API_BASE_URL return apiUrl.replace(/\/api$/, '') } // 好友故事数据 - 将在加载角色后动态更新 let STORY_USERS = [] Page({ data: { statusBarHeight: 44, navHeight: 96, // 角色数据 profiles: [], currentIndex: 0, loading: true, error: null, // 故事用户 storyUsers: STORY_USERS, // 首页 Banner homeBanner: '', bannerHeight: 240, // 交互状态 likedProfiles: {}, unlockedProfiles: {}, showVipModal: false, // 爱心弹窗 showHeartPopup: false, currentCharacter: null, heartCount: 0, heartPackages: [ { id: 1, hearts: 10, price: 6, tag: '' }, { id: 2, hearts: 30, price: 18, tag: '热门' }, { id: 3, hearts: 50, price: 28, tag: '' }, { id: 4, hearts: 100, price: 50, tag: '超值' }, { id: 5, hearts: 200, price: 98, tag: '' }, { id: 6, hearts: 500, price: 238, tag: '最划算' } ], selectedHeartPackage: 1, // 默认选中第二个(热门) purchasing: false, // 未读消息数 totalUnread: 0, // 滑动相关 startX: 0, startY: 0, offsetX: 0, rotation: 0, swipeDirection: '', // 分享配置 shareConfig: null, // 注册奖励 showRegistrationReward: false, registrationRewardAmount: 0, claiming: false, auditStatus: 0, // GF100 弹窗 showGf100Popup: false, gf100ImageUrl: '', // 解锁配置 unlockHeartsCost: 500 // 默认解锁爱心成本 }, async onLoad() { // 获取系统信息 const { statusBarHeight, navHeight, auditStatus } = app.globalData this.setData({ statusBarHeight, navHeight, auditStatus }) // 如果是审核状态,重定向到文娱页面 if (auditStatus === 1) { wx.switchTab({ url: '/pages/entertainment/entertainment' }) return } // 加载首页素材 (Banner等) this.loadHomeAssets() // 加载分享配置 this.loadShareConfig() // 加载角色列表 this.loadCharacters() // 加载用户爱心余额 this.loadHeartBalance() // 加载解锁配置 this.loadUnlockConfig() }, /** * 加载解锁配置 */ async loadUnlockConfig() { try { // 获取当前列表中的第一个角色ID用于查询配置(配置是全局的,任一ID即可) const charId = this.data.profiles[0]?.id || 'default' const res = await api.chat.getQuota(charId) if (res.success && res.data && res.data.unlock_config) { const cost = res.data.unlock_config.hearts_cost if (typeof cost === 'number') { this.setData({ unlockHeartsCost: cost }) console.log('[index] 已从后端同步解锁成本:', cost) } } } catch (err) { console.log('[index] 加载解锁配置失败,使用默认值', err) } }, /** * 加载分享配置 */ async loadShareConfig() { try { const res = await api.promotion.getShareConfig('index') if (res.success && res.data) { // 处理图片URL,确保是完整路径 const shareConfig = { ...res.data, imageUrl: res.data.imageUrl ? util.getFullImageUrl(res.data.imageUrl) : '' } this.setData({ shareConfig }) } } catch (error) { console.error('加载分享配置失败:', error) } }, /** * 处理图片URL,如果是相对路径则拼接域名,并设置清晰度为85 */ processImageUrl(url) { return util.getFullImageUrl(url) }, /** * 轮播图图片加载完成,自适应高度 */ onBannerLoad(e) { const { width, height } = e.detail; const sysInfo = wx.getSystemInfoSync(); // 减去左右padding (32rpx * 2) const swiperWidth = sysInfo.windowWidth - (32 * 2 / 750 * sysInfo.windowWidth); const ratio = width / height; const bannerHeight = swiperWidth / ratio; const bannerHeightRpx = bannerHeight * (750 / sysInfo.windowWidth); this.setData({ bannerHeight: bannerHeightRpx }); }, /** * 加载首页素材 */ async loadHomeAssets() { try { const res = await api.pageAssets.getAssets('banners') console.log('首页素材 API响应:', res) if (res.success && res.data) { // 优先使用 home_banner,其次使用 companion_banner const bannerUrl = res.data.home_banner || res.data.companion_banner if (bannerUrl) { this.setData({ homeBanner: this.processImageUrl(bannerUrl) }) console.log('已加载首页Banner') } } } catch (err) { console.error('加载首页素材失败', err) } }, onShow() { // 隐藏默认tabbar wx.hideTabBar({ animation: false }) const app = getApp() this.setData({ auditStatus: app.globalData.auditStatus }) // 如果是审核状态,重定向到文娱页面 if (app.globalData.auditStatus === 1) { wx.switchTab({ url: '/pages/entertainment/entertainment' }) return } // 刷新爱心余额 this.loadHeartBalance() // 加载未读消息数 this.loadUnreadCount() // 检查AI角色主动推送消息 this.checkProactiveMessages() // 检查注册奖励 this.checkRegistrationReward() // 检查 GF100 弹窗 this.checkGf100Popup() }, onPullDownRefresh() { Promise.all([ this.loadCharacters(), this.loadHeartBalance() ]).then(() => { wx.stopPullDownRefresh() }) }, /** * 加载用户爱心值 * 使用 /api/auth/me 接口,该接口从 im_users.grass_balance 读取余额 */ async loadHeartBalance() { try { const token = wx.getStorageSync(config.STORAGE_KEYS.TOKEN) if (!token) { this.setData({ heartCount: 0 }) return } // 使用 auth.getCurrentUser 接口,该接口返回 grass_balance 字段 const res = await api.auth.getCurrentUser() if (res.success && res.data) { this.setData({ heartCount: res.data.grass_balance || 0 }) console.log('[index] 爱心值加载成功:', res.data.grass_balance) } } catch (err) { console.log('加载爱心值失败', err) } }, /** * 加载未读消息数 * 包含会话未读数 + 主动推送消息数 */ async loadUnreadCount() { if (!app.globalData.isLoggedIn) { this.setData({ totalUnread: 0 }) return } try { // 并行获取会话列表和主动推送消息 const [convRes, proactiveRes] = await Promise.all([ api.chat.getConversations(), api.proactiveMessage.getPending() ]) let totalUnread = 0 // 计算会话未读数 if (convRes.success && convRes.data) { totalUnread = convRes.data.reduce((sum, conv) => sum + (conv.unread_count || 0), 0) } // 加上主动推送消息数 if (proactiveRes.success && proactiveRes.data && Array.isArray(proactiveRes.data)) { totalUnread += proactiveRes.data.length console.log('[index] 主动推送消息数:', proactiveRes.data.length) } this.setData({ totalUnread }) console.log('[index] 总未读消息数:', totalUnread) } catch (err) { console.log('获取未读消息数失败', err) this.setData({ totalUnread: 0 }) } }, /** * 检查AI角色主动推送消息 * 注意:未读数已在 loadUnreadCount 中计算,不在首页显示Toast提示 */ async checkProactiveMessages() { if (!app.globalData.isLoggedIn) { return } try { const messages = await proactiveMessage.checkAndShowMessages({ force: true // 首次进入强制检查 }) console.log('[index] 主动推送消息检查完成,消息数:', messages.length) } catch (err) { console.log('[index] 检查主动推送消息失败', err) } }, /** * 加载角色列表 */ async loadCharacters() { this.setData({ loading: true, error: null }) try { console.log('[index] 开始加载角色列表...') const res = await api.character.getRandom(10) console.log('[index] API响应:', JSON.stringify(res)) // 兼容两种返回格式: // 1. { code: 0, data: { list: [...] } } - 新格式 // 2. { success: true, data: [...] } - 旧格式 let characters = [] if (res.code === 0 && res.data) { characters = res.data.list || res.data if (!Array.isArray(characters)) { characters = [] } } else if (res.success && res.data) { characters = Array.isArray(res.data) ? res.data : (res.data.list || []) } console.log('[index] 解析到角色数量:', characters.length) if (characters.length > 0) { // 转换数据格式:后端格式 -> 前端格式 const profiles = characters.map(char => this.transformCharacter(char)) // 更新好友故事区域(取前8个角色) const storyUsers = profiles.slice(0, 8).map(p => ({ id: p.id, name: p.name, img: p.avatar })) this.setData({ profiles, storyUsers, currentIndex: 0, loading: false }) console.log('[index] 角色加载成功,数量:', profiles.length) } else { console.log('[index] 没有角色数据') this.setData({ loading: false, profiles: [], currentIndex: 0, error: '暂无角色数据' }) } } catch (err) { console.error('[index] 加载角色失败', err) this.setData({ loading: false, profiles: [], currentIndex: 0, error: err.message || '加载失败' }) // 不显示toast,让用户看到空状态页面 } }, /** * 转换角色数据格式 * @param {object} char - 后端角色数据 */ transformCharacter(char) { // 静态资源基础URL const staticBaseUrl = getStaticBaseUrl() // 转换照片路径为完整URL const convertPhotoUrl = (url) => { if (!url) return '' // 如果已经是完整URL,直接返回 if (url.startsWith('http://') || url.startsWith('https://')) { return url } // 如果是相对路径,拼接基础URL if (url.startsWith('/characters/')) { return staticBaseUrl + url } return url } // 头像URL(用于推荐栏圆形小头像)- 优先使用 avatar/logo const avatarUrl = convertPhotoUrl(char.avatar || char.logo || char.image) // 宣传图URL(用于卡片大图)- 优先使用 promoImage const promoImageUrl = convertPhotoUrl(char.promoImage || char.promo_image || char.avatar || char.image) return { id: char.id, name: char.name || '未知', height: char.height || '', location: char.location || char.province || '', occupation: char.occupation || '', hobbies: char.hobbies || '', tags: char.tags || [], bio: char.bio || char.description || char.self_introduction || '', image: promoImageUrl, // 卡片大图使用宣传图 avatar: avatarUrl, // 小头像使用头像 gender: char.gender || 'female', age: char.age || '', voiceId: char.voice_id, isVipOnly: char.is_vip_only || false, // 开场白音频URL greetingAudioUrl: char.greetingAudioUrl || char.greeting_audio_url || '' } }, // ==================== 滑动交互 ==================== onTouchStart(e) { this.setData({ startX: e.touches[0].clientX, startY: e.touches[0].clientY, swipeDirection: '' }) }, onTouchMove(e) { const moveX = e.touches[0].clientX - this.data.startX const rotation = moveX * 0.05 this.setData({ offsetX: moveX, rotation: Math.max(-15, Math.min(15, rotation)) }) }, onTouchEnd(e) { const threshold = 80 const { offsetX } = this.data if (offsetX > threshold) { // 右滑 - 喜欢 this.handleSwipeLike() } else if (offsetX < -threshold) { // 左滑 - 跳过 this.handlePass() } else { // 重置位置 this.setData({ offsetX: 0, rotation: 0 }) } }, /** * 处理喜欢操作 */ async handleSwipeLike() { const { currentIndex, profiles, likedProfiles } = this.data if (currentIndex >= profiles.length) return const currentProfile = profiles[currentIndex] const currentId = currentProfile.id // 更新本地状态 const newLiked = { ...likedProfiles } newLiked[currentId] = true this.setData({ swipeDirection: 'swipe-right', likedProfiles: newLiked }) // 调用API(静默操作,不显示提示) try { await api.character.toggleLike(currentId) } catch (err) { console.log('喜欢操作失败', err) } // 移动到下一张 setTimeout(() => { this.moveToNext() }, 300) }, /** * 处理跳过操作 */ handlePass() { this.setData({ swipeDirection: 'swipe-left' }) setTimeout(() => { this.moveToNext() }, 300) }, /** * 移动到下一张卡片 */ moveToNext() { const { currentIndex, profiles } = this.data const nextIndex = currentIndex + 1 // 如果快到末尾,加载更多 if (nextIndex >= profiles.length - 2) { this.loadMoreCharacters() } this.setData({ currentIndex: nextIndex, offsetX: 0, rotation: 0, swipeDirection: '' }) }, /** * 加载更多角色 * 传递已显示的角色ID,避免重复 */ async loadMoreCharacters() { try { // 获取已显示的角色ID列表 const existingIds = this.data.profiles.map(p => p.id).filter(id => id) const res = await api.character.getRandom(6, { excludeIds: existingIds }) // 兼容两种返回格式 let characters = [] if (res.code === 0 && res.data) { characters = res.data.list || res.data } else if (res.success && res.data) { characters = Array.isArray(res.data) ? res.data : (res.data.list || []) } if (characters.length > 0) { // 再次过滤,确保不重复 const existingIdSet = new Set(existingIds) const filteredCharacters = characters.filter(char => !existingIdSet.has(char.id)) if (filteredCharacters.length > 0) { const newProfiles = filteredCharacters.map(char => this.transformCharacter(char)) this.setData({ profiles: [...this.data.profiles, ...newProfiles] }) } } } catch (err) { console.log('加载更多失败', err) } }, // ==================== 按钮操作 ==================== /** * 切换喜欢状态 - 显示爱心套餐弹窗 */ onToggleLike() { const { currentIndex, profiles } = this.data if (currentIndex >= profiles.length) return const currentProfile = profiles[currentIndex] // 保存当前角色信息并显示弹窗 this.setData({ showHeartPopup: true, currentCharacter: currentProfile }) }, /** * 播放语音(优先使用预录制的开场白音频) */ async onPlayVoice() { const { currentIndex, profiles } = this.data if (currentIndex >= profiles.length) return const currentProfile = profiles[currentIndex] // 优先使用预录制的开场白音频 const greetingAudioUrl = currentProfile.greetingAudioUrl || currentProfile.greeting_audio_url if (greetingAudioUrl) { // 处理相对路径,拼接完整URL let audioUrl = greetingAudioUrl if (audioUrl.startsWith('/')) { audioUrl = getStaticBaseUrl() + audioUrl } console.log('[index] 播放开场白音频:', audioUrl) // 停止之前的音频 if (this.audioContext) { try { this.audioContext.stop() this.audioContext.destroy() } catch (e) {} this.audioContext = null } // 先检查音频文件是否存在 wx.request({ url: audioUrl, method: 'HEAD', success: (res) => { if (res.statusCode === 200) { // 文件存在,播放音频 this.playAudioFile(audioUrl) } else { console.log('[index] 音频文件不存在:', res.statusCode) wx.showToast({ title: '该角色暂无独白音频', icon: 'none' }) } }, fail: (err) => { console.log('[index] 检查音频文件失败:', err) // 即使HEAD请求失败,也尝试播放(有些服务器不支持HEAD) this.playAudioFile(audioUrl) } }) return } // 没有预录制音频,提示用户 wx.showToast({ title: '该角色暂无独白音频', icon: 'none', duration: 2000 }) }, /** * 播放音频文件 */ playAudioFile(audioUrl) { // 显示播放中提示 wx.showToast({ title: '播放独白中...', icon: 'none', duration: 5000 }) // 创建音频上下文 const innerAudioContext = wx.createInnerAudioContext() this.audioContext = innerAudioContext // 配置音频属性 innerAudioContext.src = audioUrl innerAudioContext.volume = 1.0 innerAudioContext.obeyMuteSwitch = false // 不受系统静音开关影响 innerAudioContext.autoplay = false // 确保在 iOS 上即使在静音模式下也能播放(双重保险) if (wx.setInnerAudioOption) { wx.setInnerAudioOption({ obeyMuteSwitch: false, speakerOn: true }) } innerAudioContext.onCanplay(() => { console.log('[index] 音频可以播放, duration:', innerAudioContext.duration) }) innerAudioContext.onPlay(() => { console.log('[index] 音频开始播放, volume:', innerAudioContext.volume) }) innerAudioContext.onTimeUpdate(() => { // 每秒打印一次进度 const currentTime = Math.floor(innerAudioContext.currentTime) if (currentTime !== this._lastLogTime) { console.log('[index] 播放进度:', currentTime, '/', Math.floor(innerAudioContext.duration || 0)) this._lastLogTime = currentTime } }) innerAudioContext.onError((err) => { console.error('[index] 音频播放错误:', JSON.stringify(err)) wx.hideToast() let errMsg = '播放失败' if (err.errCode === 10001 || err.errCode === -1) { errMsg = '音频文件不存在' } else if (err.errCode === 10002) { errMsg = '网络错误' } else if (err.errCode === 10003 || err.errCode === 10004) { errMsg = '音频格式不支持' } wx.showToast({ title: errMsg, icon: 'none' }) }) innerAudioContext.onEnded(() => { console.log('[index] 音频播放结束') wx.hideToast() }) // 延迟播放 setTimeout(() => { console.log('[index] 调用 play()') innerAudioContext.play() }, 100) }, /** * 选择角色(进入详情) */ onSelectCharacter() { const { currentIndex, profiles } = this.data if (currentIndex >= profiles.length) return const profile = profiles[currentIndex] wx.navigateTo({ url: `/pages/character-detail/character-detail?id=${profile.id}` }) }, /** * 点击卡片进入详情 */ onCardTap() { // 如果有滑动偏移,不触发点击 if (Math.abs(this.data.offsetX) > 10) { return } this.onSelectCharacter() }, /** * 刷新角色列表 */ onRefresh() { this.loadCharacters() wx.showToast({ title: '已为您换一批', icon: 'success' }) }, // ==================== VIP弹窗 ==================== closeVipModal() { this.setData({ showVipModal: false }) }, preventBubble() { // 阻止事件冒泡 }, preventTouchMove() { // 阻止滚动穿透 }, // ==================== 爱心弹窗 ==================== /** * 显示爱心弹窗 */ showHeartPopup() { this.setData({ showHeartPopup: true }) }, /** * 关闭爱心弹窗 */ closeHeartPopup() { this.setData({ showHeartPopup: false, currentCharacter: null }) }, /** * 选择爱心套餐 */ selectHeartPackage(e) { const index = e.currentTarget.dataset.index this.setData({ selectedHeartPackage: index }) }, /** * 购买爱心 * 使用 /api/payment/unified-order 接口 * 测试模式下返回 testMode: true,订单直接完成,无需调用微信支付 */ async buyHearts() { const token = wx.getStorageSync(config.STORAGE_KEYS.TOKEN) if (!token) { wx.showToast({ title: '请先登录', icon: 'none' }) setTimeout(() => { wx.navigateTo({ url: '/pages/login/login' }) }, 1500) return } const selectedPackage = this.data.heartPackages[this.data.selectedHeartPackage] if (!selectedPackage) { wx.showToast({ title: '请选择套餐', icon: 'none' }) return } this.setData({ purchasing: true }) try { wx.showLoading({ title: '创建订单中...' }) // 调用统一支付订单接口 const res = await api.payment.createUnifiedOrder({ type: 'recharge', amount: selectedPackage.price, rechargeValue: selectedPackage.hearts }) wx.hideLoading() console.log('[buyHearts] API返回:', JSON.stringify(res)) if (res.success) { // 检查是否为测试模式(兼容多种判断方式) const isTestMode = res.testMode || res.test_mode || res.data?.testMode || res.data?.test_mode || // 如果 payParams.package 包含 mock_prepay,也认为是测试模式 (res.payParams?.package && res.payParams.package.includes('mock_prepay')) if (isTestMode) { // 测试模式:订单已直接完成,无需调用微信支付 wx.showToast({ title: '购买成功', icon: 'success' }) await this.loadHeartBalance() this.closeHeartPopup() } else { // 正式模式:调用微信支付 const payParams = res.payParams || res.pay_params || res.data?.payParams || res.data?.pay_params if (!payParams || !payParams.timeStamp) { wx.showToast({ title: '支付参数错误', icon: 'none' }) return } wx.requestPayment({ timeStamp: payParams.timeStamp, nonceStr: payParams.nonceStr, package: payParams.package, signType: payParams.signType || 'RSA', paySign: payParams.paySign, success: async () => { wx.showToast({ title: '购买成功', icon: 'success' }) await this.loadHeartBalance() this.closeHeartPopup() }, fail: (err) => { if (err.errMsg !== 'requestPayment:fail cancel') { wx.showToast({ title: '支付失败', icon: 'none' }) } } }) } } else { wx.showToast({ title: res.message || '创建订单失败', icon: 'none' }) } } catch (err) { wx.hideLoading() console.error('购买爱心失败', err) wx.showToast({ title: '网络错误,请重试', icon: 'none' }) } finally { this.setData({ purchasing: false }) } }, /** * 分享解锁 */ onShareUnlock() { wx.showToast({ title: '分享功能开发中', icon: 'none' }) // TODO: 实现分享解锁逻辑 }, /** * 爱心兑换解锁 */ async onExchangeHearts() { const token = wx.getStorageSync(config.STORAGE_KEYS.TOKEN) if (!token) { wx.showToast({ title: '请先登录', icon: 'none' }) setTimeout(() => { wx.navigateTo({ url: '/pages/login/login' }) }, 1500) return } const { currentCharacter, heartCount, unlockHeartsCost } = this.data if (!currentCharacter) return // 检查爱心值,不足时提示并跳转充值页面 if (heartCount < unlockHeartsCost) { wx.showToast({ title: '爱心值不足,去充值', icon: 'none' }) setTimeout(() => { this.setData({ showHeartPopup: false }) wx.navigateTo({ url: '/pages/recharge/recharge' }) }, 1500) return } this.setData({ purchasing: true }) try { wx.showLoading({ title: '兑换中...' }) const res = await api.character.unlock({ character_id: currentCharacter.id, unlock_type: 'hearts' }) wx.hideLoading() if (res.success || res.code === 0) { wx.showToast({ title: '解锁成功', icon: 'success' }) // 更新爱心余额(优先使用后端返回的余额,否则本地扣减) const newBalance = res.data?.remaining_hearts ?? (this.data.heartCount - unlockHeartsCost) this.setData({ heartCount: newBalance }) // 记录已解锁 const newUnlocked = { ...this.data.unlockedProfiles } newUnlocked[currentCharacter.id] = true this.setData({ unlockedProfiles: newUnlocked, showHeartPopup: false, currentCharacter: null }) // 延迟后跳转到聊天页面 setTimeout(() => { wx.navigateTo({ url: `/pages/chat-detail/chat-detail?id=${currentCharacter.id}&name=${encodeURIComponent(currentCharacter.name)}` }) }, 1000) } else { wx.showToast({ title: res.message || '兑换失败', icon: 'none' }) } } catch (err) { wx.hideLoading() console.error('爱心兑换失败', err) wx.showToast({ title: '网络错误,请重试', icon: 'none' }) } finally { this.setData({ purchasing: false }) } }, /** * 直接购买解锁(9.9元) * 使用 /api/payment/unified-order 接口 * 测试模式下返回 testMode: true,订单直接完成,无需调用微信支付 */ async onPurchaseDirect() { const token = wx.getStorageSync(config.STORAGE_KEYS.TOKEN) if (!token) { wx.showToast({ title: '请先登录', icon: 'none' }) setTimeout(() => { wx.navigateTo({ url: '/pages/login/login' }) }, 1500) return } const { currentCharacter } = this.data if (!currentCharacter) return this.setData({ purchasing: true }) try { wx.showLoading({ title: '创建订单中...' }) // 调用统一支付订单接口 const res = await api.payment.createUnifiedOrder({ type: 'character_unlock', character_id: currentCharacter.id, amount: 9.9 }) wx.hideLoading() console.log('[onPurchaseDirect] API返回:', JSON.stringify(res)) if (res.success) { // 检查是否为测试模式(兼容多种判断方式) const isTestMode = res.testMode || res.test_mode || res.data?.testMode || res.data?.test_mode || // 如果 payParams.package 包含 mock_prepay,也认为是测试模式 (res.payParams?.package && res.payParams.package.includes('mock_prepay')) if (isTestMode) { // 测试模式:订单已直接完成,无需调用微信支付 wx.showToast({ title: '购买成功', icon: 'success' }) // 记录已解锁 const newUnlocked = { ...this.data.unlockedProfiles } newUnlocked[currentCharacter.id] = true this.setData({ unlockedProfiles: newUnlocked, showHeartPopup: false, currentCharacter: null }) // 延迟后跳转到聊天页面 setTimeout(() => { wx.navigateTo({ url: `/pages/chat-detail/chat-detail?id=${currentCharacter.id}&name=${encodeURIComponent(currentCharacter.name)}` }) }, 1000) } else { // 正式模式:调用微信支付 const payParams = res.payParams || res.pay_params || res.data?.payParams || res.data?.pay_params if (!payParams || !payParams.timeStamp) { wx.showToast({ title: '支付参数错误', icon: 'none' }) return } wx.requestPayment({ timeStamp: payParams.timeStamp, nonceStr: payParams.nonceStr, package: payParams.package, signType: payParams.signType || 'RSA', paySign: payParams.paySign, success: () => { wx.showToast({ title: '购买成功', icon: 'success' }) // 记录已解锁 const newUnlocked = { ...this.data.unlockedProfiles } newUnlocked[currentCharacter.id] = true this.setData({ unlockedProfiles: newUnlocked, showHeartPopup: false, currentCharacter: null }) // 延迟后跳转到聊天页面 setTimeout(() => { wx.navigateTo({ url: `/pages/chat-detail/chat-detail?id=${currentCharacter.id}&name=${encodeURIComponent(currentCharacter.name)}` }) }, 1000) }, fail: (err) => { if (err.errMsg !== 'requestPayment:fail cancel') { wx.showToast({ title: '支付失败', icon: 'none' }) } } }) } } else { wx.showToast({ title: res.message || '创建订单失败', icon: 'none' }) } } catch (err) { wx.hideLoading() console.error('购买解锁失败', err) wx.showToast({ title: '网络错误,请重试', icon: 'none' }) } finally { this.setData({ purchasing: false }) } }, /** * 购买并解锁角色聊天(套餐购买) * 使用 /api/payment/unified-order 接口 * 测试模式下返回 testMode: true,订单直接完成,无需调用微信支付 */ async buyAndUnlock() { const token = wx.getStorageSync(config.STORAGE_KEYS.TOKEN) if (!token) { wx.showToast({ title: '请先登录', icon: 'none' }) setTimeout(() => { wx.navigateTo({ url: '/pages/login/login' }) }, 1500) return } const { currentCharacter, heartPackages, selectedHeartPackage } = this.data if (!currentCharacter) return const selectedPackage = heartPackages[selectedHeartPackage] if (!selectedPackage) { wx.showToast({ title: '请选择套餐', icon: 'none' }) return } this.setData({ purchasing: true }) try { wx.showLoading({ title: '创建订单中...' }) // 调用统一支付订单接口 const res = await api.payment.createUnifiedOrder({ type: 'character_unlock', character_id: currentCharacter.id, amount: selectedPackage.price }) wx.hideLoading() console.log('[buyAndUnlock] API返回:', JSON.stringify(res)) if (res.success) { // 检查是否为测试模式(兼容多种判断方式) const isTestMode = res.testMode || res.test_mode || res.data?.testMode || res.data?.test_mode || // 如果 payParams.package 包含 mock_prepay,也认为是测试模式 (res.payParams?.package && res.payParams.package.includes('mock_prepay')) if (isTestMode) { // 测试模式:订单已直接完成,无需调用微信支付 wx.showToast({ title: '购买成功', icon: 'success' }) // 刷新爱心余额 this.loadHeartBalance() // 记录已解锁 const newUnlocked = { ...this.data.unlockedProfiles } newUnlocked[currentCharacter.id] = true this.setData({ unlockedProfiles: newUnlocked, showHeartPopup: false, currentCharacter: null }) // 延迟后跳转到聊天页面 setTimeout(() => { wx.navigateTo({ url: `/pages/chat-detail/chat-detail?id=${currentCharacter.id}&name=${encodeURIComponent(currentCharacter.name)}` }) }, 1000) } else { // 正式模式:调用微信支付 const payParams = res.payParams || res.pay_params || res.data?.payParams || res.data?.pay_params if (!payParams || !payParams.timeStamp) { wx.showToast({ title: '支付参数错误', icon: 'none' }) return } wx.requestPayment({ timeStamp: payParams.timeStamp, nonceStr: payParams.nonceStr, package: payParams.package, signType: payParams.signType || 'RSA', paySign: payParams.paySign, success: async () => { wx.showToast({ title: '购买成功', icon: 'success' }) // 刷新爱心余额 this.loadHeartBalance() // 记录已解锁 const newUnlocked = { ...this.data.unlockedProfiles } newUnlocked[currentCharacter.id] = true this.setData({ unlockedProfiles: newUnlocked, showHeartPopup: false, currentCharacter: null }) // 延迟后跳转到聊天页面 setTimeout(() => { wx.navigateTo({ url: `/pages/chat-detail/chat-detail?id=${currentCharacter.id}&name=${encodeURIComponent(currentCharacter.name)}` }) }, 1000) }, fail: (err) => { if (err.errMsg !== 'requestPayment:fail cancel') { wx.showToast({ title: '支付失败', icon: 'none' }) } } }) } } else { wx.showToast({ title: res.message || '创建订单失败', icon: 'none' }) } } catch (err) { wx.hideLoading() console.error('解锁角色失败', err) wx.showToast({ title: '网络错误,请重试', icon: 'none' }) } finally { this.setData({ purchasing: false }) } }, /** * 跳转到用户协议 */ goToUserAgreement() { wx.navigateTo({ url: '/pages/agreement/agreement?code=user-agreement' }) }, /** * 跳转到隐私政策 */ goToPrivacyPolicy() { wx.navigateTo({ url: '/pages/agreement/agreement?code=privacy-policy' }) }, onUpgrade() { const { currentIndex, profiles, unlockedProfiles, heartCount, unlockHeartsCost } = this.data if (currentIndex >= profiles.length) return // 检查爱心值,不足时提示并跳转充值页面 if (heartCount < unlockHeartsCost) { wx.showToast({ title: '爱心值不足,去充值', icon: 'none' }) setTimeout(() => { this.setData({ showVipModal: false }) wx.navigateTo({ url: '/pages/recharge/recharge' }) }, 1500) return } const currentId = profiles[currentIndex].id const newUnlocked = { ...unlockedProfiles } newUnlocked[currentId] = true // 扣减爱心值 this.setData({ unlockedProfiles: newUnlocked, showVipModal: false, heartCount: heartCount - unlockHeartsCost }) wx.showToast({ title: '兑换成功!', icon: 'success' }) }, onPurchase() { // 跳转到充值页面 wx.navigateTo({ url: '/pages/recharge/recharge' }) }, // ==================== 其他操作 ==================== onStoryTap(e) { const index = e.currentTarget.dataset.index const storyUser = this.data.storyUsers[index] if (storyUser && storyUser.id) { // 跳转到角色详情页 wx.navigateTo({ url: `/pages/character-detail/character-detail?id=${storyUser.id}` }) } else { wx.showToast({ title: storyUser.name + '的故事', icon: 'none' }) } }, onNotification() { wx.showToast({ title: '暂无新消息', icon: 'none' }) }, // Tab bar navigation - 需要登录的页面检查登录状态 switchTab(e) { const path = e.currentTarget.dataset.path const app = getApp() // 消息页面需要登录 if (path === '/pages/chat/chat') { if (!app.globalData.isLoggedIn) { wx.navigateTo({ url: '/pages/login/login?redirect=' + encodeURIComponent(path) }) return } } wx.switchTab({ url: path }) }, onTest() { wx.showToast({ title: '测试功能', icon: 'none' }) }, /** * 检查注册奖励领取资格 */ async checkRegistrationReward() { if (!app.globalData.isLoggedIn) return try { const res = await api.lovePoints.checkRegistrationReward() console.log('[index] 注册奖励检查结果:', res) if (res.success && res.data && res.data.eligible) { this.setData({ showRegistrationReward: true, registrationRewardAmount: res.data.amount || 0 }) } } catch (err) { console.error('[index] 检查注册奖励失败:', err) } }, /** * 领取注册奖励 */ async onClaimReward() { if (this.data.claiming) return this.setData({ claiming: true }) wx.showLoading({ title: '领取中...' }) try { const res = await api.lovePoints.claimRegistrationReward() wx.hideLoading() if (res.success) { wx.showToast({ title: '领取成功', icon: 'success', duration: 2000 }) this.setData({ showRegistrationReward: false }) // 如果后端返回了免费畅聊时间,可以做提示 if (res.data && res.data.free_chat_time) { console.log('[index] 获得免费畅聊时间:', res.data.free_chat_time); setTimeout(() => { wx.showModal({ title: '领取成功', content: '恭喜获得 100 爱心 + 60 分钟免费畅聊时间!', confirmText: '去聊天', success: (modalRes) => { if (modalRes.confirm) { // 如果有当前正在查看的角色,直接跳过去 const { currentIndex, profiles } = this.data if (profiles && profiles[currentIndex]) { const char = profiles[currentIndex] wx.navigateTo({ url: `/pages/chat-detail/chat-detail?id=${char.id}&name=${encodeURIComponent(char.name)}` }) } else { wx.switchTab({ url: '/pages/chat/chat' }) } } } }); }, 2000); } // 刷新余额 this.loadHeartBalance() } else { wx.showToast({ title: res.message || '领取失败', icon: 'none' }) } } catch (err) { wx.hideLoading() console.error('[index] 领取注册奖励失败:', err) wx.showToast({ title: '网络错误,请重试', icon: 'none' }) } finally { this.setData({ claiming: false }) } }, /** * 关闭注册奖励弹窗 */ closeRewardPopup() { this.setData({ showRegistrationReward: false }) }, /** * 检查 GF100 弹窗状态 */ async checkGf100Popup() { if (!app.globalData.isLoggedIn) return try { const res = await api.lovePoints.checkGf100Status() console.log('[index] GF100 检查结果:', res) if (res.success && res.data && res.data.showPopup) { const imageUrl = res.data.imageUrl; this.setData({ showGf100Popup: true, gf100ImageUrl: imageUrl ? util.getFullImageUrl(imageUrl) : '/images/gf100.png' }) } } catch (err) { console.error('[index] 检查 GF100 弹窗失败:', err) } }, /** * 领取 GF100 奖励 */ async onClaimGf100() { if (this.data.claiming) return this.setData({ claiming: true }) wx.showLoading({ title: '领取中...', mask: true }) try { const res = await api.lovePoints.claimGf100() wx.hideLoading() if (res.success) { wx.showToast({ title: '领取成功', icon: 'success', duration: 2000 }) this.setData({ showGf100Popup: false }) // 弹出获得 60 分钟免费畅聊的提示 setTimeout(() => { wx.showModal({ title: '领取成功', content: '恭喜获得 100 爱心 + 60 分钟免费畅聊时间!', confirmText: '去聊天', success: (modalRes) => { if (modalRes.confirm) { wx.switchTab({ url: '/pages/chat/chat' }) } } }); }, 500); // 刷新余额 this.loadHeartBalance() } else { wx.showToast({ title: res.message || '领取失败', icon: 'none' }) } } catch (err) { wx.hideLoading() console.error('[index] 领取 GF100 失败:', err) wx.showToast({ title: '网络错误,请重试', icon: 'none' }) } finally { this.setData({ claiming: false }) } }, /** * 关闭 GF100 弹窗 */ closeGf100Popup() { this.setData({ showGf100Popup: false }) }, /** * 用户点击右上角分享 */ onShareAppMessage() { const { shareConfig } = this.data const referralCode = wx.getStorageSync('referralCode') || '' api.promotion.recordShare({ type: 'app_message', page: '/pages/index/index', referralCode: referralCode }).catch(err => console.error('记录分享失败:', err)) this.recordShareReward() if (shareConfig) { return { title: shareConfig.title, path: `${shareConfig.path}?referralCode=${referralCode}`, imageUrl: shareConfig.imageUrl } } return { title: '欢迎来到心伴俱乐部', desc: '随时可聊 一直陪伴', path: `/pages/index/index?referralCode=${referralCode}`, imageUrl: '/images/icon-heart-new.png' } }, /** * 用户分享到朋友圈 */ onShareTimeline() { const { shareConfig } = this.data const referralCode = wx.getStorageSync('referralCode') || '' api.promotion.recordShare({ type: 'timeline', page: '/pages/index/index', referralCode: referralCode }).catch(err => console.error('记录分享失败:', err)) this.recordShareReward() if (shareConfig) { return { title: shareConfig.title, query: `referralCode=${referralCode}`, imageUrl: shareConfig.imageUrl } } return { title: '心伴俱乐部 - 随时可聊 一直陪伴', query: `referralCode=${referralCode}`, imageUrl: '/images/icon-heart-new.png' } }, /** * 静默记录分享奖励(分享人A获得+100爱心值) */ async recordShareReward() { try { const res = await api.lovePoints.share() console.log('[index] 分享爱心值奖励:', res) } catch (err) { console.error('[index] 记录分享奖励失败:', err) } } })