wuzj 6 days ago
parent
commit
3aca7a63e4

+ 3 - 1
components.d.ts

@@ -7,6 +7,8 @@ export {}
 
 declare module 'vue' {
   export interface GlobalComponents {
-    NutButton: typeof import('@nutui/nutui-taro')['Button']
+    HostOnly: typeof import('./src/components/HostOnly/index.vue')['default']
+    NutEmpty: typeof import('@nutui/nutui-taro')['Empty']
+    PlayerOnly: typeof import('./src/components/PlayerOnly/index.vue')['default']
   }
 }

+ 1 - 0
package.json

@@ -25,6 +25,7 @@
   "license": "MIT",
   "dependencies": {
     "@babel/runtime": "^7.7.7",
+    "@nutui/icons-vue-taro": "^0.0.9",
     "@nutui/nutui-taro": "^4.2.8",
     "@tarojs/components": "4.0.9",
     "@tarojs/helper": "4.0.9",

+ 3 - 0
pnpm-lock.yaml

@@ -11,6 +11,9 @@ importers:
       '@babel/runtime':
         specifier: ^7.7.7
         version: 7.27.0
+      '@nutui/icons-vue-taro':
+        specifier: ^0.0.9
+        version: 0.0.9
       '@nutui/nutui-taro':
         specifier: ^4.2.8
         version: 4.3.13(unplugin-vue-components@0.26.0(@babel/parser@7.27.0)(rollup@3.29.5)(vue@3.5.13(typescript@5.8.2)))(vue@3.5.13(typescript@5.8.2))

+ 37 - 4
src/app.config.ts

@@ -1,11 +1,44 @@
-export default {
+export default defineAppConfig({
   pages: [
-    'pages/index/index'
+    'pages/index/index',
+    'pages/game-detail/index',
+    'pages/room/create/index',
+    'pages/room/join/index',
+    'pages/room/waiting/index',
+    'pages/room/play/index',
+    'pages/profile/index',
+    'pages/history/index'
   ],
   window: {
     backgroundTextStyle: 'light',
     navigationBarBackgroundColor: '#fff',
     navigationBarTitleText: 'LineJoy',
     navigationBarTextStyle: 'black'
-  }
-}
+  },
+  tabBar: {
+    color: '#999',
+    selectedColor: '#3C92FB',
+    backgroundColor: '#fff',
+    borderStyle: 'black',
+    custom: true,  // 使用自定义tabBar
+    list: [
+      {
+        pagePath: 'pages/index/index',
+        text: '游戏广场'
+      },
+      {
+        pagePath: 'pages/room/join/index',
+        text: '加入房间'
+      },
+      {
+        pagePath: 'pages/history/index',
+        text: '历史记录'
+      },
+      {
+        pagePath: 'pages/profile/index',
+        text: '我的'
+      }
+    ]
+  },
+  lazyCodeLoading: 'requiredComponents'
+})

+ 47 - 6
src/app.scss

@@ -1,7 +1,48 @@
-// 全局样式
+@import "./assets/styles/variables.scss";
+@import "./assets/styles/mixins.scss";
+
+// 全局重置样式
 page {
-    box-sizing: border-box;
-    height: 100%;
-    background-color: #f7f8fa;
-    font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', Helvetica, Segoe UI, Arial, Roboto, 'PingFang SC', 'miui', 'Hiragino Sans GB', 'Microsoft Yahei', sans-serif;
-  }
+  font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', Helvetica,
+    Segoe UI, Arial, Roboto, 'PingFang SC', miui, 'Hiragino Sans GB', 'Microsoft Yahei',
+    sans-serif;
+  font-size: $font-size-base;
+  line-height: 1.5;
+  color: $text-color-primary;
+  background-color: $background-color-base;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+}
+
+view, text, scroll-view, swiper, button, input, textarea {
+  box-sizing: border-box;
+}
+
+// 隐藏滚动条但保留滚动功能
+::-webkit-scrollbar {
+  display: none;
+  width: 0;
+  height: 0;
+  color: transparent;
+}
+
+// 常用辅助类
+.text-primary { color: $primary-color; }
+.text-success { color: $success-color; }
+.text-warning { color: $warning-color; }
+.text-danger { color: $danger-color; }
+
+.text-center { text-align: center; }
+.text-left { text-align: left; }
+.text-right { text-align: right; }
+
+.flex { display: flex; }
+.flex-center { @include center; }
+.flex-between { @include flex(row, space-between, center); }
+.flex-around { @include flex(row, space-around, center); }
+.flex-column { @include flex(column); }
+
+.ellipsis { @include text-ellipsis; }
+.ellipsis-2 { @include multi-line-ellipsis(2); }
+
+.safe-area-bottom { @include safe-area-bottom; }

+ 12 - 1
src/app.ts

@@ -1,7 +1,6 @@
 import { createApp } from 'vue'
 import { createPinia } from 'pinia'
 import './app.scss'
-
 // NutUI样式导入
 import '@nutui/nutui-taro/dist/style.css'
 
@@ -10,6 +9,18 @@ const App = createApp({
     console.log('App onShow', options)
   },
   // 入口组件不需要实现render方法,即使实现了也会被忽略
+  onLaunch() {
+    // 初始化云环境
+    if (process.env.TARO_ENV === 'weapp') {
+      const Taro = require('@tarojs/taro')
+      Taro.cloud.init({
+        // 使用默认环境(自动创建,无需认证)
+        env: Taro.cloud.DYNAMIC_CURRENT_ENV, 
+        traceUser: true
+      })
+      console.log('已启用微信云开发默认环境')
+    }
+  }
 })
 
 App.use(createPinia())

+ 68 - 0
src/assets/styles/mixins.scss

@@ -0,0 +1,68 @@
+// 单行文本溢出省略
+@mixin text-ellipsis {
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+  }
+  
+  // 多行文本溢出省略
+  @mixin multi-line-ellipsis($line: 2) {
+    display: -webkit-box;
+    -webkit-line-clamp: $line;
+    -webkit-box-orient: vertical;
+    overflow: hidden;
+    text-overflow: ellipsis;
+  }
+  
+  // flex布局
+  @mixin flex($direction: row, $justify: flex-start, $align: stretch) {
+    display: flex;
+    flex-direction: $direction;
+    justify-content: $justify;
+    align-items: $align;
+  }
+  
+  // 居中布局
+  @mixin center {
+    display: flex;
+    justify-content: center;
+    align-items: center;
+  }
+  
+  // 绝对定位居中
+  @mixin absolute-center {
+    position: absolute;
+    top: 50%;
+    left: 50%;
+    transform: translate(-50%, -50%);
+  }
+  
+  // 按钮基本样式
+  @mixin button-base {
+    display: inline-flex;
+    align-items: center;
+    justify-content: center;
+    border: none;
+    outline: none;
+    padding: 0 $spacing-base;
+    border-radius: $border-radius-base;
+    font-size: $font-size-base;
+    height: 40px;
+    cursor: pointer;
+    transition: opacity $animation-duration-fast;
+    
+    &:active {
+      opacity: 0.8;
+    }
+    
+    &.disabled {
+      opacity: 0.5;
+      cursor: not-allowed;
+    }
+  }
+  
+  // 安全区域适配
+  @mixin safe-area-bottom {
+    padding-bottom: constant(safe-area-inset-bottom);
+    padding-bottom: env(safe-area-inset-bottom);
+  }

+ 52 - 0
src/assets/styles/variables.scss

@@ -0,0 +1,52 @@
+// 主题色
+$primary-color: #3C92FB;
+$secondary-color: #5DABFF;
+$success-color: #07C160;
+$danger-color: #FF5650;
+$warning-color: #FFB11B;
+
+// 文字颜色
+$text-color-primary: #333;
+$text-color-regular: #666;
+$text-color-secondary: #999;
+$text-color-disabled: #ccc;
+
+// 边框颜色
+$border-color-base: #eee;
+$border-color-light: #f5f5f5;
+$border-color-dark: #ddd;
+
+// 背景色
+$background-color-base: #f7f8fa;
+$background-color-light: #fff;
+
+// 圆角
+$border-radius-small: 2px;
+$border-radius-base: 4px;
+$border-radius-large: 8px;
+$border-radius-circle: 50%;
+
+// 字体大小
+$font-size-mini: 10px;
+$font-size-small: 12px;
+$font-size-base: 14px;
+$font-size-medium: 16px;
+$font-size-large: 18px;
+$font-size-xlarge: 20px;
+
+// 间距
+$spacing-mini: 4px;
+$spacing-small: 8px;
+$spacing-base: 16px;
+$spacing-large: 24px;
+$spacing-xlarge: 32px;
+
+// 动画
+$animation-duration-base: 0.3s;
+$animation-duration-slow: 0.5s;
+$animation-duration-fast: 0.2s;
+
+// z-index
+$zindex-popup: 1000;
+$zindex-modal: 1100;
+$zindex-toast: 1200;

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

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

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

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

+ 196 - 0
src/custom-tab-bar/index.vue

@@ -0,0 +1,196 @@
+<template>
+    <view class="tab-bar-container">
+      <view 
+        v-for="(item, index) in list" 
+        :key="index"
+        class="tab-item"
+        :class="{ active: current === index }"
+        @tap="switchTab(item.pagePath, index)"
+      >
+        <view class="icon-container">
+          <component 
+            :is="item.icon"
+            size="24px"
+            :color="current === index ? '#3C92FB' : '#999'"
+          />
+        </view>
+        <view class="text">{{ item.text }}</view>
+      </view>
+    </view>
+  </template>
+  
+  <script>
+  import Taro from '@tarojs/taro'
+  import { 
+    Home, 
+    Find, 
+    Clock,
+    My
+  } from '@nutui/icons-vue-taro'
+  
+  export default {
+    name: 'CustomTabBar',
+    components: {
+      Home, 
+      Find, 
+      Clock, 
+      My
+    },
+    data() {
+      return {
+        current: 0,
+        list: [
+          {
+            pagePath: '/pages/index/index',
+            text: '游戏广场',
+            icon: Home
+          },
+          {
+            pagePath: '/pages/room/join/index',
+            text: '加入房间',
+            icon: Find
+          },
+          {
+            pagePath: '/pages/history/index',
+            text: '历史记录',
+            icon: Clock
+          },
+          {
+            pagePath: '/pages/profile/index',
+            text: '我的',
+            icon: My
+          }
+        ]
+      }
+    },
+    created() {
+      // 创建时立即尝试更新状态
+      this.updateSelected()
+    },
+    mounted() {
+      // 确保组件挂载后初始化tab状态
+      this.updateSelected()
+      
+      // 绑定页面显示事件
+      Taro.eventCenter.on('taroPageShow', this.updateSelected)
+      
+      // 绑定路由变化事件(如果使用Taro路由)
+      Taro.eventCenter.on('routeChange', this.updateSelected)
+      
+      // 额外监听小程序显示事件
+      Taro.onAppShow(this.updateSelected)
+    },
+    beforeUnmount() {
+      // 组件销毁前取消所有事件绑定
+      Taro.eventCenter.off('taroPageShow', this.updateSelected)
+      Taro.eventCenter.off('routeChange', this.updateSelected)
+      Taro.offAppShow(this.updateSelected)
+    },
+    methods: {
+      // 更新当前选中的tab - 添加防抖处理
+      updateSelected() {
+        // 使用setTimeout确保在DOM更新后执行
+        setTimeout(() => {
+          const pages = Taro.getCurrentPages()
+          if (!pages || pages.length === 0) return
+          
+          const currentPage = pages[pages.length - 1]
+          if (!currentPage || !currentPage.route) return
+          
+          const url = `/${currentPage.route}`
+          
+          console.log('当前页面路径:', url)
+          
+          // 查找当前页面在哪个tab
+          const index = this.list.findIndex(item => item.pagePath === url)
+          
+          // 只有确实匹配到tab时才更新状态
+          if (index !== -1) {
+            console.log('更新选中tab为:', index)
+            this.current = index
+            // 强制更新视图
+            this.$forceUpdate()
+          } else {
+            console.log('当前页面不在tab列表中:', url)
+          }
+        }, 100)
+      },
+      
+      // 切换tab - 使用状态锁防止重复点击
+      switchTab(url, index) {
+        // 记录操作中的目标index
+        const targetIndex = index
+        console.log('尝试切换到:', url, targetIndex)
+        
+        // 防止重复点击导致状态混乱
+        if (this._switching) return
+        this._switching = true
+        
+        // 先更新UI状态,提高响应感知
+        this.current = targetIndex
+        
+        // 执行页面跳转
+        Taro.switchTab({
+          url,
+          success: () => {
+            console.log('跳转成功到:', url)
+            // 确保跳转成功后状态正确
+            this.current = targetIndex
+            this.$forceUpdate()
+          },
+          fail: (error) => {
+            console.error('跳转失败:', error)
+            // 可以考虑失败后恢复原状态
+          },
+          complete: () => {
+            // 操作完成,解除锁定
+            this._switching = false
+          }
+        })
+      }
+    }
+  }
+  </script>
+  
+  <style lang="scss">
+  /* 样式部分与之前相同 */
+  .tab-bar-container {
+    display: flex;
+    position: fixed;
+    bottom: 0;
+    left: 0;
+    right: 0;
+    height: 50px;
+    background-color: #ffffff;
+    box-shadow: 0 -1px 5px rgba(0, 0, 0, 0.1);
+    padding-bottom: env(safe-area-inset-bottom);
+    z-index: 999;
+  }
+  
+  .tab-item {
+    flex: 1;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    padding: 5px 0;
+    color: #999;
+    
+    &.active {
+      color: #3C92FB;
+    }
+    
+    .icon-container {
+      display: flex;
+      justify-content: center;
+      align-items: center;
+      height: 24px;
+    }
+    
+    .text {
+      font-size: 10px;
+      line-height: 1.2;
+      margin-top: 2px;
+    }
+  }
+  </style>

+ 23 - 0
src/pages/game-detail/index.vue

@@ -0,0 +1,23 @@
+<template>
+    <view class="game-detail-page">
+      <view class="title">游戏详情</view>
+      <nut-empty description="正在开发中..." image="empty" />
+    </view>
+  </template>
+  
+  <script setup lang="ts">
+  // 游戏详情页逻辑
+  </script>
+  
+  <style lang="scss">
+  .game-detail-page {
+    padding: 20px;
+    
+    .title {
+      font-size: 20px;
+      font-weight: bold;
+      margin-bottom: 20px;
+      text-align: center;
+    }
+  }
+  </style>

+ 23 - 0
src/pages/history/index.vue

@@ -0,0 +1,23 @@
+<template>
+    <view class="history-page">
+      <view class="title">历史记录</view>
+      <nut-empty description="正在开发中..." image="empty" />
+    </view>
+  </template>
+  
+  <script setup lang="ts">
+  // 历史记录页逻辑
+  </script>
+  
+  <style lang="scss">
+  .history-page {
+    padding: 20px;
+    
+    .title {
+      font-size: 20px;
+      font-weight: bold;
+      margin-bottom: 20px;
+      text-align: center;
+    }
+  }
+  </style>

+ 12 - 12
src/pages/index/index.vue

@@ -1,23 +1,23 @@
 <template>
-  <view class="index">
-    <nut-button type="primary">LineJoy 开始体验</nut-button>
+  <view class="index-page">
+    <view class="title">游戏广场</view>
+    <nut-empty description="正在开发中..." image="empty" />
   </view>
 </template>
 
 <script setup lang="ts">
-import { ref, onMounted } from 'vue'
-
-onMounted(() => {
-  console.log('首页加载完成')
-})
+// 首页逻辑
 </script>
 
 <style lang="scss">
-.index {
+.index-page {
   padding: 20px;
-  display: flex;
-  justify-content: center;
-  align-items: center;
-  height: 100vh;
+  
+  .title {
+    font-size: 20px;
+    font-weight: bold;
+    margin-bottom: 20px;
+    text-align: center;
+  }
 }
 </style>

+ 23 - 0
src/pages/profile/index.vue

@@ -0,0 +1,23 @@
+<template>
+    <view class="profile-page">
+      <view class="title">个人中心</view>
+      <nut-empty description="正在开发中..." image="empty" />
+    </view>
+  </template>
+  
+  <script setup lang="ts">
+  // 个人中心页逻辑
+  </script>
+  
+  <style lang="scss">
+  .profile-page {
+    padding: 20px;
+    
+    .title {
+      font-size: 20px;
+      font-weight: bold;
+      margin-bottom: 20px;
+      text-align: center;
+    }
+  }
+  </style>

+ 23 - 0
src/pages/room/create/index.vue

@@ -0,0 +1,23 @@
+<template>
+    <view class="create-room-page">
+      <view class="title">创建房间</view>
+      <nut-empty description="正在开发中..." image="empty" />
+    </view>
+  </template>
+  
+  <script setup lang="ts">
+  // 创建房间页逻辑
+  </script>
+  
+  <style lang="scss">
+  .create-room-page {
+    padding: 20px;
+    
+    .title {
+      font-size: 20px;
+      font-weight: bold;
+      margin-bottom: 20px;
+      text-align: center;
+    }
+  }
+  </style>

+ 23 - 0
src/pages/room/join/index.vue

@@ -0,0 +1,23 @@
+<template>
+    <view class="join-room-page">
+      <view class="title">加入房间</view>
+      <nut-empty description="正在开发中..." image="empty" />
+    </view>
+  </template>
+  
+  <script setup lang="ts">
+  // 加入房间页逻辑
+  </script>
+  
+  <style lang="scss">
+  .join-room-page {
+    padding: 20px;
+    
+    .title {
+      font-size: 20px;
+      font-weight: bold;
+      margin-bottom: 20px;
+      text-align: center;
+    }
+  }
+  </style>

+ 23 - 0
src/pages/room/play/index.vue

@@ -0,0 +1,23 @@
+<template>
+    <view class="play-room-page">
+      <view class="title">游戏进行中</view>
+      <nut-empty description="正在开发中..." image="empty" />
+    </view>
+  </template>
+  
+  <script setup lang="ts">
+  // 游戏进行中页逻辑
+  </script>
+  
+  <style lang="scss">
+  .play-room-page {
+    padding: 20px;
+    
+    .title {
+      font-size: 20px;
+      font-weight: bold;
+      margin-bottom: 20px;
+      text-align: center;
+    }
+  }
+  </style>

+ 23 - 0
src/pages/room/waiting/index.vue

@@ -0,0 +1,23 @@
+<template>
+    <view class="waiting-room-page">
+      <view class="title">等待室</view>
+      <nut-empty description="正在开发中..." image="empty" />
+    </view>
+  </template>
+  
+  <script setup lang="ts">
+  // 等待室页逻辑
+  </script>
+  
+  <style lang="scss">
+  .waiting-room-page {
+    padding: 20px;
+    
+    .title {
+      font-size: 20px;
+      font-weight: bold;
+      margin-bottom: 20px;
+      text-align: center;
+    }
+  }
+  </style>

+ 47 - 0
src/services/api/game.ts

@@ -0,0 +1,47 @@
+import { callCloudFunction } from '../cloud'
+import type { TurtleSoupGame, GameResult } from '@/types/game'
+
+/**
+ * 获取游戏数据
+ * @param gameId 游戏ID
+ * @param role 用户角色
+ */
+export function getGameData(gameId: string, role: string) {
+  return callCloudFunction<{ game: TurtleSoupGame }>('getGameData', { gameId, role })
+}
+
+/**
+ * 提交问题
+ * @param gameId 游戏ID
+ * @param content 问题内容
+ */
+export function submitQuestion(gameId: string, content: string) {
+  return callCloudFunction<{ questionId: string }>('submitQuestion', { gameId, content })
+}
+
+/**
+ * 回答问题
+ * @param questionId 问题ID
+ * @param answer 答案
+ */
+export function answerQuestion(questionId: string, answer: string) {
+  return callCloudFunction<{ success: boolean }>('answerQuestion', { questionId, answer })
+}
+
+/**
+ * 公开提示
+ * @param gameId 游戏ID
+ * @param hintIndex 提示索引
+ */
+export function revealHint(gameId: string, hintIndex: number) {
+  return callCloudFunction<{ success: boolean }>('revealHint', { gameId, hintIndex })
+}
+
+/**
+ * 结束游戏
+ * @param gameId 游戏ID
+ * @param result 游戏结果
+ */
+export function endGame(gameId: string, result: { solved: boolean; solvedBy?: string }) {
+  return callCloudFunction<{ gameResult: GameResult }>('endGame', { gameId, result })
+}

+ 2 - 0
src/services/api/index.ts

@@ -0,0 +1,2 @@
+export * from './room'
+export * from './game'

+ 46 - 0
src/services/api/room.ts

@@ -0,0 +1,46 @@
+import { callCloudFunction } from '../cloud'
+import type { Room, RoomSettings } from '@/types/room'
+
+/**
+ * 创建游戏房间
+ * @param gameType 游戏类型
+ * @param settings 房间设置
+ */
+export function createRoom(gameType: string, settings: RoomSettings) {
+  return callCloudFunction<{ roomId: string; roomCode: string }>('createRoom', {
+    gameType,
+    settings
+  })
+}
+
+/**
+ * 加入游戏房间
+ * @param roomCode 房间码
+ */
+export function joinRoom(roomCode: string) {
+  return callCloudFunction<{ room: Room }>('joinRoom', { roomCode })
+}
+
+/**
+ * 获取房间详情
+ * @param roomId 房间ID
+ */
+export function getRoomDetail(roomId: string) {
+  return callCloudFunction<{ room: Room }>('getRoomDetail', { roomId })
+}
+
+/**
+ * 开始游戏
+ * @param roomId 房间ID
+ */
+export function startGame(roomId: string) {
+  return callCloudFunction<{ gameId: string }>('startGame', { roomId })
+}
+
+/**
+ * 离开房间
+ * @param roomId 房间ID
+ */
+export function leaveRoom(roomId: string) {
+  return callCloudFunction<{ success: boolean }>('leaveRoom', { roomId })
+}

+ 89 - 0
src/services/cloud.ts

@@ -0,0 +1,89 @@
+import Taro from '@tarojs/taro'
+
+// 请求选项
+interface CloudOptions {
+  showLoading?: boolean
+  loadingText?: string
+  showError?: boolean
+}
+
+// 默认选项
+const defaultOptions: CloudOptions = {
+  showLoading: true,
+  loadingText: '加载中...',
+  showError: true
+}
+
+/**
+ * 云函数调用封装
+ * @param name 云函数名称
+ * @param data 请求数据
+ * @param options 请求选项
+ */
+export async function callCloudFunction<T = any>(
+  name: string,
+  data?: any,
+  options?: CloudOptions
+): Promise<T> {
+  const opt = { ...defaultOptions, ...options }
+  
+  // 显示加载中
+  if (opt.showLoading) {
+    Taro.showLoading({
+      title: opt.loadingText || '加载中...'
+    })
+  }
+  
+  try {
+    // 基于Taro 4.x的调用方式
+    const { result } = await Taro.cloud.callFunction({
+      name,
+      data
+    })
+    
+    // 隐藏加载中
+    if (opt.showLoading) {
+      Taro.hideLoading()
+    }
+    
+    if (!result) {
+      if (opt.showError) {
+        Taro.showToast({
+          title: '请求失败',
+          icon: 'none'
+        })
+      }
+      throw new Error('云函数返回结果为空')
+    }
+    
+    // 业务错误处理 (假设result可能包含error和success字段)
+    const typedResult = result as any
+    if (typedResult.error || typedResult.success === false) {
+      if (opt.showError) {
+        Taro.showToast({
+          title: typedResult.error || '请求失败',
+          icon: 'none'
+        })
+      }
+      throw new Error(typedResult.error || '请求失败')
+    }
+    
+    // 返回data字段或整个result对象
+    return (typedResult.data !== undefined ? typedResult.data : typedResult) as T
+  } catch (error: any) {
+    // 隐藏加载中
+    if (opt.showLoading) {
+      Taro.hideLoading()
+    }
+    
+    // 显示错误
+    if (opt.showError) {
+      Taro.showToast({
+        title: error.message || '请求异常',
+        icon: 'none'
+      })
+    }
+    
+    throw error
+  }
+}

+ 100 - 0
src/services/request.ts

@@ -0,0 +1,100 @@
+import Taro from '@tarojs/taro'
+
+// 接口响应类型
+interface ApiResponse<T = any> {
+  code: number
+  data: T
+  message: string
+}
+
+// 请求选项
+interface RequestOptions {
+  showLoading?: boolean
+  loadingText?: string
+  showError?: boolean
+}
+
+// 默认选项
+const defaultOptions: RequestOptions = {
+  showLoading: true,
+  loadingText: '加载中...',
+  showError: true
+}
+
+/**
+ * 请求封装
+ * @param url 请求地址
+ * @param method 请求方法
+ * @param data 请求数据
+ * @param options 请求选项
+ */
+export async function request<T = any>(
+  url: string,
+  method: keyof Taro.request.Method = 'GET',
+  data?: any,
+  options?: RequestOptions
+): Promise<T> {
+  const opt = { ...defaultOptions, ...options }
+  
+  // 显示加载中
+  if (opt.showLoading) {
+    Taro.showLoading({
+      title: opt.loadingText || '加载中...' // 添加默认值,确保不会是undefined
+    })
+  }
+  
+  try {
+    const response = await Taro.request<ApiResponse<T>>({
+      url,
+      method,
+      data,
+      header: {
+        'content-type': 'application/json'
+      }
+    })
+    
+    // 隐藏加载中
+    if (opt.showLoading) {
+      Taro.hideLoading()
+    }
+    
+    // 接口请求错误
+    if (response.statusCode !== 200) {
+      if (opt.showError) {
+        Taro.showToast({
+          title: `请求错误: ${response.statusCode}`,
+          icon: 'none'
+        })
+      }
+      throw new Error(`请求错误: ${response.statusCode}`)
+    }
+    
+    // 业务错误
+    if (response.data.code !== 0) {
+      if (opt.showError) {
+        Taro.showToast({
+          title: response.data.message || '请求失败', // 添加默认值
+          icon: 'none'
+        })
+      }
+      throw new Error(response.data.message || '请求失败') // 添加默认值
+    }
+    
+    return response.data.data
+  } catch (error: any) { // 指定error类型为any,以便访问message属性
+    // 隐藏加载中
+    if (opt.showLoading) {
+      Taro.hideLoading()
+    }
+    
+    // 显示错误
+    if (opt.showError) {
+      Taro.showToast({
+        title: error.message || '请求异常', // 添加默认值
+        icon: 'none'
+      })
+    }
+    
+    throw error
+  }
+}

+ 4 - 0
src/stores/index.ts

@@ -0,0 +1,4 @@
+// 统一导出所有store
+export { useUserStore } from './modules/user'
+export { useRoomStore } from './modules/room'
+export { useGameStore } from './modules/game'

+ 190 - 0
src/stores/modules/game.ts

@@ -0,0 +1,190 @@
+import { defineStore } from 'pinia'
+import { ref, computed } from 'vue'
+// 只导入需要的类型和枚举
+import { TurtleSoupGame, GameStatus } from '@/types/game'
+// 使用类型导入避免命名冲突
+import type { GameResult as GameResultType } from '@/types/game'
+import Taro from '@tarojs/taro'
+
+// 定义云函数返回结果的接口,重命名避免冲突
+interface CloudFunctionResult {
+  game?: TurtleSoupGame;
+  questionId?: string;
+  success?: boolean;
+  error?: string;
+  gameResult?: GameResultType;
+  [key: string]: any;
+}
+
+export const useGameStore = defineStore('game', () => {
+  // 当前游戏
+  const currentGame = ref<TurtleSoupGame | null>(null)
+  // 是否正在加载
+  const loading = ref(false)
+  
+  // 当前游戏状态
+  const gameStatus = computed(() => currentGame.value?.status || null)
+  // 是否游戏中
+  const isPlaying = computed(() => gameStatus.value === GameStatus.ONGOING)
+  // 游戏是否已解决
+  const isSolved = computed(() => gameStatus.value === GameStatus.SOLVED)
+  // 已公开的提示
+  const revealedHints = computed(() => {
+    if (!currentGame.value) return []
+    return currentGame.value.revealedHints.map(index => currentGame.value!.hints[index])
+  })
+  
+  // 获取游戏数据
+  async function getGameData(gameId: string, role: string) {
+    loading.value = true
+    try {
+      const res = await Taro.cloud.callFunction({
+        name: 'getGameData',
+        data: { gameId, role }
+      })
+      
+      // 使用类型断言
+      const result = res.result as CloudFunctionResult
+      
+      if (result && result.game) {
+        currentGame.value = result.game
+        return { success: true, game: currentGame.value }
+      }
+      return { success: false, error: result?.error || '获取游戏数据失败' }
+    } catch (error) {
+      console.error('获取游戏数据失败', error)
+      return { success: false, error: '获取游戏数据失败' }
+    } finally {
+      loading.value = false
+    }
+  }
+  
+  // 提交问题
+  async function submitQuestion(content: string) {
+    if (!currentGame.value) {
+      return { success: false, error: '游戏未初始化' }
+    }
+    
+    try {
+      const res = await Taro.cloud.callFunction({
+        name: 'submitQuestion',
+        data: { 
+          gameId: currentGame.value.id, 
+          content 
+        }
+      })
+      
+      // 使用类型断言
+      const result = res.result as CloudFunctionResult
+      
+      if (result && result.questionId) {
+        // 可以在这里直接更新本地游戏状态,也可以重新获取游戏数据
+        return { success: true, questionId: result.questionId }
+      }
+      return { success: false, error: result?.error || '提交问题失败' }
+    } catch (error) {
+      console.error('提交问题失败', error)
+      return { success: false, error: '提交问题失败' }
+    }
+  }
+  
+  // 回答问题 (主持人用)
+  async function answerQuestion(questionId: string, answer: string) {
+    try {
+      const res = await Taro.cloud.callFunction({
+        name: 'answerQuestion',
+        data: { questionId, answer }
+      })
+      
+      // 使用类型断言
+      const result = res.result as CloudFunctionResult
+      
+      if (result && result.success) {
+        // 可以在这里直接更新本地游戏状态,也可以重新获取游戏数据
+        return { success: true }
+      }
+      return { success: false, error: result?.error || '回答问题失败' }
+    } catch (error) {
+      console.error('回答问题失败', error)
+      return { success: false, error: '回答问题失败' }
+    }
+  }
+  
+  // 公开提示 (主持人用)
+  async function revealHint(hintIndex: number) {
+    if (!currentGame.value) {
+      return { success: false, error: '游戏未初始化' }
+    }
+    
+    try {
+      const res = await Taro.cloud.callFunction({
+        name: 'revealHint',
+        data: { 
+          gameId: currentGame.value.id, 
+          hintIndex 
+        }
+      })
+      
+      // 使用类型断言
+      const result = res.result as CloudFunctionResult
+      
+      if (result && result.success) {
+        // 可以在这里直接更新本地游戏状态,也可以重新获取游戏数据
+        return { success: true }
+      }
+      return { success: false, error: result?.error || '公开提示失败' }
+    } catch (error) {
+      console.error('公开提示失败', error)
+      return { success: false, error: '公开提示失败' }
+    }
+  }
+  
+  // 结束游戏
+  async function endGame(result: { solved: boolean, solvedBy?: string }) {
+    if (!currentGame.value) {
+      return { success: false, error: '游戏未初始化' }
+    }
+    
+    try {
+      const res = await Taro.cloud.callFunction({
+        name: 'endGame',
+        data: { 
+          gameId: currentGame.value.id, 
+          result 
+        }
+      })
+      
+      // 使用类型断言
+      const finalResult = res.result as CloudFunctionResult
+      
+      if (finalResult && finalResult.success) {
+        // 可以在这里直接更新本地游戏状态,也可以重新获取游戏数据
+        return { success: true, gameResult: finalResult.gameResult }
+      }
+      return { success: false, error: finalResult?.error || '结束游戏失败' }
+    } catch (error) {
+      console.error('结束游戏失败', error)
+      return { success: false, error: '结束游戏失败' }
+    }
+  }
+  
+  // 清除游戏数据
+  function clearGame() {
+    currentGame.value = null
+  }
+  
+  return {
+    currentGame,
+    loading,
+    gameStatus,
+    isPlaying,
+    isSolved,
+    revealedHints,
+    getGameData,
+    submitQuestion,
+    answerQuestion,
+    revealHint,
+    endGame,
+    clearGame
+  }
+})

+ 185 - 0
src/stores/modules/room.ts

@@ -0,0 +1,185 @@
+import { defineStore } from 'pinia'
+import { ref, computed } from 'vue'
+import { Room, RoomRole, RoomSettings } from '@/types/room'
+import Taro from '@tarojs/taro'
+
+// 定义云函数返回结果的接口
+interface RoomResult {
+  room?: Room;
+  roomCode?: string;
+  role?: RoomRole;
+  error?: string;
+  success?: boolean;
+  [key: string]: any;
+}
+
+export const useRoomStore = defineStore('room', () => {
+  // 当前房间
+  const currentRoom = ref<Room | null>(null)
+  // 用户在当前房间的角色
+  const userRole = ref<RoomRole | null>(null)
+  // 是否正在加载
+  const loading = ref(false)
+  
+  // 用户是否是主持人
+  const isHost = computed(() => userRole.value === RoomRole.HOST)
+  // 用户是否是玩家
+  const isPlayer = computed(() => userRole.value === RoomRole.PLAYER)
+  // 房间是否已满
+  const isRoomFull = computed(() => {
+    if (!currentRoom.value) return false
+    return currentRoom.value.players.length >= currentRoom.value.settings.maxPlayers
+  })
+  
+  // 创建房间
+  async function createRoom(gameType: string, settings: RoomSettings) {
+    loading.value = true
+    try {
+      const res = await Taro.cloud.callFunction({
+        name: 'createRoom',
+        data: { gameType, settings }
+      })
+      
+      // 使用类型断言
+      const result = res.result as RoomResult
+      
+      if (result && result.room) {
+        currentRoom.value = result.room
+        userRole.value = RoomRole.HOST
+        return { success: true, roomCode: currentRoom.value.roomCode }
+      }
+      return { success: false, error: '创建房间失败' }
+    } catch (error) {
+      console.error('创建房间失败', error)
+      return { success: false, error: '创建房间失败' }
+    } finally {
+      loading.value = false
+    }
+  }
+  
+  // 加入房间
+  async function joinRoom(roomCode: string) {
+    loading.value = true
+    try {
+      const res = await Taro.cloud.callFunction({
+        name: 'joinRoom',
+        data: { roomCode }
+      })
+      
+      // 使用类型断言
+      const result = res.result as RoomResult
+      
+      if (result && result.room) {
+        currentRoom.value = result.room
+        userRole.value = RoomRole.PLAYER
+        return { success: true, room: currentRoom.value }
+      }
+      return { success: false, error: result?.error || '加入房间失败' }
+    } catch (error) {
+      console.error('加入房间失败', error)
+      return { success: false, error: '加入房间失败' }
+    } finally {
+      loading.value = false
+    }
+  }
+  
+  // 获取房间详情
+  async function getRoomDetail(roomId: string) {
+    loading.value = true
+    try {
+      const res = await Taro.cloud.callFunction({
+        name: 'getRoomDetail',
+        data: { roomId }
+      })
+      
+      // 使用类型断言
+      const result = res.result as RoomResult
+      
+      if (result && result.room) {
+        currentRoom.value = result.room
+        // 这里可能需要从结果中获取用户角色
+        if (result.role) {
+          userRole.value = result.role
+        }
+        return { success: true, room: currentRoom.value }
+      }
+      return { success: false, error: '获取房间详情失败' }
+    } catch (error) {
+      console.error('获取房间详情失败', error)
+      return { success: false, error: '获取房间详情失败' }
+    } finally {
+      loading.value = false
+    }
+  }
+  
+  // 离开房间
+  async function leaveRoom() {
+    if (!currentRoom.value) return { success: true }
+    
+    try {
+      const res = await Taro.cloud.callFunction({
+        name: 'leaveRoom',
+        data: { roomId: currentRoom.value.id }
+      })
+      
+      // 使用类型断言
+      const result = res.result as RoomResult
+      
+      if (result && result.success) {
+        currentRoom.value = null
+        userRole.value = null
+        return { success: true }
+      }
+      return { success: false, error: '离开房间失败' }
+    } catch (error) {
+      console.error('离开房间失败', error)
+      return { success: false, error: '离开房间失败' }
+    }
+  }
+  
+  // 开始游戏
+  async function startGame() {
+    if (!currentRoom.value || !isHost.value) {
+      return { success: false, error: '没有权限开始游戏' }
+    }
+    
+    try {
+      const res = await Taro.cloud.callFunction({
+        name: 'startGame',
+        data: { roomId: currentRoom.value.id }
+      })
+      
+      // 使用类型断言
+      const result = res.result as { gameId?: string; success?: boolean; error?: string }
+      
+      if (result && result.gameId) {
+        return { success: true, gameId: result.gameId }
+      }
+      return { success: false, error: result?.error || '开始游戏失败' }
+    } catch (error) {
+      console.error('开始游戏失败', error)
+      return { success: false, error: '开始游戏失败' }
+    }
+  }
+  
+  // 清除房间数据
+  function clearRoom() {
+    currentRoom.value = null
+    userRole.value = null
+  }
+  
+  return {
+    currentRoom,
+    userRole,
+    loading,
+    isHost,
+    isPlayer,
+    isRoomFull,
+    createRoom,
+    joinRoom,
+    getRoomDetail,
+    leaveRoom,
+    startGame,
+    clearRoom
+  }
+})

+ 95 - 0
src/stores/modules/user.ts

@@ -0,0 +1,95 @@
+import { defineStore } from 'pinia'
+import { ref } from 'vue'
+import type { User, UserSettings } from '@/types/user'
+import Taro from '@tarojs/taro'
+
+// 定义云函数返回结果的接口
+interface LoginResult {
+  user?: User;
+  openid?: string;
+  [key: string]: any;
+}
+
+export const useUserStore = defineStore('user', () => {
+  // 用户信息
+  const userInfo = ref<User | null>(null)
+  // 用户设置
+  const settings = ref<UserSettings>({
+    theme: 'light',
+    notificationsEnabled: true,
+    vibrationEnabled: true
+  })
+  // 是否已登录
+  const isLoggedIn = ref(false)
+  
+  // 登录
+  async function login() {
+    try {
+      // 微信登录
+      const { code } = await Taro.login()
+      
+      // 调用云函数进行登录
+      const res = await Taro.cloud.callFunction({
+        name: 'login',
+        data: { code }
+      })
+      
+      // 使用类型断言来明确结果类型
+      const result = res.result as LoginResult
+      
+      if (result && result.user) {
+        userInfo.value = result.user
+        isLoggedIn.value = true
+        return true
+      }
+      return false
+    } catch (error) {
+      console.error('登录失败', error)
+      return false
+    }
+  }
+  
+  // 获取用户信息
+  async function getUserInfo() {
+    try {
+      const res = await Taro.cloud.callFunction({
+        name: 'getUserInfo'
+      })
+      
+      // 使用类型断言
+      const result = res.result as LoginResult
+      
+      if (result && result.user) {
+        userInfo.value = result.user
+        return userInfo.value
+      }
+      return null
+    } catch (error) {
+      console.error('获取用户信息失败', error)
+      return null
+    }
+  }
+  
+  // 更新用户设置
+  function updateSettings(newSettings: Partial<UserSettings>) {
+    settings.value = { ...settings.value, ...newSettings }
+    // 可以在这里同步到服务端
+  }
+  
+  // 退出登录
+  function logout() {
+    userInfo.value = null
+    isLoggedIn.value = false
+    // 可以在这里清除存储的登录状态
+  }
+  
+  return {
+    userInfo,
+    settings,
+    isLoggedIn,
+    login,
+    getUserInfo,
+    updateSettings,
+    logout
+  }
+})

+ 57 - 0
src/types/game.ts

@@ -0,0 +1,57 @@
+// 游戏状态枚举
+export enum GameStatus {
+    PREPARING = 'preparing', // 准备中
+    ONGOING = 'ongoing',     // 进行中
+    SOLVED = 'solved',       // 已解决
+    EXPIRED = 'expired'      // 已过期
+}
+  
+// 问题模型
+export interface Question {
+    id: string;            // 问题ID
+    playerId: string;      // 提问玩家ID
+    playerName: string;    // 提问玩家名称
+    content: string;       // 问题内容
+    answer?: string;       // 回答 (是/否/不相关)
+    createdAt: Date;       // 创建时间
+    answeredAt?: Date;     // 回答时间
+}
+  
+// 基础游戏模型
+export interface BaseGame {
+    id: string;            // 游戏ID
+    roomId: string;        // 关联房间ID
+    type: string;          // 游戏类型
+    title: string;         // 标题
+    startTime: Date;       // 开始时间
+    endTime?: Date;        // 结束时间
+    status: GameStatus;    // 游戏状态
+}
+  
+// 海龟汤游戏模型
+export interface TurtleSoupGame extends BaseGame {
+    initialStory: string;  // 初始故事
+    hints: string[];       // 提示列表
+    solution: string;      // 解答
+    revealedHints: number[]; // 已公开提示索引
+    questions: Question[]; // 问题列表
+}
+  
+// 游戏结果模型
+export interface GameResult {
+    gameId: string;
+    solved: boolean;
+    solvedBy?: string;
+    solvedAt?: Date;
+    duration: number;      // 游戏持续时间(秒)
+    questionsCount: number;
+    hintsRevealed: number;
+}
+  
+// 游戏配置选项
+export interface GameOptions {
+    gameType: string;
+    difficulty: string;
+    timeLimit?: number;
+    customSettings?: Record<string, any>;
+}

+ 51 - 0
src/types/room.ts

@@ -0,0 +1,51 @@
+// 房间角色枚举
+export enum RoomRole {
+    HOST = 'host',
+    PLAYER = 'player'
+  }
+  
+  // 房间状态枚举
+  export enum RoomStatus {
+    WAITING = 'waiting',   // 等待中
+    PLAYING = 'playing',   // 游戏中
+    ENDED = 'ended'        // 已结束
+  }
+  
+  // 房间成员模型
+  export interface RoomMember {
+    id: string;            // 用户ID
+    role: RoomRole;        // 主持人/玩家
+    nickname: string;      // 昵称
+    avatarUrl: string;     // 头像
+    joinTime: Date;        // 加入时间
+    isReady: boolean;      // 准备状态
+  }
+  
+  // 房间模型
+  export interface Room {
+    id: string;            // 房间ID
+    roomCode: string;      // 6位房间码
+    gameType: string;      // 游戏类型
+    gameId: string;        // 游戏内容ID
+    hostId: string;        // 主持人ID
+    createdAt: Date;       // 创建时间
+    status: RoomStatus;    // 房间状态
+    settings: RoomSettings; // 房间设置
+    location?: RoomLocation; // 可选位置信息
+    players: RoomMember[]; // 玩家列表
+  }
+  
+  // 房间设置
+  export interface RoomSettings {
+    maxPlayers: number;
+    isPublic: boolean;
+    difficulty: string;
+    hintRevealMode: string; // 自动/手动
+  }
+  
+  // 房间位置
+  export interface RoomLocation {
+    latitude: number;
+    longitude: number;
+    name?: string;       // 位置名称
+  }

+ 23 - 0
src/types/user.ts

@@ -0,0 +1,23 @@
+// 用户模型
+export interface User {
+    id: string;            // 用户唯一ID
+    openId: string;        // 微信OpenID
+    nickname: string;      // 昵称
+    avatarUrl: string;     // 头像
+    createdAt: Date;       // 注册时间
+    stats: UserStats;      // 统计数据
+  }
+  
+  // 用户统计数据
+  export interface UserStats {
+    gamesPlayed: number;   // 玩过的游戏
+    gamesHosted: number;   // 主持的游戏
+    solvedRate: number;    // 解谜成功率
+  }
+  
+  // 用户配置
+  export interface UserSettings {
+    theme: 'light' | 'dark';
+    notificationsEnabled: boolean;
+    vibrationEnabled: boolean;
+  }

+ 10 - 0
src/utils/common.ts

@@ -0,0 +1,10 @@
+/**
+ * 解析URL,确保以/开头
+ * @param url 路径
+ */
+export function resolveUrl(url: string): string {
+    if (url.startsWith('/')) {
+      return url
+    }
+    return `/${url}`
+  }