// pages/stats/session/session.js import request from '../../../utils/request' Page({ data: { sessionId: null, session: null, finalRanking: [], playerStats: [], recentRecords: [], totalRounds: 0, totalChips: 0, duration: '', // 图表数据 chartLegend: [], chartWidth: 750, scoreData: [], // 分析数据 mostIntenseRound: 0, mostIntenseScore: 0, avgRoundScore: 0, longestStreak: { player: '', count: 0 }, comebackKing: { player: '', points: 0 }, // 分享海报 showSharePoster: false, gameTypeText: { 'mahjong': '麻将', 'poker': '扑克', 'other': '其他' } }, onLoad(options) { if (!options.id) { wx.showToast({ title: '参数错误', icon: 'none' }) setTimeout(() => { wx.navigateBack() }, 1500) return } this.setData({ sessionId: options.id }) this.loadStatistics() }, onReady() { // 延迟绘制图表,确保canvas已准备好 setTimeout(() => { this.drawScoreChart() }, 500) }, // 加载统计数据 async loadStatistics() { wx.showLoading({ title: '加载中...' }) try { // 获取牌局详情 const sessionData = await request.get(`/rooms/${this.data.sessionId}`) // 获取统计数据 const statsData = await request.get(`/stats/session/${this.data.sessionId}`) // 获取所有记录 const recordsData = await request.get(`/records/session/${this.data.sessionId}`, { page: 1, pageSize: 100 }) // 计算游戏时长 const duration = this.calculateDuration(sessionData.session) // 计算最终排名 const finalRanking = this.calculateFinalRanking(sessionData.players, statsData) // 计算玩家统计 const playerStats = this.calculatePlayerStats(statsData, recordsData.list) // 准备图表数据 const chartData = this.prepareChartData(recordsData.list, sessionData.players) // 分析数据 const analysis = this.analyzeData(recordsData.list, sessionData.players) // 格式化最近记录 const recentRecords = this.formatRecords(recordsData.list.slice(0, 10)) this.setData({ session: sessionData.session, finalRanking, playerStats, recentRecords, totalRounds: recordsData.total || 0, totalChips: Math.abs(statsData.total_chips || 0), duration, chartLegend: chartData.legend, scoreData: chartData.data, chartWidth: Math.max(750, chartData.data.length * 50), ...analysis }) wx.hideLoading() } catch (error) { wx.hideLoading() wx.showToast({ title: error.message || '加载失败', icon: 'none' }) } }, // 计算游戏时长 calculateDuration(session) { const start = session.created_at * 1000 const end = session.ended_at ? session.ended_at * 1000 : Date.now() const diff = end - start const hours = Math.floor(diff / 3600000) const minutes = Math.floor((diff % 3600000) / 60000) if (hours > 0) { return `${hours}小时${minutes}分钟` } else { return `${minutes}分钟` } }, // 计算最终排名 calculateFinalRanking(players, stats) { return players.map(p => { const playerStats = stats.players?.[p.player_id] || {} const winRounds = playerStats.win_rounds || 0 const totalRounds = playerStats.total_rounds || 1 return { player_id: p.player_id, nickname: p.nickname, avatar_url: p.avatar_url, final_chips: p.final_chips || 0, win_rate: totalRounds > 0 ? Math.round((winRounds / totalRounds) * 100) : 0, rounds_played: totalRounds } }).sort((a, b) => b.final_chips - a.final_chips) }, // 计算玩家统计 calculatePlayerStats(stats, records) { const playerStats = [] for (const playerId in stats.players) { const player = stats.players[playerId] // 计算每个玩家的详细统计 const playerRecords = [] records.forEach(record => { const score = record.playerScores?.find(s => s.player_id == playerId) if (score) { playerRecords.push(score.chips_change) } }) const maxWin = Math.max(...playerRecords.filter(s => s > 0), 0) const maxLose = Math.min(...playerRecords.filter(s => s < 0), 0) const avgScore = playerRecords.length > 0 ? Math.round(playerRecords.reduce((a, b) => a + b, 0) / playerRecords.length) : 0 // 计算连胜 let currentStreak = 0 let maxStreak = 0 playerRecords.forEach(score => { if (score > 0) { currentStreak++ maxStreak = Math.max(maxStreak, currentStreak) } else { currentStreak = 0 } }) playerStats.push({ player_id: playerId, nickname: player.nickname, avatar_url: player.avatar_url, max_win: maxWin, max_lose: maxLose, avg_score: avgScore, win_streak: maxStreak }) } return playerStats }, // 准备图表数据 prepareChartData(records, players) { const colors = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7', '#DDA0DD'] const legend = players.map((p, i) => ({ player_id: p.player_id, nickname: p.nickname, color: colors[i % colors.length] })) // 构建积分走势数据 const data = [] const runningScores = {} // 初始化积分 players.forEach(p => { runningScores[p.player_id] = 0 }) // 添加起始点 data.push({ round: 0, scores: { ...runningScores } }) // 计算每轮后的积分 records.forEach(record => { record.playerScores?.forEach(score => { runningScores[score.player_id] = (runningScores[score.player_id] || 0) + score.chips_change }) data.push({ round: record.round_number, scores: { ...runningScores } }) }) return { legend, data } }, // 分析数据 analyzeData(records, players) { // 最激烈对局 let mostIntenseRound = 0 let mostIntenseScore = 0 records.forEach(record => { const totalChange = record.playerScores?.reduce((sum, s) => sum + Math.abs(s.chips_change), 0) || 0 if (totalChange > mostIntenseScore) { mostIntenseScore = totalChange mostIntenseRound = record.round_number } }) // 平均每局分数 const totalScore = records.reduce((sum, record) => { const roundTotal = record.playerScores?.reduce((s, score) => s + Math.abs(score.chips_change), 0) || 0 return sum + roundTotal }, 0) const avgRoundScore = records.length > 0 ? Math.round(totalScore / records.length / 2) : 0 // 最长连胜 const streaks = {} const currentStreaks = {} players.forEach(p => { streaks[p.player_id] = 0 currentStreaks[p.player_id] = 0 }) records.forEach(record => { record.playerScores?.forEach(score => { if (score.chips_change > 0) { currentStreaks[score.player_id] = (currentStreaks[score.player_id] || 0) + 1 streaks[score.player_id] = Math.max( streaks[score.player_id] || 0, currentStreaks[score.player_id] ) } else { currentStreaks[score.player_id] = 0 } }) }) let longestStreak = { player: '', count: 0 } for (const playerId in streaks) { if (streaks[playerId] > longestStreak.count) { const player = players.find(p => p.player_id == playerId) longestStreak = { player: player?.nickname || '', count: streaks[playerId] } } } // 逆袭王(从最低点到最高点的差值最大的玩家) const comebacks = {} const runningScores = {} const minScores = {} players.forEach(p => { runningScores[p.player_id] = 0 minScores[p.player_id] = 0 comebacks[p.player_id] = 0 }) records.forEach(record => { record.playerScores?.forEach(score => { runningScores[score.player_id] = (runningScores[score.player_id] || 0) + score.chips_change minScores[score.player_id] = Math.min( minScores[score.player_id] || 0, runningScores[score.player_id] ) }) }) let comebackKing = { player: '', points: 0 } for (const playerId in runningScores) { const comeback = runningScores[playerId] - minScores[playerId] if (comeback > comebackKing.points) { const player = players.find(p => p.player_id == playerId) comebackKing = { player: player?.nickname || '', points: comeback } } } return { mostIntenseRound, mostIntenseScore: mostIntenseScore / 2, avgRoundScore, longestStreak, comebackKing } }, // 格式化记录 formatRecords(records) { return 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')}` } }) }, // 绘制积分走势图 drawScoreChart() { const ctx = wx.createCanvasContext('scoreChart') const { scoreData, chartLegend } = this.data if (scoreData.length === 0) return const width = this.data.chartWidth const height = 300 const padding = 40 const chartWidth = width - padding * 2 const chartHeight = height - padding * 2 // 找出最大最小值 let minScore = 0 let maxScore = 0 scoreData.forEach(data => { for (const playerId in data.scores) { minScore = Math.min(minScore, data.scores[playerId]) maxScore = Math.max(maxScore, data.scores[playerId]) } }) const scoreRange = maxScore - minScore || 100 const xStep = chartWidth / (scoreData.length - 1 || 1) const yScale = chartHeight / scoreRange // 绘制网格 ctx.setStrokeStyle('#e0e0e0') ctx.setLineWidth(0.5) // 横线 for (let i = 0; i <= 5; i++) { const y = padding + (chartHeight / 5) * i ctx.beginPath() ctx.moveTo(padding, y) ctx.lineTo(width - padding, y) ctx.stroke() } // 绘制每个玩家的线 chartLegend.forEach(legend => { ctx.setStrokeStyle(legend.color) ctx.setLineWidth(2) ctx.beginPath() scoreData.forEach((data, index) => { const x = padding + xStep * index const y = padding + chartHeight - (data.scores[legend.player_id] - minScore) * yScale if (index === 0) { ctx.moveTo(x, y) } else { ctx.lineTo(x, y) } }) ctx.stroke() // 绘制点 ctx.setFillStyle(legend.color) scoreData.forEach((data, index) => { const x = padding + xStep * index const y = padding + chartHeight - (data.scores[legend.player_id] - minScore) * yScale ctx.beginPath() ctx.arc(x, y, 3, 0, 2 * Math.PI) ctx.fill() }) }) // 绘制坐标轴标签 ctx.setFillStyle('#666') ctx.setFontSize(10) // X轴标签 scoreData.forEach((data, index) => { if (index % Math.ceil(scoreData.length / 10) === 0) { const x = padding + xStep * index ctx.fillText(`第${data.round}局`, x - 15, height - 10) } }) ctx.draw() }, // 查看所有记录 viewAllRecords() { wx.navigateTo({ url: `/pages/game/records/records?id=${this.data.sessionId}` }) }, // 导出数据 async exportData() { wx.showToast({ title: '功能开发中', icon: 'none' }) }, // 分享战绩 shareStats() { this.setData({ showSharePoster: true }) this.drawSharePoster() }, // 绘制分享海报 drawSharePoster() { const ctx = wx.createCanvasContext('sharePoster') const { session, finalRanking, totalRounds } = this.data // 背景 ctx.setFillStyle('#fff') ctx.fillRect(0, 0, 375, 600) // 标题 ctx.setFillStyle('#333') ctx.setFontSize(24) ctx.setTextAlign('center') ctx.fillText(session.session_name, 187, 50) // 副标题 ctx.setFontSize(14) ctx.setFillStyle('#999') ctx.fillText(`${this.data.gameTypeText[session.game_type]} · 共${totalRounds}局`, 187, 80) // 排名 ctx.setTextAlign('left') finalRanking.slice(0, 3).forEach((player, index) => { const y = 150 + index * 80 // 排名标记 const medals = ['🥇', '🥈', '🥉'] ctx.setFontSize(30) ctx.fillText(medals[index], 30, y) // 玩家名称 ctx.setFontSize(18) ctx.setFillStyle('#333') ctx.fillText(player.nickname, 80, y) // 积分 ctx.setTextAlign('right') ctx.setFillStyle(player.final_chips > 0 ? '#4CAF50' : '#F44336') ctx.fillText(`${player.final_chips > 0 ? '+' : ''}${player.final_chips}`, 330, y) ctx.setTextAlign('left') }) // 底部信息 ctx.setFillStyle('#999') ctx.setFontSize(12) ctx.setTextAlign('center') ctx.fillText('打牌记账小程序', 187, 550) ctx.draw() }, // 保存海报 savePoster() { wx.canvasToTempFilePath({ canvasId: 'sharePoster', success: (res) => { wx.saveImageToPhotosAlbum({ filePath: res.tempFilePath, success: () => { wx.showToast({ title: '已保存到相册', icon: 'success' }) }, fail: () => { wx.showToast({ title: '保存失败', icon: 'none' }) } }) } }) }, // 关闭分享海报 closeSharePoster() { this.setData({ showSharePoster: false }) }, // 分享给朋友 onShareAppMessage() { const { session, finalRanking } = this.data const winner = finalRanking[0] return { title: `${winner.nickname}获得${session.session_name}冠军!`, path: `/pages/stats/session/session?id=${this.data.sessionId}`, imageUrl: '/images/share-bg.png' } } })