dapaijizhang3/pages/game/detail/detail.js

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