dapaijizhang3/pages/stats/session/session.js

552 lines
14 KiB
JavaScript
Raw Permalink Normal View History

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