Files
-----/scoring/pages/index/singleplay/singleplay.vue
2025-11-24 16:01:45 +08:00

891 lines
23 KiB
Vue
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.

<template>
<view class="singleplay">
<!-- 顶部功能区 -->
<view class="top-functions">
<view class="function-btn" @click="addPlayer">
<image src="/static/add.png" class="btn-icon" mode="aspectFill"></image>
<text>添加玩家</text>
</view>
<view class="function-btn" @click="transferScorer">
<image src="/static/transfer.png" class="btn-icon" mode="aspectFill"></image>
<text>转让计分员</text>
</view>
<view class="switch-group">
<text>语音播报</text>
<switch :checked="voiceBroadcast" @change="voiceBroadcast = $event.detail.value" color="#007aff" />
</view>
<view class="switch-group">
<text>台板</text>
<switch :checked="tableMode" @change="tableMode = $event.detail.value" color="#007aff" />
</view>
</view>
<!-- 对局记录区域 -->
<view class="game-record">
<view class="record-title">
对局记录
<text class="hint">点击对局分数进行修改</text>
</view>
<!-- 用户信息区域 -->
<view class="user-section">
<text class="user-hint">点击自己头像编辑用户头像和昵称~</text>
<view class="user-info" @click="editUserInfo">
<image class="user-avatar" :src="currentUser.avatar" mode="aspectFill"></image>
<view class="user-details">
<view class="user-name">
{{ currentUser.name }}
<text class="self-tag">自己</text>
</view>
</view>
</view>
</view>
<!-- 玩家列表 -->
<view class="players-section">
<view class="section-header">
<text class="header-title">玩家</text>
<text class="player-count">({{ players.length }})</text>
</view>
<!-- 玩家表格表头在左侧 -->
<view class="players-table-container vertical">
<view class="players-table vertical">
<!-- 表头行 -->
<view class="table-row header-row">
<!-- 固定表头玩家 -->
<view class="header-cell player-column">玩家</view>
<!-- 每个玩家作为一列 -->
<view v-for="(player, index) in displayPlayers" :key="player.id" class="data-cell">
<view class="player-info">
<image :src="player.avatar" mode="aspectFill"></image>
<text>{{ player.name }}</text>
</view>
</view>
</view>
<!-- 总分行 -->
<view class="table-row">
<view class="header-cell score-column">总分</view>
<view v-for="(player, index) in displayPlayers" :key="player.id" class="data-cell score-display" >
{{ player.score }}
</view>
</view>
<!-- 每局分数行 -->
<view v-for="(round, roundIndex) in rounds" :key="round" class="table-row">
<view class="header-cell score-column">{{ round }}</view>
<view @click="editScore(index)" v-for="(player, playerIndex) in displayPlayers" :key="player.id" class="data-cell score-display">
{{ player.roundScores[roundIndex] || 0 }}
</view>
</view>
</view>
</view>
</view>
</view>
<!--
<view>
<button @click="postscore">提交分数</button>
<input v-model="score.value.playerId" type="number" placeholder="输入分数" :src="score.value.playerId">
</view> -->
<!-- 底部操作按钮 -->
<view class="bottom-buttons">
<button class="start-btn" type="primary" @click="startScoring">开局计分</button>
<button class="settle-btn" type="default" @click="settleRoom">结算房间</button>
</view>
</view>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted, toRaw } from 'vue'
import { GET,POST } from '../../../utils/request'
import { BASE_URL } from '../../../utils/CommonValues.js';
const res=ref(null)
// const score=ref(
// playerId: currentUser.id
// )
const getuserinfo =()=>{
GET('/score/info/list',null,).then(res=>{
console.log('获取用户信息成功:', res.data)
// 检查API返回数据结构
if(res.data && res.data.code === 200 && Array.isArray(res.data.rows)) {
const userList = res.data.rows;
// 清空现有的玩家列表(保留自己)
const selfPlayer = players.value.find(p => p.id === 'self');
players.value = selfPlayer ? [selfPlayer] : [];
// 将API返回的用户信息添加到玩家列表
userList.forEach(user => {
if(user.nickName) {
// 处理头像URL如果是相对路径则与BASE_URL拼接
let avatarUrl = user.avatars;
if (avatarUrl) {
// 检查是否是相对路径(以/开头)
if (avatarUrl.startsWith('/')) {
avatarUrl = `${BASE_URL}${avatarUrl}`;
} else if (!avatarUrl.startsWith('http://') && !avatarUrl.startsWith('https://')) {
// 不是绝对URL也不是以/开头的相对路径,拼接完整路径
avatarUrl = `${BASE_URL}/${avatarUrl}`;
}
} else {
// 如果没有头像则使用默认头像
avatarUrl = '/static/avatar14.png';
}
const newPlayer = {
id: `player_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
name: user.nickName, // 使用API返回的nickName作为玩家名称
avatar: avatarUrl, // 使用处理后的头像URL
score: 0,
roundScores: []
};
players.value.push(newPlayer);
}
});
console.log('玩家列表更新完成:', players.value);
uni.showToast({
title: `成功添加${userList.length}位玩家`,
icon: 'success'
});
} else {
console.error('API返回数据结构异常:', res.data);
uni.showToast({
title: '获取玩家信息失败,数据结构异常',
icon: 'none'
});
}
}).catch(err=>{
console.error('获取用户信息失败:', err);
// 使用request.js中提供的友好错误信息
const errorMsg = err.userFriendlyMsg || '获取玩家信息失败';
uni.showToast({
title: errorMsg,
icon: 'none',
duration: 2000
});
})
}
// const postscore = () => {
// POST('/a/post',score.value).then(r=>{
// console.log('提交分数成功:', r.data,)
// console.log(score.value)
// res.value=r.data
// }).catch(err=>{
// console.error('提交分数失败:', err);
// })
// }
// 状态管理
const voiceBroadcast = ref(false)
const tableMode = ref(false)
const roomId = ref('')
// 当前用户信息 - 从本地存储获取
const storedUserInfo = uni.getStorageSync('userInfo');
const currentUser = ref({
id: 'self',
name: storedUserInfo?.name||'玩家50920' ,
avatar: storedUserInfo?.avatar ||'/static/logo.png' })
// 局数信息
const rounds = ref([]) // 存储每局信息,每个元素是局数编号
// 玩家列表(初始包含自己)
const players = ref([
{
id: 'self',
name: currentUser.value.name ||'玩家50920',
avatar: currentUser.value.avatar||'/static/logo.png',
score: 0,
roundScores: [] // 存储每局的分数
}
])
// 台板玩家对象
const tablePlayer = {
id: 'table',
name: '台板',
avatar: '/static/robot.png',
score: 0,
roundScores: [] // 存储每局的分数
}
// 计算属性:动态返回包含或不包含台板玩家的列表
const displayPlayers = computed(() => {
if (tableMode.value) {
// 检查台板玩家是否已存在,避免重复添加
const hasTablePlayer = players.value.some(player => player.id === 'table')
if (hasTablePlayer) {
return players.value
}
// 添加台板玩家
return [...players.value, tablePlayer]
} else {
// 移除台板玩家
return players.value.filter(player => player.id !== 'table')
}
})
// 添加玩家
const addPlayer = () => {
console.log('添加玩家')
// 生成默认玩家名
const defaultPlayerName = '请输入玩家名称'
// 弹出对话框让用户输入玩家名称
uni.showModal({
title: '添加玩家',
editable: true,
placeholderText: defaultPlayerName,
// #ifdef MP-WEIXIN
confirmText: '添加',
cancelText: '取消',
autofocus: true, // 微信小程序下自动聚焦输入框
// #endif
success: (res) => {
if (res.confirm) {
// 获取用户输入的名称,如果为空则使用默认名称
const playerName = res.content && res.content.trim() !== '' ? res.content.trim() : defaultPlayerName
// 创建新玩家
const newPlayer = {
id: `player_${Date.now()}`,
name: playerName,
avatar: '/static/avatar14.png',
score: 0,
roundScores: [] // 存储每局的分数
}
// 添加到玩家列表
players.value.push(newPlayer)
// 语音提示或普通提示(如果开启)
if (voiceBroadcast.value) {
uni.showToast({
title: `已添加玩家 ${newPlayer.name}`,
icon: 'none'
})
// 这里可以添加语音播报逻辑
} else {
uni.showToast({
title: `已添加玩家 ${newPlayer.name}`,
icon: 'none'
})
}
}
},
fail: (err) => {
console.error('添加玩家失败:', err)
}
})
}
// 转让计分员
const transferScorer = () => {
console.log('转让计分员')
// 显示提示对话框
uni.showModal({
title: '提示',
content: '房间内暂无扫码或分享加入房间的玩家,无法转让计分员。',
showCancel: false,
// #ifdef MP-WEIXIN
confirmText: '确定',
// #endif
success: (res) => {
if (res.confirm) {
console.log('用户确认提示')
}
},
fail: (err) => {
console.error('显示提示失败:', err)
}
})
}
// 编辑用户信息 - 跳转到change页面
const editUserInfo = () => {
console.log('跳转到用户信息编辑页面')
// 跳转到change页面并传递当前用户信息
uni.navigateTo({
url: `/pages/index/singleplay/change?userData=${encodeURIComponent(JSON.stringify(currentUser.value))}`
})
}
// 更新用户数据 - 从change页面返回时调用
const updateUserData = (updatedUser) => {
console.log('更新用户数据', updatedUser)
// 更新当前用户信息
currentUser.value = { ...updatedUser }
// 更新玩家列表中的用户信息
const selfPlayerIndex = players.value.findIndex(p => p.id === 'self')
if (selfPlayerIndex !== -1) {
players.value[selfPlayerIndex] = {
...players.value[selfPlayerIndex],
name: updatedUser.name,
avatar: updatedUser.avatar
}
}
// 将更新后的用户信息保存到本地存储
uni.setStorageSync('userInfo', updatedUser)
uni.setStorageSync('currentUserInfo', updatedUser)
}
// 编辑分数 - 点击分数时调用
const editScore = (playerIndex) => {
console.log('编辑分数,玩家索引:', playerIndex)
// 获取当前选中的玩家
const selectedPlayer = displayPlayers.value[playerIndex]
// 检查是否有局数记录
if (rounds.value.length === 0) {
uni.showToast({
title: '暂无局数记录,无法编辑分数',
icon: 'none'
})
return
}
// 获取当前局数
const currentRound = rounds.value.length
// 准备要传递的玩家数据
const playersToPass = [...displayPlayers.value]
// 保存到临时存储,用于页面间数据传递
uni.setStorageSync('currentPlayers', JSON.stringify(playersToPass))
// 保存当前局数
uni.setStorageSync('currentRound', currentRound)
// 保存选中的玩家索引
uni.setStorageSync('selectedPlayerIndex', playerIndex)
// 跳转到计分页面进行分数编辑
uni.navigateTo({
url: '/pages/index/singleplay/scoring'
})
}
// 开局计分
const startScoring = () => {
console.log('开局计分')
// 检查是否有足够的玩家至少需要2个玩家才能开始计分
if (displayPlayers.value.length < 2) {
uni.showToast({
title: '玩家数不超过两个人时无法点击进行开始计分',
icon: 'none'
})
return
}
// 新增一局
const newRound = rounds.value.length + 1
rounds.value.push(newRound)
// 准备要传递的玩家数据
const playersToPass = [...displayPlayers.value]
// 保存到临时存储,用于页面间数据传递
uni.setStorageSync('currentPlayers', JSON.stringify(playersToPass))
// 保存当前局数
uni.setStorageSync('currentRound', newRound)
// 跳转到计分页面
uni.navigateTo({
url: '/pages/index/singleplay/scoring'
})
}
// 结算房间
const settleRoom = () => {
console.log('结算房间')
// 显示结算结果使用displayPlayers确保包含台板玩家
let result = '结算结果:\n'
displayPlayers.value.forEach(player => {
result += `${player.name}: ${player.score}\n`
})
uni.showModal({
title: '结算',
content: result,
confirmText: '保存记录',
cancelText: '关闭',
success: (res) => {
if (res.confirm) {
// 这里可以保存记录到数据库
uni.showToast({
title: '记录已保存',
icon: 'success'
})
}
}
})
}
// 生命周期
onMounted(() => {
console.log('单人模式页面加载完成')
// 可以在这里初始化数据或加载用户信息
getuserinfo();
// 生成随机房间号4位数字
roomId.value = Math.floor(1000 + Math.random() * 9000).toString()
// 动态设置导航栏标题
uni.setNavigationBarTitle({
title: `单人 - ${roomId.value}号房间`
})
// 监听页面返回事件,用于接收从计分页面传回的更新后的分数
const updateListener = () => {
// 尝试获取更新后的玩家数据
const updatedPlayers = uni.getStorageSync('updatedPlayers')
if (updatedPlayers) {
try {
const parsedPlayers = JSON.parse(updatedPlayers)
console.log('收到更新的玩家数据:', parsedPlayers)
// 获取当前局数
const currentRound = uni.getStorageSync('currentRound') || 1
const roundIndex = currentRound - 1
// 更新玩家列表中的分数
parsedPlayers.forEach(updatedPlayer => {
// 查找对应的玩家
const playerIndex = players.value.findIndex(p => p.id === updatedPlayer.id)
if (playerIndex !== -1) {
// 更新普通玩家当前局的分数
// 确保roundScores数组有足够的空间
if (players.value[playerIndex].roundScores.length <= roundIndex) {
players.value[playerIndex].roundScores.length = roundIndex + 1
}
// 保存当前局的分数
players.value[playerIndex].roundScores[roundIndex] = updatedPlayer.score
// 计算总分:所有局分数的总和
players.value[playerIndex].score = players.value[playerIndex].roundScores.reduce((sum, score) => sum + (score || 0), 0)
} else if (updatedPlayer.id === 'table') {
// 更新台板玩家当前局的分数
// 确保roundScores数组有足够的空间
if (tablePlayer.roundScores.length <= roundIndex) {
tablePlayer.roundScores.length = roundIndex + 1
}
// 保存当前局的分数
tablePlayer.roundScores[roundIndex] = updatedPlayer.score
// 计算总分:所有局分数的总和
tablePlayer.score = tablePlayer.roundScores.reduce((sum, score) => sum + (score || 0), 0)
}
})
// 清除临时存储的数据
uni.removeStorageSync('updatedPlayers')
} catch (error) {
console.error('解析更新的玩家数据失败:', error)
}
}
}
// 监听页面显示事件
uni.$on('updatePlayers', updateListener)
// 监听用户数据更新事件来自change页面
const userDataListener = (updatedUser) => {
updateUserData(updatedUser)
}
uni.$on('userDataUpdated', userDataListener)
// 监听页面显示生命周期
uni.onShow(() => {
updateListener()
// 从本地存储获取最新的用户信息
try {
const storedUserInfo = uni.getStorageSync('userInfo');
if (storedUserInfo) {
console.log('从本地存储加载用户信息:', storedUserInfo);
// 更新当前用户信息
currentUser.value = { ...storedUserInfo };
// 更新玩家列表中的用户信息
const selfPlayerIndex = players.value.findIndex(p => p.id === 'self');
if (selfPlayerIndex !== -1) {
players.value[selfPlayerIndex] = {
...players.value[selfPlayerIndex],
name: storedUserInfo.name,
avatar: storedUserInfo.avatar
};
}
}
} catch (error) {
console.error('从本地存储获取用户信息失败:', error);
}
})
})
onUnmounted(() => {
// 移除事件监听器
uni.$off('updatePlayers');
uni.$off('userDataUpdated');
})
</script>
<style lang="less" scoped>
.singleplay {
width: 750rpx;
margin: 0;
padding: 20rpx;
box-sizing: border-box;
background-color: #f8f8f8;
min-height: 100vh;
display: flex;
flex-direction: column;
/* #ifdef MP-WEIXIN */
user-select: none;
/* #endif */
}
/* 顶部功能区 */
.top-functions {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
width: 100%;
//padding: 10rpx 15rpx;
margin-bottom: 20rpx;
box-sizing: border-box;
overflow-x: auto;
.function-btn {
display: flex;
flex-direction: row;
align-items: center;
gap: 5rpx;
padding: 8rpx 8rpx;
background-color: #fff;
border-radius: 10rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
margin: 0 8rpx;
transition: all 0.3s ease;
white-space: nowrap;
flex-shrink: 0;
// 图标样式
.btn-icon {
width: 30rpx;
height: 30rpx;
}
// 文本样式
text {
font-size: 22rpx;
color: #333;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
// 微信小程序特有点击反馈
/* #ifdef MP-WEIXIN */
&:active {
opacity: 0.8;
transform: scale(0.95);
}
/* #endif */
}
.switch-group {
display: flex;
flex-direction: row;
align-items: center;
gap: 6rpx;
margin: 0 8rpx;
white-space: nowrap;
flex-shrink: 0;
text {
font-size: 22rpx;
color: #333;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
switch {
transform: scale(0.75);
}
}
}
/* 对局记录区域 */
.game-record {
background-color: #fff;
border-radius: 20rpx;
padding: 20rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
margin-bottom: 30rpx;
flex: 1;
overflow-y: auto;
padding-bottom: 20rpx;
}
.record-title {
font-size: 28rpx;
font-weight: bold;
color: #333;
margin-bottom: 10rpx;
.hint {
font-size: 24rpx;
font-weight: normal;
color: #666;
margin-left: 10rpx;
}
}
/* 用户信息区域 */
.user-section {
margin-bottom: 20rpx;
.user-hint {
font-size: 24rpx;
color: #fa5d5d;
margin-bottom: 15rpx;
display: block;
}
.user-info {
display: flex;
flex-direction: row;
align-items: center;
gap: 15rpx;
padding: 10rpx;
border-radius: 10rpx;
background-color: #f0f8ff;
.user-avatar {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
}
.user-details {
.user-name {
font-size: 28rpx;
color: #333;
.self-tag {
background-color: #007aff;
color: #fff;
font-size: 20rpx;
padding: 2rpx 10rpx;
border-radius: 10rpx;
margin-left: 8rpx;
}
}
}
}
}
/* 玩家列表区域 */
.players-section {
.section-header {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
margin-bottom: 15rpx;
.header-title {
font-size: 28rpx;
font-weight: bold;
color: #333;
align-items: center;
justify-content: center;
}
.player-count {
font-size: 24rpx;
color: #666;
margin-left: 10rpx;
}
}
}
/* 玩家表格容器 - 实现横向滚动 */
.players-table-container {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
border-radius: 10rpx;
border: 1rpx solid #e0e0e0;
}
/* 垂直表格容器 - 实现垂直滚动和横向滑动 */
.players-table-container.vertical {
overflow-y: auto;
overflow-x: auto;
max-height: 500rpx;
-webkit-overflow-scrolling: touch; /* 优化移动端滚动体验 */
}
/* 玩家表格 */
.players-table {
min-width: 100%;
overflow: visible;
}
/* 垂直表格样式 */
.players-table.vertical {
.table-row {
display: flex;
flex-direction: row;
border-bottom: 1rpx solid #e0e0e0;
&:last-child {
border-bottom: none;
}
}
.header-row {
background-color: #f5f5f5;
}
.header-cell {
width: 150rpx;
flex-shrink: 0;
padding: 15rpx;
font-size: 26rpx;
font-weight: bold;
color: #333;
border-right: 1rpx solid #e0e0e0;
background-color: #f5f5f5;
display: flex;
align-items: center;
justify-content: center;
}
.player-column {
justify-content: flex-start;
align-items: center;
justify-content: center;
}
.score-column {
text-align: center;
}
.data-cell {
min-width: 120rpx;
flex-shrink: 0;
padding: 15rpx;
border-right: 1rpx solid #e0e0e0;
display: flex;
align-items: center;
justify-content: center;
/* 最后一列不需要右边框 */
&:last-child {
border-right: none;
}
}
.player-info {
display: flex;
flex-direction: column;
align-items: center;
gap: 8rpx;
width: 100%;
image {
width: 60rpx;
height: 60rpx;
border-radius: 50%;
}
text {
font-size: 24rpx;
color: #333;
text-align: center;
word-break: break-word;
}
}
.score-display {
font-size: 28rpx;
font-weight: bold;
color: #007aff;
&:active {
background-color: #f0f8ff;
}
}
}
/* 底部操作按钮 */
.bottom-buttons {
display: flex;
flex-direction: row;
gap: 20rpx;
position: sticky;
bottom: 0;
left: 0;
right: 0;
padding: 20rpx;
background-color: #fff;
box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.1);
/* #ifdef MP-WEIXIN */
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
/* #endif */
.start-btn,
.settle-btn {
flex: 1;
height: 90rpx;
font-size: 32rpx;
line-height: 90rpx;
margin: 0;
}
.start-btn {
background-color: #007aff;
}
.settle-btn {
border: 1rpx solid #007aff;
color: #007aff;
}
}
/* 适配不同屏幕尺寸 */
@media screen and (max-width: 375px) {
.singleplay {
padding: 15rpx;
}
.top-functions {
gap: 8rpx;
.function-btn {
padding: 1rpx 1rpx;
text {
font-size: 22rpx;
}
}
}
}
</style>