552 lines
14 KiB
JavaScript
552 lines
14 KiB
JavaScript
// 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'
|
||
}
|
||
}
|
||
}) |