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

1587 lines
44 KiB
JavaScript
Raw 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/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)
}
}
})