This commit is contained in:
2025-11-11 17:07:13 +08:00
commit be86799071
2224 changed files with 271177 additions and 0 deletions

View File

@@ -0,0 +1,255 @@
<template>
<view class="container">
<!-- 自定义头部 -->
<view class="header">
<view class="back-btn" @click="goBack">
<uni-icons type="left" size="22" color="#fff" />
</view>
<view class="title">用户信息</view>
<view class="placeholder"></view>
</view>
<!-- 内容区域 -->
<view class="content">
<!-- 昵称输入 -->
<view class="form-item">
<view class="label">昵称:</view>
<input class="input" v-model="userInfo.nickName" placeholder="请输入昵称" />
</view>
<!-- 头像选择 -->
<view class="form-item">
<view class="label">头像:</view>
<view class="avatar-wrapper" @click="chooseNewAvatar">
<image :src="userInfo.avatarUrl || '/static/logo.png'" mode="aspectFill" class="avatar" />
<view class="avatar-tip">点击更新头像</view>
</view>
</view>
<!-- 保存按钮 -->
<button class="save-btn" @click="saveUserInfo">保存</button>
</view>
</view>
</template>
<script setup>
import { ref, getCurrentInstance } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { updateGlobalUserInfo } from '../../../main.js'
// 用户信息
const userInfo = ref({
id: '',
nickName: '',
avatarUrl: ''
})
// 初始化
onLoad((options) => {
// 从选项中获取用户数据
if (options.userData) {
try {
const data = JSON.parse(decodeURIComponent(options.userData))
userInfo.value = { ...data }
} catch (e) {
console.error('解析用户数据失败:', e)
}
}
// 记录来源页面信息,支持返回到正确页面
if (options.from) {
console.log('来源页面:', options.from)
}
})
// 返回上一页
const goBack = () => {
// 检查是否有未保存的修改
if (userInfo.value.nickName.trim()) {
// 使用全局方法保存用户信息,确保全局可用
updateGlobalUserInfo(userInfo.value)
}
wx.navigateBack()
}
// 选择新头像
const chooseNewAvatar = () => {
wx.chooseMedia({
count: 1,
mediaType: ['image'],
sourceType: ['album', 'camera'],
maxDuration: 30,
camera: 'back',
success: (res) => {
// 统一处理头像选择逻辑,支持所有平台
try {
const tempFilePath = res.tempFiles[0].tempFilePath
userInfo.value.avatarUrl = tempFilePath
} catch (error) {
console.error('处理头像文件路径失败:', error)
wx.showToast({
title: '头像设置失败',
icon: 'none'
})
}
},
fail: (err) => {
console.error('选择图片失败:', err)
wx.showToast({
title: '选择图片失败',
icon: 'none'
})
}
})
}
// 保存用户信息
const saveUserInfo = () => {
if (!userInfo.value.nickName.trim()) {
wx.showToast({
title: '昵称不能为空',
icon: 'none'
})
return
}
// 使用全局方法保存用户信息,确保全局可用
updateGlobalUserInfo(userInfo.value)
// 保存成功,返回上一页并传递更新的数据
const pages = getCurrentPages()
const prevPage = pages[pages.length - 2]
if (prevPage) {
// 通过事件通知上一页更新数据
if (prevPage.$vm && prevPage.$vm.updateUserData) {
prevPage.$vm.updateUserData(userInfo.value)
} else {
// 如果上一页没有updateUserData方法通过全局事件通知
wx.$emit && wx.$emit('userDataUpdated', userInfo.value)
}
}
// 显示保存成功提示
wx.showToast({
title: '保存成功',
icon: 'success',
duration: 1500,
success: () => {
setTimeout(() => {
wx.navigateBack()
}, 1500)
}
})
}
</script>
<style scoped>
.container {
min-height: 100vh;
background-color: #f5f5f5;
}
/* 自定义头部 */
.header {
display: flex;
align-items: center;
justify-content: space-between;
height: 44px;
background-color: #1989fa;
padding: 0 16px;
box-sizing: border-box;
/* 适配刘海屏 */
padding-top: env(safe-area-inset-top, 0);
height: calc(44px + env(safe-area-inset-top, 0));
}
.back-btn {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
}
.title {
color: #fff;
font-size: 17px;
font-weight: 500;
}
.placeholder {
width: 40px;
}
/* 内容区域 */
.content {
padding: 20rpx;
box-sizing: border-box;
}
.form-item {
display: flex;
align-items: center;
padding: 20rpx 0;
background-color: #fff;
margin-bottom: 10rpx;
padding: 30rpx;
border-radius: 12rpx;
}
.label {
width: 120rpx;
font-size: 28rpx;
color: #333;
}
.input {
flex: 1;
height: 60rpx;
font-size: 28rpx;
color: #333;
padding: 0 20rpx;
box-sizing: border-box;
border: 1px solid #e0e0e0;
border-radius: 8rpx;
}
/* 头像样式 */
.avatar-wrapper {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
}
.avatar {
width: 200rpx;
height: 200rpx;
border-radius: 50%;
margin-bottom: 10rpx;
}
.avatar-tip {
font-size: 24rpx;
color: #999;
}
/* 保存按钮 */
.save-btn {
width: 100%;
height: 88rpx;
line-height: 88rpx;
background-color: #1989fa;
color: #fff;
font-size: 32rpx;
border-radius: 44rpx;
margin-top: 40rpx;
/* 去除默认边框和背景 */
border: none;
}
.save-btn:active {
background-color: #0d75d4;
}
</style>

View File

@@ -0,0 +1,805 @@
<template>
<view class="scoring-page">
<!-- 顶部导航栏 -->
<view class="header">
<view class="back-btn" @click="goBack">
<uni-icons type="arrow-left" size="24" color="#fff"></uni-icons>
</view>
<view class="title">{{ roundCount }}</view>
<view class="header-right">
<view class="more-btn" @click="showMoreOptions">...</view>
<view class="separator"></view>
<view class="sound-toggle" @click="toggleSound">
<text class="sound-icon">{{ soundEnabled ? '🔊' : '🔇' }}</text>
</view>
</view>
</view>
<!-- 玩家列表区域 -->
<view class="players-list">
<!-- 表头 -->
<view class="list-header">
<view class="col-player">玩家</view>
<view class="col-result">胜负</view>
<view class="col-score">得分</view>
</view>
<!-- 玩家行 -->
<view
v-for="(player, index) in players"
:key="player.id"
class="player-row"
:class="{ 'selected': selectedPlayerIndex === index }"
@click="selectPlayer(index)"
>
<view class="col-player">
<image :src="player.avatar" mode="aspectFill" class="player-avatar"></image>
<text class="player-name">{{ player.name }}</text>
</view>
<view class="col-result">
<view class="result-buttons">
<button
class="result-btn win"
:class="{ 'active': player.result === 'win' }"
@click.stop="setPlayerResult(index, 'win')"
></button>
<button
class="result-btn lose"
:class="{ 'active': player.result === 'lose' }"
@click.stop="setPlayerResult(index, 'lose')"
></button>
</view>
</view>
<view class="col-score">
<view class="score-input-container">
<input
type="number"
class="score-input"
:value="selectedPlayerIndex === index && showKeyboard ? currentInput : player.score"
@focus.prevent
@click.stop="selectPlayer(index)"
readonly
disabled
/>
<!-- 合分按钮 -->
<button
class="combine-btn"
@click.stop="combineScore(index)"
:disabled="!selectedPlayerIndex === index"
>合分</button>
</view>
</view>
</view>
</view>
<!-- 底部数字键盘 -->
<view class="number-keyboard" v-if="showKeyboard">
<view class="keyboard-row">
<view class="key" @click="inputNumber(1)">1</view>
<view class="key" @click="inputNumber(2)">2</view>
<view class="key" @click="inputNumber(3)">3</view>
<view class="key operation" @click="inputOperation('+')">+</view>
<view class="key delete" @click="deleteLastChar">
<text></text>
</view>
</view>
<view class="keyboard-row">
<view class="key" @click="inputNumber(4)">4</view>
<view class="key" @click="inputNumber(5)">5</view>
<view class="key" @click="inputNumber(6)">6</view>
<view class="key operation" @click="inputOperation('-')">-</view>
<view class="key empty"></view>
</view>
<view class="keyboard-row">
<view class="key" @click="inputNumber(7)">7</view>
<view class="key" @click="inputNumber(8)">8</view>
<view class="key" @click="inputNumber(9)">9</view>
<view class="key operation" @click="clearInput">C</view>
<view class="key confirm" @click="confirmInput">提交</view>
</view>
<view class="keyboard-row">
<view class="key zero" @click="inputNumber(0)">0</view>
</view>
</view>
<!-- 底部操作栏 -->
<view class="bottom-actions" v-if="!showKeyboard">
<button class="action-btn" type="default" @click="nextRound">下一局</button>
<button class="action-btn primary" type="primary" @click="endGame">结束对局</button>
</view>
</view>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
// 从上个页面接收的玩家数据
const players = ref([])
const roundCount = ref(1)
const history = ref([])
const selectedPlayerIndex = ref(-1)
const showKeyboard = ref(false)
const currentInput = ref('')
const soundEnabled = ref(true)
// 初始化玩家数据
const loadPlayersData = () => {
try {
const playersData = uni.getStorageSync('currentPlayers')
if (playersData) {
// 解析并添加result字段
const parsedPlayers = JSON.parse(playersData).map(player => ({
...player,
result: '', // 初始胜负状态为空
score: player.score || 0
}))
players.value = parsedPlayers
saveHistoryState()
}
} catch (error) {
console.error('加载玩家数据失败:', error)
uni.showToast({
title: '加载数据失败',
icon: 'none'
})
}
}
// 保存历史状态
const saveHistoryState = () => {
const stateCopy = JSON.parse(JSON.stringify(players.value))
history.value.push(stateCopy)
if (history.value.length > 50) {
history.value.shift()
}
}
// 返回上一页
const goBack = () => {
uni.showModal({
title: '提示',
content: '当前分数未保存,确定要返回吗?',
success: (res) => {
if (res.confirm) {
uni.navigateBack()
}
}
})
}
// 选择玩家
const selectPlayer = (index) => {
selectedPlayerIndex.value = index
showKeyboard.value = true
// 显示当前分数
currentInput.value = players.value[index].score.toString()
}
// 设置玩家胜负
const setPlayerResult = (index, result) => {
// 如果已经是当前状态,则取消选择
if (players.value[index].result === result) {
players.value[index].result = ''
// 取消状态时,保持当前分数符号不变
} else {
players.value[index].result = result
// 根据胜负状态调整分数正负号,并考虑当前输入框的内容
let currentScore = players.value[index].score
let currentInputValue = currentInput.value
if (result === 'win') {
// 胜利时确保分数为正数
if (currentScore !== 0) {
// 根据输入框当前内容或分数值判断是否需要调整符号
if (currentInputValue && currentInputValue !== '-' && parseInt(currentInputValue) !== 0) {
// 使用输入框的值并确保为正数
currentScore = Math.abs(parseInt(currentInputValue))
} else {
// 使用现有分数值并确保为正数
currentScore = Math.abs(currentScore)
}
}
} else if (result === 'lose') {
// 失败时确保分数为负数
if (currentScore !== 0) {
// 根据输入框当前内容或分数值判断是否需要调整符号
if (currentInputValue && currentInputValue !== '-' && parseInt(currentInputValue) !== 0) {
// 使用输入框的值并确保为负数
currentScore = -Math.abs(parseInt(currentInputValue))
} else {
// 使用现有分数值并确保为负数
currentScore = -Math.abs(currentScore)
}
}
}
// 更新分数
players.value[index].score = currentScore
}
// 如果当前正在编辑该玩家同步更新currentInput
if (selectedPlayerIndex.value === index && showKeyboard.value) {
currentInput.value = players.value[index].score.toString()
}
saveHistoryState()
}
// 数字键盘输入
const inputNumber = (num) => {
// 处理特殊情况:如果当前是负号,直接添加数字
if (currentInput.value === '-') {
currentInput.value += num.toString()
}
// 避免以0开头的数字除了单独的0和负数中的0
else if (currentInput.value === '0' && num !== 0) {
currentInput.value = num.toString()
}
// 避免输入过多的数字
else if (currentInput.value.length < 10) {
currentInput.value += num.toString()
}
// 实时保存输入内容到玩家分数(不等待提交)
if (selectedPlayerIndex.value !== -1) {
let score = 0
if (currentInput.value && currentInput.value !== '-') {
score = parseInt(currentInput.value)
if (isNaN(score)) {
score = 0
}
}
players.value[selectedPlayerIndex.value].score = score
saveHistoryState()
}
}
// 操作符输入
const inputOperation = (op) => {
if (op === '-') {
// 支持负数输入
if (currentInput.value === '') {
// 当输入框为空时,按减号开始负数输入
currentInput.value = '-'
} else if (currentInput.value.startsWith('-')) {
// 如果已经是负数,移除负号变为正数
currentInput.value = currentInput.value.substring(1)
} else {
// 如果已有数字,添加负号变为负数
currentInput.value = '-' + currentInput.value
}
} else if (op === '+') {
// 按加号确保是正数(移除负号)
if (currentInput.value.startsWith('-')) {
currentInput.value = currentInput.value.substring(1)
}
}
// 实时保存操作符结果到玩家分数
if (selectedPlayerIndex.value !== -1) {
let score = 0
if (currentInput.value && currentInput.value !== '-') {
score = parseInt(currentInput.value)
if (isNaN(score)) {
score = 0
}
}
players.value[selectedPlayerIndex.value].score = score
saveHistoryState()
}
}
// 删除最后一个字符
const deleteLastChar = () => {
if (currentInput.value.length > 0) {
currentInput.value = currentInput.value.slice(0, -1)
// 实时保存删除后的内容到玩家分数
if (selectedPlayerIndex.value !== -1) {
let score = 0
if (currentInput.value && currentInput.value !== '-') {
score = parseInt(currentInput.value)
if (isNaN(score)) {
score = 0
}
}
players.value[selectedPlayerIndex.value].score = score
saveHistoryState()
}
}
}
// 清空输入
const clearInput = () => {
currentInput.value = ''
// 实时保存清空操作到玩家分数
if (selectedPlayerIndex.value !== -1) {
players.value[selectedPlayerIndex.value].score = 0
saveHistoryState()
}
}
// 确认输入
const confirmInput = () => {
if (selectedPlayerIndex.value !== -1) {
// 播放音效
if (soundEnabled.value) {
playSound()
}
// 隐藏键盘
showKeyboard.value = false
}
}
// 合分功能
const combineScore = (index) => {
// 计算其他玩家分数的总和
const otherPlayersTotal = players.value.reduce((sum, player, i) => {
if (i !== index) {
return sum + player.score
}
return sum
}, 0)
// 设置被点击玩家的分数为其他玩家分数总和的相反数,确保所有分数相加为零
players.value[index].score = -otherPlayersTotal
// 如果当前正在编辑该玩家同步更新currentInput
if (selectedPlayerIndex.value === index && showKeyboard.value) {
currentInput.value = players.value[index].score.toString()
}
saveHistoryState()
uni.showToast({
title: '合分成功',
icon: 'success'
})
}
// 下一局
const nextRound = () => {
uni.showModal({
title: '确认',
content: '确定要开始下一局吗?',
success: (res) => {
if (res.confirm) {
// 保存当前状态
saveHistoryState()
// 重置分数和胜负状态
players.value.forEach(player => {
player.score = 0
player.result = ''
})
roundCount.value++
uni.showToast({
title: `${roundCount.value}`,
icon: 'none'
})
}
}
})
}
// 播放音效
const playSound = () => {
// #ifdef MP-WEIXIN
const innerAudioContext = uni.createInnerAudioContext()
innerAudioContext.src = '/static/sound/click.mp3' // 需要准备音效文件
innerAudioContext.play()
innerAudioContext.onEnded(() => {
innerAudioContext.destroy()
})
// #endif
}
// 切换音效
const toggleSound = () => {
soundEnabled.value = !soundEnabled.value
uni.setStorageSync('soundEnabled', soundEnabled.value)
}
// 显示更多选项
const showMoreOptions = () => {
uni.showActionSheet({
itemList: ['历史记录', '设置', '帮助'],
success: (res) => {
switch (res.tapIndex) {
case 0:
uni.navigateTo({ url: '/pages/history-game/index' })
break
case 1:
uni.showToast({ title: '设置功能开发中', icon: 'none' })
break
case 2:
uni.showToast({ title: '帮助文档开发中', icon: 'none' })
break
}
}
})
}
// 结束对局
const endGame = () => {
uni.showModal({
title: '确认',
content: '确定要结束当前对局吗?',
success: (res) => {
if (res.confirm) {
try {
// 保存最终分数
uni.setStorageSync('updatedPlayers', JSON.stringify(players.value))
// 触发全局事件通知其他页面
uni.$emit('updatePlayers')
uni.showToast({
title: '对局已结束',
icon: 'success',
duration: 1500,
success: () => {
setTimeout(() => {
uni.navigateBack()
}, 1500)
}
})
} catch (error) {
console.error('结束对局失败:', error)
uni.showToast({
title: '操作失败',
icon: 'none'
})
}
}
}
})
}
// 生命周期
onMounted(() => {
console.log('计分页面加载完成')
loadPlayersData()
// 加载音效设置
const savedSound = uni.getStorageSync('soundEnabled')
if (savedSound !== '') {
soundEnabled.value = savedSound
}
})
onUnmounted(() => {
// 清理工作
})
</script>
<style lang="less" scoped>
.scoring-page {
width: 750rpx;
margin: 0;
padding: 0;
box-sizing: border-box;
background-color: #f5f5f5;
min-height: 100vh;
display: flex;
flex-direction: column;
/* #ifdef MP-WEIXIN */
user-select: none;
/* #endif */
}
/* 顶部导航栏 */
.header {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
height: 90rpx;
background-color: #3a68b9;
color: white;
padding: 0 30rpx;
box-sizing: border-box;
}
.back-btn {
font-size: 36rpx;
padding: 10rpx;
}
.back-btn:active {
opacity: 0.8;
}
.title {
font-size: 36rpx;
font-weight: bold;
}
.header-right {
display: flex;
flex-direction: row;
align-items: center;
}
.more-btn,
.sound-toggle {
font-size: 36rpx;
padding: 10rpx;
}
.separator {
width: 2rpx;
height: 40rpx;
background-color: rgba(255, 255, 255, 0.3);
margin: 0 10rpx;
}
/* 玩家列表区域 */
.players-list {
flex: 1;
background-color: #fff;
margin: 20rpx;
border-radius: 10rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
overflow: hidden;
}
/* 列表表头 */
.list-header {
display: flex;
flex-direction: row;
height: 80rpx;
background-color: #f0f0f0;
border-bottom: 1rpx solid #e0e0e0;
}
.col-player,
.col-result,
.col-score {
display: flex;
align-items: center;
justify-content: center;
font-size: 28rpx;
font-weight: bold;
color: #666;
}
.col-player {
flex: 2;
justify-content: flex-start;
padding-left: 30rpx;
}
.col-result {
flex: 1;
}
.col-score {
flex: 1;
}
/* 玩家行 */
.player-row {
display: flex;
flex-direction: row;
height: 100rpx;
border-bottom: 1rpx solid #f0f0f0;
transition: background-color 0.2s;
}
.player-row:last-child {
border-bottom: none;
}
.player-row.selected {
background-color: #e6f0ff;
}
.player-row .col-player {
display: flex;
flex-direction: row;
align-items: center;
}
.player-avatar {
width: 60rpx;
height: 60rpx;
border-radius: 30rpx;
margin-right: 20rpx;
}
.player-name {
font-size: 28rpx;
color: #333;
}
/* 胜负按钮 */
.result-buttons {
display: flex;
flex-direction: row;
gap: 10rpx;
}
.result-btn {
width: 60rpx;
height: 40rpx;
line-height: 40rpx;
padding: 0;
margin: 0;
font-size: 24rpx;
border-radius: 20rpx;
border: 1rpx solid #ddd;
}
.result-btn.win.active {
background-color: #4cd964;
color: white;
border-color: #4cd964;
}
.result-btn.lose.active {
background-color: #ff3b30;
color: white;
border-color: #ff3b30;
}
/* 分数输入区域 */
.score-input-container {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
}
.score-input {
width: 100rpx;
height: 50rpx;
text-align: center;
font-size: 32rpx;
font-weight: bold;
border: 1rpx solid #ddd;
border-radius: 8rpx;
padding: 0;
margin: 0;
}
.combine-btn {
position: absolute;
bottom: -40rpx;
width: 80rpx;
height: 30rpx;
line-height: 30rpx;
font-size: 20rpx;
padding: 0;
margin: 0;
background-color: #34aadc;
color: white;
border: none;
border-radius: 15rpx;
}
/* 数字键盘 */
.number-keyboard {
width: 100%;
background-color: #f0f0f0;
padding: 20rpx;
box-sizing: border-box;
}
.keyboard-row {
display: flex;
flex-direction: row;
justify-content: space-between;
margin-bottom: 20rpx;
}
.keyboard-row:last-child {
margin-bottom: 0;
}
.key {
flex: 1;
height: 80rpx;
display: flex;
align-items: center;
justify-content: center;
background-color: white;
border-radius: 10rpx;
margin: 0 10rpx;
font-size: 36rpx;
font-weight: bold;
color: #333;
box-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.1);
}
.key:active {
background-color: #e0e0e0;
}
.key.zero {
flex: 3;
}
.key.operation {
background-color: #ff9500;
color: white;
}
.key.delete {
background-color: #ff3b30;
color: white;
position: relative;
display: flex;
align-items: center;
justify-content: center;
min-width: 80rpx; /* 确保按钮有足够宽度 */
}
.key.delete img {
width: 52rpx;
height: 52rpx;
object-fit: contain;
transition: transform 0.1s ease, opacity 0.1s ease;
/* #ifdef MP-WEIXIN */
pointer-events: none; /* 防止图片在微信小程序中捕获点击事件 */
/* #endif */
}
.key.delete:active {
background-color: #e03024; /* 点击时背景色变深 */
transform: scale(0.98);
}
.key.delete:active img {
transform: scale(0.9);
opacity: 0.85;
}
.key.delete-icon {
font-size: 36rpx;
transform: rotate(180deg);
}
.key.confirm {
background-color: #34c759;
color: white;
}
.key.empty {
background-color: transparent;
box-shadow: none;
}
/* 底部操作栏 */
.bottom-actions {
padding: 20rpx;
background-color: white;
border-top: 1rpx solid #e0e0e0;
}
.action-btn {
width: 100%;
height: 80rpx;
line-height: 80rpx;
font-size: 32rpx;
margin-bottom: 15rpx;
border-radius: 40rpx;
}
.action-btn:last-child {
margin-bottom: 0;
}
.action-btn.primary {
background-color: #007aff;
color: white;
border: none;
}
</style>

View File

@@ -0,0 +1,751 @@
<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">
<view class="table-header">
<view class="player-column">玩家</view>
<view class="score-column">总分</view>
</view>
<view class="table-body">
<view v-for="(player, index) in displayPlayers" :key="player.id" class="player-row">
<view class="player-info">
<image :src="player.avatar" mode="aspectFill"></image>
<text>{{ player.name }}</text>
</view>
<view class="score-display" @click="editScore(index)">
{{ player.score }}
</view>
</view>
</view>
</view>
</view>
</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 } from 'vue'
// 状态管理
const voiceBroadcast = ref(false)
const tableMode = ref(false)
const roomId = ref('')
const currentUser = ref({
id: 'self',
name: '玩家50950',
avatar: 'https://t14.baidu.com/it/u=3165460156,649373630&fm=224&app=112&f=JPEG?w=500&h=500'
})
// 玩家列表(初始包含自己)
const players = ref([
{
id: 'self',
name: '玩家50950',
avatar: 'https://t14.baidu.com/it/u=3165460156,649373630&fm=224&app=112&f=JPEG?w=500&h=500',
score: 0
}
])
// 台板玩家对象
const tablePlayer = {
id: 'table',
name: '台板',
avatar: '/static/robot.png',
score: 0
}
// 计算属性:动态返回包含或不包含台板玩家的列表
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/people.png',
score: 0
}
// 添加到玩家列表
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
}
}
// 当启用台板模式时,确保台板玩家信息正确
if (tableMode.value) {
const tablePlayerIndex = players.value.findIndex(p => p.id === 'table')
if (tablePlayerIndex === -1) {
// 如果需要,重新添加台板玩家(这通常不会发生,因为计算属性会处理)
players.value.push(tablePlayer)
}
}
}
// 头像和昵称编辑功能已迁移到change页面
// 编辑分数
const editScore = (index) => {
console.log('编辑分数', index)
const player = displayPlayers.value[index]
// 检查是否是台板玩家
if (player.id === 'table') {
// 台板玩家的分数编辑逻辑
uni.showModal({
title: '修改台板分数',
content: `当前分数: ${player.score}`,
editable: true,
placeholderText: '请输入新分数',
success: (res) => {
if (res.confirm && res.content !== null) {
const newScore = parseInt(res.content)
if (!isNaN(newScore)) {
// 找到台板玩家在原始数组中的位置
const tableIndex = players.value.findIndex(p => p.id === 'table')
if (tableIndex !== -1) {
players.value[tableIndex].score = newScore
} else {
// 如果台板玩家不在原始数组中,创建一个副本并更新
tablePlayer.score = newScore
}
// 语音播报分数(如果开启)
if (voiceBroadcast.value) {
console.log(`语音播报: 台板 分数更新为 ${newScore}`)
}
} else {
uni.showToast({
title: '请输入有效的数字',
icon: 'none'
})
}
}
}
})
return
}
// 普通玩家的分数编辑逻辑
uni.showModal({
title: '修改分数',
content: `${player.score}`,
editable: true,
placeholderText: '请输入新分数',
success: (res) => {
if (res.confirm && res.content !== null) {
const newScore = parseInt(res.content)
if (!isNaN(newScore)) {
// 找到对应的普通玩家
const playerIndex = players.value.findIndex(p => p.id === player.id)
if (playerIndex !== -1) {
players.value[playerIndex].score = newScore
}
// 语音播报分数(如果开启)
if (voiceBroadcast.value) {
console.log(`语音播报: ${player.name} 分数更新为 ${newScore}`)
}
} else {
uni.showToast({
title: '请输入有效的数字',
icon: 'none'
})
}
}
}
})
}
// 开局计分
const startScoring = () => {
console.log('开局计分')
// 检查是否有足够的玩家至少需要2个玩家才能开始计分
if (displayPlayers.value.length < 2) {
uni.showToast({
title: '玩家数不超过两个人时无法点击进行开始计分',
icon: 'none'
})
return
}
// 重置所有玩家分数
players.value.forEach(player => {
player.score = 0
})
// 重置台板玩家分数(即使不在原始数组中)
if (tableMode.value) {
tablePlayer.score = 0
}
// 准备要传递的玩家数据
const playersToPass = [...displayPlayers.value]
// 保存到临时存储,用于页面间数据传递
uni.setStorageSync('currentPlayers', JSON.stringify(playersToPass))
// 跳转到计分页面
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('单人模式页面加载完成')
// 可以在这里初始化数据或加载用户信息
// 生成随机房间号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)
// 更新玩家列表中的分数
parsedPlayers.forEach(updatedPlayer => {
// 查找对应的玩家
const playerIndex = players.value.findIndex(p => p.id === updatedPlayer.id)
if (playerIndex !== -1) {
// 更新普通玩家的分数
players.value[playerIndex].score = updatedPlayer.score
} else if (updatedPlayer.id === 'table') {
// 更新台板玩家的分数
tablePlayer.score = updatedPlayer.score
}
})
// 清除临时存储的数据
uni.removeStorageSync('updatedPlayers')
} catch (error) {
console.error('解析更新的玩家数据失败:', error)
}
}
}
// 监听页面显示事件
uni.$on('updatePlayers', updateListener)
// 监听页面显示生命周期
uni.onShow(() => {
updateListener()
})
// 组件卸载时移除监听
onUnmounted(() => {
uni.$off('updatePlayers', updateListener)
})
})
</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;
margin-bottom: 15rpx;
.header-title {
font-size: 28rpx;
font-weight: bold;
color: #333;
}
.player-count {
font-size: 24rpx;
color: #666;
margin-left: 10rpx;
}
}
}
/* 玩家表格 */
.players-table {
border: 1rpx solid #e0e0e0;
border-radius: 10rpx;
overflow: hidden;
.table-header {
display: flex;
flex-direction: row;
background-color: #f5f5f5;
border-bottom: 1rpx solid #e0e0e0;
.player-column {
flex: 2;
padding: 15rpx;
font-size: 26rpx;
font-weight: bold;
color: #333;
border-right: 1rpx solid #e0e0e0;
}
.score-column {
flex: 1;
padding: 15rpx;
font-size: 26rpx;
font-weight: bold;
color: #333;
text-align: center;
}
}
.table-body {
.player-row {
display: flex;
flex-direction: row;
border-bottom: 1rpx solid #e0e0e0;
&:last-child {
border-bottom: none;
}
.player-info {
flex: 2;
display: flex;
flex-direction: row;
align-items: center;
gap: 15rpx;
padding: 15rpx;
border-right: 1rpx solid #e0e0e0;
image {
width: 60rpx;
height: 60rpx;
border-radius: 50%;
}
text {
font-size: 26rpx;
color: #333;
}
}
.score-display {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
padding: 15rpx;
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>