// pages/game/detail/detail.js import request from '../../../utils/request' import { formatTime } from '../../../utils/format' // 格式化积分显示(超过1000显示k) function formatScore(score) { const num = parseFloat(score) || 0 if (Math.abs(num) >= 1000) { return (num / 1000).toFixed(1) + 'k' } return num.toString() } Page({ data: { sessionId: null, session: null, players: [], records: [], logs: [], logsScrollTop: 0, hasMoreLogs: false, logsPage: 1, userInfo: null, isHost: false, isInSession: false, totalRounds: 0, showShareModal: false, showPlayerModal: false, showExpenseModal: false, showSettingsModal: false, selectedPlayer: null, scoreInput: '', otherPlayers: [], expenseInputs: {}, tableFee: 0, quickInputs: [50, 100, 200, 500], ttsEnabled: true, // 语音播报开关,默认开启 refreshTimer: null, navTitle: '房间详情', announcement: '', navbarHeight: 0, // 导航栏高度 qrcodeUrl: '', // 二维码URL hasShownRejoinPrompt: false, // 标记是否已显示过重新加入提示 statusText: { 'waiting': '等待中', 'playing': '游戏中', 'paused': '已暂停', 'finished': '已结束' }, gameTypeText: { 'mahjong': '麻将', 'poker': '扑克', 'other': '其他' } }, onLoad(options) { // 计算导航栏高度 const windowInfo = wx.getWindowInfo() const menuButtonInfo = wx.getMenuButtonBoundingClientRect() const statusBarHeight = windowInfo.statusBarHeight const menuButtonTop = menuButtonInfo.top const navbarHeight = menuButtonInfo.bottom + (menuButtonTop - statusBarHeight) this.setData({ navbarHeight }) if (!options.id) { wx.showToast({ title: '牌局不存在', icon: 'none' }) setTimeout(() => { wx.navigateBack() }, 1500) return } console.log(options.id) this.setData({ sessionId: options.id }) // 获取用户信息 const app = getApp() const userInfo = app.getUserInfo() if (!userInfo) { wx.showToast({ title: '请先登录', icon: 'none' }) setTimeout(() => { wx.navigateBack() }, 1500) return } this.setData({ userInfo }) // 加载TTS设置 const ttsEnabled = wx.getStorageSync('ttsEnabled') if (ttsEnabled !== '') { this.setData({ ttsEnabled: ttsEnabled }) } // 加载公告 this.loadAnnouncements() // 加载牌局详情(包含日志) this.loadSessionDetail() // 设置自动刷新 this.startAutoRefresh() }, onShow() { if (this.data.sessionId) { this.loadSessionDetail() // 页面重新显示时,重启自动刷新 this.startAutoRefresh() } }, onHide() { // 页面隐藏时清除定时器,避免后台继续请求 this.stopAutoRefresh() }, onUnload() { // 页面卸载时清除定时器 this.stopAutoRefresh() }, // 停止自动刷新 stopAutoRefresh() { if (this.data.refreshTimer) { clearInterval(this.data.refreshTimer) this.setData({ refreshTimer: null }) } }, // 启动自动刷新 startAutoRefresh() { // 如果已有定时器,先清除 this.stopAutoRefresh() // 每5秒刷新一次 const timer = setInterval(() => { if (this.data.session) { this.loadSessionDetail(true) } }, 5000) this.setData({ refreshTimer: timer }) }, // 加载公告 async loadAnnouncements() { try { const data = await request.get('/announcements') if (data && data.length > 0) { // 将最新的2条公告用"|"连接 const announcementText = data.map(item => item.content).join(' | ') this.setData({ announcement: announcementText }) } } catch (error) { console.error('加载公告失败:', error) // 使用默认公告 this.setData({ announcement: '欢迎来到打牌记账,祝大家游戏愉快!' }) } }, // 加载牌局实时数据(统一接口) async loadSessionDetail(silent = false) { if (!silent) { wx.showLoading({ title: '加载中...' }) } try { // 调用统一的实时数据接口 const realtimeData = await request.get(`/rooms/${this.data.sessionId}/realtime`) const { room, players, records, logs, user_info } = realtimeData // 检测用户是否曾经在房间但已离开(仅在初次加载时,不是自动刷新,并且未显示过提示) if (!silent && user_info.has_left && !this.data.hasShownRejoinPrompt) { if (!silent) { wx.hideLoading() } // 标记已显示过提示,避免重复弹窗 this.setData({ hasShownRejoinPrompt: true }) // 停止自动刷新 this.stopAutoRefresh() wx.showModal({ title: '检测到您已离开此房间', content: '您之前已离开过此房间,是否重新加入?', confirmText: '重新加入', cancelText: '返回首页', success: async (res) => { if (res.confirm) { // 重新加入房间 try { wx.showLoading({ title: '加入中...' }) await request.post('/rooms/join', { room_code: room.room_code }) wx.hideLoading() wx.showToast({ title: '重新加入成功', icon: 'success' }) // 刷新页面并重启自动刷新 this.loadSessionDetail() this.startAutoRefresh() } catch (error) { wx.hideLoading() wx.showToast({ title: error.message || '加入失败', icon: 'none' }) // 返回首页 setTimeout(() => { wx.switchTab({ url: '/pages/index/index' }) }, 1500) } } else { // 返回首页(tabBar页面使用switchTab) wx.switchTab({ url: '/pages/index/index' }) } } }) return } // 检测房间状态变化(仅在自动刷新时) if (silent && this.data.session) { const oldStatus = this.data.session.status const newStatus = room.status const oldIsInSession = this.data.isInSession const newIsInSession = user_info.is_in_session // 1. 检测房间是否已关闭 if (oldStatus === 'playing' && newStatus === 'finished') { // 停止自动刷新 this.stopAutoRefresh() wx.showModal({ title: '牌局已结束', content: '牌局已经结束,是否查看战绩?', confirmText: '查看战绩', cancelText: '退出', success: (res) => { if (res.confirm) { // 跳转到统计页(tabBar页面使用switchTab) wx.switchTab({ url: '/pages/stats/personal/personal' }) } else { // 返回首页(tabBar页面使用switchTab) wx.switchTab({ url: '/pages/index/index' }) } } }) return } // 2. 检测用户是否离开了房间 if (oldIsInSession && !newIsInSession) { // 停止自动刷新 this.stopAutoRefresh() wx.showModal({ title: '已离开房间', content: '您已离开此牌局,是否重新加入?', confirmText: '重新加入', cancelText: '返回首页', success: async (res) => { if (res.confirm) { // 重新加入房间 try { wx.showLoading({ title: '加入中...' }) await request.post('/rooms/join', { room_code: this.data.session.room_code }) wx.hideLoading() wx.showToast({ title: '重新加入成功', icon: 'success' }) // 刷新页面并重启自动刷新 this.loadSessionDetail() this.startAutoRefresh() } catch (error) { wx.hideLoading() wx.showToast({ title: error.message || '加入失败', icon: 'none' }) // 返回首页 setTimeout(() => { wx.redirectTo({ url: '/pages/index/index' }) }, 1500) } } else { // 返回首页(tabBar页面使用switchTab) wx.switchTab({ url: '/pages/index/index' }) } } }) return } } // 格式化创建时间 const createTime = formatTime(new Date(room.created_at * 1000)) // 格式化战绩时间 const formattedRecords = (records || []).map(record => { const time = new Date(record.created_at * 1000) return { ...record, timeText: `${time.getMonth() + 1}/${time.getDate()} ${time.getHours()}:${String(time.getMinutes()).padStart(2, '0')}` } }) // 格式化日志时间 const formattedLogs = (logs || []).map(log => { const time = new Date(log.created_at * 1000) const month = String(time.getMonth() + 1).padStart(2, '0') const day = String(time.getDate()).padStart(2, '0') const hours = String(time.getHours()).padStart(2, '0') const minutes = String(time.getMinutes()).padStart(2, '0') const seconds = String(time.getSeconds()).padStart(2, '0') return { ...log, timeText: `${month}-${day} ${hours}:${minutes}:${seconds}` } }) // 查找房主昵称 const hostPlayer = players.find(p => p.is_host) //const hostNickname = hostPlayer ? hostPlayer.nickname : '房主' const navTitle = `${room.room_name}` // 获取除自己外的其他玩家 const otherPlayers = players.filter(p => p.player_id !== this.data.userInfo.id) // 更新快捷输入设置 const quickInputs = room.quick_settings || [10, 20, 30] // 格式化玩家积分显示 const formattedPlayers = players.map(p => ({ ...p, formatted_score: formatScore(p.total_win_loss) })) this.setData({ session: room, players: formattedPlayers, records: formattedRecords, logs: formattedLogs, isInSession: user_info.is_in_session, isHost: user_info.is_host, createTime, totalRounds: room.total_rounds || 0, navTitle, otherPlayers, quickInputs, tableFee: room.table_fee || 0 }) if (!silent) { wx.hideLoading() } } catch (error) { // 静默刷新时忽略错误(网络问题等),避免打扰用户 if (!silent) { wx.hideLoading() wx.showToast({ title: error.message || '加载失败', icon: 'none' }) } else { // 静默刷新失败时,仅在控制台记录错误,不显示给用户 console.warn('自动刷新失败:', error) } } }, // 开始游戏 async startGame() { if (this.data.players.length < 2) { wx.showToast({ title: '至少需要2人才能开始', icon: 'none' }) return } wx.showModal({ title: '开始游戏', content: `确定要开始游戏吗?当前${this.data.players.length}人`, success: async (res) => { if (res.confirm) { wx.showLoading({ title: '启动中...' }) try { await request.post(`/rooms/${this.data.sessionId}/start`) wx.hideLoading() wx.showToast({ title: '游戏已开始', icon: 'success' }) // 刷新页面 this.loadSessionDetail() // 跳转到记账页 setTimeout(() => { this.goToPlay() }, 1500) } catch (error) { wx.hideLoading() wx.showToast({ title: error.message || '启动失败', icon: 'none' }) } } } }) }, // 暂停游戏 async pauseGame() { wx.showModal({ title: '暂停游戏', content: '确定要暂停游戏吗?', success: async (res) => { if (res.confirm) { try { await request.post(`/rooms/${this.data.sessionId}/pause`) wx.showToast({ title: '已暂停', icon: 'success' }) this.loadSessionDetail() } catch (error) { wx.showToast({ title: error.message || '操作失败', icon: 'none' }) } } } }) }, // 结束牌局 confirmEndSession() { wx.showModal({ title: '结束牌局', content: '确定要结束牌局吗?结束后将无法继续记账', confirmColor: '#ff4444', success: async (res) => { if (res.confirm) { wx.showLoading({ title: '处理中...' }) try { await request.post(`/rooms/${this.data.sessionId}/end`) wx.hideLoading() wx.showToast({ title: '牌局已结束', icon: 'success' }) this.loadSessionDetail() } catch (error) { wx.hideLoading() wx.showToast({ title: error.message || '操作失败', icon: 'none' }) } } } }) }, // 返回上一页(不离开房间) confirmLeaveSession() { wx.navigateBack() }, // 加入牌局 async joinSession() { wx.showLoading({ title: '加入中...' }) try { await request.post('/rooms/join', { session_id: this.data.sessionId }) wx.hideLoading() wx.showToast({ title: '加入成功', icon: 'success' }) // 刷新页面 this.loadSessionDetail() } catch (error) { wx.hideLoading() wx.showToast({ title: error.message || '加入失败', icon: 'none' }) } }, // 显示分享弹窗 async shareSession() { // 打开分享弹窗时暂停自动刷新 this.stopAutoRefresh() this.setData({ showShareModal: true, qrcodeUrl: '' // 重置二维码 }) // 生成二维码 await this.generateQRCode() }, // 生成二维码 async generateQRCode() { if (!this.data.session || !this.data.session.invite_code) { console.error('无法生成二维码: session或invite_code不存在', this.data.session) wx.showToast({ title: '房间信息不完整', icon: 'none' }) return } try { const app = getApp() const scene = `qrcode&code=${this.data.session.invite_code}` console.log('开始生成二维码, scene:', scene, 'invite_code:', this.data.session.invite_code) wx.showLoading({ title: '生成二维码...' }) const qrcodeBuffer = await app.getQrcode(scene) console.log('二维码数据返回:', qrcodeBuffer) wx.hideLoading() // 检查返回数据类型 if (!qrcodeBuffer) { throw new Error('二维码数据为空') } // 将arraybuffer转换为base64 const base64 = wx.arrayBufferToBase64(qrcodeBuffer) const qrcodeUrl = `data:image/png;base64,${base64}` console.log('base64长度:', base64.length) this.setData({ qrcodeUrl: qrcodeUrl }) } catch (error) { wx.hideLoading() console.error('生成二维码失败:', error) wx.showToast({ title: error.message || '二维码生成失败', icon: 'none' }) } }, // 关闭分享弹窗 closeShareModal() { this.setData({ showShareModal: false }) // 关闭分享弹窗后恢复自动刷新 this.startAutoRefresh() }, // 复制邀请码 copyInviteCode() { wx.setClipboardData({ data: this.data.session.invite_code, success: () => { wx.showToast({ title: '已复制邀请码', icon: 'success' }) } }) }, // 进入记账页 goToPlay() { wx.navigateTo({ url: `/pages/game/settlement/settlement?id=${this.data.sessionId}` }) }, // 查看统计 goToStats() { wx.navigateTo({ url: `/pages/stats/session/session?id=${this.data.sessionId}` }) }, // 查看所有战绩 goToRecords() { wx.navigateTo({ url: `/pages/game/records/records?id=${this.data.sessionId}` }) }, // 再来一局 createNewSession() { wx.showModal({ title: '再来一局', content: '是否使用相同设置创建新牌局?', success: (res) => { if (res.confirm) { wx.navigateTo({ url: `/pages/game/create/create?copy=${this.data.sessionId}` }) } } }) }, // 下拉刷新 onPullDownRefresh() { this.loadSessionDetail().then(() => { wx.stopPullDownRefresh() }) }, // 点击玩家卡片 onPlayerTap(e) { const player = e.currentTarget.dataset.player // 打开玩家弹窗时暂停自动刷新 this.stopAutoRefresh() this.setData({ selectedPlayer: player, showPlayerModal: true, scoreInput: '' }) }, // 关闭玩家弹窗 closePlayerModal() { this.setData({ showPlayerModal: false, selectedPlayer: null, scoreInput: '' }) // 关闭玩家弹窗后恢复自动刷新 this.startAutoRefresh() }, // 分数输入 onScoreInput(e) { this.setData({ scoreInput: e.detail.value }) }, // 快捷输入 quickInput(e) { const value = e.currentTarget.dataset.value this.setData({ scoreInput: value }) }, // 提交分数 async submitScore() { const score = parseFloat(this.data.scoreInput) // 验证金额格式(正数,最多2位小数) if (isNaN(score) || score <= 0) { wx.showToast({ title: '请输入有效金额', icon: 'none' }) return } // 验证最大值99999 if (score > 99999) { wx.showToast({ title: '金额不能超过99999', icon: 'none' }) return } // 验证最多2位小数 if (!/^\d+(\.\d{1,2})?$/.test(this.data.scoreInput)) { wx.showToast({ title: '金额最多保留2位小数', icon: 'none' }) return } wx.showLoading({ title: '提交中...' }) try { // 调用记分转账API(当前用户输给选中的玩家) await request.post('/records/transfer', { room_id: this.data.sessionId, to_player_id: this.data.selectedPlayer.player_id, amount: score }) wx.hideLoading() wx.showToast({ title: '记分成功', icon: 'success' }) // 播放TTS语音 const ttsText = `${this.data.userInfo.nickname}付给${this.data.selectedPlayer.nickname}${score}元` this.playTTS(ttsText) this.closePlayerModal() this.loadSessionDetail() } catch (error) { wx.hideLoading() wx.showToast({ title: error.message || '提交失败', icon: 'none' }) } }, // 显示支出弹窗 showExpenseModal() { // 打开支出弹窗时暂停自动刷新 this.stopAutoRefresh() this.setData({ showExpenseModal: true, expenseInputs: {} }) }, // 关闭支出弹窗 closeExpenseModal() { this.setData({ showExpenseModal: false, expenseInputs: {} }) // 关闭支出弹窗后恢复自动刷新 this.startAutoRefresh() }, // 支出金额输入 onExpenseInput(e) { const index = e.currentTarget.dataset.index const value = e.detail.value const expenseInputs = {...this.data.expenseInputs} expenseInputs[index] = value this.setData({ expenseInputs }) }, // 提交支出 async submitExpense() { const expenses = [] Object.keys(this.data.expenseInputs).forEach(index => { const amount = parseFloat(this.data.expenseInputs[index]) if (!isNaN(amount) && amount > 0) { // 验证最大值99999 if (amount > 99999) { wx.showToast({ title: '金额不能超过99999', icon: 'none' }) return } // 验证最多2位小数 if (!/^\d+(\.\d{1,2})?$/.test(this.data.expenseInputs[index])) { wx.showToast({ title: '金额最多保留2位小数', icon: 'none' }) return } expenses.push({ playerId: this.data.otherPlayers[index].player_id, amount: amount }) } }) if (expenses.length === 0) { wx.showToast({ title: '请至少输入一项支出', icon: 'none' }) return } wx.showLoading({ title: '提交中...' }) try { // 串行调用记分转账API(避免数据库死锁) let successCount = 0 const successExpenses = [] for (const expense of expenses) { try { await request.post('/records/transfer', { room_id: this.data.sessionId, to_player_id: expense.playerId, amount: expense.amount }) successCount++ successExpenses.push(expense) } catch (error) { console.error('记分失败:', expense, error) // 继续执行其他记分,不中断 } } wx.hideLoading() if (successCount === expenses.length) { wx.showToast({ title: `成功记录${successCount}笔支出`, icon: 'success' }) // 播放批量支出语音 if (successExpenses.length > 0) { const ttsTexts = successExpenses.map(exp => { const player = this.data.otherPlayers.find(p => p.player_id === exp.playerId) return `${this.data.userInfo.nickname}付给${player.nickname}${exp.amount}元` }) // 播放第一条(可以根据需要调整为播放所有或汇总) this.playTTS(ttsTexts.join(',')) } } else if (successCount > 0) { wx.showToast({ title: `成功${successCount}笔,失败${expenses.length - successCount}笔`, icon: 'none', duration: 3000 }) // 播放成功的语音 if (successExpenses.length > 0) { const ttsTexts = successExpenses.map(exp => { const player = this.data.otherPlayers.find(p => p.player_id === exp.playerId) return `${this.data.userInfo.nickname}付给${player.nickname}${exp.amount}元` }) this.playTTS(ttsTexts.join(',')) } } else { wx.showToast({ title: '所有记分都失败了', icon: 'none' }) } this.closeExpenseModal() this.loadSessionDetail() } catch (error) { wx.hideLoading() wx.showToast({ title: error.message || '提交失败', icon: 'none' }) } }, // 显示设置弹窗 showSettingsModal() { // 打开设置弹窗时暂停自动刷新 this.stopAutoRefresh() this.setData({ showSettingsModal: true }) }, // 关闭设置弹窗 closeSettingsModal() { this.setData({ showSettingsModal: false }) // 关闭设置弹窗后恢复自动刷新 this.startAutoRefresh() }, // 桌位费输入 onTableFeeInput(e) { this.setData({ tableFee: e.detail.value }) }, // 语音播报开关切换 onTtsToggle(e) { const ttsEnabled = e.detail.value this.setData({ ttsEnabled }) // 保存到本地存储 wx.setStorageSync('ttsEnabled', ttsEnabled) }, // TTS语音播放 async playTTS(text) { // 如果语音播报关闭,直接返回 if (!this.data.ttsEnabled) { return } try { const result = await request.post('/tts/convert', { text }) if (result.audioUrl) { // 使用音频URL直接播放 const innerAudioContext = wx.createInnerAudioContext() innerAudioContext.src = `https://ca.miniappapi.com/mp${result.audioUrl}` innerAudioContext.play() // 播放完成后销毁音频上下文 innerAudioContext.onEnded(() => { innerAudioContext.destroy() }) // 播放失败处理 innerAudioContext.onError((error) => { console.error('音频播放失败:', error) innerAudioContext.destroy() }) } } catch (error) { console.error('TTS转换失败:', error) } }, // 快捷输入值修改 onQuickInputChange(e) { const index = e.currentTarget.dataset.index const value = e.detail.value const quickInputs = [...this.data.quickInputs] quickInputs[index] = value this.setData({ quickInputs }) }, // 删除快捷输入 deleteQuickInput(e) { const index = e.currentTarget.dataset.index const quickInputs = [...this.data.quickInputs] quickInputs.splice(index, 1) this.setData({ quickInputs }) }, // 添加快捷输入 addQuickInput() { const quickInputs = [...this.data.quickInputs] quickInputs.push('') this.setData({ quickInputs }) }, // 保存设置 async saveSettings() { wx.showLoading({ title: '保存中...' }) try { await request.put(`/rooms/${this.data.sessionId}/settings`, { table_fee: parseFloat(this.data.tableFee) || 0, quick_settings: this.data.quickInputs.map(v => parseFloat(v) || 0) }) wx.hideLoading() wx.showToast({ title: '保存成功', icon: 'success' }) this.closeSettingsModal() this.loadSessionDetail(true) } catch (error) { wx.hideLoading() wx.showToast({ title: error.message || '保存失败', icon: 'none' }) } }, // 分享给朋友 onShareAppMessage() { return { title: `邀请你加入牌局:${this.data.session.room_name}`, path: `/pages/game/join/join?code=${this.data.session.invite_code}`, imageUrl: '/images/share-bg.png' } } })