index.vue 33 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109
  1. <template>
  2. <view class="waiting-room-page">
  3. <!-- 房间头部信息 -->
  4. <view class="room-header">
  5. <view class="room-title">
  6. <view class="title">{{ currentRoom?.name || '等待房间' }}</view>
  7. <view class="tag" :class="isHost ? 'host-tag' : 'player-tag'">
  8. {{ isHost ? '主持人' : '玩家' }}
  9. </view>
  10. </view>
  11. <view class="status-info">
  12. <view class="players-count">{{ playerCount }}/{{ currentRoom?.maxPlayers || 0 }}人已加入</view>
  13. <view class="status-text">{{ statusText }}</view>
  14. </view>
  15. </view>
  16. <!-- 房间码展示和分享 -->
  17. <RoomCode
  18. :code="currentRoom?.id || ''"
  19. :password="currentRoom?.password"
  20. @share="handleShare"
  21. @copy="handleCopy"
  22. />
  23. <!-- 主持人视图 -->
  24. <view v-if="isHost" class="host-view">
  25. <!-- 游戏设置模块 -->
  26. <view class="game-settings">
  27. <nut-divider
  28. content-position="center"
  29. :style="{ color: '#3C92FB', borderColor: '#3C92FB', padding: '0 16px', margin: '10px 0 20px 0' }"
  30. >
  31. 游戏设置
  32. </nut-divider>
  33. <!-- 主题选择 -->
  34. <view class="setting-item">
  35. <view class="setting-label">主题选择</view>
  36. <nut-cell
  37. :desc="selectedTheme?.text || '请选择游戏主题'"
  38. @click="showThemeSelector = true"
  39. >
  40. <template #link>
  41. <IconFont name="right" size="16"></IconFont>
  42. </template>
  43. </nut-cell>
  44. </view>
  45. <!-- 题目选择 -->
  46. <view class="setting-item" v-if="selectedThemeId">
  47. <view class="setting-label">题目选择</view>
  48. <nut-cell
  49. :desc="selectedPuzzle?.text || '请选择游戏题目'"
  50. @click="showPuzzleSelector = true"
  51. >
  52. <template #link>
  53. <IconFont name="right" size="16"></IconFont>
  54. </template>
  55. </nut-cell>
  56. </view>
  57. <!-- 难度选择 -->
  58. <view class="setting-item">
  59. <view class="setting-label">游戏难度</view>
  60. <nut-cell
  61. :desc="difficultyText"
  62. @click="showDifficultySelector = true"
  63. >
  64. <template #link>
  65. <IconFont name="right" size="16"></IconFont>
  66. </template>
  67. </nut-cell>
  68. </view>
  69. </view>
  70. <!-- 玩家列表 -->
  71. <view class="player-list">
  72. <nut-divider
  73. content-position="center"
  74. :style="{ color: '#3C92FB', borderColor: '#3C92FB', padding: '0 16px', margin: '20px 0 10px 0' }"
  75. >
  76. 玩家列表
  77. </nut-divider>
  78. <view class="players">
  79. <view
  80. v-for="user in currentRoom?.users"
  81. :key="user.openid"
  82. class="player-item"
  83. :class="{ 'is-host': user.roomRole === 'hoster' }"
  84. >
  85. <view class="player-avatar">
  86. <image :src="user.avatar || '/assets/default-avatar.png'" mode="aspectFill" @error="handleAvatarError" />
  87. </view>
  88. <view class="player-info">
  89. <view class="player-name">{{ user.nickname }}</view>
  90. <view class="player-role">{{ user.roomRole === 'hoster' ? '主持人' : '玩家' }}</view>
  91. </view>
  92. <view class="player-status" :class="{ 'ready': user.isReady }">
  93. {{ user.isReady ? '已准备' : '未准备' }}
  94. </view>
  95. </view>
  96. </view>
  97. </view>
  98. <!-- 主持人操作按钮 -->
  99. <view class="action-buttons">
  100. <nut-button
  101. block
  102. color="#3C92FB"
  103. class="start-button"
  104. :disabled="!canStartGame"
  105. @click="startGame"
  106. >
  107. 开始游戏
  108. </nut-button>
  109. </view>
  110. </view>
  111. <!-- 玩家视图 -->
  112. <view v-else class="player-view">
  113. <!-- 游戏信息展示 -->
  114. <view class="game-info">
  115. <nut-divider
  116. content-position="center"
  117. :style="{ color: '#3C92FB', borderColor: '#3C92FB', padding: '0 16px', margin: '10px 0 20px 0' }"
  118. >
  119. 游戏信息
  120. </nut-divider>
  121. <view class="info-card">
  122. <view class="game-title">{{ gameTitle }}</view>
  123. <view class="game-meta">
  124. <view class="meta-item">
  125. <view class="meta-label">游戏主题</view>
  126. <view class="meta-value">{{ themeTitle || '待定' }}</view>
  127. </view>
  128. <view class="meta-item">
  129. <view class="meta-label">游戏难度</view>
  130. <view class="meta-value">{{ difficultyText }}</view>
  131. </view>
  132. </view>
  133. <view class="game-desc">
  134. {{ gameDescription || '正在等待主持人设置游戏...' }}
  135. </view>
  136. </view>
  137. </view>
  138. <!-- 玩家列表 -->
  139. <view class="player-list">
  140. <nut-divider
  141. content-position="center"
  142. :style="{ color: '#3C92FB', borderColor: '#3C92FB', padding: '0 16px', margin: '20px 0 10px 0' }"
  143. >
  144. 玩家列表
  145. </nut-divider>
  146. <view class="players">
  147. <view
  148. v-for="user in currentRoom?.users"
  149. :key="user.openid"
  150. class="player-item"
  151. :class="{ 'is-host': user.roomRole === 'hoster' }"
  152. >
  153. <view class="player-avatar">
  154. <image :src="user.avatar || '/assets/default-avatar.png'" mode="aspectFill" @error="handleAvatarError" />
  155. </view>
  156. <view class="player-info">
  157. <view class="player-name">{{ user.nickname }}</view>
  158. <view class="player-role">{{ user.roomRole === 'hoster' ? '主持人' : '玩家' }}</view>
  159. </view>
  160. <view class="player-status" :class="{ 'ready': user.isReady }">
  161. {{ user.isReady ? '已准备' : '未准备' }}
  162. </view>
  163. </view>
  164. </view>
  165. </view>
  166. <!-- 玩家操作按钮 -->
  167. <view class="action-buttons">
  168. <nut-button
  169. block
  170. :color="currentUserReady ? '#999' : '#3C92FB'"
  171. class="ready-button"
  172. @click="toggleReady"
  173. >
  174. {{ currentUserReady ? '取消准备' : '准备' }}
  175. </nut-button>
  176. </view>
  177. </view>
  178. <!-- 主题选择弹窗 -->
  179. <nut-popup v-model:visible="showThemeSelector" position="bottom">
  180. <view class="selector-container">
  181. <view class="selector-header">
  182. <view class="selector-title">选择游戏主题</view>
  183. <nut-button size="small" @click="showThemeSelector = false">取消</nut-button>
  184. </view>
  185. <scroll-view
  186. scroll-y
  187. class="theme-scroll"
  188. :style="{ maxHeight: themeScrollHeight + 'px' }"
  189. >
  190. <view class="theme-list">
  191. <view
  192. v-for="theme in themeOptions"
  193. :key="theme.value"
  194. class="theme-option"
  195. :class="{ 'disabled': theme.disabled, 'selected': selectedThemeId === theme.value }"
  196. @click="!theme.disabled && handleThemeSelect(theme)"
  197. >
  198. <view class="option-content">
  199. <view class="option-title">{{ theme.text }}</view>
  200. <view v-if="theme.description" class="option-desc">{{ theme.description }}</view>
  201. <view v-if="theme.locked" class="theme-locked">
  202. <IconFont name="lock" size="12"></IconFont>
  203. <view class="lock-text">解锁条件: {{ theme.unlockRequirement }}</view>
  204. </view>
  205. </view>
  206. <IconFont v-if="selectedThemeId === theme.value" name="check" color="#3C92FB" size="16"></IconFont>
  207. </view>
  208. </view>
  209. </scroll-view>
  210. </view>
  211. </nut-popup>
  212. <!-- 题目选择弹窗 -->
  213. <nut-popup v-model:visible="showPuzzleSelector" position="bottom">
  214. <view class="selector-container">
  215. <view class="selector-header">
  216. <view class="selector-title">选择游戏题目</view>
  217. <nut-button size="small" @click="showPuzzleSelector = false">取消</nut-button>
  218. </view>
  219. <scroll-view
  220. scroll-y
  221. class="puzzle-scroll"
  222. :style="{ maxHeight: puzzleScrollHeight + 'px' }"
  223. >
  224. <view class="puzzle-list">
  225. <view
  226. v-for="puzzle in puzzleOptions"
  227. :key="puzzle.value"
  228. class="puzzle-option"
  229. :class="{ 'disabled': puzzle.disabled, 'selected': selectedPuzzleId === puzzle.value }"
  230. @click="!puzzle.disabled && handlePuzzleSelect(puzzle)"
  231. >
  232. <view class="option-content">
  233. <view class="option-title">{{ puzzle.text }}</view>
  234. <view v-if="puzzle.description" class="option-desc">{{ puzzle.description }}</view>
  235. </view>
  236. <IconFont v-if="selectedPuzzleId === puzzle.value" name="check" color="#3C92FB" size="16"></IconFont>
  237. </view>
  238. </view>
  239. </scroll-view>
  240. </view>
  241. </nut-popup>
  242. <!-- 难度选择弹窗 -->
  243. <nut-popup v-model:visible="showDifficultySelector" position="bottom">
  244. <view class="selector-container">
  245. <view class="selector-header">
  246. <view class="selector-title">选择游戏难度</view>
  247. <nut-button size="small" @click="showDifficultySelector = false">取消</nut-button>
  248. </view>
  249. <view class="difficulty-list">
  250. <view
  251. class="difficulty-option"
  252. :class="{ 'selected': selectedDifficulty === TurtleSoupDifficulty.EASY }"
  253. @click="handleDifficultySelect(TurtleSoupDifficulty.EASY)"
  254. >
  255. <view class="option-content">
  256. <view class="option-title">简单</view>
  257. <view class="option-desc">适合新手玩家,游戏时间较短</view>
  258. </view>
  259. <IconFont v-if="selectedDifficulty === TurtleSoupDifficulty.EASY" name="check" color="#3C92FB" size="16"></IconFont>
  260. </view>
  261. <view
  262. class="difficulty-option"
  263. :class="{ 'selected': selectedDifficulty === TurtleSoupDifficulty.MEDIUM }"
  264. @click="handleDifficultySelect(TurtleSoupDifficulty.MEDIUM)"
  265. >
  266. <view class="option-content">
  267. <view class="option-title">中等</view>
  268. <view class="option-desc">平衡挑战与乐趣,适合大多数玩家</view>
  269. </view>
  270. <IconFont v-if="selectedDifficulty === TurtleSoupDifficulty.MEDIUM" name="check" color="#3C92FB" size="16"></IconFont>
  271. </view>
  272. <view
  273. class="difficulty-option"
  274. :class="{ 'selected': selectedDifficulty === TurtleSoupDifficulty.HARD }"
  275. @click="handleDifficultySelect(TurtleSoupDifficulty.HARD)"
  276. >
  277. <view class="option-content">
  278. <view class="option-title">困难</view>
  279. <view class="option-desc">高难度挑战,适合有经验的玩家</view>
  280. </view>
  281. <IconFont v-if="selectedDifficulty === TurtleSoupDifficulty.HARD" name="check" color="#3C92FB" size="16"></IconFont>
  282. </view>
  283. </view>
  284. </view>
  285. </nut-popup>
  286. </view>
  287. <Tabbar></Tabbar>
  288. </template>
  289. <script lang="ts">
  290. import Taro from '@tarojs/taro'
  291. import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
  292. import { useRoomStore } from '@/stores/room'
  293. import { useUserStore } from '@/stores/user'
  294. import { useTurtleSoupStore } from '@/stores/games/turtlesoup'
  295. import Tabbar from '@/components/Tabbar.vue'
  296. import RoomCode from '@/components/RoomCode/index.vue'
  297. import { IconFont } from '@nutui/icons-vue-taro'
  298. import { RoomRole, RoomStatus } from '@/types/room'
  299. import { TurtleSoupDifficulty } from '@/types/games/turtlesoup'
  300. // 主题和题目的数据类型
  301. interface CascaderOption {
  302. value: string;
  303. text: string;
  304. description?: string;
  305. disabled?: boolean;
  306. locked?: boolean;
  307. unlockRequirement?: string;
  308. children?: CascaderOption[];
  309. }
  310. export default {
  311. components: {
  312. Tabbar,
  313. RoomCode,
  314. IconFont
  315. },
  316. // 生命周期钩子 - 页面显示
  317. onShow() {
  318. // 隐藏返回首页按钮
  319. Taro.hideHomeButton()
  320. },
  321. // Composition API
  322. setup() {
  323. // 初始化store
  324. const roomStore = useRoomStore()
  325. const userStore = useUserStore()
  326. const turtleSoupStore = useTurtleSoupStore()
  327. // 房间ID
  328. const roomId = ref('')
  329. // 获取当前房间
  330. const currentRoom = computed(() => roomStore.currentRoom)
  331. // 判断当前用户是否是主持人
  332. const isHost = computed(() => {
  333. if (!currentRoom.value || !userStore.openid) return false
  334. return roomStore.getUserRole(userStore.openid) === RoomRole.HOSTER
  335. })
  336. // 计算玩家数量
  337. const playerCount = computed(() => {
  338. return currentRoom.value?.users.length || 0
  339. })
  340. // 房间状态文本
  341. const statusText = computed(() => {
  342. if (!currentRoom.value) return '等待中'
  343. switch(currentRoom.value.status) {
  344. case RoomStatus.WAITING:
  345. return '等待玩家加入...'
  346. case RoomStatus.PLAYING:
  347. return '游戏中...'
  348. case RoomStatus.ENDED:
  349. return '已结束'
  350. default:
  351. return '等待中'
  352. }
  353. })
  354. // 当前用户是否已准备
  355. const currentUserReady = computed(() => {
  356. if (!currentRoom.value || !userStore.openid) return false
  357. const currentUser = currentRoom.value.users.find(u => u.openid === userStore.openid)
  358. return currentUser ? currentUser.isReady : false
  359. })
  360. // 处理头像加载错误
  361. const handleAvatarError = (e: any) => {
  362. console.log('头像加载失败:', e);
  363. // 设置默认头像
  364. e.target.src = '/assets/default-avatar.png';
  365. }
  366. // 是否可以开始游戏(主持人功能)
  367. const canStartGame = computed(() => {
  368. if (!currentRoom.value) return false
  369. // 至少有一个玩家
  370. const players = currentRoom.value.users.filter(u => u.roomRole === RoomRole.PLAYER)
  371. if (players.length === 0) return false
  372. // 所有玩家都已准备
  373. const allReady = players.every(p => p.isReady)
  374. // 主题和题目已选择
  375. const settingsReady = selectedThemeId.value && selectedPuzzleId.value
  376. return allReady && settingsReady
  377. })
  378. // 游戏设置相关变量
  379. const selectedDifficulty = ref(TurtleSoupDifficulty.MEDIUM)
  380. const selectedThemeId = ref('') // 添加选中主题ID
  381. const selectedPuzzleId = ref('') // 添加选中题目ID
  382. const selectedTheme = ref<CascaderOption | null>(null)
  383. const selectedPuzzle = ref<CascaderOption | null>(null)
  384. const showThemeSelector = ref(false)
  385. const showPuzzleSelector = ref(false)
  386. const showDifficultySelector = ref(false)
  387. // 主题选项
  388. const themeOptions = ref<CascaderOption[]>([])
  389. // 题目选项
  390. const puzzleOptions = ref<CascaderOption[]>([])
  391. // 设置滚动区域高度限制
  392. const themeScrollHeight = ref(300) // 最多显示约3个选项
  393. const puzzleScrollHeight = ref(300) // 最多显示约3个选项
  394. // 游戏信息(玩家视图)
  395. const gameTitle = computed(() => currentRoom.value?.gameTitle || '')
  396. const themeTitle = computed(() => {
  397. const theme = themeOptions.value.find(t => t.value === selectedThemeId.value)
  398. return theme?.text || ''
  399. })
  400. const gameDescription = ref('')
  401. // 难度文本
  402. const difficultyText = computed(() => {
  403. switch(selectedDifficulty.value) {
  404. case TurtleSoupDifficulty.EASY:
  405. return '简单'
  406. case TurtleSoupDifficulty.MEDIUM:
  407. return '中等'
  408. case TurtleSoupDifficulty.HARD:
  409. return '困难'
  410. default:
  411. return '中等'
  412. }
  413. })
  414. // 初始化页面
  415. const initPage = async () => {
  416. // 获取路由参数中的房间ID
  417. const pages = Taro.getCurrentPages()
  418. const currentPage = pages[pages.length - 1]
  419. const routeParams = currentPage.$taroParams
  420. if (routeParams && routeParams.roomId) {
  421. roomId.value = routeParams.roomId
  422. // 加载房间信息
  423. await loadRoomInfo(roomId.value)
  424. // 如果是主持人,加载可用的主题
  425. if (isHost.value) {
  426. await loadThemes()
  427. }
  428. // 开始监听房间变化
  429. startRoomListener()
  430. } else {
  431. Taro.showToast({
  432. title: '房间ID不存在',
  433. icon: 'none'
  434. })
  435. // 延迟返回
  436. setTimeout(() => {
  437. Taro.navigateBack()
  438. }, 1500)
  439. }
  440. }
  441. // 加载房间信息
  442. const loadRoomInfo = async (id: string) => {
  443. try {
  444. const result = await roomStore.loadRoomInfo(id)
  445. if (!result.success) {
  446. throw new Error(result.message || '加载房间信息失败')
  447. }
  448. } catch (error) {
  449. console.error('加载房间信息失败:', error)
  450. Taro.showToast({
  451. title: error instanceof Error ? error.message : '加载房间信息失败',
  452. icon: 'none'
  453. })
  454. }
  455. }
  456. // 加载主题列表
  457. const loadThemes = async () => {
  458. try {
  459. Taro.showLoading({ title: '加载主题中...' })
  460. // 从turtleSoupStore加载主题
  461. const themes = await turtleSoupStore.loadThemes()
  462. // 将API返回的主题格式化为显示需要的格式
  463. if (themes && themes.length) {
  464. themeOptions.value = themes.map(theme => ({
  465. value: theme.id,
  466. text: theme.name,
  467. description: theme.description,
  468. disabled: theme.isLocked, // 根据是否解锁决定是否可选
  469. locked: theme.isLocked,
  470. unlockRequirement: theme.unlockRequirement || '完成相关任务'
  471. }))
  472. console.log('已加载主题:', themeOptions.value)
  473. }
  474. Taro.hideLoading()
  475. } catch (error) {
  476. console.error('加载主题失败:', error)
  477. Taro.hideLoading()
  478. Taro.showToast({
  479. title: '加载主题失败',
  480. icon: 'none'
  481. })
  482. }
  483. }
  484. // 根据主题和难度加载题目列表
  485. const loadPuzzles = async () => {
  486. if (!selectedThemeId.value) {
  487. return
  488. }
  489. try {
  490. Taro.showLoading({ title: '加载题目中...' })
  491. // 从API加载指定主题和难度的题目列表
  492. const puzzles = await turtleSoupStore.loadPuzzles(selectedThemeId.value, selectedDifficulty.value)
  493. // 格式化为显示需要的格式
  494. if (puzzles && puzzles.length) {
  495. puzzleOptions.value = puzzles.map(puzzle => ({
  496. value: puzzle.id,
  497. text: puzzle.title,
  498. description: puzzle.description,
  499. disabled: puzzle.isLocked
  500. }))
  501. console.log('已加载题目:', puzzleOptions.value)
  502. } else {
  503. puzzleOptions.value = []
  504. Taro.showToast({
  505. title: '当前主题下没有题目',
  506. icon: 'none'
  507. })
  508. }
  509. Taro.hideLoading()
  510. } catch (error) {
  511. console.error('加载题目失败:', error)
  512. Taro.hideLoading()
  513. Taro.showToast({
  514. title: '加载题目失败',
  515. icon: 'none'
  516. })
  517. }
  518. }
  519. // 处理主题选择
  520. const handleThemeSelect = (theme: CascaderOption) => {
  521. if (theme.disabled) {
  522. Taro.showToast({
  523. title: '该主题暂未解锁',
  524. icon: 'none'
  525. })
  526. return
  527. }
  528. selectedTheme.value = theme
  529. selectedThemeId.value = theme.value // 设置选中的主题ID
  530. showThemeSelector.value = false
  531. // 选择主题后提示选择难度
  532. setTimeout(() => {
  533. showDifficultySelector.value = true
  534. }, 300)
  535. }
  536. // 处理难度选择
  537. const handleDifficultySelect = (difficulty: TurtleSoupDifficulty) => {
  538. selectedDifficulty.value = difficulty
  539. showDifficultySelector.value = false
  540. // 加载对应难度的题目
  541. loadPuzzles()
  542. // 选择难度后提示选择题目
  543. setTimeout(() => {
  544. showPuzzleSelector.value = true
  545. }, 300)
  546. }
  547. // 处理题目选择
  548. const handlePuzzleSelect = (puzzle: CascaderOption) => {
  549. if (puzzle.disabled) {
  550. Taro.showToast({
  551. title: '该题目暂未解锁',
  552. icon: 'none'
  553. })
  554. return
  555. }
  556. selectedPuzzle.value = puzzle
  557. selectedPuzzleId.value = puzzle.value // 设置选中的题目ID
  558. showPuzzleSelector.value = false
  559. }
  560. // 准备/取消准备(玩家)
  561. const toggleReady = async () => {
  562. if (!userStore.openid || !currentRoom.value) return
  563. try {
  564. Taro.showLoading({ title: '处理中...' })
  565. // 更新玩家准备状态
  566. const newStatus = !currentUserReady.value
  567. // 调用API更新准备状态
  568. await roomStore.updateUserInRoom(userStore.openid, { isReady: newStatus })
  569. Taro.hideLoading()
  570. } catch (error) {
  571. console.error('切换准备状态失败:', error)
  572. Taro.showToast({
  573. title: '操作失败',
  574. icon: 'none'
  575. })
  576. Taro.hideLoading()
  577. }
  578. }
  579. // 开始游戏(主持人)
  580. const startGame = async () => {
  581. if (!currentRoom.value || !isHost.value) return
  582. try {
  583. Taro.showLoading({ title: '开始游戏中...' })
  584. // 准备游戏设置
  585. const gameSettings = {
  586. themeId: selectedThemeId.value,
  587. puzzleId: selectedPuzzleId.value,
  588. difficulty: selectedDifficulty.value,
  589. maxPlayers: currentRoom.value.maxPlayers,
  590. isPrivate: currentRoom.value.visibility === 'private'
  591. }
  592. // 创建游戏
  593. const result = await turtleSoupStore.createGame(gameSettings)
  594. if (result.success && result.gameId) {
  595. // 更新房间状态为游戏中
  596. await roomStore.updateRoomStatus(RoomStatus.PLAYING)
  597. // 导航到游戏页面
  598. Taro.redirectTo({
  599. url: `/pages/room/play/index?roomId=${currentRoom.value.id}&gameId=${result.gameId}`
  600. })
  601. } else {
  602. throw new Error('创建游戏失败')
  603. }
  604. } catch (error) {
  605. console.error('开始游戏失败:', error)
  606. Taro.showToast({
  607. title: error instanceof Error ? error.message : '开始游戏失败',
  608. icon: 'none'
  609. })
  610. } finally {
  611. Taro.hideLoading()
  612. }
  613. }
  614. // 房间监听器
  615. let roomInterval: NodeJS.Timeout | null = null
  616. // 开始监听房间变化
  617. const startRoomListener = () => {
  618. // 定期刷新房间状态
  619. roomInterval = setInterval(async () => {
  620. if (roomId.value) {
  621. await loadRoomInfo(roomId.value)
  622. // 检查房间状态,如果变为"游戏中",需要跳转到游戏页面
  623. if (currentRoom.value && currentRoom.value.status === RoomStatus.PLAYING) {
  624. stopRoomListener()
  625. // 跳转到游戏页面
  626. Taro.redirectTo({
  627. url: `/pages/room/play/index?roomId=${roomId.value}`
  628. })
  629. }
  630. }
  631. }, 3000) // 每3秒刷新一次
  632. }
  633. // 停止监听房间变化
  634. const stopRoomListener = () => {
  635. if (roomInterval) {
  636. clearInterval(roomInterval)
  637. roomInterval = null
  638. }
  639. }
  640. // 处理复制按钮点击
  641. const handleCopy = (code: string) => {
  642. console.log('房间码已复制:', code)
  643. }
  644. // 处理分享按钮点击
  645. const handleShare = (code: string) => {
  646. console.log('分享房间码:', code)
  647. }
  648. // 页面加载时初始化
  649. onMounted(() => {
  650. initPage()
  651. })
  652. // 页面卸载时停止监听
  653. onUnmounted(() => {
  654. stopRoomListener()
  655. })
  656. // 监听难度变化,如果已选择主题则重新加载题目
  657. watch(selectedDifficulty, async () => {
  658. if (selectedThemeId.value) {
  659. await loadPuzzles()
  660. selectedPuzzleId.value = '' // 重置题目选择
  661. selectedPuzzle.value = null
  662. }
  663. })
  664. // 监听选中的主题ID
  665. watch(selectedThemeId, (newVal) => {
  666. if (newVal) {
  667. const theme = themeOptions.value.find(t => t.value === newVal)
  668. if (theme) {
  669. selectedTheme.value = theme
  670. }
  671. }
  672. })
  673. // 监听选中的题目ID
  674. watch(selectedPuzzleId, (newVal) => {
  675. if (newVal) {
  676. const puzzle = puzzleOptions.value.find(p => p.value === newVal)
  677. if (puzzle) {
  678. selectedPuzzle.value = puzzle
  679. }
  680. }
  681. })
  682. return {
  683. currentRoom,
  684. isHost,
  685. playerCount,
  686. statusText,
  687. currentUserReady,
  688. canStartGame,
  689. selectedDifficulty,
  690. TurtleSoupDifficulty,
  691. selectedThemeId,
  692. selectedPuzzleId,
  693. selectedTheme,
  694. selectedPuzzle,
  695. showThemeSelector,
  696. showPuzzleSelector,
  697. showDifficultySelector,
  698. themeOptions,
  699. puzzleOptions,
  700. gameTitle,
  701. themeTitle,
  702. gameDescription,
  703. difficultyText,
  704. toggleReady,
  705. startGame,
  706. handleCopy,
  707. handleShare,
  708. handleThemeSelect,
  709. handleDifficultySelect,
  710. handlePuzzleSelect,
  711. handleAvatarError,
  712. themeScrollHeight,
  713. puzzleScrollHeight
  714. }
  715. },
  716. // 生命周期钩子 - 页面加载
  717. onLoad() {
  718. // 使用setup方法处理页面加载
  719. }
  720. }
  721. </script>
  722. <style lang="scss">
  723. .waiting-room-page {
  724. padding: $spacing-base;
  725. background-color: $background-color-base;
  726. min-height: 100vh;
  727. padding-bottom: $spacing-large * 4; // 为底部tabbar留出空间
  728. .room-header {
  729. margin-bottom: $spacing-base;
  730. .room-title {
  731. display: flex;
  732. align-items: center;
  733. margin-bottom: $spacing-base;
  734. .title {
  735. font-size: $font-size-large;
  736. font-weight: $font-weight-bold;
  737. color: $text-color-primary;
  738. }
  739. .tag {
  740. margin-left: $spacing-base;
  741. padding: $spacing-mini $spacing-base;
  742. border-radius: $border-radius-mini;
  743. font-size: $font-size-small;
  744. &.host-tag {
  745. background-color: $orange-color;
  746. color: white;
  747. }
  748. &.player-tag {
  749. background-color: $blue-light-color;
  750. color: white;
  751. }
  752. }
  753. }
  754. .status-info {
  755. display: flex;
  756. justify-content: space-between;
  757. align-items: center;
  758. .players-count, .status-text {
  759. font-size: $font-size-base;
  760. color: $text-color-secondary;
  761. }
  762. }
  763. }
  764. .host-view, .player-view {
  765. margin-top: $spacing-large;
  766. }
  767. .game-settings {
  768. background-color: $background-color-light;
  769. border-radius: $border-radius-small;
  770. padding: $spacing-large;
  771. margin-bottom: $spacing-large;
  772. box-shadow: $shadow-light;
  773. .setting-item {
  774. margin-bottom: $spacing-large;
  775. .setting-label {
  776. font-size: $font-size-small;
  777. color: $text-color-secondary;
  778. margin-bottom: $spacing-mini;
  779. font-weight: $font-weight-medium;
  780. }
  781. .setting-value {
  782. padding: $spacing-base 0;
  783. border-bottom: 1px solid $border-color-light;
  784. display: flex;
  785. justify-content: space-between;
  786. align-items: center;
  787. font-size: $font-size-medium;
  788. color: $text-color-primary;
  789. &:active {
  790. background-color: $background-color-gray;
  791. }
  792. }
  793. .difficulty-options {
  794. margin-top: $spacing-base;
  795. }
  796. }
  797. }
  798. .game-info {
  799. background-color: $background-color-light;
  800. border-radius: $border-radius-small;
  801. padding: $spacing-large;
  802. margin-bottom: $spacing-large;
  803. box-shadow: $shadow-light;
  804. .info-card {
  805. .game-title {
  806. font-size: $font-size-medium;
  807. font-weight: $font-weight-medium;
  808. color: $text-color-primary;
  809. margin-bottom: $spacing-base;
  810. }
  811. .game-meta {
  812. display: flex;
  813. flex-wrap: wrap;
  814. margin-bottom: $spacing-large;
  815. .meta-item {
  816. flex: 1;
  817. min-width: 50%;
  818. margin-bottom: $spacing-base;
  819. .meta-label {
  820. font-size: $font-size-small;
  821. color: $text-color-secondary;
  822. margin-bottom: $spacing-mini;
  823. }
  824. .meta-value {
  825. font-size: $font-size-base;
  826. color: $text-color-primary;
  827. font-weight: $font-weight-medium;
  828. }
  829. }
  830. }
  831. .game-desc {
  832. font-size: $font-size-base;
  833. color: $text-color-regular;
  834. line-height: $line-height-loose;
  835. }
  836. }
  837. }
  838. .player-list {
  839. background-color: $background-color-light;
  840. border-radius: $border-radius-small;
  841. padding: $spacing-large;
  842. margin-bottom: $spacing-large;
  843. box-shadow: $shadow-light;
  844. .players {
  845. .player-item {
  846. display: flex;
  847. align-items: center;
  848. padding: $spacing-base 0;
  849. border-bottom: 1px solid $border-color-light;
  850. &:last-child {
  851. border-bottom: none;
  852. }
  853. &.is-host {
  854. background-color: rgba(255, 235, 210, 0.3); // 浅橙色背景
  855. border-radius: $border-radius-mini;
  856. padding: $spacing-base;
  857. }
  858. .player-avatar {
  859. width: 40px;
  860. height: 40px;
  861. margin-right: $spacing-base;
  862. border-radius: 50%;
  863. overflow: hidden;
  864. image {
  865. width: 100%;
  866. height: 100%;
  867. border-radius: 50%;
  868. }
  869. }
  870. .player-info {
  871. flex: 1;
  872. .player-name {
  873. font-size: $font-size-base;
  874. color: $text-color-primary;
  875. font-weight: $font-weight-medium;
  876. }
  877. .player-role {
  878. font-size: $font-size-small;
  879. color: $text-color-secondary;
  880. }
  881. }
  882. .player-status {
  883. padding: $spacing-mini $spacing-base;
  884. border-radius: $border-radius-mini;
  885. font-size: $font-size-small;
  886. background-color: $background-color-gray;
  887. color: $text-color-secondary;
  888. &.ready {
  889. background-color: $success-color;
  890. color: white;
  891. }
  892. }
  893. }
  894. }
  895. }
  896. .action-buttons {
  897. margin-top: $spacing-large;
  898. .start-button, .ready-button {
  899. height: 44px;
  900. font-size: $font-size-medium;
  901. border-radius: $border-radius-base;
  902. }
  903. }
  904. // 选择器相关样式
  905. .selector-container {
  906. padding: $spacing-base;
  907. .selector-header {
  908. display: flex;
  909. justify-content: space-between;
  910. align-items: center;
  911. padding: $spacing-base 0;
  912. margin-bottom: $spacing-base;
  913. border-bottom: 1px solid $border-color-light;
  914. .selector-title {
  915. font-size: $font-size-medium;
  916. font-weight: $font-weight-medium;
  917. color: $text-color-primary;
  918. }
  919. }
  920. .theme-scroll, .puzzle-scroll {
  921. border: 1px solid $border-color-light;
  922. border-radius: $border-radius-small;
  923. margin-top: $spacing-base;
  924. background-color: $background-color-light;
  925. }
  926. .theme-list, .puzzle-list, .difficulty-list {
  927. padding: $spacing-small;
  928. .theme-option, .puzzle-option, .difficulty-option {
  929. background-color: $background-color-base;
  930. padding: $spacing-base;
  931. border-radius: $border-radius-mini;
  932. margin-bottom: $spacing-small;
  933. box-shadow: $shadow-light;
  934. display: flex;
  935. justify-content: space-between;
  936. align-items: center;
  937. &:last-child {
  938. margin-bottom: 0;
  939. }
  940. &.disabled {
  941. opacity: 0.5;
  942. }
  943. &.selected {
  944. background-color: rgba(60, 146, 251, 0.1);
  945. }
  946. .option-content {
  947. flex: 1;
  948. .option-title {
  949. font-size: $font-size-base;
  950. color: $text-color-primary;
  951. margin-bottom: $spacing-mini;
  952. }
  953. .option-desc {
  954. font-size: $font-size-small;
  955. color: $text-color-secondary;
  956. }
  957. }
  958. .theme-locked {
  959. display: flex;
  960. align-items: center;
  961. margin-top: $spacing-mini;
  962. font-size: $font-size-small;
  963. color: $text-color-disabled;
  964. .lock-text {
  965. margin-left: $spacing-mini;
  966. }
  967. }
  968. }
  969. }
  970. }
  971. }
  972. </style>