Files
2025-12-16 09:32:55 +08:00

1616 lines
40 KiB
Vue
Raw Permalink 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="single-room">
<!-- 顶部导航栏 -->
<view class="nav-bar">
<view class="nav-back" @click="goBack">
<uni-icons type="arrowleft" size="24" color="#fff"></uni-icons>
</view>
<view class="nav-title">单人 - {{ roomId }}房间</view>
<view class="nav-actions">
<uni-icons type="more" size="24" color="#fff" @click="showMoreOptions"></uni-icons>
</view>
</view>
<!-- 功能栏 -->
<view class="function-bar">
<view class="function-item" @click="openAddPlayerPopup">
<uni-icons type="plus" size="20" color="#fff"></uni-icons>
<text>添加玩家</text>
</view>
<view class="function-item" @click="transferScorer">
<uni-icons type="exchange" size="20" color="#fff"></uni-icons>
<text>转让计分员</text>
</view>
<view class="function-item">
<switch :checked="voiceEnabled" @change="toggleVoice" color="#4CAF50"></switch>
<text>语音播报</text>
</view>
<view class="function-item">
<switch :checked="boardEnabled" @change="toggleBoard" color="#4CAF50"></switch>
<text>台板</text>
</view>
</view>
<!-- 对局记录区域 -->
<view class="record-section">
<view class="record-title">对局记录</view>
<view class="record-tip">点击对局分数进行修改</view>
<view class="warning-tip">
本工具不涉及金钱禁止用于非法行为
</view>
</view>
<!-- 玩家列表 - 使用自定义横向滚动 -->
<view class="player-list-container">
<view class="scroll-wrapper"
@touchstart="onTouchStart"
@touchmove="onTouchMove"
@touchend="onTouchEnd"
@touchcancel="onTouchEnd"
>
<view class="player-list" :style="{ transform: `translateX(${-scrollLeft}px)` }">
<!-- 玩家信息行 -->
<view class="player-row">
<view class="player-label">玩家 ({{ players.length }})</view>
<view class="player-columns">
<view class="player-column" v-for="(player, index) in players" :key="player.id">
<view class="player-info" @click="editUserInfo(player)">
<view class="avatar-container">
<image class="player-avatar" :src="player.avatar" mode="aspectFill"></image>
<view v-if="player.isSelf" class="self-indicator"></view>
</view>
<text class="player-name">{{ player.name }}</text>
<view v-if="player.isSelf" class="self-tag">自己</view>
</view>
</view>
</view>
</view>
<!-- 总分行 -->
<view class="player-row">
<view class="player-label">总分</view>
<view class="player-columns">
<view class="player-column" v-for="player in players" :key="player.id">
<view class="player-score">
{{ formatScore(player.totalScore) }}
</view>
</view>
</view>
</view>
<!-- 每局得分行 -->
<view class="player-row" v-for="(round, roundIndex) in gameRounds" :key="roundIndex">
<view class="player-label">{{ roundIndex + 1 }}</view>
<view class="player-columns">
<view class="player-column" v-for="player in players" :key="player.id">
<view class="round-score" @click="editRoundScore(roundIndex, player.id)">
{{ formatScore(getPlayerRoundScore(roundIndex, player.id)) }}
</view>
</view>
</view>
</view>
</view>
</view>
<!-- 自定义滚动条 -->
<view class="custom-scrollbar" v-if="showScrollbar">
<view class="scrollbar-track">
<view
class="scrollbar-thumb"
:style="{
width: thumbWidth + 'px',
left: thumbPosition + 'px'
}"
@touchstart="onThumbTouchStart"
@touchmove="onThumbTouchMove"
@touchend="onThumbTouchEnd"
></view>
</view>
</view>
</view>
<!-- 底部操作按钮 -->
<view class="action-buttons">
<button class="start-btn" @click="startScoring">开局计分</button>
<button class="settle-btn" @click="openSettlePopup">结算房间</button>
</view>
<!-- 添加玩家弹窗 -->
<view v-if="showAddPlayerPopup" class="popup-mask" @click="closeAddPlayerPopup">
<view class="popup-content" @click.stop>
<view class="popup-header">
<view class="popup-title">扫码加入房间</view>
<view class="popup-close" @click="closeAddPlayerPopup">
<uni-icons type="close" size="24" color="#999"></uni-icons>
</view>
</view>
<view class="popup-body">
<view class="popup-tip">邀请好友扫描下方二维码加入房间</view>
<view class="qrcode-container">
<image class="qrcode" src="https://tse2-mm.cn.bing.net/th/id/OIP-C.Pbhgd_vCFFNQWXi7y-HynAAAAA?w=209&h=209&c=7&r=0&o=7&cb=ucfimgc2&dpr=1.5&pid=1.7&rm=3" mode="aspectFit"></image>
</view>
<view class="popup-buttons">
<button class="share-btn" @click="shareRoom">分享给好友邀请加入房间</button>
<button class="add-virtual-btn" @click="openAddVirtualPlayerPopup">手动添加虚拟玩家</button>
</view>
<view class="popup-footer">
<view class="footer-tip">点击自己头像编辑用户头像和昵称~</view>
</view>
</view>
</view>
</view>
<!-- 添加虚拟玩家弹窗 -->
<view v-if="showAddVirtualPlayerPopup" class="popup-mask" @click="closeAddVirtualPlayerPopup">
<view class="add-virtual-popup-content" @click.stop>
<view class="add-virtual-popup-body">
<view class="add-virtual-title">添加玩家</view>
<view class="name-input-section">
<input
class="name-input"
type="text"
placeholder="请输入"
v-model="virtualPlayerName"
maxlength="10"
focus
/>
</view>
<view class="add-virtual-popup-buttons">
<button class="cancel-btn" @click="closeAddVirtualPlayerPopup">取消</button>
<button class="confirm-btn" @click="confirmAddVirtualPlayer">确定</button>
</view>
</view>
</view>
</view>
<!-- 结算房间弹窗 -->
<view v-if="showSettlePopup" class="popup-mask" @click="closeSettlePopup">
<view class="settle-popup-content" @click.stop>
<view class="settle-popup-body">
<view class="settle-tip">输入倍率快速结算</view>
<view class="rate-input-section">
<input
class="rate-input"
type="number"
placeholder="请输入倍率"
v-model="rateValue"
focus
/>
</view>
<view class="settle-popup-buttons">
<button class="cancel-btn" @click="closeSettlePopup">取消</button>
<button class="confirm-btn" @click="confirmSettlement">确定</button>
</view>
</view>
</view>
</view>
<!-- 转让计分员弹窗 -->
<view v-if="showTransferPopup" class="popup-mask" @click="closeTransferPopup">
<view class="transfer-popup-content" @click.stop>
<view class="transfer-popup-body">
<view class="transfer-tip">房间内暂无扫码或分享加入房间的玩家无法转让计分员</view>
<view class="transfer-popup-buttons">
<button class="confirm-btn" @click="closeTransferPopup">确定</button>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, onMounted, onUnmounted, watch, nextTick, getCurrentInstance } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import userApi from '@/api/user'
import roomApi from '@/api/room'
import roomUserApi from '@/api/roomUser'
import roomDetailApi from '@/api/roomDetail'
// 获取当前组件实例
const instance = getCurrentInstance()
// 房间数据
const roomId = ref('15198520')
const voiceEnabled = ref(false)
const boardEnabled = ref(false)
// 玩家数据
const players = ref([])
// 游戏对局记录
const gameRounds = ref([])
// 弹窗显示状态
const showAddPlayerPopup = ref(false)
const showAddVirtualPlayerPopup = ref(false)
const showSettlePopup = ref(false)
const showTransferPopup = ref(false)
const rateValue = ref('')
const virtualPlayerName = ref('')
// 滚动相关状态
const scrollLeft = ref(0) // 当前滚动位置
const scrollWrapperWidth = ref(0) // 滚动容器宽度
const playerListWidth = ref(0) // 玩家列表总宽度
const thumbWidth = ref(0) // 滚动条滑块宽度
const thumbPosition = ref(0) // 滚动条滑块位置
const showScrollbar = ref(false) // 是否显示滚动条
// 触摸相关状态
let isDragging = false // 是否正在拖动滚动条
let isScrolling = false // 是否正在滚动内容
let startX = 0 // 触摸开始X坐标
let startScrollLeft = 0 // 触摸开始时的滚动位置
let startThumbPosition = 0 // 触摸开始时的滑块位置
// 刷新标志
const shouldRefresh = ref(false)
// 刷新间隔ID
const refreshTimer = ref(null)
// 定时器ID用于清理
const scrollInfoTimer = ref(null)
// 格式化分数显示
const formatScore = (score) => {
if (score === 0) return '0'
return score > 0 ? `+${score}` : `${score}`
}
// 获取玩家在某一局的得分
const getPlayerRoundScore = (roundIndex, playerId) => {
if (!gameRounds.value[roundIndex]) return 0
const playerScore = gameRounds.value[roundIndex].find(item => item.playerId === playerId)
return playerScore ? playerScore.score : 0
}
// 获取滚动容器和内容尺寸
const getScrollInfo = () => {
nextTick(() => {
// 清理之前的定时器
if (scrollInfoTimer.value) {
clearTimeout(scrollInfoTimer.value)
}
scrollInfoTimer.value = setTimeout(() => {
// 使用 getCurrentInstance 获取当前组件实例
const query = uni.createSelectorQuery().in(instance)
query.select('.scroll-wrapper').boundingClientRect()
query.select('.player-list').boundingClientRect()
query.exec((res) => {
if (res[0] && res[1]) {
scrollWrapperWidth.value = res[0].width
playerListWidth.value = res[1].width
console.log('滚动容器宽度:', scrollWrapperWidth.value)
console.log('玩家列表宽度:', playerListWidth.value)
// 计算是否需要显示滚动条
if (playerListWidth.value > scrollWrapperWidth.value) {
showScrollbar.value = true
// 计算滑块宽度最小40px
const ratio = scrollWrapperWidth.value / playerListWidth.value
thumbWidth.value = Math.max(scrollWrapperWidth.value * ratio, 40)
} else {
showScrollbar.value = false
thumbWidth.value = scrollWrapperWidth.value
}
console.log('是否需要滚动条:', showScrollbar.value)
console.log('滑块宽度:', thumbWidth.value)
// 更新滑块位置
updateThumbPosition()
}
})
}, 100)
})
}
// 更新滑块位置
const updateThumbPosition = () => {
if (playerListWidth.value <= scrollWrapperWidth.value || scrollWrapperWidth.value === 0) {
thumbPosition.value = 0
return
}
const maxScroll = playerListWidth.value - scrollWrapperWidth.value
const maxThumbMove = scrollWrapperWidth.value - thumbWidth.value
if (maxScroll <= 0) {
thumbPosition.value = 0
} else {
thumbPosition.value = (scrollLeft.value / maxScroll) * maxThumbMove
}
}
// 内容触摸开始
const onTouchStart = (e) => {
if (!showScrollbar.value) return
isScrolling = true
startX = e.touches[0].clientX
startScrollLeft = scrollLeft.value
e.stopPropagation()
}
// 内容触摸移动
const onTouchMove = (e) => {
if (!isScrolling || !showScrollbar.value) return
const deltaX = startX - e.touches[0].clientX
let newScrollLeft = startScrollLeft + deltaX
// 限制滚动范围
const maxScroll = Math.max(0, playerListWidth.value - scrollWrapperWidth.value)
newScrollLeft = Math.max(0, Math.min(newScrollLeft, maxScroll))
scrollLeft.value = newScrollLeft
updateThumbPosition()
e.stopPropagation()
}
// 内容触摸结束
const onTouchEnd = () => {
isScrolling = false
}
// 滑块触摸开始
const onThumbTouchStart = (e) => {
if (!showScrollbar.value) return
isDragging = true
startX = e.touches[0].clientX
startThumbPosition = thumbPosition.value
e.stopPropagation()
}
// 滑块触摸移动
const onThumbTouchMove = (e) => {
if (!isDragging || !showScrollbar.value) return
const deltaX = e.touches[0].clientX - startX
const maxThumbMove = scrollWrapperWidth.value - thumbWidth.value
let newPosition = startThumbPosition + deltaX
newPosition = Math.max(0, Math.min(newPosition, maxThumbMove))
thumbPosition.value = newPosition
// 根据滑块位置计算滚动位置
if (maxThumbMove > 0) {
const maxScroll = playerListWidth.value - scrollWrapperWidth.value
const newScrollLeft = (newPosition / maxThumbMove) * maxScroll
scrollLeft.value = newScrollLeft
}
e.stopPropagation()
}
// 滑块触摸结束
const onThumbTouchEnd = () => {
isDragging = false
}
// 加载虚拟玩家
const loadVirtualPlayers = () => {
const savedVirtualPlayers = uni.getStorageSync('virtualPlayers') || []
const currentPlayers = players.value
savedVirtualPlayers.forEach(vp => {
const exists = currentPlayers.some(p =>
p.isVirtual && (p.id === vp.id || p.name === vp.name)
)
if (!exists) {
const uniqueId = vp.id || `virtual_${Date.now()}_${Math.random().toString(36).substr(2, 6)}`
players.value.push({
...vp,
id: uniqueId,
totalScore: 0
})
}
})
}
// 清理无效的虚拟玩家
const cleanupInvalidVirtualPlayers = () => {
const validPlayers = players.value.filter(player => {
if (player.isVirtual) {
const hasRecords = gameRounds.value.some(round =>
round.some(item =>
item.playerId === player.id || item.playerName === player.name
)
)
if (hasRecords) {
return true
}
if (player.createdTime && (Date.now() - player.createdTime) < 24 * 60 * 60 * 1000) {
return true
}
return false
}
return true
})
if (validPlayers.length !== players.value.length) {
players.value = validPlayers
uni.setStorageSync('players', players.value)
}
}
// 刷新房间数据
const refreshRoomData = async () => {
try {
uni.showLoading({
title: '刷新数据中...',
mask: true
})
await loadRoomUsers()
await loadGameRounds()
updateTotalScores()
// 重新计算滚动条
setTimeout(() => {
getScrollInfo()
}, 200)
} catch (error) {
console.error('刷新数据失败:', error)
} finally {
uni.hideLoading()
shouldRefresh.value = false
}
}
// 监听刷新事件
const setupRefreshListener = () => {
uni.$on('refreshRoomData', (data) => {
if (data.roomId === roomId.value || data.forceRefresh) {
shouldRefresh.value = true
refreshRoomData()
if (refreshTimer.value) {
clearTimeout(refreshTimer.value)
}
refreshTimer.value = setTimeout(() => {
refreshRoomData()
}, 5000)
}
})
uni.$on('appActivated', () => {
refreshRoomData()
})
}
// 在onMounted中添加事件监听
onMounted(() => {
console.log('组件已挂载')
// 从URL参数获取房间ID
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1]
if (currentPage.options && currentPage.options.roomId) {
roomId.value = currentPage.options.roomId.toString()
}
// 保存房间ID
uni.setStorageSync('currentRoomId', roomId.value)
// 1. 先加载虚拟玩家
loadVirtualPlayers()
// 2. 加载房间用户数据
loadRoomUsers().then(() => {
// 3. 加载对局记录
setTimeout(() => {
loadGameRounds()
}, 300)
})
// 4. 加载用户信息
setTimeout(() => {
loadUserInfo()
}, 200)
// 5. 设置监听器
setupRefreshListener()
// 6. 初始化滚动条信息
setTimeout(() => {
getScrollInfo()
}, 500)
})
onShow(() => {
// 检查房间ID变化
const savedRoomId = uni.getStorageSync('currentRoomId')
if (savedRoomId && savedRoomId !== roomId.value) {
roomId.value = savedRoomId.toString()
loadRoomUsers()
setTimeout(() => {
loadGameRounds()
}, 500)
}
// 同步玩家信息
syncPlayerInfo()
loadUserInfo()
// 清理无效虚拟玩家
cleanupInvalidVirtualPlayers()
// 重新计算滚动条
setTimeout(() => {
getScrollInfo()
}, 300)
})
// 在onUnmounted中清理
onUnmounted(() => {
uni.$off('refreshRoomData')
uni.$off('appActivated')
// 清理所有定时器
if (refreshTimer.value) {
clearTimeout(refreshTimer.value)
}
if (scrollInfoTimer.value) {
clearTimeout(scrollInfoTimer.value)
}
})
// 更新玩家总分
const updateTotalScores = () => {
players.value.forEach(player => {
player.totalScore = 0
})
gameRounds.value.forEach((round, roundIndex) => {
round.forEach(item => {
let matchedPlayer = null
if (item.userId) {
matchedPlayer = players.value.find(p =>
p.userId && p.userId.toString() === item.userId.toString()
)
}
if (!matchedPlayer && item.playerId) {
matchedPlayer = players.value.find(p =>
(p.id && p.id.toString() === item.playerId.toString()) ||
(p.userId && p.userId.toString() === item.playerId.toString())
)
}
if (!matchedPlayer && item.playerName) {
matchedPlayer = players.value.find(p =>
p.name === item.playerName
)
}
if (!matchedPlayer && item.playerName) {
const virtualPlayer = players.value.find(p =>
p.isVirtual && (p.name === item.playerName || item.playerName.includes(p.name))
)
if (virtualPlayer) {
matchedPlayer = virtualPlayer
}
}
if (matchedPlayer) {
matchedPlayer.totalScore = (matchedPlayer.totalScore || 0) + (item.score || 0)
}
})
})
}
// 监听玩家数量变化,重新计算滚动条
watch(players, () => {
setTimeout(() => {
getScrollInfo()
}, 300)
}, { deep: true })
// 编辑用户信息
const editUserInfo = (player) => {
if (player.isSelf) {
uni.navigateTo({
url: '/pages/edit-user-info/index'
})
} else {
uni.showToast({
title: '只能编辑自己的信息',
icon: 'none'
})
}
}
// 加载用户信息
const loadUserInfo = () => {
const savedUserInfo = uni.getStorageSync('userInfo')
if (savedUserInfo) {
const selfPlayerIndex = players.value.findIndex(player => player.isSelf)
if (selfPlayerIndex === -1) {
const selfPlayer = {
id: savedUserInfo.userId,
userId: savedUserInfo.userId,
name: savedUserInfo.nickname || savedUserInfo.nickName || '自己',
avatar: savedUserInfo.avatar || savedUserInfo.avatars || 'https://ts1.tc.mm.bing.net/th/id/OIP-C.QQG4bvcAR3CJ0WeQULA9UQAAAA?w=275&h=211&c=8&rs=1&qlt=90&o=6&cb=ucfimgc1&dpr=1.5&pid=3.1&rm=2',
totalScore: 0,
isSelf: true,
isVirtual: false,
order: 0
}
players.value.unshift(selfPlayer)
} else {
players.value[selfPlayerIndex].name = savedUserInfo.nickname || savedUserInfo.nickName || '自己'
players.value[selfPlayerIndex].avatar = savedUserInfo.avatar || savedUserInfo.avatars || 'https://ts1.tc.mm.bing.net/th/id/OIP-C.QQG4bvcAR3CJ0WeQULA9UQAAAA?w=275&h=211&c=8&rs=1&qlt=90&o=6&cb=ucfimgc1&dpr=1.5&pid=3.1&rm=2'
players.value[selfPlayerIndex].userId = savedUserInfo.userId
players.value[selfPlayerIndex].order = 0
}
players.value = [...players.value]
updatePlayerOrders()
}
}
// 同步玩家信息
const syncPlayerInfo = () => {
const savedPlayers = uni.getStorageSync('players')
if (savedPlayers && savedPlayers.length > 0) {
const otherPlayers = savedPlayers.filter(p => !p.isSelf)
players.value.forEach((player, index) => {
if (!player.isSelf) {
const savedPlayer = otherPlayers.find(p => {
return (p.userId && player.userId && p.userId === player.userId) ||
(p.id === player.id) ||
(p.name === player.name && p.avatar === player.avatar)
})
if (savedPlayer) {
player.avatar = savedPlayer.avatar
player.name = savedPlayer.name
}
}
})
}
}
// 加载房间用户数据
const loadRoomUsers = async () => {
try {
const savedUserInfo = uni.getStorageSync('userInfo')
players.value = []
// 首先添加玩家自己(如果存在)
if (savedUserInfo && savedUserInfo.userId) {
const selfPlayer = {
id: savedUserInfo.userId,
userId: savedUserInfo.userId,
name: savedUserInfo.nickname || savedUserInfo.nickName || '自己',
avatar: savedUserInfo.avatar || savedUserInfo.avatars || 'https://ts1.tc.mm.bing.net/th/id/OIP-C.QQG4bvcAR3CJ0WeQULA9UQAAAA?w=275&h=211&c=8&rs=1&qlt=90&o=6&cb=ucfimgc1&dpr=1.5&pid=3.1&rm=2',
totalScore: 0,
isSelf: true,
isVirtual: false,
order: 0
}
players.value.push(selfPlayer)
}
// 从本地存储加载虚拟玩家
const savedPlayers = uni.getStorageSync('players') || []
const virtualPlayers = savedPlayers.filter(p => p.isVirtual && !p.isSelf)
let orderIndex = 1
virtualPlayers.forEach(vp => {
const exists = players.value.some(p => p.isVirtual && p.name === vp.name)
if (!exists) {
players.value.push({
...vp,
id: vp.id || `virtual_${Date.now()}_${orderIndex}`,
totalScore: 0,
order: orderIndex
})
orderIndex++
}
})
// 保存到本地存储(排除玩家自己)
const playersToSave = players.value.filter(p => !p.isSelf)
uni.setStorageSync('players', playersToSave)
} catch (error) {
console.error('加载房间用户失败:', error)
}
}
const loadGameRounds = async () => {
try {
const savedRounds = uni.getStorageSync('gameRounds')
if (savedRounds && Array.isArray(savedRounds)) {
gameRounds.value = savedRounds
} else {
gameRounds.value = []
}
updateTotalScores()
uni.setStorageSync('gameRounds', gameRounds.value)
} catch (error) {
console.error('加载对局记录失败:', error)
gameRounds.value = []
updateTotalScores()
}
}
// 更新玩家排序
const updatePlayerOrders = () => {
let orderIndex = 0
players.value.forEach((player, index) => {
if (player.isSelf) {
player.order = 0
} else {
player.order = orderIndex + 1
orderIndex++
}
})
}
// 返回上一页
const goBack = () => {
uni.navigateBack()
}
// 显示更多选项
const showMoreOptions = () => {
uni.showActionSheet({
itemList: ['添加到我的小程序', '分享房间', '设置'],
success: (res) => {
if (res.tapIndex === 0) {
uni.showToast({
title: '已添加到我的小程序',
icon: 'success'
})
}
}
})
}
// 打开添加玩家弹窗
const openAddPlayerPopup = () => {
showAddPlayerPopup.value = true
}
// 关闭添加玩家弹窗
const closeAddPlayerPopup = () => {
showAddPlayerPopup.value = false
}
// 打开添加虚拟玩家弹窗
const openAddVirtualPlayerPopup = () => {
showAddVirtualPlayerPopup.value = true
virtualPlayerName.value = '' // 重置输入
}
// 关闭添加虚拟玩家弹窗
const closeAddVirtualPlayerPopup = () => {
showAddVirtualPlayerPopup.value = false
}
// 确认添加虚拟玩家
const confirmAddVirtualPlayer = async () => {
if (!virtualPlayerName.value.trim()) {
uni.showToast({
title: '请输入玩家名称',
icon: 'none'
})
return
}
uni.showLoading({
title: '添加中...'
})
try {
// 检查是否已存在相同名称的玩家
const existingPlayer = players.value.find(p => p.name === virtualPlayerName.value.trim())
if (existingPlayer) {
uni.showToast({
title: '该玩家名称已存在',
icon: 'none'
})
uni.hideLoading()
return
}
// 计算新的order值放在玩家自己之后
const maxOrder = players.value.reduce((max, p) => {
return p.isSelf ? max : Math.max(max, p.order || 0)
}, 0)
const newOrder = maxOrder + 1
// 生成唯一的虚拟用户ID
const virtualUserId = 1000000 + Math.floor(Math.random() * 1000000)
const virtualId = `virtual_${Date.now()}_${Math.floor(Math.random() * 1000)}`
// 创建新玩家对象
const newPlayer = {
id: virtualId,
userId: virtualUserId,
name: virtualPlayerName.value.trim(),
avatar: 'https://ts3.tc.mm.bing.net/th/id/OIP-C.0FQSOvu3OYJFe-h_sZKA6wHaNK?cb=ucfimg2ucfimg=1&rs=1&pid=ImgDetMain&o=7&rm=3',
totalScore: 0,
isSelf: false,
isVirtual: true,
playerType: 'user',
order: newOrder,
createdTime: Date.now()
}
console.log('创建新虚拟玩家:', newPlayer)
// 添加到玩家列表(玩家自己之后)
const selfIndex = players.value.findIndex(p => p.isSelf)
if (selfIndex >= 0) {
players.value.splice(selfIndex + 1, 0, newPlayer)
} else {
players.value.push(newPlayer)
}
// 重新计算order保持连续
updatePlayerOrders()
// 保存到本地存储(排除玩家自己)
const playersToSave = players.value.filter(p => !p.isSelf)
uni.setStorageSync('players', playersToSave)
uni.showToast({
title: '已添加虚拟玩家',
icon: 'success'
})
// 关闭弹窗
closeAddVirtualPlayerPopup()
closeAddPlayerPopup()
// 重新计算滚动条
setTimeout(() => {
getScrollInfo()
}, 100)
} catch (error) {
console.error('添加玩家失败:', error)
uni.showToast({
title: '添加失败',
icon: 'error'
})
} finally {
uni.hideLoading()
}
}
// 打开结算弹窗
const openSettlePopup = () => {
showSettlePopup.value = true
rateValue.value = '' // 重置倍率输入
}
// 关闭结算弹窗
const closeSettlePopup = () => {
showSettlePopup.value = false
}
// 打开转让计分员弹窗
const transferScorer = () => {
showTransferPopup.value = true
}
// 关闭转让计分员弹窗
const closeTransferPopup = () => {
showTransferPopup.value = false
}
// 切换台板
const toggleBoard = (e) => {
boardEnabled.value = e.detail.value
if (boardEnabled.value) {
addBoardPlayer()
} else {
removeBoardPlayer()
}
}
// 添加台板玩家
const addBoardPlayer = () => {
// 确保ID唯一
let playerId = Date.now()
const usedIds = new Set(players.value.map(p => p.id))
while (usedIds.has(playerId)) {
playerId = Date.now() + Math.floor(Math.random() * 1000)
}
const newPlayer = {
id: playerId,
name: '台板',
avatar: 'https://tse3-mm.cn.bing.net/th/id/OIP-C.32slU2a6Cq1Sxvd1GV_LvgHaDe?w=292&h=164&c=7&r=0&o=7&cb=ucfimgc2&dpr=1.5&pid=1.7&rm=3',
totalScore: 0,
isSelf: false
}
players.value.push(newPlayer)
// 保存到本地存储
uni.setStorageSync('players', players.value)
// 重新计算滚动条
setTimeout(() => {
getScrollInfo()
}, 100)
}
// 移除台板玩家
const removeBoardPlayer = () => {
players.value = players.value.filter(player => player.name !== '台板')
uni.setStorageSync('players', players.value)
// 重新计算滚动条
setTimeout(() => {
getScrollInfo()
}, 100)
}
// 确认结算
const confirmSettlement = () => {
if (!rateValue.value) {
uni.showToast({
title: '请输入倍率',
icon: 'none'
})
return
}
// 保存玩家数据到本地存储,供结算页面使用
uni.setStorageSync('players', players.value)
// 保存游戏记录到本地存储
uni.setStorageSync('gameRounds', gameRounds.value)
// 关闭弹窗
closeSettlePopup()
// 跳转到结算页面,传递倍率参数
uni.navigateTo({
url: `/pages/settle/index?rate=${rateValue.value}&roomId=${roomId.value}`
})
}
// 切换语音播报
const toggleVoice = (e) => {
voiceEnabled.value = e.detail.value
}
// 开局计分
const startScoring = () => {
// 保存当前玩家数据到本地存储
uni.setStorageSync('players', players.value)
// 将玩家数据传递到计分页面
uni.navigateTo({
url: `/pages/scoring/index?players=${encodeURIComponent(JSON.stringify(players.value))}&round=${gameRounds.value.length + 1}&roomId=${roomId.value}`
})
}
// 编辑对局分数
const editRoundScore = async (roundIndex, playerId) => {
// 获取该局的所有玩家得分数据
const roundScores = gameRounds.value[roundIndex]
if (!roundScores) {
uni.showToast({
title: '该局数据不存在',
icon: 'none'
})
return
}
// 构建玩家数据,包含当前得分和胜负状态
const playersWithScores = players.value.map(player => {
const playerScore = roundScores.find(score => score.playerId === player.id)
const scoreValue = playerScore ? playerScore.score : 0
return {
...player,
currentScore: Math.abs(scoreValue).toString(),
isWin: scoreValue >= 0,
finalScore: scoreValue
}
})
// 保存当前玩家数据到本地存储
uni.setStorageSync('players', players.value)
// 跳转到计分页面,传递玩家数据、对局索引和编辑模式标识
uni.navigateTo({
url: `/pages/scoring/index?players=${encodeURIComponent(JSON.stringify(playersWithScores))}&round=${roundIndex + 1}&mode=edit&roundIndex=${roundIndex}&roomId=${roomId.value}`
})
}
// 分享房间
const shareRoom = () => {
uni.showShareMenu({
withShareTicket: true
})
uni.showToast({
title: '请使用分享功能',
icon: 'none'
})
}
</script>
<style lang="less" scoped>
.single-room {
background-color: #fff;
min-height: 100vh;
}
/* 顶部导航栏 */
.nav-bar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20rpx 30rpx;
background-color: #41479b;
color: #fff;
.nav-back, .nav-actions {
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
}
.nav-title {
font-size: 32rpx;
font-weight: 500;
}
}
/* 功能栏 */
.function-bar {
display: flex;
padding: 15rpx 20rpx;
background-color: #41479b;
color: #fff;
border-top: 1rpx solid #555aaf;
border-bottom: 1rpx solid #555aaf;
.function-item {
display: flex;
align-items: center;
margin-right: 30rpx;
text {
margin-left: 10rpx;
font-size: 28rpx;
}
switch {
transform: scale(0.8);
}
}
}
/* 对局记录区域 */
.record-section {
padding: 20rpx 30rpx;
.record-title {
font-size: 32rpx;
font-weight: 600;
color: #000;
margin-bottom: 10rpx;
}
.record-tip {
font-size: 26rpx;
color: #666;
margin-bottom: 10rpx;
}
.warning-tip {
font-size: 26rpx;
color: #ff6b6b;
margin-bottom: 10rpx;
}
}
/* 玩家列表容器 */
.player-list-container {
padding: 0 30rpx 60rpx; /* 为滚动条留出底部空间 */
position: relative;
}
/* 滚动容器 */
.scroll-wrapper {
width: 100%;
overflow: hidden;
position: relative;
touch-action: pan-x; /* 允许水平滑动 */
}
/* 玩家列表 */
.player-list {
display: flex;
flex-direction: column;
transition: transform 0.2s ease;
will-change: transform;
min-width: fit-content;
}
.player-row {
display: flex;
padding: 20rpx 0;
border-bottom: 1rpx solid #eee;
&:last-child {
border-bottom: none;
}
.player-label {
width: 120rpx;
flex-shrink: 0;
font-size: 28rpx;
color: #000;
display: flex;
align-items: center;
justify-content: center;
}
.player-columns {
display: flex;
min-width: fit-content;
}
.player-column {
display: flex;
flex-direction: column;
align-items: center;
width: 140rpx;
flex-shrink: 0;
margin-right: 10rpx;
&:last-child {
margin-right: 0;
}
}
.player-info {
display: flex;
flex-direction: column;
align-items: center;
&:active {
opacity: 0.7;
}
.avatar-container {
position: relative;
width: 110rpx;
height: 110rpx;
margin-bottom: 10rpx;
display: flex;
align-items: center;
justify-content: center;
.player-avatar {
width: 100rpx;
height: 100rpx;
border-radius: 50%;
border: 5rpx solid #fff;
box-shadow: 0 6rpx 20rpx rgba(65, 71, 155, 0.3);
background: linear-gradient(135deg, #41479b 0%, #8b91e2 100%);
transition: all 0.3s ease;
}
.self-indicator {
position: absolute;
bottom: 0;
right: 0;
width: 24rpx;
height: 24rpx;
background-color: #4CAF50;
border: 3rpx solid #fff;
border-radius: 50%;
z-index: 2;
}
}
.player-name {
font-size: 26rpx;
color: #000;
margin-bottom: 5rpx;
text-align: center;
max-width: 120rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-weight: 500;
}
.self-tag {
background-color: #007aff;
color: #fff;
font-size: 20rpx;
padding: 2rpx 8rpx;
border-radius: 10rpx;
margin-top: 2rpx;
}
}
.player-score {
font-size: 36rpx;
color: #007aff;
font-weight: 600;
margin-top: 10rpx;
}
.round-score {
font-size: 28rpx;
color: #333;
margin-top: 10rpx;
padding: 5rpx 10rpx;
border-radius: 5rpx;
&:active {
background-color: #f0f0f0;
}
}
}
/* 自定义滚动条 */
.custom-scrollbar {
position: absolute;
bottom: 20rpx;
left: 30rpx;
right: 30rpx;
height: 12rpx;
z-index: 100;
.scrollbar-track {
position: relative;
width: 100%;
height: 100%;
background: rgba(240, 240, 240, 0.9);
border-radius: 6rpx;
overflow: visible;
.scrollbar-thumb {
position: absolute;
top: 0;
height: 100%;
min-width: 40px;
background: linear-gradient(90deg, #41479b, #6a6fcc);
border-radius: 6rpx;
border: 2rpx solid rgba(255, 255, 255, 0.8);
box-shadow: 0 2rpx 6rpx rgba(65, 71, 155, 0.3);
transition: background 0.3s ease;
touch-action: none;
&:active {
background: #33367a;
}
}
}
}
/* 底部操作按钮 */
.action-buttons {
display: flex;
padding: 40rpx;
gap: 40rpx;
margin-top: 450rpx;
button {
flex: 1;
height: 90rpx;
border-radius: 8rpx;
font-size: 32rpx;
font-weight: 600;
}
.start-btn {
background-color: #41479b;
color: #fff;
}
.settle-btn {
background-color: #fff;
color: #41479b;
border: 1rpx solid #41479b;
}
}
/* 弹窗遮罩层 */
.popup-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 999;
}
/* 添加玩家弹窗样式 */
.popup-content {
width: 650rpx;
background-color: #fff;
border-radius: 16rpx;
overflow: hidden;
.popup-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 30rpx;
border-bottom: 1rpx solid #eee;
.popup-title {
font-size: 32rpx;
font-weight: 600;
color: #000;
}
.popup-close {
width: 40rpx;
height: 40rpx;
display: flex;
align-items: center;
justify-content: center;
}
}
.popup-body {
padding: 30rpx;
.popup-tip {
font-size: 28rpx;
color: #666;
text-align: center;
margin-bottom: 40rpx;
}
.qrcode-container {
display: flex;
justify-content: center;
margin-bottom: 40rpx;
.qrcode {
width: 400rpx;
height: 400rpx;
border: 1rpx solid #eee;
}
}
.popup-buttons {
display: flex;
flex-direction: column;
gap: 20rpx;
margin-bottom: 30rpx;
button {
height: 80rpx;
border-radius: 8rpx;
font-size: 30rpx;
font-weight: 500;
}
.share-btn {
background-color: #41479b;
color: #fff;
}
.add-virtual-btn {
background-color: #fff;
color: #41479b;
border: 1rpx solid #41479b;
}
}
.popup-footer {
.footer-tip {
font-size: 24rpx;
color: #999;
text-align: center;
}
}
}
}
/* 添加虚拟玩家弹窗样式 */
.add-virtual-popup-content {
width: 600rpx;
background-color: #fff;
border-radius: 16rpx;
overflow: hidden;
.add-virtual-popup-body {
padding: 30rpx;
.add-virtual-title {
font-size: 32rpx;
font-weight: 600;
color: #000;
margin-bottom: 30rpx;
text-align: center;
}
.name-input-section {
margin-bottom: 40rpx;
.name-input {
width: 100%;
height: 80rpx;
border: 1rpx solid #ddd;
border-radius: 8rpx;
padding: 0 20rpx;
font-size: 28rpx;
text-align: center;
&:focus {
border-color: #41479b;
}
}
}
.add-virtual-popup-buttons {
display: flex;
gap: 20rpx;
button {
flex: 1;
height: 80rpx;
border-radius: 8rpx;
font-size: 30rpx;
font-weight: 500;
}
.cancel-btn {
background-color: #f5f5f5;
color: #333;
}
.confirm-btn {
background-color: #41479b;
color: #fff;
}
}
}
}
/* 结算弹窗样式 */
.settle-popup-content {
width: 600rpx;
background-color: #fff;
border-radius: 16rpx;
overflow: hidden;
.settle-popup-body {
padding: 30rpx;
.settle-tip {
font-size: 32rpx;
font-weight: 600;
color: #000;
margin-bottom: 30rpx;
text-align: center;
}
.rate-input-section {
margin-bottom: 40rpx;
.rate-input {
width: 100%;
height: 80rpx;
border: 1rpx solid #ddd;
border-radius: 8rpx;
padding: 0 20rpx;
font-size: 28rpx;
text-align: center;
&:focus {
border-color: #41479b;
}
}
}
.settle-popup-buttons {
display: flex;
gap: 20rpx;
button {
flex: 1;
height: 80rpx;
border-radius: 8rpx;
font-size: 30rpx;
font-weight: 500;
}
.cancel-btn {
background-color: #f5f5f5;
color: #333;
}
.confirm-btn {
background-color: #41479b;
color: #fff;
}
}
}
}
/* 转让计分员弹窗样式 */
.transfer-popup-content {
width: 600rpx;
background-color: #fff;
border-radius: 16rpx;
overflow: hidden;
.transfer-popup-body {
padding: 40rpx 30rpx 30rpx;
.transfer-tip {
font-size: 28rpx;
color: #000;
text-align: center;
margin-bottom: 40rpx;
line-height: 1.5;
}
.transfer-popup-buttons {
display: flex;
justify-content: center;
button {
width: 200rpx;
height: 70rpx;
border-radius: 8rpx;
font-size: 30rpx;
font-weight: 500;
background-color: #41479b;
color: #fff;
}
}
}
}
</style>