Browse Source

分离waiting组件

wuzj 3 days ago
parent
commit
59a5ff1252

+ 8 - 0
components.d.ts

@@ -7,8 +7,11 @@ export {}
 
 
 declare module 'vue' {
 declare module 'vue' {
   export interface GlobalComponents {
   export interface GlobalComponents {
+    DifficultySelector: typeof import('./src/components/room/selectors/DifficultySelector.vue')['default']
     GameCard: typeof import('./src/components/GameCard/index.vue')['default']
     GameCard: typeof import('./src/components/GameCard/index.vue')['default']
     HintCard: typeof import('./src/components/HintCard/index.vue')['default']
     HintCard: typeof import('./src/components/HintCard/index.vue')['default']
+    HostSettings: typeof import('./src/components/room/host/HostSettings.vue')['default']
+    HostView: typeof import('./src/components/room/host/HostView.vue')['default']
     NutBarrage: typeof import('@nutui/nutui-taro')['Barrage']
     NutBarrage: typeof import('@nutui/nutui-taro')['Barrage']
     NutButton: typeof import('@nutui/nutui-taro')['Button']
     NutButton: typeof import('@nutui/nutui-taro')['Button']
     NutCell: typeof import('@nutui/nutui-taro')['Cell']
     NutCell: typeof import('@nutui/nutui-taro')['Cell']
@@ -28,8 +31,13 @@ declare module 'vue' {
     NutTabbarItem: typeof import('@nutui/nutui-taro')['TabbarItem']
     NutTabbarItem: typeof import('@nutui/nutui-taro')['TabbarItem']
     NutTabPane: typeof import('@nutui/nutui-taro')['TabPane']
     NutTabPane: typeof import('@nutui/nutui-taro')['TabPane']
     NutTabs: typeof import('@nutui/nutui-taro')['Tabs']
     NutTabs: typeof import('@nutui/nutui-taro')['Tabs']
+    PlayerList: typeof import('./src/components/room/PlayerList.vue')['default']
+    PlayerView: typeof import('./src/components/room/player/PlayerView.vue')['default']
+    PuzzleSelector: typeof import('./src/components/room/selectors/PuzzleSelector.vue')['default']
     RoomCode: typeof import('./src/components/RoomCode/index.vue')['default']
     RoomCode: typeof import('./src/components/RoomCode/index.vue')['default']
+    RoomHeader: typeof import('./src/components/room/RoomHeader.vue')['default']
     Tabbar: typeof import('./src/components/Tabbar.vue')['default']
     Tabbar: typeof import('./src/components/Tabbar.vue')['default']
+    ThemeSelector: typeof import('./src/components/room/selectors/ThemeSelector.vue')['default']
     WereWolf: typeof import('./src/components/WereWolf.vue')['default']
     WereWolf: typeof import('./src/components/WereWolf.vue')['default']
   }
   }
 }
 }

+ 1 - 1
project.config.json

@@ -2,7 +2,7 @@
   "miniprogramRoot": "./dist",
   "miniprogramRoot": "./dist",
   "projectname": "miniprogram-linejoy",
   "projectname": "miniprogram-linejoy",
   "description": "linejoy",
   "description": "linejoy",
-  "appid": "wx0b170c7cbab32743",
+  "appid": "wx72f9783f24443ce9",
   "setting": {
   "setting": {
     "urlCheck": true,
     "urlCheck": true,
     "es6": false,
     "es6": false,

+ 1 - 1
project.tt.json

@@ -2,7 +2,7 @@
   "miniprogramRoot": "./",
   "miniprogramRoot": "./",
   "projectname": "miniprogram-linejoy",
   "projectname": "miniprogram-linejoy",
   "description": "linejoy",
   "description": "linejoy",
-  "appid": "wxa9425d95031e81a0",
+  "appid": "wx72f9783f24443ce9",
   "setting": {
   "setting": {
     "urlCheck": true,
     "urlCheck": true,
     "es6": false,
     "es6": false,

+ 126 - 0
src/components/room/PlayerList.vue

@@ -0,0 +1,126 @@
+<template>
+    <view class="player-list">
+      <nut-divider 
+        content-position="center"
+        :style="{ color: '#3C92FB', borderColor: '#3C92FB', padding: '0 16px', margin: '12px 0 12px 0' }"
+      >
+        玩家列表
+      </nut-divider>
+      
+      <view class="players">
+        <view 
+          v-for="user in users" 
+          :key="user.openid" 
+          class="player-item"
+          :class="{ 'is-host': user.roomRole === 'hoster' }"
+        >
+          <view class="player-avatar">
+            <image :src="user.avatar || '/assets/default-avatar.png'" mode="aspectFill" @error="handleAvatarError" />
+          </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>
+  </template>
+  
+  <script lang="ts">
+  import { defineComponent, PropType } from 'vue'
+  import { RoomRole, RoomUserInfo } from '@/types/room'
+  
+  export default defineComponent({
+    name: 'PlayerList',
+    props: {
+      users: {
+        type: Array as PropType<RoomUserInfo[]>,
+        default: () => []
+      }
+    },
+    setup() {
+      const handleAvatarError = (e: any) => {
+        e.target.src = '/assets/default-avatar.png'
+      }
+  
+      return {
+        handleAvatarError,
+        RoomRole
+      }
+    }
+  })
+  </script>
+  
+  <style lang="scss">
+  .player-list {
+    background-color: $background-color-light;
+    border-radius: $border-radius-small;
+    padding: $spacing-base;
+    margin-bottom: $spacing-base;
+    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: rgba(255, 235, 210, 0.3);
+          border-radius: $border-radius-mini;
+          padding: $spacing-base;
+        }
+        
+        .player-avatar {
+          width: 40px;
+          height: 40px;
+          margin-right: $spacing-base;
+          border-radius: 50%;
+          overflow: hidden;
+          
+          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;
+          }
+        }
+      }
+    }
+  }
+  </style>

+ 92 - 0
src/components/room/RoomHeader.vue

@@ -0,0 +1,92 @@
+<template>
+    <view
+      class="room-header"
+      :class="{ 'host-background': isHost, 'player-background': !isHost }"
+    >
+      <view 
+        class="room-title"
+        :class="{ 'host-title': isHost, 'player-title': !isHost }"
+      >
+        {{ roomName }}
+      </view>
+      <view 
+        class="room-status"
+        :class="{ 'host-status': isHost, 'player-status': !isHost }"
+      >
+        {{ playerCount }}/{{ maxPlayers }} 已加入,{{ statusText }}
+      </view>
+    </view>
+  </template>
+  
+  <script lang="ts">
+  import { defineComponent } from 'vue'
+  
+  export default defineComponent({
+    name: 'RoomHeader',
+    props: {
+      roomName: {
+        type: String,
+        default: '等待房间'
+      },
+      isHost: {
+        type: Boolean,
+        default: false
+      },
+      playerCount: {
+        type: Number,
+        default: 0
+      },
+      maxPlayers: {
+        type: Number,
+        default: 0
+      },
+      statusText: {
+        type: String,
+        default: '等待中'
+      }
+    }
+  })
+  </script>
+  
+  <style lang="scss">
+  .room-header {
+    padding: $spacing-base;
+    height: 45px;
+    text-align: center;
+  
+    .room-title {
+      font-size: 12px;
+      font-weight: bold;
+      margin-bottom: 4px;
+  
+      &.host-title {
+        color: $text-color-orange;
+      }
+  
+      &.player-title {
+        color: $text-color-blue;
+      }
+    }
+    
+    .room-status {
+      font-size: 8px;
+      margin-bottom: 4px;
+  
+      &.host-status {
+        color: $text-color-orange;
+      }
+  
+      &.player-status {
+        color: $text-color-blue;
+      }
+    }
+  
+    &.host-background {
+      background-color: $orange-light-color;
+    }
+  
+    &.player-background {
+      background-color: $blue-light-color;
+    }
+  }
+  </style>

+ 125 - 0
src/components/room/host/HostSettings.vue

@@ -0,0 +1,125 @@
+<template>
+    <view class="game-settings">
+      <nut-divider 
+        content-position="center"
+        :style="{ color: '#3C92FB', borderColor: '#3C92FB', padding: '0 16px', margin: '12px 0 12px 0' }"
+      >
+        游戏设置
+      </nut-divider>
+      
+      <!-- 1. 主题选择 -->
+      <view class="setting-item">
+        <view class="setting-label">主题选择</view>
+        <nut-cell 
+          :desc="selectedTheme?.text || '请选择游戏主题'" 
+          @click="onShowThemeSelector"
+        >
+          <template #link>
+            <IconFont name="right" size="16"></IconFont>
+          </template>
+        </nut-cell>
+      </view>
+      
+      <!-- 2. 难度选择 -->
+      <view class="setting-item">
+        <view class="setting-label">游戏难度</view>
+        <nut-cell 
+          :desc="difficultyText" 
+          @click="onShowDifficultySelector"
+        >
+          <template #link>
+            <IconFont name="right" size="16"></IconFont>
+          </template>
+        </nut-cell>
+      </view>
+      
+      <!-- 3. 题目选择 (只有当主题和难度都选择后才显示) -->
+      <view class="setting-item" v-if="selectedThemeId && selectedDifficulty">
+        <view class="setting-label">题目选择</view>
+        <nut-cell 
+          :desc="selectedPuzzle?.text || '请选择游戏题目'" 
+          @click="selectedThemeId ? onShowPuzzleSelector() : null"
+        >
+          <template #link>
+            <IconFont name="right" size="16"></IconFont>
+          </template>
+        </nut-cell>
+      </view>
+    </view>
+  </template>
+  
+  <script lang="ts">
+  import { defineComponent, PropType } from 'vue'
+  import { IconFont } from '@nutui/icons-vue-taro'
+  import { TurtleSoupDifficulty } from '@/types/games/turtlesoup'
+  import { CascaderOption } from '@/types/cascader'
+  
+  export default defineComponent({
+    name: 'GameSettings',
+    components: {
+      IconFont
+    },
+    props: {
+      selectedTheme: {
+        type: Object as PropType<CascaderOption | null>,
+        default: null
+      },
+      selectedPuzzle: {
+        type: Object as PropType<CascaderOption | null>,
+        default: null
+      },
+      selectedThemeId: {
+        type: String,
+        default: ''
+      },
+      selectedDifficulty: {
+        type: String,
+        default: TurtleSoupDifficulty.MEDIUM
+      },
+      difficultyText: {
+        type: String,
+        default: '中等'
+      }
+    },
+    setup(props, { emit }) {
+      const onShowThemeSelector = () => {
+        emit('show-theme-selector')
+      }
+      
+      const onShowDifficultySelector = () => {
+        emit('show-difficulty-selector')
+      }
+      
+      const onShowPuzzleSelector = () => {
+        emit('show-puzzle-selector')
+      }
+  
+      return {
+        onShowThemeSelector,
+        onShowDifficultySelector,
+        onShowPuzzleSelector
+      }
+    }
+  })
+  </script>
+  
+  <style lang="scss">
+  .game-settings {
+    background-color: $background-color-light;
+    border-radius: $border-radius-small;
+    padding: $spacing-base;
+    margin-bottom: $spacing-base;
+    box-shadow: $shadow-light;
+    
+    .setting-item {
+      margin-bottom: $spacing-base;
+      
+      .setting-label {
+        font-size: $font-size-small;
+        color: $text-color-secondary;
+        margin-bottom: $spacing-mini;
+        font-weight: $font-weight-medium;
+      }
+    }
+  }
+  </style>

+ 94 - 0
src/components/room/host/HostView.vue

@@ -0,0 +1,94 @@
+<template>
+    <view class="host-view">
+      <!-- 游戏设置模块 -->
+      <GameSettings 
+        :selectedTheme="selectedTheme" 
+        :selectedPuzzle="selectedPuzzle"
+        :selectedThemeId="selectedThemeId"
+        :selectedDifficulty="selectedDifficulty"
+        :difficultyText="difficultyText"
+        @show-theme-selector="$emit('show-theme-selector')"
+        @show-difficulty-selector="$emit('show-difficulty-selector')"
+        @show-puzzle-selector="$emit('show-puzzle-selector')"
+      />
+      
+      <!-- 玩家列表 -->
+      <PlayerList :users="users" />
+      
+      <!-- 主持人操作按钮 -->
+      <view class="action-buttons">
+        <nut-button 
+          block 
+          color="#3C92FB" 
+          class="start-button" 
+          :disabled="!canStartGame"
+          @click="$emit('start-game')"
+        >
+          开始游戏
+        </nut-button>
+      </view>
+    </view>
+  </template>
+  
+  <script lang="ts">
+  import { defineComponent, PropType } from 'vue'
+  import GameSettings from './HostSettings.vue'
+  import PlayerList from '../PlayerList.vue'
+  import { RoomUserInfo } from '@/types/room'
+  import { type CascaderOption } from '@/types/cascader'
+  
+  export default defineComponent({
+    name: 'HostView',
+    components: {
+      GameSettings,
+      PlayerList
+    },
+    props: {
+      users: {
+        type: Array as PropType<RoomUserInfo[]>,
+        default: () => []
+      },
+      selectedTheme: {
+        type: Object as PropType<CascaderOption | null>,
+        default: null
+      },
+      selectedPuzzle: {
+        type: Object as PropType<CascaderOption | null>,
+        default: null
+      },
+      selectedThemeId: {
+        type: String,
+        default: ''
+      },
+      selectedDifficulty: {
+        type: String,
+        default: ''
+      },
+      difficultyText: {
+        type: String,
+        default: '中等'
+      },
+      canStartGame: {
+        type: Boolean,
+        default: false
+      }
+    },
+    emits: ['show-theme-selector', 'show-difficulty-selector', 'show-puzzle-selector', 'start-game']
+  })
+  </script>
+  
+  <style lang="scss">
+  .host-view {
+    margin-top: $spacing-large;
+    
+    .action-buttons {
+      margin-top: $spacing-large;
+      
+      .start-button {
+        height: 44px;
+        font-size: $font-size-medium;
+        border-radius: $border-radius-base;
+      }
+    }
+  }
+  </style>

+ 148 - 0
src/components/room/player/PlayerView.vue

@@ -0,0 +1,148 @@
+<template>
+    <view class="player-view">
+      <!-- 游戏信息展示 -->
+      <view class="game-info">
+        <nut-divider 
+          content-position="center"
+          :style="{ color: '#3C92FB', borderColor: '#3C92FB', padding: '0 16px', margin: '12px 0 12px 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>
+      
+      <!-- 玩家列表 -->
+      <PlayerList :users="users" />
+      
+      <!-- 玩家操作按钮 -->
+      <view class="action-buttons">
+        <nut-button 
+          block 
+          :color="currentUserReady ? '#999' : '#3C92FB'" 
+          class="ready-button" 
+          @click="$emit('toggle-ready')"
+        >
+          {{ currentUserReady ? '取消准备' : '准备' }}
+        </nut-button>
+      </view>
+    </view>
+  </template>
+  
+  <script lang="ts">
+  import { defineComponent, PropType } from 'vue'
+  import PlayerList from '../PlayerList.vue'
+  import { RoomUserInfo } from '@/types/room'
+  
+  export default defineComponent({
+    name: 'PlayerView',
+    components: {
+      PlayerList
+    },
+    props: {
+      gameTitle: {
+        type: String,
+        default: ''
+      },
+      themeTitle: {
+        type: String,
+        default: ''
+      },
+      difficultyText: {
+        type: String,
+        default: '中等'
+      },
+      gameDescription: {
+        type: String,
+        default: ''
+      },
+      users: {
+        type: Array as PropType<RoomUserInfo[]>,
+        default: () => []
+      },
+      currentUserReady: {
+        type: Boolean,
+        default: false
+      }
+    },
+    emits: ['toggle-ready']
+  })
+  </script>
+  
+  <style lang="scss">
+  .player-view {
+    margin-top: $spacing-large;
+    
+    .game-info {
+      background-color: $background-color-light;
+      border-radius: $border-radius-small;
+      padding: $spacing-base;
+      margin-bottom: $spacing-base;
+      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;
+        }
+        
+        .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;
+        }
+      }
+    }
+    
+    .action-buttons {
+      margin-top: $spacing-large;
+      
+      .ready-button {
+        height: 44px;
+        font-size: $font-size-medium;
+        border-radius: $border-radius-base;
+      }
+    }
+  }
+  </style>

+ 149 - 0
src/components/room/selectors/DifficultySelector.vue

@@ -0,0 +1,149 @@
+<template>
+    <nut-popup v-model:visible="show" position="bottom">
+      <view class="selector-container">
+        <view class="selector-header">
+          <view class="selector-title">选择游戏难度</view>
+          <nut-button size="small" @click="onClose">取消</nut-button>
+        </view>
+        <view class="difficulty-list">
+          <view 
+            class="difficulty-option" 
+            :class="{ 'selected': selectedDifficulty === difficulties.EASY }"
+            @click="onSelect(difficulties.EASY)"
+          >
+            <view class="option-content">
+              <view class="option-title">简单</view>
+              <view class="option-desc">适合新手玩家,游戏时间较短</view>
+            </view>
+            <IconFont v-if="selectedDifficulty === difficulties.EASY" name="check" color="#3C92FB" size="16"></IconFont>
+          </view>
+          <view 
+            class="difficulty-option" 
+            :class="{ 'selected': selectedDifficulty === difficulties.MEDIUM }"
+            @click="onSelect(difficulties.MEDIUM)"
+          >
+            <view class="option-content">
+              <view class="option-title">中等</view>
+              <view class="option-desc">平衡挑战与乐趣,适合大多数玩家</view>
+            </view>
+            <IconFont v-if="selectedDifficulty === difficulties.MEDIUM" name="check" color="#3C92FB" size="16"></IconFont>
+          </view>
+          <view 
+            class="difficulty-option" 
+            :class="{ 'selected': selectedDifficulty === difficulties.HARD }"
+            @click="onSelect(difficulties.HARD)"
+          >
+            <view class="option-content">
+              <view class="option-title">困难</view>
+              <view class="option-desc">高难度挑战,适合有经验的玩家</view>
+            </view>
+            <IconFont v-if="selectedDifficulty === difficulties.HARD" name="check" color="#3C92FB" size="16"></IconFont>
+          </view>
+        </view>
+      </view>
+    </nut-popup>
+  </template>
+  
+  <script lang="ts">
+  import { defineComponent, computed } from 'vue'
+  import { IconFont } from '@nutui/icons-vue-taro'
+  import { TurtleSoupDifficulty } from '@/types/games/turtlesoup'
+  
+  export default defineComponent({
+    name: 'DifficultySelector',
+    components: {
+      IconFont
+    },
+    props: {
+      visible: {
+        type: Boolean,
+        default: false
+      },
+      selectedDifficulty: {
+        type: String,
+        default: TurtleSoupDifficulty.MEDIUM
+      }
+    },
+    emits: ['update:visible', 'select-difficulty'],
+    setup(props, { emit }) {
+      const show = computed({
+        get: () => props.visible,
+        set: (val) => emit('update:visible', val)
+      })
+  
+      const onClose = () => {
+        show.value = false
+      }
+  
+      const onSelect = (difficulty) => {
+        emit('select-difficulty', difficulty)
+      }
+  
+      return {
+        show,
+        onClose,
+        onSelect,
+        difficulties: TurtleSoupDifficulty
+      }
+    }
+  })
+  </script>
+  
+  <style lang="scss">
+  .selector-container {
+    padding: $spacing-base;
+    
+    .selector-header {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      padding: $spacing-base 0;
+      margin-bottom: $spacing-base;
+      border-bottom: 1px solid $border-color-light;
+      
+      .selector-title {
+        font-size: $font-size-medium;
+        font-weight: $font-weight-medium;
+        color: $text-color-primary;
+      }
+    }
+    
+    .difficulty-list {
+      padding: $spacing-small;
+      
+      .difficulty-option {
+        background-color: $background-color-base;
+        padding: $spacing-base;
+        border-radius: $border-radius-mini;
+        margin-bottom: $spacing-small;
+        box-shadow: $shadow-light;
+        display: flex;
+        justify-content: space-between;
+        align-items: center;
+        
+        &:last-child {
+          margin-bottom: 0;
+        }
+        
+        &.selected {
+          background-color: rgba(60, 146, 251, 0.1);
+        }
+        
+        .option-content {
+          flex: 1;
+          
+          .option-title {
+            font-size: $font-size-base;
+            color: $text-color-primary;
+            margin-bottom: $spacing-mini;
+          }
+          
+          .option-desc {
+            font-size: $font-size-small;
+            color: $text-color-secondary;
+          }
+        }
+      }
+    }
+  }
+  </style>

+ 153 - 0
src/components/room/selectors/PuzzleSelector.vue

@@ -0,0 +1,153 @@
+<template>
+    <nut-popup v-model:visible="show" position="bottom">
+      <view class="selector-container">
+        <view class="selector-header">
+          <view class="selector-title">选择游戏题目</view>
+          <nut-button size="small" @click="onClose">取消</nut-button>
+        </view>
+        <scroll-view 
+          scroll-y 
+          class="puzzle-scroll"
+          :style="{ maxHeight: scrollHeight + 'px' }"
+        >
+          <view class="puzzle-list">
+            <view 
+              v-for="puzzle in puzzleOptions" 
+              :key="puzzle.value" 
+              class="puzzle-option"
+              :class="{ 'disabled': puzzle.disabled, 'selected': selectedPuzzleId === puzzle.value }"
+              @click="!puzzle.disabled && onSelect(puzzle)"
+            >
+              <view class="option-content">
+                <view class="option-title">{{ puzzle.text }}</view>
+                <view v-if="puzzle.description" class="option-desc">{{ puzzle.description }}</view>
+              </view>
+              <IconFont v-if="selectedPuzzleId === puzzle.value" name="check" color="#3C92FB" size="16"></IconFont>
+            </view>
+          </view>
+        </scroll-view>
+      </view>
+    </nut-popup>
+  </template>
+  
+  <script lang="ts">
+  import { defineComponent, computed, PropType } from 'vue'
+  import { IconFont } from '@nutui/icons-vue-taro'
+  import { CascaderOption } from '@/types/cascader'
+  
+  export default defineComponent({
+    name: 'PuzzleSelector',
+    components: {
+      IconFont
+    },
+    props: {
+      visible: {
+        type: Boolean,
+        default: false
+      },
+      puzzleOptions: {
+        type: Array as PropType<CascaderOption[]>,
+        default: () => []
+      },
+      selectedPuzzleId: {
+        type: String,
+        default: ''
+      },
+      scrollHeight: {
+        type: Number,
+        default: 300
+      }
+    },
+    emits: ['update:visible', 'select-puzzle'],
+    setup(props, { emit }) {
+      const show = computed({
+        get: () => props.visible,
+        set: (val) => emit('update:visible', val)
+      })
+  
+      const onClose = () => {
+        show.value = false
+      }
+  
+      const onSelect = (puzzle) => {
+        emit('select-puzzle', puzzle)
+      }
+  
+      return {
+        show,
+        onClose,
+        onSelect
+      }
+    }
+  })
+  </script>
+  
+  <style lang="scss">
+  .selector-container {
+    padding: $spacing-base;
+    
+    .selector-header {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      padding: $spacing-base 0;
+      margin-bottom: $spacing-base;
+      border-bottom: 1px solid $border-color-light;
+      
+      .selector-title {
+        font-size: $font-size-medium;
+        font-weight: $font-weight-medium;
+        color: $text-color-primary;
+      }
+    }
+    
+    .puzzle-scroll {
+      border: 1px solid $border-color-light;
+      border-radius: $border-radius-small;
+      margin-top: $spacing-base;
+      background-color: $background-color-light;
+    }
+    
+    .puzzle-list {
+      padding: $spacing-small;
+      
+      .puzzle-option {
+        background-color: $background-color-base;
+        padding: $spacing-base;
+        border-radius: $border-radius-mini;
+        margin-bottom: $spacing-small;
+        box-shadow: $shadow-light;
+        display: flex;
+        justify-content: space-between;
+        align-items: center;
+        
+        &:last-child {
+          margin-bottom: 0;
+        }
+        
+        &.disabled {
+          opacity: 0.5;
+        }
+        
+        &.selected {
+          background-color: rgba(60, 146, 251, 0.1);
+        }
+        
+        .option-content {
+          flex: 1;
+          
+          .option-title {
+            font-size: $font-size-base;
+            color: $text-color-primary;
+            margin-bottom: $spacing-mini;
+          }
+          
+          .option-desc {
+            font-size: $font-size-small;
+            color: $text-color-secondary;
+          }
+        }
+      }
+    }
+  }
+  </style>

+ 197 - 0
src/components/room/selectors/ThemeSelector.vue

@@ -0,0 +1,197 @@
+<template>
+    <nut-popup v-model:visible="show" position="bottom">
+      <view class="selector-container">
+        <view class="selector-header">
+          <view class="selector-title">选择游戏主题</view>
+          <nut-button size="small" @click="onClose">取消</nut-button>
+        </view>
+        <scroll-view 
+          scroll-y 
+          class="theme-scroll"
+          :style="{ maxHeight: scrollHeight + 'px' }"
+        >
+          <view class="theme-list">
+            <view 
+              v-for="theme in themeOptions" 
+              :key="theme.value" 
+              class="theme-option"
+              :class="{ 'disabled': theme.disabled && !canPurchase, 'selected': selectedThemeId === theme.value }"
+              @click="handleThemeClick(theme)"
+            >
+              <view class="option-content">
+                <view class="option-title">{{ theme.text }}</view>
+                <view v-if="theme.description" class="option-desc">{{ theme.description }}</view>
+                <view v-if="theme.locked" class="theme-locked">
+                  <IconFont name="lock" size="12"></IconFont>
+                  <view class="lock-text">{{ theme.unlockRequirement }}</view>
+                </view>
+              </view>
+              <!-- 添加解锁购买按钮 -->
+              <view v-if="theme.locked && canPurchase" class="unlock-button" @click.stop="onPurchase(theme)">
+                <nut-button size="small" type="primary">解锁 ({{ theme.price || '5' }}元)</nut-button>
+              </view>
+              <IconFont v-else-if="selectedThemeId === theme.value" name="check" color="#3C92FB" size="16"></IconFont>
+            </view>
+          </view>
+        </scroll-view>
+      </view>
+    </nut-popup>
+  </template>
+  
+  <script lang="ts">
+  import { defineComponent, computed, PropType } from 'vue'
+  import { IconFont } from '@nutui/icons-vue-taro'
+  import { CascaderOption } from '@/types/cascader'
+  
+  export default defineComponent({
+    name: 'ThemeSelector',
+    components: {
+      IconFont
+    },
+    props: {
+      visible: {
+        type: Boolean,
+        default: false
+      },
+      themeOptions: {
+        type: Array as PropType<CascaderOption[]>,
+        default: () => []
+      },
+      selectedThemeId: {
+        type: String,
+        default: ''
+      },
+      scrollHeight: {
+        type: Number,
+        default: 300
+      },
+      canPurchase: {
+        type: Boolean,
+        default: true
+      }
+    },
+    emits: ['update:visible', 'select-theme', 'purchase-theme'],
+    setup(props, { emit }) {
+      const show = computed({
+        get: () => props.visible,
+        set: (val) => emit('update:visible', val)
+      })
+  
+      const onClose = () => {
+        show.value = false
+      }
+  
+      const handleThemeClick = (theme) => {
+        // 如果主题被锁定且不可购买,则不做任何操作
+        if (theme.disabled && !props.canPurchase) {
+          return
+        }
+        
+        // 如果主题被锁定且可购买,不处理(让购买按钮处理)
+        if (theme.locked && props.canPurchase) {
+          return
+        }
+        
+        // 正常选择主题
+        emit('select-theme', theme)
+      }
+  
+      const onPurchase = (theme) => {
+        emit('purchase-theme', theme)
+      }
+  
+      return {
+        show,
+        onClose,
+        handleThemeClick,
+        onPurchase
+      }
+    }
+  })
+  </script>
+  
+  <style lang="scss">
+  .selector-container {
+    padding: $spacing-base;
+    
+    .selector-header {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      padding: $spacing-base 0;
+      margin-bottom: $spacing-base;
+      border-bottom: 1px solid $border-color-light;
+      
+      .selector-title {
+        font-size: $font-size-medium;
+        font-weight: $font-weight-medium;
+        color: $text-color-primary;
+      }
+    }
+    
+    .theme-scroll {
+      border: 1px solid $border-color-light;
+      border-radius: $border-radius-small;
+      margin-top: $spacing-base;
+      background-color: $background-color-light;
+    }
+    
+    .theme-list {
+      padding: $spacing-small;
+      
+      .theme-option {
+        background-color: $background-color-base;
+        padding: $spacing-base;
+        border-radius: $border-radius-mini;
+        margin-bottom: $spacing-small;
+        box-shadow: $shadow-light;
+        display: flex;
+        justify-content: space-between;
+        align-items: center;
+        
+        &:last-child {
+          margin-bottom: 0;
+        }
+        
+        &.disabled {
+          opacity: 0.5;
+        }
+        
+        &.selected {
+          background-color: rgba(60, 146, 251, 0.1);
+        }
+        
+        .option-content {
+          flex: 1;
+          
+          .option-title {
+            font-size: $font-size-base;
+            color: $text-color-primary;
+            margin-bottom: $spacing-mini;
+          }
+          
+          .option-desc {
+            font-size: $font-size-small;
+            color: $text-color-secondary;
+          }
+          
+          .theme-locked {
+            display: flex;
+            align-items: center;
+            margin-top: $spacing-mini;
+            font-size: $font-size-small;
+            color: $text-color-disabled;
+            
+            .lock-text {
+              margin-left: $spacing-mini;
+            }
+          }
+        }
+        
+        .unlock-button {
+          margin-left: $spacing-mini;
+        }
+      }
+    }
+  }
+  </style>

+ 84 - 667
src/pages/room/waiting/index.vue

@@ -1,25 +1,15 @@
 <template>
 <template>
   <view class="waiting-room-page">
   <view class="waiting-room-page">
     <!-- 顶部标题 -->
     <!-- 顶部标题 -->
-    <view
-      class="room-header"
-      :class="{ 'host-background': isHost, 'player-background': !isHost }"
-    >
-      <view 
-        class="room-title"
-        :class="{ 'host-title': isHost, 'player-title': !isHost }"
-      >
-        {{ currentRoom?.name || '等待房间' }}
-      </view>
-      <view 
-        class="room-status"
-        :class="{ 'host-status': isHost, 'player-status': !isHost }"
-      >
-        {{ playerCount }}/{{ currentRoom?.maxPlayers || 0 }} 已加入,{{ statusText }}
-      </view>
-    </view>
+    <RoomHeader 
+      :roomName="currentRoom?.name" 
+      :isHost="isHost" 
+      :playerCount="playerCount"
+      :maxPlayers="currentRoom?.maxPlayers || 0"
+      :statusText="statusText"
+    />
 
 
-    <!-- 房间码展示和分享 保持不变 -->
+    <!-- 房间码展示和分享 -->
     <RoomCode
     <RoomCode
       :code="currentRoom?.id || ''"
       :code="currentRoom?.id || ''"
       :password="currentRoom?.password"
       :password="currentRoom?.password"
@@ -27,287 +17,54 @@
       @copy="handleCopy"
       @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: '12px 0 12px 0' }"
-        >
-          游戏设置
-        </nut-divider>
-        
-        <!-- 1. 主题选择 -->
-        <view class="setting-item">
-          <view class="setting-label">主题选择</view>
-          <nut-cell 
-            :desc="selectedTheme?.text || '请选择游戏主题'" 
-            @click="showThemeSelector = true"
-          >
-            <template #link>
-              <IconFont name="right" size="16"></IconFont>
-            </template>
-          </nut-cell>
-        </view>
-        
-        <!-- 2. 难度选择 -->
-        <view class="setting-item">
-          <view class="setting-label">游戏难度</view>
-          <nut-cell 
-            :desc="difficultyText" 
-            @click="showDifficultySelector = true"
-          >
-            <template #link>
-              <IconFont name="right" size="16"></IconFont>
-            </template>
-          </nut-cell>
-        </view>
-        
-        <!-- 3. 题目选择 (只有当主题和难度都选择后才显示) -->
-        <view class="setting-item" v-if="selectedThemeId && selectedDifficulty">
-          <view class="setting-label">题目选择</view>
-          <nut-cell 
-            :desc="selectedPuzzle?.text || '请选择游戏题目'" 
-            @click="selectedThemeId ? showPuzzleSelector = true : null"
-          >
-            <template #link>
-              <IconFont name="right" size="16"></IconFont>
-            </template>
-          </nut-cell>
-        </view>
-      </view>
-      
-      <!-- 玩家列表 保持不变 -->
-      <view class="player-list">
-        <nut-divider 
-          content-position="center"
-          :style="{ color: '#3C92FB', borderColor: '#3C92FB', padding: '0 16px', margin: '12px 0 12px 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 || '/assets/default-avatar.png'" mode="aspectFill" @error="handleAvatarError" />
-            </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: '12px 0 12px 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: '12px 0 12px 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 || '/assets/default-avatar.png'" mode="aspectFill" @error="handleAvatarError" />
-            </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 v-model:visible="showThemeSelector" position="bottom">
-      <view class="selector-container">
-        <view class="selector-header">
-          <view class="selector-title">选择游戏主题</view>
-          <nut-button size="small" @click="showThemeSelector = false">取消</nut-button>
-        </view>
-        <scroll-view 
-          scroll-y 
-          class="theme-scroll"
-          :style="{ maxHeight: themeScrollHeight + 'px' }"
-        >
-          <view class="theme-list">
-            <view 
-              v-for="theme in themeOptions" 
-              :key="theme.value" 
-              class="theme-option"
-              :class="{ 'disabled': theme.disabled && !canPurchase, 'selected': selectedThemeId === theme.value }"
-              @click="handleThemeClick(theme)"
-            >
-              <view class="option-content">
-                <view class="option-title">{{ theme.text }}</view>
-                <view v-if="theme.description" class="option-desc">{{ theme.description }}</view>
-                <view v-if="theme.locked" class="theme-locked">
-                  <IconFont name="lock" size="12"></IconFont>
-                  <view class="lock-text">{{ theme.unlockRequirement }}</view>
-                </view>
-              </view>
-              <!-- 添加解锁购买按钮 -->
-              <view v-if="theme.locked && canPurchase" class="unlock-button" @click.stop="purchaseTheme(theme)">
-                <nut-button size="small" type="primary">解锁 ({{ theme.price || '5' }}元)</nut-button>
-              </view>
-              <IconFont v-else-if="selectedThemeId === theme.value" name="check" color="#3C92FB" size="16"></IconFont>
-            </view>
-          </view>
-        </scroll-view>
-      </view>
-    </nut-popup>
-
-    <!-- 难度选择弹窗 -->
-    <nut-popup v-model:visible="showDifficultySelector" position="bottom">
-      <view class="selector-container">
-        <view class="selector-header">
-          <view class="selector-title">选择游戏难度</view>
-          <nut-button size="small" @click="showDifficultySelector = false">取消</nut-button>
-        </view>
-        <view class="difficulty-list">
-          <view 
-            class="difficulty-option" 
-            :class="{ 'selected': selectedDifficulty === TurtleSoupDifficulty.EASY }"
-            @click="handleDifficultySelect(TurtleSoupDifficulty.EASY)"
-          >
-            <view class="option-content">
-              <view class="option-title">简单</view>
-              <view class="option-desc">适合新手玩家,游戏时间较短</view>
-            </view>
-            <IconFont v-if="selectedDifficulty === TurtleSoupDifficulty.EASY" name="check" color="#3C92FB" size="16"></IconFont>
-          </view>
-          <view 
-            class="difficulty-option" 
-            :class="{ 'selected': selectedDifficulty === TurtleSoupDifficulty.MEDIUM }"
-            @click="handleDifficultySelect(TurtleSoupDifficulty.MEDIUM)"
-          >
-            <view class="option-content">
-              <view class="option-title">中等</view>
-              <view class="option-desc">平衡挑战与乐趣,适合大多数玩家</view>
-            </view>
-            <IconFont v-if="selectedDifficulty === TurtleSoupDifficulty.MEDIUM" name="check" color="#3C92FB" size="16"></IconFont>
-          </view>
-          <view 
-            class="difficulty-option" 
-            :class="{ 'selected': selectedDifficulty === TurtleSoupDifficulty.HARD }"
-            @click="handleDifficultySelect(TurtleSoupDifficulty.HARD)"
-          >
-            <view class="option-content">
-              <view class="option-title">困难</view>
-              <view class="option-desc">高难度挑战,适合有经验的玩家</view>
-            </view>
-            <IconFont v-if="selectedDifficulty === TurtleSoupDifficulty.HARD" name="check" color="#3C92FB" size="16"></IconFont>
-          </view>
-        </view>
-      </view>
-    </nut-popup>
+    <!-- 主持人/玩家视图 -->
+    <HostView 
+      v-if="isHost" 
+      :users="currentRoom?.users || []"
+      :selectedTheme="selectedTheme"
+      :selectedPuzzle="selectedPuzzle"
+      :selectedThemeId="selectedThemeId"
+      :selectedDifficulty="selectedDifficulty"
+      :difficultyText="difficultyText"
+      :canStartGame="canStartGame"
+      @show-theme-selector="showThemeSelector = true"
+      @show-difficulty-selector="showDifficultySelector = true"
+      @show-puzzle-selector="showPuzzleSelector = true"
+      @start-game="startGame"
+    />
+    <PlayerView 
+      v-else 
+      :users="currentRoom?.users || []"
+      :gameTitle="gameTitle"
+      :themeTitle="themeTitle"
+      :difficultyText="difficultyText"
+      :gameDescription="gameDescription"
+      :currentUserReady="currentUserReady"
+      @toggle-ready="toggleReady"
+    />
 
 
-    <!-- 题目选择弹窗 -->
-    <nut-popup v-model:visible="showPuzzleSelector" position="bottom">
-      <view class="selector-container">
-        <view class="selector-header">
-          <view class="selector-title">选择游戏题目</view>
-          <nut-button size="small" @click="showPuzzleSelector = false">取消</nut-button>
-        </view>
-        <scroll-view 
-          scroll-y 
-          class="puzzle-scroll"
-          :style="{ maxHeight: puzzleScrollHeight + 'px' }"
-        >
-          <view class="puzzle-list">
-            <view 
-              v-for="puzzle in puzzleOptions" 
-              :key="puzzle.value" 
-              class="puzzle-option"
-              :class="{ 'disabled': puzzle.disabled, 'selected': selectedPuzzleId === puzzle.value }"
-              @click="!puzzle.disabled && handlePuzzleSelect(puzzle)"
-            >
-              <view class="option-content">
-                <view class="option-title">{{ puzzle.text }}</view>
-                <view v-if="puzzle.description" class="option-desc">{{ puzzle.description }}</view>
-              </view>
-              <IconFont v-if="selectedPuzzleId === puzzle.value" name="check" color="#3C92FB" size="16"></IconFont>
-            </view>
-          </view>
-        </scroll-view>
-      </view>
-    </nut-popup>
+    <!-- 选择器组件 -->
+    <ThemeSelector 
+      v-model:visible="showThemeSelector" 
+      :themeOptions="themeOptions"
+      :selectedThemeId="selectedThemeId"
+      :scrollHeight="themeScrollHeight"
+      :canPurchase="canPurchase"
+      @select-theme="handleThemeSelect"
+      @purchase-theme="purchaseTheme"
+    />
+    <DifficultySelector 
+      v-model:visible="showDifficultySelector" 
+      :selectedDifficulty="selectedDifficulty"
+      @select-difficulty="handleDifficultySelect"
+    />
+    <PuzzleSelector 
+      v-model:visible="showPuzzleSelector" 
+      :puzzleOptions="puzzleOptions"
+      :selectedPuzzleId="selectedPuzzleId"
+      :scrollHeight="puzzleScrollHeight"
+      @select-puzzle="handlePuzzleSelect"
+    />
   </view>
   </view>
   <Tabbar></Tabbar>
   <Tabbar></Tabbar>
 </template>
 </template>
@@ -321,27 +78,31 @@ import { useTabBarStore } from '@/stores/tabbar'
 import { useTurtleSoupStore } from '@/stores/games/turtlesoup'
 import { useTurtleSoupStore } from '@/stores/games/turtlesoup'
 import Tabbar from '@/components/Tabbar.vue'
 import Tabbar from '@/components/Tabbar.vue'
 import RoomCode from '@/components/RoomCode/index.vue'
 import RoomCode from '@/components/RoomCode/index.vue'
-import { IconFont } from '@nutui/icons-vue-taro'
+
+// 导入重构的组件
+import RoomHeader from '@/components/room/RoomHeader.vue'
+import PlayerList from '@/components/room/PlayerList.vue'
+import HostView from '@/components/room/host/HostView.vue'
+import PlayerView from '@/components/room/player/PlayerView.vue'
+import ThemeSelector from '@/components/room/selectors/ThemeSelector.vue'
+import DifficultySelector from '@/components/room/selectors/DifficultySelector.vue'
+import PuzzleSelector from '@/components/room/selectors/PuzzleSelector.vue'
+
 import { RoomRole, RoomStatus } from '@/types/room'
 import { RoomRole, RoomStatus } from '@/types/room'
 import { TurtleSoupDifficulty } from '@/types/games/turtlesoup'
 import { TurtleSoupDifficulty } from '@/types/games/turtlesoup'
-
-// 主题和题目的数据类型
-interface CascaderOption {
-  value: string;
-  text: string;
-  description?: string;
-  disabled?: boolean;
-  locked?: boolean;
-  unlockRequirement?: string;
-  price?: string; // 添加价格字段
-  children?: CascaderOption[];
-}
+import { type CascaderOption } from '@/types/cascader'
 
 
 export default {
 export default {
   components: {
   components: {
     Tabbar,
     Tabbar,
     RoomCode,
     RoomCode,
-    IconFont
+    RoomHeader,
+    PlayerList,
+    HostView,
+    PlayerView,
+    ThemeSelector,
+    DifficultySelector,
+    PuzzleSelector
   },
   },
   
   
   // 生命周期钩子 - 页面显示
   // 生命周期钩子 - 页面显示
@@ -402,13 +163,6 @@ export default {
       return currentUser ? currentUser.isReady : false
       return currentUser ? currentUser.isReady : false
     })
     })
     
     
-    // 处理头像加载错误
-    const handleAvatarError = (e: any) => {
-      console.log('头像加载失败:', e);
-      // 设置默认头像
-      e.target.src = '/assets/default-avatar.png';
-    }
-    
     // 是否可以开始游戏(主持人功能)
     // 是否可以开始游戏(主持人功能)
     const canStartGame = computed(() => {
     const canStartGame = computed(() => {
       if (!currentRoom.value) return false
       if (!currentRoom.value) return false
@@ -421,15 +175,15 @@ export default {
       const allReady = players.every(p => p.isReady)
       const allReady = players.every(p => p.isReady)
       
       
       // 主题和题目已选择
       // 主题和题目已选择
-      const settingsReady = selectedThemeId.value && selectedPuzzleId.value
-      
+      const settingsReady = !!selectedThemeId.value && !!selectedPuzzleId.value
+
       return allReady && settingsReady
       return allReady && settingsReady
     })
     })
     
     
     // 游戏设置相关变量
     // 游戏设置相关变量
     const selectedDifficulty = ref(TurtleSoupDifficulty.MEDIUM)
     const selectedDifficulty = ref(TurtleSoupDifficulty.MEDIUM)
-    const selectedThemeId = ref('') // 添加选中主题ID
-    const selectedPuzzleId = ref('') // 添加选中题目ID
+    const selectedThemeId = ref('')
+    const selectedPuzzleId = ref('')
     const selectedTheme = ref<CascaderOption | null>(null)
     const selectedTheme = ref<CascaderOption | null>(null)
     const selectedPuzzle = ref<CascaderOption | null>(null)
     const selectedPuzzle = ref<CascaderOption | null>(null)
     const showThemeSelector = ref(false)
     const showThemeSelector = ref(false)
@@ -443,8 +197,8 @@ export default {
     const puzzleOptions = ref<CascaderOption[]>([])
     const puzzleOptions = ref<CascaderOption[]>([])
     
     
     // 设置滚动区域高度限制
     // 设置滚动区域高度限制
-    const themeScrollHeight = ref(300) // 最多显示约3个选项
-    const puzzleScrollHeight = ref(300) // 最多显示约3个选项
+    const themeScrollHeight = ref(300)
+    const puzzleScrollHeight = ref(300)
     
     
     // 游戏信息(玩家视图)
     // 游戏信息(玩家视图)
     const gameTitle = computed(() => currentRoom.value?.gameTitle || '')
     const gameTitle = computed(() => currentRoom.value?.gameTitle || '')
@@ -530,9 +284,9 @@ export default {
             value: theme.id,
             value: theme.id,
             text: theme.name,
             text: theme.name,
             description: theme.description,
             description: theme.description,
-            disabled: theme.isLocked, // 根据是否解锁决定是否可选
+            disabled: theme.isLocked,
             locked: theme.isLocked,
             locked: theme.isLocked,
-            price: theme.price ? String(theme.price) : '5' // 添加价格,默认5元
+            price: theme.price ? String(theme.price) : '5'
           }))
           }))
           console.log('已加载主题:', themeOptions.value)
           console.log('已加载主题:', themeOptions.value)
         }
         }
@@ -586,40 +340,10 @@ export default {
       }
       }
     }
     }
     
     
-    // 处理主题点击
-    const handleThemeClick = (theme: CascaderOption) => {
-      // 如果主题被锁定且不可购买,则不做任何操作
-      if (theme.disabled && !canPurchase) {
-        Taro.showToast({
-          title: '该主题暂未解锁',
-          icon: 'none'
-        })
-        return
-      }
-      
-      // 如果主题被锁定且可购买,弹出购买提示
-      if (theme.locked && canPurchase) {
-        // 购买流程在 purchaseTheme 中处理
-        return
-      }
-      
-      // 正常选择主题
-      handleThemeSelect(theme)
-    }
-    
     // 处理主题选择
     // 处理主题选择
     const handleThemeSelect = (theme: CascaderOption) => {
     const handleThemeSelect = (theme: CascaderOption) => {
-      if (theme.disabled) {
-        Taro.showToast({
-          title: '该主题暂未解锁',
-          icon: 'none'
-        })
-        return
-      }
-      
       selectedTheme.value = theme
       selectedTheme.value = theme
-      selectedThemeId.value = theme.value // 设置选中的主题ID
-      showThemeSelector.value = false
+      selectedThemeId.value = theme.value
       
       
       // 选择主题后提示选择难度
       // 选择主题后提示选择难度
       setTimeout(() => {
       setTimeout(() => {
@@ -638,7 +362,7 @@ export default {
           nonceStr: Math.random().toString(36).substring(2, 15),
           nonceStr: Math.random().toString(36).substring(2, 15),
           package: `prepay_id=wx${Date.now()}`,
           package: `prepay_id=wx${Date.now()}`,
           signType: 'MD5',
           signType: 'MD5',
-          paySign: 'test_sign', // 实际项目中需要服务端生成真实的支付参数
+          paySign: 'test_sign', // 实际项目中需要服务端生成
           success: async () => {
           success: async () => {
             // 支付成功后,解锁主题
             // 支付成功后,解锁主题
             Taro.hideLoading()
             Taro.hideLoading()
@@ -710,16 +434,8 @@ export default {
     
     
     // 处理题目选择
     // 处理题目选择
     const handlePuzzleSelect = (puzzle: CascaderOption) => {
     const handlePuzzleSelect = (puzzle: CascaderOption) => {
-      if (puzzle.disabled) {
-        Taro.showToast({
-          title: '该题目暂未解锁',
-          icon: 'none'
-        })
-        return
-      }
-      
       selectedPuzzle.value = puzzle
       selectedPuzzle.value = puzzle
-      selectedPuzzleId.value = puzzle.value // 设置选中的题目ID
+      selectedPuzzleId.value = puzzle.value
       showPuzzleSelector.value = false
       showPuzzleSelector.value = false
     }
     }
     
     
@@ -899,21 +615,14 @@ export default {
       startGame,
       startGame,
       handleCopy,
       handleCopy,
       handleShare,
       handleShare,
-      handleThemeClick,
       handleThemeSelect,
       handleThemeSelect,
       handleDifficultySelect,
       handleDifficultySelect,
       handlePuzzleSelect,
       handlePuzzleSelect,
       purchaseTheme,
       purchaseTheme,
-      handleAvatarError,
       themeScrollHeight,
       themeScrollHeight,
       puzzleScrollHeight,
       puzzleScrollHeight,
       canPurchase
       canPurchase
     }
     }
-  },
-  
-  // 生命周期钩子 - 页面加载
-  onLoad() {
-    // 使用setup方法处理页面加载
   }
   }
 }
 }
 </script>
 </script>
@@ -924,297 +633,5 @@ export default {
   background-color: $background-color-base;
   background-color: $background-color-base;
   min-height: 100vh;
   min-height: 100vh;
   padding-bottom: $spacing-large * 4; // 为底部tabbar留出空间
   padding-bottom: $spacing-large * 4; // 为底部tabbar留出空间
-  
-  .room-header {
-    padding: $spacing-base;
-    height: 45px;
-    text-align: center;
-
-    .room-title {
-      font-size: 12px;
-      font-weight: bold;
-      margin-bottom: 4px;
-
-      &.host-title {
-        color: $text-color-orange;
-      }
-
-      &.player-title {
-        color: $text-color-blue;
-      }
-    }
-    .room-status {
-      font-size: 8px;
-      margin-bottom: 4px;
-
-      &.host-status {
-        color: $text-color-orange;
-      }
-
-      &.player-status {
-        color: $text-color-blue;
-      }
-    }
-
-    &.host-background {
-    background-color: $orange-light-color;
-    }
-
-    &.player-background {
-      background-color: $blue-light-color;
-    }
-  }
-  
-  .host-view, .player-view {
-    margin-top: $spacing-large;
-  }
-  
-  .game-settings {
-    background-color: $background-color-light;
-    border-radius: $border-radius-small;
-    padding: $spacing-base;
-    margin-bottom: $spacing-base;
-    box-shadow: $shadow-light;
-    
-    .setting-item {
-      margin-bottom: $spacing-base;
-      
-      .setting-label {
-        font-size: $font-size-small;
-        color: $text-color-secondary;
-        margin-bottom: $spacing-mini;
-        font-weight: $font-weight-medium;
-      }
-      
-      .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-base;
-    margin-bottom: $spacing-base;
-    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;
-      }
-      
-      .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-base;
-    margin-bottom: $spacing-base;
-    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: rgba(255, 235, 210, 0.3); // 浅橙色背景
-          border-radius: $border-radius-mini;
-          padding: $spacing-base;
-        }
-        
-        .player-avatar {
-          width: 40px;
-          height: 40px;
-          margin-right: $spacing-base;
-          border-radius: 50%;
-          overflow: hidden;
-          
-          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;
-    
-    .start-button, .ready-button {
-      height: 44px;
-      font-size: $font-size-medium;
-      border-radius: $border-radius-base;
-    }
-  }
-  
-  // 选择器相关样式
-  .selector-container {
-    padding: $spacing-base;
-    
-    .selector-header {
-      display: flex;
-      justify-content: space-between;
-      align-items: center;
-      padding: $spacing-base 0;
-      margin-bottom: $spacing-base;
-      border-bottom: 1px solid $border-color-light;
-      
-      .selector-title {
-        font-size: $font-size-medium;
-        font-weight: $font-weight-medium;
-        color: $text-color-primary;
-      }
-    }
-    
-    .theme-scroll, .puzzle-scroll {
-      border: 1px solid $border-color-light;
-      border-radius: $border-radius-small;
-      margin-top: $spacing-base;
-      background-color: $background-color-light;
-    }
-    
-    .theme-list, .puzzle-list, .difficulty-list {
-      padding: $spacing-small;
-      
-      .theme-option, .puzzle-option, .difficulty-option {
-        background-color: $background-color-base;
-        padding: $spacing-base;
-        border-radius: $border-radius-mini;
-        margin-bottom: $spacing-small;
-        box-shadow: $shadow-light;
-        display: flex;
-        justify-content: space-between;
-        align-items: center;
-        
-        &:last-child {
-          margin-bottom: 0;
-        }
-        
-        &.disabled {
-          opacity: 0.5;
-        }
-        
-        &.selected {
-          background-color: rgba(60, 146, 251, 0.1);
-        }
-        
-        .option-content {
-          flex: 1;
-          
-          .option-title {
-            font-size: $font-size-base;
-            color: $text-color-primary;
-            margin-bottom: $spacing-mini;
-          }
-          
-          .option-desc {
-            font-size: $font-size-small;
-            color: $text-color-secondary;
-          }
-        }
-        
-        .theme-locked {
-          display: flex;
-          align-items: center;
-          margin-top: $spacing-mini;
-          font-size: $font-size-small;
-          color: $text-color-disabled;
-          
-          .lock-text {
-            margin-left: $spacing-mini;
-          }
-        }
-        
-        .unlock-button {
-          margin-left: $spacing-mini;
-        }
-      }
-    }
-  }
 }
 }
 </style>
 </style>

+ 11 - 0
src/types/cascader.ts

@@ -0,0 +1,11 @@
+// @/types/cascader.ts
+export interface CascaderOption {
+    value: string;
+    text: string;
+    description?: string;
+    disabled?: boolean;
+    locked?: boolean;
+    unlockRequirement?: string;
+    price?: string;
+    children?: CascaderOption[];
+  }