|
@@ -1,62 +1,966 @@
|
|
<template>
|
|
<template>
|
|
- <view class="waiting-room-page">
|
|
|
|
- <view class="title">等待室</view>
|
|
|
|
- <nut-empty description="正在开发中..." image="empty" />
|
|
|
|
|
|
+ <view class="waiting-room-page">
|
|
|
|
+ <!-- 房间头部信息 -->
|
|
|
|
+ <view class="room-header">
|
|
|
|
+ <view class="room-title">
|
|
|
|
+ <view class="title">{{ currentRoom?.name || '等待房间' }}</view>
|
|
|
|
+ <view class="tag" :class="isHost ? 'host-tag' : 'player-tag'">
|
|
|
|
+ {{ isHost ? '主持人' : '玩家' }}
|
|
|
|
+ </view>
|
|
|
|
+ </view>
|
|
|
|
+ <view class="status-info">
|
|
|
|
+ <view class="players-count">{{ playerCount }}/{{ currentRoom?.maxPlayers || 0 }}人已加入</view>
|
|
|
|
+ <view class="status-text">{{ statusText }}</view>
|
|
|
|
+ </view>
|
|
</view>
|
|
</view>
|
|
- <Tabbar></Tabbar>
|
|
|
|
- </template>
|
|
|
|
|
|
+
|
|
|
|
+ <!-- 房间码展示和分享 -->
|
|
|
|
+ <RoomCode
|
|
|
|
+ :code="currentRoom?.id || ''"
|
|
|
|
+ @share="handleShare"
|
|
|
|
+ @copy="handleCopy"
|
|
|
|
+ />
|
|
|
|
+
|
|
|
|
+ <!-- 主持人视图 -->
|
|
|
|
+ <view v-if="isHost" class="host-view">
|
|
|
|
+ <!-- 游戏设置模块 -->
|
|
|
|
+ <view class="game-settings">
|
|
|
|
+ <nut-divider
|
|
|
|
+ content-position="center"
|
|
|
|
+ :style="{ color: '#3C92FB', borderColor: '#3C92FB', padding: '0 16px', margin: '10px 0 20px 0' }"
|
|
|
|
+ >
|
|
|
|
+ 游戏设置
|
|
|
|
+ </nut-divider>
|
|
|
|
+
|
|
|
|
+ <!-- 主题选择 -->
|
|
|
|
+ <view class="setting-item">
|
|
|
|
+ <view class="setting-label">游戏主题</view>
|
|
|
|
+ <view class="setting-value" @click="showThemeSelector = true">
|
|
|
|
+ {{ selectedTheme?.text || '请选择主题' }}
|
|
|
|
+ <down size="12" color="#999"></down>
|
|
|
|
+ </view>
|
|
|
|
+ </view>
|
|
|
|
+
|
|
|
|
+ <!-- 题目选择 -->
|
|
|
|
+ <view class="setting-item" v-if="selectedTheme">
|
|
|
|
+ <view class="setting-label">游戏题目</view>
|
|
|
|
+ <view class="setting-value" @click="showPuzzleSelector = true">
|
|
|
|
+ {{ selectedPuzzle?.text || '请选择题目' }}
|
|
|
|
+ <down size="12" color="#999"></down>
|
|
|
|
+ </view>
|
|
|
|
+ </view>
|
|
|
|
+
|
|
|
|
+ <!-- 难度选择 -->
|
|
|
|
+ <view class="setting-item">
|
|
|
|
+ <view class="setting-label">游戏难度</view>
|
|
|
|
+ <view class="difficulty-options">
|
|
|
|
+ <nut-radiogroup v-model="selectedDifficulty" direction="horizontal">
|
|
|
|
+ <nut-radio label="easy" icon-name="check" icon-active-color="#3C92FB">简单</nut-radio>
|
|
|
|
+ <nut-radio label="medium" icon-name="check" icon-active-color="#3C92FB">中等</nut-radio>
|
|
|
|
+ <nut-radio label="hard" icon-name="check" icon-active-color="#3C92FB">困难</nut-radio>
|
|
|
|
+ </nut-radiogroup>
|
|
|
|
+ </view>
|
|
|
|
+ </view>
|
|
|
|
+ </view>
|
|
|
|
+
|
|
|
|
+ <!-- 玩家列表 -->
|
|
|
|
+ <view class="player-list">
|
|
|
|
+ <nut-divider
|
|
|
|
+ content-position="center"
|
|
|
|
+ :style="{ color: '#3C92FB', borderColor: '#3C92FB', padding: '0 16px', margin: '20px 0 10px 0' }"
|
|
|
|
+ >
|
|
|
|
+ 玩家列表
|
|
|
|
+ </nut-divider>
|
|
|
|
+
|
|
|
|
+ <view class="players">
|
|
|
|
+ <view
|
|
|
|
+ v-for="user in currentRoom?.users"
|
|
|
|
+ :key="user.openid"
|
|
|
|
+ class="player-item"
|
|
|
|
+ :class="{ 'is-host': user.roomRole === 'hoster' }"
|
|
|
|
+ >
|
|
|
|
+ <view class="player-avatar">
|
|
|
|
+ <image :src="user.avatar" mode="aspectFill" />
|
|
|
|
+ </view>
|
|
|
|
+ <view class="player-info">
|
|
|
|
+ <view class="player-name">{{ user.nickname }}</view>
|
|
|
|
+ <view class="player-role">{{ user.roomRole === 'hoster' ? '主持人' : '玩家' }}</view>
|
|
|
|
+ </view>
|
|
|
|
+ <view class="player-status" :class="{ 'ready': user.isReady }">
|
|
|
|
+ {{ user.isReady ? '已准备' : '未准备' }}
|
|
|
|
+ </view>
|
|
|
|
+ </view>
|
|
|
|
+ </view>
|
|
|
|
+ </view>
|
|
|
|
+
|
|
|
|
+ <!-- 主持人操作按钮 -->
|
|
|
|
+ <view class="action-buttons">
|
|
|
|
+ <nut-button
|
|
|
|
+ block
|
|
|
|
+ color="#3C92FB"
|
|
|
|
+ class="start-button"
|
|
|
|
+ :disabled="!canStartGame"
|
|
|
|
+ @click="startGame"
|
|
|
|
+ >
|
|
|
|
+ 开始游戏
|
|
|
|
+ </nut-button>
|
|
|
|
+ </view>
|
|
|
|
+ </view>
|
|
|
|
+
|
|
|
|
+ <!-- 玩家视图 -->
|
|
|
|
+ <view v-else class="player-view">
|
|
|
|
+ <!-- 游戏信息展示 -->
|
|
|
|
+ <view class="game-info">
|
|
|
|
+ <nut-divider
|
|
|
|
+ content-position="center"
|
|
|
|
+ :style="{ color: '#3C92FB', borderColor: '#3C92FB', padding: '0 16px', margin: '10px 0 20px 0' }"
|
|
|
|
+ >
|
|
|
|
+ 游戏信息
|
|
|
|
+ </nut-divider>
|
|
|
|
+
|
|
|
|
+ <view class="info-card">
|
|
|
|
+ <view class="game-title">{{ gameTitle }}</view>
|
|
|
|
+ <view class="game-meta">
|
|
|
|
+ <view class="meta-item">
|
|
|
|
+ <view class="meta-label">游戏主题</view>
|
|
|
|
+ <view class="meta-value">{{ themeTitle || '待定' }}</view>
|
|
|
|
+ </view>
|
|
|
|
+ <view class="meta-item">
|
|
|
|
+ <view class="meta-label">游戏难度</view>
|
|
|
|
+ <view class="meta-value">{{ difficultyText }}</view>
|
|
|
|
+ </view>
|
|
|
|
+ </view>
|
|
|
|
+ <view class="game-desc">
|
|
|
|
+ {{ gameDescription || '正在等待主持人设置游戏...' }}
|
|
|
|
+ </view>
|
|
|
|
+ </view>
|
|
|
|
+ </view>
|
|
|
|
+
|
|
|
|
+ <!-- 玩家列表 -->
|
|
|
|
+ <view class="player-list">
|
|
|
|
+ <nut-divider
|
|
|
|
+ content-position="center"
|
|
|
|
+ :style="{ color: '#3C92FB', borderColor: '#3C92FB', padding: '0 16px', margin: '20px 0 10px 0' }"
|
|
|
|
+ >
|
|
|
|
+ 玩家列表
|
|
|
|
+ </nut-divider>
|
|
|
|
+
|
|
|
|
+ <view class="players">
|
|
|
|
+ <view
|
|
|
|
+ v-for="user in currentRoom?.users"
|
|
|
|
+ :key="user.openid"
|
|
|
|
+ class="player-item"
|
|
|
|
+ :class="{ 'is-host': user.roomRole === 'hoster' }"
|
|
|
|
+ >
|
|
|
|
+ <view class="player-avatar">
|
|
|
|
+ <image :src="user.avatar" mode="aspectFill" />
|
|
|
|
+ </view>
|
|
|
|
+ <view class="player-info">
|
|
|
|
+ <view class="player-name">{{ user.nickname }}</view>
|
|
|
|
+ <view class="player-role">{{ user.roomRole === 'hoster' ? '主持人' : '玩家' }}</view>
|
|
|
|
+ </view>
|
|
|
|
+ <view class="player-status" :class="{ 'ready': user.isReady }">
|
|
|
|
+ {{ user.isReady ? '已准备' : '未准备' }}
|
|
|
|
+ </view>
|
|
|
|
+ </view>
|
|
|
|
+ </view>
|
|
|
|
+ </view>
|
|
|
|
+
|
|
|
|
+ <!-- 玩家操作按钮 -->
|
|
|
|
+ <view class="action-buttons">
|
|
|
|
+ <nut-button
|
|
|
|
+ block
|
|
|
|
+ :color="currentUserReady ? '#999' : '#3C92FB'"
|
|
|
|
+ class="ready-button"
|
|
|
|
+ @click="toggleReady"
|
|
|
|
+ >
|
|
|
|
+ {{ currentUserReady ? '取消准备' : '准备' }}
|
|
|
|
+ </nut-button>
|
|
|
|
+ </view>
|
|
|
|
+ </view>
|
|
|
|
+
|
|
|
|
+ <!-- 主题选择器 -->
|
|
|
|
+ <nut-popup position="bottom" v-model:visible="showThemeSelector">
|
|
|
|
+ <nut-cascader
|
|
|
|
+ v-model="selectedThemeValue"
|
|
|
|
+ title="选择游戏主题"
|
|
|
|
+ :options="themeOptions"
|
|
|
|
+ @change="onThemeChange"
|
|
|
|
+ @close="showThemeSelector = false"
|
|
|
|
+ />
|
|
|
|
+ </nut-popup>
|
|
|
|
+
|
|
|
|
+ <!-- 题目选择器 -->
|
|
|
|
+ <nut-popup position="bottom" v-model:visible="showPuzzleSelector">
|
|
|
|
+ <nut-cascader
|
|
|
|
+ v-model="selectedPuzzleValue"
|
|
|
|
+ title="选择游戏题目"
|
|
|
|
+ :options="puzzleOptions"
|
|
|
|
+ @change="onPuzzleChange"
|
|
|
|
+ @close="showPuzzleSelector = false"
|
|
|
|
+ />
|
|
|
|
+ </nut-popup>
|
|
|
|
+
|
|
|
|
+ <!-- 分享房间弹窗 -->
|
|
|
|
+ <nut-dialog
|
|
|
|
+ title="分享房间"
|
|
|
|
+ content="邀请好友扫描二维码加入房间"
|
|
|
|
+ v-model:visible="showShareDialog"
|
|
|
|
+ footer-direction="horizontal"
|
|
|
|
+ >
|
|
|
|
+ <template #footer>
|
|
|
|
+ <view class="share-dialog-content">
|
|
|
|
+ <view class="qrcode-container">
|
|
|
|
+ <!-- 将canvas标签替换为image标签 -->
|
|
|
|
+ <image id="qrcode" class="qrcode-img"></image>
|
|
|
|
+ </view>
|
|
|
|
+ <view class="room-info">
|
|
|
|
+ <view class="room-id">房间号: {{ currentRoom?.id }}</view>
|
|
|
|
+ <view class="room-password" v-if="currentRoom?.password">
|
|
|
|
+ 密码: {{ currentRoom.password }}
|
|
|
|
+ </view>
|
|
|
|
+ </view>
|
|
|
|
+ </view>
|
|
|
|
+ </template>
|
|
|
|
+ </nut-dialog>
|
|
|
|
+ </view>
|
|
|
|
+ <Tabbar></Tabbar>
|
|
|
|
+</template>
|
|
|
|
+
|
|
|
|
+<script lang="ts">
|
|
|
|
+import Taro from '@tarojs/taro'
|
|
|
|
+import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
|
|
|
|
+import { useRoomStore } from '@/stores/room'
|
|
|
|
+import { useUserStore } from '@/stores/user'
|
|
|
|
+import { useTurtleSoupStore } from '@/stores/games/turtlesoup'
|
|
|
|
+import Tabbar from '@/components/Tabbar.vue'
|
|
|
|
+import RoomCode from '@/components/RoomCode/index.vue'
|
|
|
|
+import { RoomRole, RoomStatus } from '@/types/room'
|
|
|
|
+import { TurtleSoupDifficulty } from '@/types/games/turtlesoup'
|
|
|
|
+import qrcodeBase64 from 'qrcode-base64'
|
|
|
|
+
|
|
|
|
+// 主题和题目的数据类型
|
|
|
|
+interface CascaderOption {
|
|
|
|
+ value: string;
|
|
|
|
+ text: string;
|
|
|
|
+ disabled?: boolean;
|
|
|
|
+ children?: CascaderOption[];
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+export default {
|
|
|
|
+ components: {
|
|
|
|
+ Tabbar,
|
|
|
|
+ RoomCode
|
|
|
|
+ },
|
|
|
|
|
|
- <script lang="ts">
|
|
|
|
- import Taro from '@tarojs/taro'
|
|
|
|
- import Tabbar from '@/components/Tabbar.vue'
|
|
|
|
- import { onMounted, onUnmounted } from 'vue'
|
|
|
|
|
|
+ // 生命周期钩子 - 页面显示
|
|
|
|
+ onShow() {
|
|
|
|
+ // 隐藏返回首页按钮
|
|
|
|
+ Taro.hideHomeButton()
|
|
|
|
+ },
|
|
|
|
|
|
- export default {
|
|
|
|
- components: {
|
|
|
|
- Tabbar
|
|
|
|
- },
|
|
|
|
|
|
+ // Composition API
|
|
|
|
+ setup() {
|
|
|
|
+ // 初始化store
|
|
|
|
+ const roomStore = useRoomStore()
|
|
|
|
+ const userStore = useUserStore()
|
|
|
|
+ const turtleSoupStore = useTurtleSoupStore()
|
|
|
|
|
|
- // 生命周期钩子 - 页面显示
|
|
|
|
- onShow() {
|
|
|
|
- // 隐藏返回首页按钮
|
|
|
|
- Taro.hideHomeButton()
|
|
|
|
- console.log('已隐藏返回首页按钮')
|
|
|
|
- },
|
|
|
|
|
|
+ // 房间ID
|
|
|
|
+ const roomId = ref('')
|
|
|
|
|
|
- // Composition API
|
|
|
|
- setup() {
|
|
|
|
- // 这里可以添加等待室页面的逻辑
|
|
|
|
- // 在页面加载时开始监听
|
|
|
|
- onMounted(() => {
|
|
|
|
-
|
|
|
|
|
|
+ // 获取当前房间
|
|
|
|
+ const currentRoom = computed(() => roomStore.currentRoom)
|
|
|
|
+
|
|
|
|
+ // 判断当前用户是否是主持人
|
|
|
|
+ const isHost = computed(() => {
|
|
|
|
+ if (!currentRoom.value || !userStore.openid) return false
|
|
|
|
+ return roomStore.getUserRole(userStore.openid) === RoomRole.HOSTER
|
|
|
|
+ })
|
|
|
|
+
|
|
|
|
+ // 计算玩家数量
|
|
|
|
+ const playerCount = computed(() => {
|
|
|
|
+ return currentRoom.value?.users.length || 0
|
|
|
|
+ })
|
|
|
|
+
|
|
|
|
+ // 房间状态文本
|
|
|
|
+ const statusText = computed(() => {
|
|
|
|
+ if (!currentRoom.value) return '等待中'
|
|
|
|
+
|
|
|
|
+ switch(currentRoom.value.status) {
|
|
|
|
+ case RoomStatus.WAITING:
|
|
|
|
+ return '等待玩家加入...'
|
|
|
|
+ case RoomStatus.PLAYING:
|
|
|
|
+ return '游戏中...'
|
|
|
|
+ case RoomStatus.ENDED:
|
|
|
|
+ return '已结束'
|
|
|
|
+ default:
|
|
|
|
+ return '等待中'
|
|
|
|
+ }
|
|
|
|
+ })
|
|
|
|
+
|
|
|
|
+ // 当前用户是否已准备
|
|
|
|
+ const currentUserReady = computed(() => {
|
|
|
|
+ if (!currentRoom.value || !userStore.openid) return false
|
|
|
|
+
|
|
|
|
+ const currentUser = currentRoom.value.users.find(u => u.openid === userStore.openid)
|
|
|
|
+ return currentUser ? currentUser.isReady : false
|
|
|
|
+ })
|
|
|
|
+
|
|
|
|
+ // 是否可以开始游戏(主持人功能)
|
|
|
|
+ const canStartGame = computed(() => {
|
|
|
|
+ if (!currentRoom.value) return false
|
|
|
|
+
|
|
|
|
+ // 至少有一个玩家
|
|
|
|
+ const players = currentRoom.value.users.filter(u => u.roomRole === RoomRole.PLAYER)
|
|
|
|
+ if (players.length === 0) return false
|
|
|
|
+
|
|
|
|
+ // 所有玩家都已准备
|
|
|
|
+ const allReady = players.every(p => p.isReady)
|
|
|
|
+
|
|
|
|
+ // 主题和题目已选择
|
|
|
|
+ const settingsReady = selectedTheme.value && selectedPuzzle.value
|
|
|
|
+
|
|
|
|
+ return allReady && settingsReady
|
|
|
|
+ })
|
|
|
|
+
|
|
|
|
+ // 游戏设置相关变量
|
|
|
|
+ const selectedDifficulty = ref(TurtleSoupDifficulty.MEDIUM)
|
|
|
|
+ const selectedTheme = ref<CascaderOption | null>(null)
|
|
|
|
+ const selectedPuzzle = ref<CascaderOption | null>(null)
|
|
|
|
+ const showThemeSelector = ref(false)
|
|
|
|
+ const showPuzzleSelector = ref(false)
|
|
|
|
+
|
|
|
|
+ // 分享相关
|
|
|
|
+ const showShareDialog = ref(false)
|
|
|
|
+
|
|
|
|
+ // 主题选项
|
|
|
|
+ const themeOptions = ref<CascaderOption[]>([
|
|
|
|
+ {
|
|
|
|
+ value: 'mystery',
|
|
|
|
+ text: '神秘主题',
|
|
|
|
+ children: [
|
|
|
|
+ { value: 'detective', text: '侦探故事' },
|
|
|
|
+ { value: 'supernatural', text: '超自然现象' }
|
|
|
|
+ ]
|
|
|
|
+ },
|
|
|
|
+ {
|
|
|
|
+ value: 'adventure',
|
|
|
|
+ text: '冒险主题',
|
|
|
|
+ children: [
|
|
|
|
+ { value: 'treasure', text: '寻宝冒险' },
|
|
|
|
+ { value: 'survival', text: '荒野求生' }
|
|
|
|
+ ]
|
|
|
|
+ },
|
|
|
|
+ {
|
|
|
|
+ value: 'scifi',
|
|
|
|
+ text: '科幻主题',
|
|
|
|
+ disabled: false, // 暂时改为false或删除此属性
|
|
|
|
+ children: [
|
|
|
|
+ { value: 'space', text: '太空探险' },
|
|
|
|
+ { value: 'future', text: '未来世界' }
|
|
|
|
+ ]
|
|
|
|
+ }
|
|
|
|
+ ])
|
|
|
|
+
|
|
|
|
+ // 题目选项(将根据选择的主题动态加载)
|
|
|
|
+ const puzzleOptions = ref<CascaderOption[]>([])
|
|
|
|
+
|
|
|
|
+ // 游戏信息(玩家视图)
|
|
|
|
+ const gameTitle = computed(() => currentRoom.value?.gameTitle || '')
|
|
|
|
+ const themeTitle = computed(() => selectedTheme.value?.text || '')
|
|
|
|
+ const gameDescription = ref('')
|
|
|
|
+
|
|
|
|
+ // 难度文本
|
|
|
|
+ const difficultyText = computed(() => {
|
|
|
|
+ switch(selectedDifficulty.value) {
|
|
|
|
+ case TurtleSoupDifficulty.EASY:
|
|
|
|
+ return '简单'
|
|
|
|
+ case TurtleSoupDifficulty.MEDIUM:
|
|
|
|
+ return '中等'
|
|
|
|
+ case TurtleSoupDifficulty.HARD:
|
|
|
|
+ return '困难'
|
|
|
|
+ default:
|
|
|
|
+ return '中等'
|
|
|
|
+ }
|
|
|
|
+ })
|
|
|
|
+
|
|
|
|
+ // 初始化页面
|
|
|
|
+ const initPage = async () => {
|
|
|
|
+ // 获取路由参数中的房间ID
|
|
|
|
+ const pages = Taro.getCurrentPages()
|
|
|
|
+ const currentPage = pages[pages.length - 1]
|
|
|
|
+ const routeParams = currentPage.$taroParams
|
|
|
|
+
|
|
|
|
+ if (routeParams && routeParams.roomId) {
|
|
|
|
+ roomId.value = routeParams.roomId
|
|
|
|
|
|
- // 在组件卸载时停止监听
|
|
|
|
- onUnmounted(() => {
|
|
|
|
|
|
+ // 加载房间信息
|
|
|
|
+ await loadRoomInfo(roomId.value)
|
|
|
|
+
|
|
|
|
+ // 如果是主持人,加载可用的主题和题目
|
|
|
|
+ if (isHost.value) {
|
|
|
|
+ await loadThemes()
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // 开始监听房间变化
|
|
|
|
+ startRoomListener()
|
|
|
|
+ } else {
|
|
|
|
+ Taro.showToast({
|
|
|
|
+ title: '房间ID不存在',
|
|
|
|
+ icon: 'none'
|
|
|
|
+ })
|
|
|
|
+
|
|
|
|
+ // 延迟返回
|
|
|
|
+ setTimeout(() => {
|
|
|
|
+ Taro.navigateBack()
|
|
|
|
+ }, 1500)
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // 加载房间信息
|
|
|
|
+ const loadRoomInfo = async (id: string) => {
|
|
|
|
+ try {
|
|
|
|
+ const result = await roomStore.loadRoomInfo(id)
|
|
|
|
+ if (!result.success) {
|
|
|
|
+ throw new Error(result.message || '加载房间信息失败')
|
|
|
|
+ }
|
|
|
|
+ } catch (error) {
|
|
|
|
+ console.error('加载房间信息失败:', error)
|
|
|
|
+ Taro.showToast({
|
|
|
|
+ title: error instanceof Error ? error.message : '加载房间信息失败',
|
|
|
|
+ icon: 'none'
|
|
|
|
+ })
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // 加载主题列表
|
|
|
|
+ const loadThemes = async () => {
|
|
|
|
+ try {
|
|
|
|
+ // 可以从turtleSoupStore加载主题
|
|
|
|
+ const themes = await turtleSoupStore.loadThemes()
|
|
|
|
+
|
|
|
|
+ // 将API返回的主题格式化为Cascader需要的格式
|
|
|
|
+ // 这里仅为示例,实际需要根据API返回格式调整
|
|
|
|
+ if (themes && themes.length) {
|
|
|
|
+ themeOptions.value = themes.map(theme => ({
|
|
|
|
+ value: theme.id,
|
|
|
|
+ text: theme.name,
|
|
|
|
+ disabled: theme.isLocked, // 根据是否解锁决定是否可选
|
|
|
|
+ children: [] // 选择主题后会动态加载题目列表
|
|
|
|
+ }))
|
|
|
|
+ }
|
|
|
|
+ } catch (error) {
|
|
|
|
+ console.error('加载主题失败:', error)
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // 根据主题加载题目列表
|
|
|
|
+ const loadPuzzles = async (themeId: string) => {
|
|
|
|
+ try {
|
|
|
|
+ // 从API加载指定主题的题目列表
|
|
|
|
+ const puzzles = await turtleSoupStore.loadPuzzles(themeId, selectedDifficulty.value)
|
|
|
|
+
|
|
|
|
+ // 格式化为Cascader需要的格式
|
|
|
|
+ if (puzzles && puzzles.length) {
|
|
|
|
+ puzzleOptions.value = puzzles.map(puzzle => ({
|
|
|
|
+ value: puzzle.id,
|
|
|
|
+ text: puzzle.title,
|
|
|
|
+ disabled: puzzle.isLocked // 使用isLocked替换!puzzle.isUnlocked
|
|
|
|
+ }))
|
|
|
|
+ } else {
|
|
|
|
+ puzzleOptions.value = []
|
|
|
|
+ }
|
|
|
|
+ } catch (error) {
|
|
|
|
+ console.error('加载题目失败:', error)
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // 监听主题选择变化
|
|
|
|
+ const onThemeChange = async ({ value, selectedOptions }: any) => {
|
|
|
|
+ // 保存选中的主题
|
|
|
|
+ selectedTheme.value = selectedOptions[0]
|
|
|
|
+
|
|
|
|
+ // 加载该主题下的题目
|
|
|
|
+ await loadPuzzles(value[0])
|
|
|
|
+
|
|
|
|
+ // 重置题目选择
|
|
|
|
+ selectedPuzzle.value = null
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // 监听题目选择变化
|
|
|
|
+ const onPuzzleChange = ({ selectedOptions }: any) => {
|
|
|
|
+ // 保存选中的题目
|
|
|
|
+ selectedPuzzle.value = selectedOptions[0]
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // 准备/取消准备(玩家)
|
|
|
|
+ const toggleReady = async () => {
|
|
|
|
+ if (!userStore.openid || !currentRoom.value) return
|
|
|
|
+
|
|
|
|
+ try {
|
|
|
|
+ Taro.showLoading({ title: '处理中...' })
|
|
|
|
+
|
|
|
|
+ // 更新玩家准备状态
|
|
|
|
+ const newStatus = !currentUserReady.value
|
|
|
|
+
|
|
|
|
+ // 调用API更新准备状态
|
|
|
|
+ // 假设 roomStore 有一个 updateUserReady 方法
|
|
|
|
+ await roomStore.updateUserInRoom(userStore.openid, { isReady: newStatus })
|
|
|
|
+
|
|
|
|
+ Taro.hideLoading()
|
|
|
|
+ } catch (error) {
|
|
|
|
+ console.error('切换准备状态失败:', error)
|
|
|
|
+ Taro.showToast({
|
|
|
|
+ title: '操作失败',
|
|
|
|
+ icon: 'none'
|
|
|
|
+ })
|
|
|
|
+ Taro.hideLoading()
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // 开始游戏(主持人)
|
|
|
|
+ const startGame = async () => {
|
|
|
|
+ if (!currentRoom.value || !isHost.value) return
|
|
|
|
+
|
|
|
|
+ try {
|
|
|
|
+ Taro.showLoading({ title: '开始游戏中...' })
|
|
|
|
+
|
|
|
|
+ // 准备游戏设置
|
|
|
|
+ const gameSettings = {
|
|
|
|
+ themeId: selectedTheme.value?.value || '',
|
|
|
|
+ puzzleId: selectedPuzzle.value?.value || '',
|
|
|
|
+ difficulty: selectedDifficulty.value,
|
|
|
|
+ maxPlayers: currentRoom.value.maxPlayers,
|
|
|
|
+ isPrivate: currentRoom.value.visibility === 'private'
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // 创建游戏
|
|
|
|
+ const result = await turtleSoupStore.createGame(gameSettings)
|
|
|
|
+
|
|
|
|
+ if (result.success && result.gameId) {
|
|
|
|
+ // 更新房间状态为游戏中
|
|
|
|
+ await roomStore.updateRoomStatus(RoomStatus.PLAYING)
|
|
|
|
|
|
|
|
+ // 导航到游戏页面
|
|
|
|
+ Taro.redirectTo({
|
|
|
|
+ url: `/pages/room/play/index?roomId=${currentRoom.value.id}&gameId=${result.gameId}`
|
|
|
|
+ })
|
|
|
|
+ } else {
|
|
|
|
+ throw new Error('创建游戏失败')
|
|
|
|
+ }
|
|
|
|
+ } catch (error) {
|
|
|
|
+ console.error('开始游戏失败:', error)
|
|
|
|
+ Taro.showToast({
|
|
|
|
+ title: error instanceof Error ? error.message : '开始游戏失败',
|
|
|
|
+ icon: 'none'
|
|
})
|
|
})
|
|
- })
|
|
|
|
|
|
+ } finally {
|
|
|
|
+ Taro.hideLoading()
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // 房间监听器
|
|
|
|
+ let roomInterval: NodeJS.Timeout | null = null
|
|
|
|
+
|
|
|
|
+ // 开始监听房间变化
|
|
|
|
+ const startRoomListener = () => {
|
|
|
|
+ // 定期刷新房间状态
|
|
|
|
+ roomInterval = setInterval(async () => {
|
|
|
|
+ if (roomId.value) {
|
|
|
|
+ await loadRoomInfo(roomId.value)
|
|
|
|
+
|
|
|
|
+ // 检查房间状态,如果变为"游戏中",需要跳转到游戏页面
|
|
|
|
+ if (currentRoom.value && currentRoom.value.status === RoomStatus.PLAYING) {
|
|
|
|
+ stopRoomListener()
|
|
|
|
+
|
|
|
|
+ // 跳转到游戏页面
|
|
|
|
+ Taro.redirectTo({
|
|
|
|
+ url: `/pages/room/play/index?roomId=${roomId.value}`
|
|
|
|
+ })
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }, 3000) // 每3秒刷新一次
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // 停止监听房间变化
|
|
|
|
+ const stopRoomListener = () => {
|
|
|
|
+ if (roomInterval) {
|
|
|
|
+ clearInterval(roomInterval)
|
|
|
|
+ roomInterval = null
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // 处理分享按钮点击
|
|
|
|
+ const handleShare = () => {
|
|
|
|
+ showShareDialog.value = true
|
|
|
|
+
|
|
|
|
+ // 等待弹窗显示后再生成二维码
|
|
|
|
+ setTimeout(() => {
|
|
|
|
+ generateQRCode()
|
|
|
|
+ }, 300)
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // 处理复制按钮点击
|
|
|
|
+ const handleCopy = (code: string) => {
|
|
|
|
+ // 已在RoomCode组件中处理
|
|
|
|
+ console.log('房间码已复制:', code)
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // 生成二维码
|
|
|
|
+ const generateQRCode = () => {
|
|
|
|
+ if (!currentRoom.value) return
|
|
|
|
+
|
|
|
|
+ // 构建房间链接数据
|
|
|
|
+ let roomData = `room?id=${currentRoom.value.id}`
|
|
|
|
+
|
|
|
|
+ // 如果有密码,也加入密码
|
|
|
|
+ if (currentRoom.value.password) {
|
|
|
|
+ roomData += `&pwd=${currentRoom.value.password}`
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ try {
|
|
|
|
+ // 使用qrcode-base64库生成二维码
|
|
|
|
+ const qrcodeBase64Url = qrcodeBase64.generateQRCodeBase64(roomData, {
|
|
|
|
+ size: 200,
|
|
|
|
+ margin: 2,
|
|
|
|
+ color: {
|
|
|
|
+ dark: '#333333',
|
|
|
|
+ light: '#FFFFFF'
|
|
|
|
+ }
|
|
|
|
+ });
|
|
|
|
+
|
|
|
|
+ // 获取原来的canvas元素所在的父容器
|
|
|
|
+ const container = document.getElementById('qrcode')?.parentNode;
|
|
|
|
+
|
|
|
|
+ if (container) {
|
|
|
|
+ // 移除旧的canvas元素
|
|
|
|
+ const oldCanvas = document.getElementById('qrcode');
|
|
|
|
+ if (oldCanvas) {
|
|
|
|
+ container.removeChild(oldCanvas);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // 创建新的img元素
|
|
|
|
+ const img = document.createElement('img');
|
|
|
|
+ img.id = 'qrcode';
|
|
|
|
+ img.className = 'qrcode-img';
|
|
|
|
+ img.src = qrcodeBase64Url;
|
|
|
|
+ img.style.width = '200px';
|
|
|
|
+ img.style.height = '200px';
|
|
|
|
+
|
|
|
|
+ // 添加到容器中
|
|
|
|
+ container.appendChild(img);
|
|
|
|
+ }
|
|
|
|
+ } catch (error) {
|
|
|
|
+ console.error('生成二维码失败:', error);
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // 页面加载时初始化
|
|
|
|
+ onMounted(() => {
|
|
|
|
+ initPage()
|
|
|
|
+ })
|
|
|
|
+
|
|
|
|
+ // 页面卸载时停止监听
|
|
|
|
+ onUnmounted(() => {
|
|
|
|
+ stopRoomListener()
|
|
|
|
+ })
|
|
|
|
+
|
|
|
|
+ // 监听难度变化,如果已选择主题则重新加载题目
|
|
|
|
+ watch(selectedDifficulty, async (newVal) => {
|
|
|
|
+ if (selectedTheme.value) {
|
|
|
|
+ await loadPuzzles(selectedTheme.value.value)
|
|
|
|
+ selectedPuzzle.value = null // 重置题目选择
|
|
|
|
+ }
|
|
|
|
+ })
|
|
|
|
+
|
|
|
|
+ const selectedThemeValue = ref([])
|
|
|
|
+ const selectedPuzzleValue = ref([])
|
|
|
|
+
|
|
|
|
+ return {
|
|
|
|
+ currentRoom,
|
|
|
|
+ isHost,
|
|
|
|
+ playerCount,
|
|
|
|
+ statusText,
|
|
|
|
+ currentUserReady,
|
|
|
|
+ canStartGame,
|
|
|
|
+ selectedDifficulty,
|
|
|
|
+ selectedTheme,
|
|
|
|
+ selectedPuzzle,
|
|
|
|
+ showThemeSelector,
|
|
|
|
+ showPuzzleSelector,
|
|
|
|
+ showShareDialog,
|
|
|
|
+ themeOptions,
|
|
|
|
+ puzzleOptions,
|
|
|
|
+ gameTitle,
|
|
|
|
+ themeTitle,
|
|
|
|
+ gameDescription,
|
|
|
|
+ difficultyText,
|
|
|
|
+ toggleReady,
|
|
|
|
+ startGame,
|
|
|
|
+ handleShare,
|
|
|
|
+ handleCopy,
|
|
|
|
+ onThemeChange,
|
|
|
|
+ onPuzzleChange,
|
|
|
|
+ // 添加这两个变量
|
|
|
|
+ selectedThemeValue,
|
|
|
|
+ selectedPuzzleValue
|
|
|
|
+ }
|
|
|
|
+ },
|
|
|
|
+
|
|
|
|
+ // 生命周期钩子 - 页面加载
|
|
|
|
+ onLoad() {
|
|
|
|
+ // 使用setup方法处理页面加载
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+</script>
|
|
|
|
+
|
|
|
|
+<style lang="scss">
|
|
|
|
+.waiting-room-page {
|
|
|
|
+ padding: $spacing-base;
|
|
|
|
+ background-color: $background-color-base;
|
|
|
|
+ min-height: 100vh;
|
|
|
|
+ padding-bottom: $spacing-large * 4; // 为底部tabbar留出空间
|
|
|
|
+
|
|
|
|
+ .room-header {
|
|
|
|
+ margin-bottom: $spacing-base;
|
|
|
|
+
|
|
|
|
+ .room-title {
|
|
|
|
+ display: flex;
|
|
|
|
+ align-items: center;
|
|
|
|
+ margin-bottom: $spacing-base;
|
|
|
|
+
|
|
|
|
+ .title {
|
|
|
|
+ font-size: $font-size-large;
|
|
|
|
+ font-weight: $font-weight-bold;
|
|
|
|
+ color: $text-color-primary;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ .tag {
|
|
|
|
+ margin-left: $spacing-base;
|
|
|
|
+ padding: $spacing-mini $spacing-base;
|
|
|
|
+ border-radius: $border-radius-mini;
|
|
|
|
+ font-size: $font-size-small;
|
|
|
|
+
|
|
|
|
+ &.host-tag {
|
|
|
|
+ background-color: $orange-color;
|
|
|
|
+ color: white;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ &.player-tag {
|
|
|
|
+ background-color: $blue-light-color;
|
|
|
|
+ color: white;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ .status-info {
|
|
|
|
+ display: flex;
|
|
|
|
+ justify-content: space-between;
|
|
|
|
+ align-items: center;
|
|
|
|
+
|
|
|
|
+ .players-count, .status-text {
|
|
|
|
+ font-size: $font-size-base;
|
|
|
|
+ color: $text-color-secondary;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ .host-view, .player-view {
|
|
|
|
+ margin-top: $spacing-large;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ .game-settings {
|
|
|
|
+ background-color: $background-color-light;
|
|
|
|
+ border-radius: $border-radius-small;
|
|
|
|
+ padding: $spacing-large;
|
|
|
|
+ margin-bottom: $spacing-large;
|
|
|
|
+ box-shadow: $shadow-light;
|
|
|
|
+
|
|
|
|
+ .setting-item {
|
|
|
|
+ margin-bottom: $spacing-large;
|
|
|
|
+
|
|
|
|
+ .setting-label {
|
|
|
|
+ font-size: $font-size-small;
|
|
|
|
+ color: $text-color-secondary;
|
|
|
|
+ margin-bottom: $spacing-mini;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ .setting-value {
|
|
|
|
+ padding: $spacing-base 0;
|
|
|
|
+ border-bottom: 1px solid $border-color-light;
|
|
|
|
+ display: flex;
|
|
|
|
+ justify-content: space-between;
|
|
|
|
+ align-items: center;
|
|
|
|
+ font-size: $font-size-medium;
|
|
|
|
+ color: $text-color-primary;
|
|
|
|
+
|
|
|
|
+ &:active {
|
|
|
|
+ background-color: $background-color-gray;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ .difficulty-options {
|
|
|
|
+ margin-top: $spacing-base;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ .game-info {
|
|
|
|
+ background-color: $background-color-light;
|
|
|
|
+ border-radius: $border-radius-small;
|
|
|
|
+ padding: $spacing-large;
|
|
|
|
+ margin-bottom: $spacing-large;
|
|
|
|
+ box-shadow: $shadow-light;
|
|
|
|
+
|
|
|
|
+ .info-card {
|
|
|
|
+ .game-title {
|
|
|
|
+ font-size: $font-size-medium;
|
|
|
|
+ font-weight: $font-weight-medium;
|
|
|
|
+ color: $text-color-primary;
|
|
|
|
+ margin-bottom: $spacing-base;
|
|
|
|
+ }
|
|
|
|
|
|
- return {
|
|
|
|
- // 返回需要在模板中使用的数据和方法
|
|
|
|
|
|
+ .game-meta {
|
|
|
|
+ display: flex;
|
|
|
|
+ flex-wrap: wrap;
|
|
|
|
+ margin-bottom: $spacing-large;
|
|
|
|
+
|
|
|
|
+ .meta-item {
|
|
|
|
+ flex: 1;
|
|
|
|
+ min-width: 50%;
|
|
|
|
+ margin-bottom: $spacing-base;
|
|
|
|
+
|
|
|
|
+ .meta-label {
|
|
|
|
+ font-size: $font-size-small;
|
|
|
|
+ color: $text-color-secondary;
|
|
|
|
+ margin-bottom: $spacing-mini;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ .meta-value {
|
|
|
|
+ font-size: $font-size-base;
|
|
|
|
+ color: $text-color-primary;
|
|
|
|
+ font-weight: $font-weight-medium;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ .game-desc {
|
|
|
|
+ font-size: $font-size-base;
|
|
|
|
+ color: $text-color-regular;
|
|
|
|
+ line-height: $line-height-loose;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ .player-list {
|
|
|
|
+ background-color: $background-color-light;
|
|
|
|
+ border-radius: $border-radius-small;
|
|
|
|
+ padding: $spacing-large;
|
|
|
|
+ margin-bottom: $spacing-large;
|
|
|
|
+ box-shadow: $shadow-light;
|
|
|
|
+
|
|
|
|
+ .players {
|
|
|
|
+ .player-item {
|
|
|
|
+ display: flex;
|
|
|
|
+ align-items: center;
|
|
|
|
+ padding: $spacing-base 0;
|
|
|
|
+ border-bottom: 1px solid $border-color-light;
|
|
|
|
+
|
|
|
|
+ &:last-child {
|
|
|
|
+ border-bottom: none;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ &.is-host {
|
|
|
|
+ background-color: $background-color-orange;
|
|
|
|
+ border-radius: $border-radius-mini;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ .player-avatar {
|
|
|
|
+ width: 40px;
|
|
|
|
+ height: 40px;
|
|
|
|
+ margin-right: $spacing-base;
|
|
|
|
+
|
|
|
|
+ image {
|
|
|
|
+ width: 100%;
|
|
|
|
+ height: 100%;
|
|
|
|
+ border-radius: 50%;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ .player-info {
|
|
|
|
+ flex: 1;
|
|
|
|
+
|
|
|
|
+ .player-name {
|
|
|
|
+ font-size: $font-size-base;
|
|
|
|
+ color: $text-color-primary;
|
|
|
|
+ font-weight: $font-weight-medium;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ .player-role {
|
|
|
|
+ font-size: $font-size-small;
|
|
|
|
+ color: $text-color-secondary;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ .player-status {
|
|
|
|
+ padding: $spacing-mini $spacing-base;
|
|
|
|
+ border-radius: $border-radius-mini;
|
|
|
|
+ font-size: $font-size-small;
|
|
|
|
+ background-color: $background-color-gray;
|
|
|
|
+ color: $text-color-secondary;
|
|
|
|
+
|
|
|
|
+ &.ready {
|
|
|
|
+ background-color: $success-color;
|
|
|
|
+ color: white;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
}
|
|
}
|
|
- },
|
|
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ .action-buttons {
|
|
|
|
+ margin-top: $spacing-large;
|
|
|
|
|
|
- // 生命周期钩子 - 页面加载
|
|
|
|
- onLoad() {
|
|
|
|
- // 页面加载时的初始化逻辑
|
|
|
|
|
|
+ .start-button, .ready-button {
|
|
|
|
+ height: 44px;
|
|
|
|
+ font-size: $font-size-medium;
|
|
|
|
+ border-radius: $border-radius-base;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
- </script>
|
|
|
|
|
|
|
|
- <style lang="scss">
|
|
|
|
- .waiting-room-page {
|
|
|
|
- padding: 20px;
|
|
|
|
|
|
+ .share-dialog-content {
|
|
|
|
+ padding: $spacing-large;
|
|
|
|
|
|
- .title {
|
|
|
|
- font-size: 20px;
|
|
|
|
- font-weight: bold;
|
|
|
|
- margin-bottom: 20px;
|
|
|
|
|
|
+ .qrcode-container {
|
|
|
|
+ margin: 0 auto;
|
|
|
|
+ width: 200px;
|
|
|
|
+ height: 200px;
|
|
|
|
+ display: flex;
|
|
|
|
+ justify-content: center;
|
|
|
|
+ align-items: center;
|
|
|
|
+
|
|
|
|
+ .qrcode-canvas {
|
|
|
|
+ width: 100%;
|
|
|
|
+ height: 100%;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ .qrcode-img {
|
|
|
|
+ width: 100%;
|
|
|
|
+ height: 100%;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ .room-info {
|
|
|
|
+ margin-top: $spacing-large;
|
|
text-align: center;
|
|
text-align: center;
|
|
|
|
+
|
|
|
|
+ .room-id, .room-password {
|
|
|
|
+ font-size: $font-size-base;
|
|
|
|
+ color: $text-color-primary;
|
|
|
|
+ margin-bottom: $spacing-small;
|
|
|
|
+ }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
- </style>
|
|
|
|
|
|
+}
|
|
|
|
+</style>
|