dapaijizhang3/pages/game/detail/detail.js

1038 lines
26 KiB
JavaScript
Raw Permalink Normal View History

2025-11-20 16:42:59 +08:00
// 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'
}
}
})