Bladeren bron

waiting page

wuzj 4 dagen geleden
bovenliggende
commit
9789d2c45f

+ 4 - 2
components.d.ts

@@ -8,16 +8,18 @@ export {}
 declare module 'vue' {
   export interface GlobalComponents {
     GameCard: typeof import('./src/components/GameCard/index.vue')['default']
-    GameProgress: typeof import('./src/components/GameProgress/index.vue')['default']
     HintCard: typeof import('./src/components/HintCard/index.vue')['default']
-    HostOnly: typeof import('./src/components/HostOnly/index.vue')['default']
     NutBarrage: typeof import('@nutui/nutui-taro')['Barrage']
     NutButton: typeof import('@nutui/nutui-taro')['Button']
+    NutCascader: typeof import('@nutui/nutui-taro')['Cascader']
+    NutDialog: typeof import('@nutui/nutui-taro')['Dialog']
     NutDivider: typeof import('@nutui/nutui-taro')['Divider']
     NutEmpty: typeof import('@nutui/nutui-taro')['Empty']
     NutInfiniteLoading: typeof import('@nutui/nutui-taro')['InfiniteLoading']
     NutInput: typeof import('@nutui/nutui-taro')['Input']
+    NutPopup: typeof import('@nutui/nutui-taro')['Popup']
     NutRadio: typeof import('@nutui/nutui-taro')['Radio']
+    NutRadiogroup: typeof import('@nutui/nutui-taro')['Radiogroup']
     NutRadioGroup: typeof import('@nutui/nutui-taro')['RadioGroup']
     NutRange: typeof import('@nutui/nutui-taro')['Range']
     NutRate: typeof import('@nutui/nutui-taro')['Rate']

+ 1 - 0
package.json

@@ -42,6 +42,7 @@
     "@tarojs/shared": "4.0.9",
     "@tarojs/taro": "4.0.9",
     "pinia": "^3.0.1",
+    "qrcode-base64": "^1.0.1",
     "vue": "^3.3.0"
   },
   "devDependencies": {

+ 8 - 0
pnpm-lock.yaml

@@ -62,6 +62,9 @@ importers:
       pinia:
         specifier: ^3.0.1
         version: 3.0.1(typescript@5.8.2)(vue@3.5.13(typescript@5.8.2))
+      qrcode-base64:
+        specifier: ^1.0.1
+        version: 1.0.1
       vue:
         specifier: ^3.3.0
         version: 3.5.13(typescript@5.8.2)
@@ -5116,6 +5119,9 @@ packages:
     resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
     engines: {node: '>=6'}
 
+  qrcode-base64@1.0.1:
+    resolution: {integrity: sha512-B0BDmnRSvbj47Nq/+IeZjj22afNrf4rwIloNrSXN6OwnkoO+Q0hsUa53gCEfqNQjx+OcytF3azvQhi6wV2Tk6g==}
+
   qs@6.13.0:
     resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==}
     engines: {node: '>=0.6'}
@@ -11802,6 +11808,8 @@ snapshots:
 
   punycode@2.3.1: {}
 
+  qrcode-base64@1.0.1: {}
+
   qs@6.13.0:
     dependencies:
       side-channel: 1.1.0

+ 0 - 59
src/components/GameProgress/index.vue

@@ -1,59 +0,0 @@
-<template>
-    <view class="game-progress">
-      <view class="game-progress__title">解谜进度</view>
-      <view class="game-progress__bar">
-        <view class="game-progress__track">
-          <view 
-            class="game-progress__fill" 
-            :style="{width: `${progress}%`}"
-          ></view>
-        </view>
-        <view class="game-progress__value">{{ progress }}%</view>
-      </view>
-    </view>
-  </template>
-  
-  <script setup lang="ts">
-  defineProps({
-    progress: { type: Number, default: 0 }
-  });
-  </script>
-  
-  <style lang="scss">
-  .game-progress {
-    margin: $spacing-base 0;
-    
-    &__title {
-      font-size: $font-size-small;
-      color: $text-color-secondary;
-      margin-bottom: $spacing-mini;
-    }
-    
-    &__bar {
-      display: flex;
-      align-items: center;
-    }
-    
-    &__track {
-      flex: 1;
-      height: 6px;
-      background-color: $background-color-gray;
-      border-radius: $border-radius-circle;
-      overflow: hidden;
-      margin-right: $spacing-small;
-    }
-    
-    &__fill {
-      height: 100%;
-      background-color: $primary-color;
-      border-radius: $border-radius-circle;
-      transition: width $animation-duration-base;
-    }
-    
-    &__value {
-      font-size: $font-size-small;
-      color: $text-color-secondary;
-      min-width: 36px;
-    }
-  }
-  </style>

+ 0 - 15
src/components/HostOnly/index.vue

@@ -1,15 +0,0 @@
-<template>
-    <slot v-if="isHost"></slot>
-  </template>
-  
-  <script setup lang="ts">
-  import { computed } from 'vue'
-  import { useRoomStore } from '@/stores'
-  
-  const props = defineProps<{
-    roomId?: string
-  }>()
-  
-  const roomStore = useRoomStore()
-  const isHost = computed(() => roomStore.isHost)
-  </script>

+ 28 - 14
src/pages/index/index.vue

@@ -217,22 +217,36 @@ export default {
     
     // 弹幕列表
     const allBarrages = [
-      '排队3小时,飞行5分钟……霍格沃茨的入学考试这么难吗?',
-      '分院帽说:你排队的毅力,适合去赫奇帕奇(干饭人学院)!',
-      '伏地魔当年要是知道排队这么痛苦,可能直接放弃复活了……',
-      '弹射4.5秒,排队4.5小时——这波是"速度与憋尿"!',
-      '威震天:你们人类排队的样子,比我的变形还慢!',
+      '排队3小时,飞行5分钟……',
+      '霍格沃茨的入学考试这么难吗?',
+      '分院帽说:',
+      '你排队的毅力,适合去赫奇帕奇(干饭人学院)!',
+      '伏地魔当年要是知道排队这么痛苦,',
+      '可能直接放弃复活了……',
+      '弹射4.5秒,排队4.5小时——',
+      '这波是"速度与憋尿"!',
+      '威震天:',
+      '你们人类排队的样子,比我的变形还慢!',
       '建议园区卖"小黄人式躺平"排队专用垫……',
       '这是一条广告招租位~~~',
-      '猜猜伏地魔为什么没鼻子?——来我游戏里开脑洞!',
-      '排队3小时,汤底5分钟……不如来玩「海龟汤」速开局!',
-      '建议园区WiFi覆盖「海龟汤」房间,拯救站麻的腿……',
-      '「海龟汤」新题:《为什么这个队永远不动?》——欢迎破解!',
-      '排队时玩「海龟汤」,智商碾压隔壁猜拳的!',
-      '优速通抢不到?「海龟汤」秒开房——快乐不排队!',
-      '等位2小时,瓜子嗑到上火?不如来「海龟汤」降降温!',
-      '服务员:您前面还有58桌……我:先来一局「海龟汤」压压惊!',
-      '海底捞隐藏福利:玩「海龟汤」猜对3题,送免费美甲!',
+      '猜猜伏地魔为什么没鼻子?',
+      '——来我游戏里开脑洞!',
+      '排队3小时,汤底5分钟……',
+      '不如来玩「海龟汤」速开局!',
+      '建议园区WiFi覆盖「海龟汤」房间,',
+      '拯救站麻的腿……',
+      '「海龟汤」新题:',
+      '《为什么这个队永远不动?》——欢迎破解!',
+      '排队时玩「海龟汤」,',
+      '智商碾压隔壁猜拳的!',
+      '优速通抢不到?',
+      '「海龟汤」秒开房——快乐不排队!',
+      '等位2小时,瓜子嗑到上火?',
+      '不如来「海龟汤」降降温!',
+      '服务员:您前面还有58桌……',
+      '我:先来一局「海龟汤」压压惊!',
+      '海底捞隐藏福利:',
+      '玩「海龟汤」猜对3题,送免费美甲!',
       '猜猜这部片的凶手是谁?'
     ]
     

+ 3 - 0
src/pages/room/play/index.config.ts

@@ -0,0 +1,3 @@
+export default {
+  navigationBarTitleText: '游戏房间'
+} 

+ 3 - 0
src/pages/room/waiting/index.config.ts

@@ -0,0 +1,3 @@
+export default {
+  navigationBarTitleText: '等待场景'
+} 

+ 947 - 43
src/pages/room/waiting/index.vue

@@ -1,62 +1,966 @@
 <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>
-    <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;
+      
+      .room-id, .room-password {
+        font-size: $font-size-base;
+        color: $text-color-primary;
+        margin-bottom: $spacing-small;
+      }
     }
   }
-  </style>
+}
+</style>

+ 91 - 0
src/services/room.ts

@@ -528,5 +528,96 @@ export const roomService = {
       joinTime: Date.now(),
       isReady: isHoster
     }
+  },
+
+  // 更新房间中的用户信息
+  async updateUserInRoom(roomId: string, userId: string, userData: Partial<RoomUserInfo>): Promise<{success: boolean, message?: string}> {
+    try {
+      // 从本地缓存找到房间
+      const allRooms = Taro.getStorageSync('allRooms') || '{}'
+      const roomsObj = JSON.parse(allRooms)
+      
+      if (roomsObj[roomId]) {
+        const room = roomsObj[roomId]
+        
+        // 找到对应用户
+        const userIndex = room.users.findIndex(u => u.openid === userId)
+        if (userIndex !== -1) {
+          // 更新用户信息
+          room.users[userIndex] = {
+            ...room.users[userIndex],
+            ...userData
+          }
+          
+          // 更新本地缓存
+          roomsObj[roomId] = room
+          Taro.setStorageSync('allRooms', JSON.stringify(roomsObj))
+          
+          return { success: true }
+        } else {
+          return { success: false, message: '用户不在房间中' }
+        }
+      } else if (mockRooms[roomId]) {
+        // 更新mock数据
+        const room = mockRooms[roomId]
+        const userIndex = room.users.findIndex(u => u.openid === userId)
+        
+        if (userIndex !== -1) {
+          room.users[userIndex] = {
+            ...room.users[userIndex],
+            ...userData
+          }
+          return { success: true }
+        } else {
+          return { success: false, message: '用户不在房间中' }
+        }
+      }
+      
+      return { success: false, message: '房间不存在' }
+    } catch (error) {
+      console.error('更新用户信息失败:', error)
+      return { success: false, message: '更新用户信息失败' }
+    }
+  },
+
+  // 更新房间状态
+  async updateRoomStatus(roomId: string, status: RoomStatus): Promise<{success: boolean, message?: string}> {
+    try {
+      // 从本地缓存找到房间
+      const allRooms = Taro.getStorageSync('allRooms') || '{}'
+      const roomsObj = JSON.parse(allRooms)
+      
+      if (roomsObj[roomId]) {
+        const room = roomsObj[roomId]
+        
+        // 更新状态
+        room.status = status
+        
+        // 更新本地缓存
+        roomsObj[roomId] = room
+        Taro.setStorageSync('allRooms', JSON.stringify(roomsObj))
+        
+        // 同时更新最近房间列表中的状态
+        const recentRooms = Taro.getStorageSync('recentRooms') || '[]'
+        const rooms = JSON.parse(recentRooms)
+        const roomIndex = rooms.findIndex((r: RecentRoom) => r.id === roomId)
+        
+        if (roomIndex !== -1) {
+          rooms[roomIndex].status = status
+          Taro.setStorageSync('recentRooms', JSON.stringify(rooms))
+        }
+        
+        return { success: true }
+      } else if (mockRooms[roomId]) {
+        // 更新mock数据
+        mockRooms[roomId].status = status
+        return { success: true }
+      }
+      
+      return { success: false, message: '房间不存在' }
+    } catch (error) {
+      console.error('更新房间状态失败:', error)
+      return { success: false, message: '更新房间状态失败' }
+    }
   }
 }

+ 38 - 10
src/stores/room.ts

@@ -291,22 +291,50 @@ export const useRoomStore = defineStore('room', () => {
   }
   
   // 更新用户信息
-  function updateUserInRoom(userId: string, data: Partial<RoomUserInfo>) {
-    if (currentRoom.value) {
-      const userIndex = currentRoom.value.users.findIndex(u => u.openid === userId)
-      if (userIndex !== -1) {
-        currentRoom.value.users[userIndex] = {
-          ...currentRoom.value.users[userIndex],
-          ...data
+  async function updateUserInRoom(userId: string, data: Partial<RoomUserInfo>) {
+    if (!currentRoom.value) return { success: false, message: '当前没有加入房间' }
+    
+    try {
+      // 调用API更新用户信息
+      const result = await roomService.updateUserInRoom(currentRoom.value.id, userId, data)
+      
+      if (result.success) {
+        // 更新本地状态
+        const userIndex = currentRoom.value.users.findIndex(u => u.openid === userId)
+        if (userIndex !== -1) {
+          currentRoom.value.users[userIndex] = {
+            ...currentRoom.value.users[userIndex],
+            ...data
+          }
         }
+        return result
+      } else {
+        return result
       }
+    } catch (error) {
+      console.error('更新用户信息失败:', error)
+      return { success: false, message: '更新用户信息失败' }
     }
   }
   
   // 更新房间状态
-  function updateRoomStatus(status: RoomStatus) {
-    if (currentRoom.value) {
-      currentRoom.value.status = status
+  async function updateRoomStatus(status: RoomStatus) {
+    if (!currentRoom.value) return { success: false, message: '当前没有加入房间' }
+    
+    try {
+      // 调用API更新房间状态
+      const result = await roomService.updateRoomStatus(currentRoom.value.id, status)
+      
+      if (result.success) {
+        // 更新本地状态
+        currentRoom.value.status = status
+        return result
+      } else {
+        return result
+      }
+    } catch (error) {
+      console.error('更新房间状态失败:', error)
+      return { success: false, message: '更新房间状态失败' }
     }
   }
   

+ 3 - 0
src/types/games/turtlesoup.ts

@@ -83,6 +83,9 @@ export interface TurtleSoupPuzzle {
   description: string;            // 题目描述
   difficulty: TurtleSoupDifficulty; // 题目难度
   averageDuration: number;        // 平均完成时间(分钟)
+  price?: number;                  // 价格
+  isNew?: boolean;                // 是否为新题目
+  isLocked?: boolean;             // 是否锁定(需要购买)
 }
 
 /**