Browse Source

scss definition

wuzj 6 days ago
parent
commit
0008691de4

+ 7 - 0
components.d.ts

@@ -7,8 +7,15 @@ export {}
 
 declare module 'vue' {
   export interface GlobalComponents {
+    GameCard: typeof import('./src/components/GameCard/index.vue')['default']
+    GameProgress: typeof import('./src/components/GameProgress/index.vue')['default']
+    HintCard: typeof import('./src/components/HintCard/index.vue')['default']
     HostOnly: typeof import('./src/components/HostOnly/index.vue')['default']
     NutEmpty: typeof import('@nutui/nutui-taro')['Empty']
+    PlayerList: typeof import('./src/components/PlayerList/index.vue')['default']
     PlayerOnly: typeof import('./src/components/PlayerOnly/index.vue')['default']
+    QuestionCard: typeof import('./src/components/QuestionCard/index.vue')['default']
+    RoomCode: typeof import('./src/components/RoomCode/index.vue')['default']
+    TabBar: typeof import('./src/components/TabBar/index.vue')['default']
   }
 }

+ 14 - 1
config/index.ts

@@ -1,6 +1,7 @@
 import { defineConfig, type UserConfigExport } from '@tarojs/cli'
 import TsconfigPathsPlugin from 'tsconfig-paths-webpack-plugin'
 import { VueLoaderPlugin } from 'vue-loader'
+import { resolve } from 'path' // 添加path导入
 import Components from 'unplugin-vue-components/webpack'
 import NutUIResolver from '@nutui/auto-import-resolver'
 
@@ -18,7 +19,11 @@ export default defineConfig({
   outputRoot: 'dist',
   plugins: ['@tarojs/plugin-html'],
   sass: {
-    data: `@import "@nutui/nutui-taro/dist/styles/variables.scss";`
+    data: `
+    @import "@nutui/nutui-taro/dist/styles/variables.scss";
+    @import "src/assets/styles/variables.scss";
+    @import "src/assets/styles/mixins.scss";
+    `
   },
   defineConstants: {
   },
@@ -60,6 +65,10 @@ export default defineConfig({
         resolvers: [NutUIResolver({taro: true})]
       }))
       chain.resolve.plugin('tsconfig-paths').use(TsconfigPathsPlugin)
+
+      // 为webpack添加路径别名
+      chain.resolve.alias
+        .set('@', resolve(__dirname, '..', 'src'))
     }
   },
   h5: {
@@ -84,6 +93,10 @@ export default defineConfig({
         resolvers: [NutUIResolver({taro: true})]
       }))
       chain.resolve.plugin('tsconfig-paths').use(TsconfigPathsPlugin)
+
+      // 为webpack添加路径别名
+      chain.resolve.alias
+        .set('@', resolve(__dirname, '..', 'src'))
     }
   }
 })

+ 1 - 0
src/app.ts

@@ -3,6 +3,7 @@ import { createPinia } from 'pinia'
 import './app.scss'
 // NutUI样式导入
 import '@nutui/nutui-taro/dist/style.css'
+import './assets/styles/nutui-custom.scss'
 
 const App = createApp({
   onShow(options) {

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

@@ -1,3 +1,5 @@
+// src/assets/styles/mixins.scss
+
 // 单行文本溢出省略
 @mixin text-ellipsis {
     overflow: hidden;

+ 60 - 0
src/assets/styles/nutui-custom.scss

@@ -0,0 +1,60 @@
+// 引入变量
+@import "./variables.scss";
+
+// NutUI组件自定义样式
+:root {
+  // 全局
+  --nut-primary-color: #{$primary-color};
+  --nut-primary-color-end: #{$secondary-color};
+  
+  // 按钮相关变量
+  --nut-button-border-radius: #{$border-radius-base};
+  --nut-button-default-padding: 0 #{$spacing-large};
+  --nut-button-large-height: 44px;
+  --nut-button-default-height: 40px;
+  --nut-button-small-height: 32px;
+  --nut-button-mini-height: 28px;
+  
+  // 卡片相关变量
+  --nut-card-border-radius: #{$border-radius-small};
+  --nut-card-margin: #{$spacing-base};
+  --nut-card-padding: #{$spacing-base};
+  
+  // 标签相关变量
+  --nut-tag-border-radius: #{$border-radius-mini};
+  --nut-tag-font-size: #{$font-size-small};
+  
+  // 选项卡相关变量
+  --nut-tabs-tab-font-size: #{$font-size-base};
+  --nut-tabs-titles-item-active-color: #{$primary-color};
+  
+  // 输入框相关变量
+  --nut-input-border-radius: #{$border-radius-base};
+  --nut-input-placeholder-text-color: #{$text-color-secondary};
+  
+  // 单选按钮相关变量
+  --nut-radio-label-font-size: #{$font-size-base};
+  --nut-radio-label-color: #{$text-color-regular};
+  --nut-radio-checked-icon-border-color: #{$primary-color};
+  
+  // 评分相关变量
+  --nut-rate-icon-color: #{$yellow-color};
+  --nut-rate-icon-void-color: #DCDEE0;
+  
+  // 滑块相关变量
+  --nut-range-button-background: #{$primary-color};
+  --nut-range-bar-background: #{$primary-color};
+  
+  // 对话框相关变量
+  --nut-dialog-border-radius: #{$border-radius-base};
+  --nut-dialog-title-font-size: #{$font-size-large};
+  
+  // Toast相关变量
+  --nut-toast-border-radius: #{$border-radius-base};
+  --nut-toast-text-color: #{$text-color-light};
+  
+  // 底部选项卡相关变量
+  --nut-tabbar-item-text-font-size: #{$font-size-mini};
+  --nut-tabbar-item-text-active-color: #{$primary-color};
+  --nut-tabbar-border-top: 1px solid #{$border-color-base};
+}

+ 76 - 42
src/assets/styles/variables.scss

@@ -1,52 +1,86 @@
+// src/assets/styles/variables.scss
+
 // 主题色
-$primary-color: #3C92FB;
-$secondary-color: #5DABFF;
-$success-color: #07C160;
-$danger-color: #FF5650;
-$warning-color: #FFB11B;
+$primary-color: #3C92FB;          // 主蓝色按钮、高亮元素
+$secondary-color: #5DABFF;        // 次要蓝色
+$success-color: #07C160;          // 成功色(添加这行)
+$green-color: #07C160;            // 绿色标签(已解谜、进行中)
+$orange-color: #FF9F2D;           // 橙色标签、按钮(主持人)
+$blue-light-color: #6EBCFF;       // 浅蓝色标签(玩家)
+$danger-color: #FF5650;           // 危险色(添加这行)
+$red-color: #FF5650;              // 红色标签(新标签)
+$warning-color: #FFB11B;          // 警告色(添加这行)
+$yellow-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;
+$text-color-primary: #333;        // 主要文字
+$text-color-regular: #666;        // 常规文字
+$text-color-secondary: #999;      // 次要文字
+$text-color-light: #FFF;          // 白色文字(按钮内)
 
 // 背景色
-$background-color-base: #f7f8fa;
-$background-color-light: #fff;
+$background-color-base: #F7F8FA;  // 页面背景色 - 修复变量名称
+$background-color-page: #F7F8FA;  // 页面背景
+$background-color-light: #FFF;    // 卡片背景
+$background-color-orange: #FFF8E8; // 主持人背景
+$background-color-blue: #EBF5FF;  // 玩家背景
+$background-color-gold: #FFF8DC;  // 金色背景(海龟汤介绍卡片)
+$background-color-gray: #F5F7FA;  // 灰色背景(输入框、提示区域)
 
-// 圆角
-$border-radius-small: 2px;
-$border-radius-base: 4px;
-$border-radius-large: 8px;
-$border-radius-circle: 50%;
+// 边框颜色
+$border-color-base: #EBEDF0;      // 基本边框
+$border-color-light: #F5F5F5;     // 浅色边框
+$border-color-dark: #DCDEE0;      // 深色边框
+
+// 标签颜色
+$tag-color-new: #FF5650;          // 新标签
+$tag-color-popular: #FF9F2D;      // 热门标签
 
 // 字体大小
-$font-size-mini: 10px;
-$font-size-small: 12px;
-$font-size-base: 14px;
-$font-size-medium: 16px;
-$font-size-large: 18px;
-$font-size-xlarge: 20px;
+$font-size-mini: 10px;           // 最小文字(底部Tab)
+$font-size-small: 12px;          // 小文字(标签、次要信息)
+$font-size-base: 14px;           // 基础文字
+$font-size-medium: 16px;         // 中等文字(标题)
+$font-size-large: 18px;          // 大号文字(主标题)
+$font-size-xlarge: 20px;         // 超大文字
+
+// 字体粗细
+$font-weight-regular: 400;       // 常规
+$font-weight-medium: 500;        // 中等
+$font-weight-bold: 600;          // 粗体
+
+// 行高
+$line-height-tight: 1.2;         // 紧凑
+$line-height-base: 1.5;          // 基础
+$line-height-loose: 1.8;         // 宽松
+
+// 文字间距
+$letter-spacing-tight: -0.2px;   // 紧凑
+$letter-spacing-base: 0;         // 基础
+$letter-spacing-loose: 0.2px;    // 宽松
 
 // 间距
-$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;
+$spacing-mini: 4px;              // 最小间距
+$spacing-small: 8px;             // 小间距
+$spacing-base: 16px;             // 基础间距
+$spacing-medium: 20px;           // 中等间距
+$spacing-large: 24px;            // 大间距
+$spacing-xlarge: 32px;           // 超大间距
+
+// 圆角
+$border-radius-mini: 2px;        // 最小圆角
+$border-radius-small: 4px;       // 小圆角(卡片)
+$border-radius-base: 8px;        // 基础圆角(按钮)
+$border-radius-large: 16px;      // 大圆角
+$border-radius-circle: 50%;      // 圆形
+
+// 阴影
+$shadow-light: 0 2px 6px rgba(0, 0, 0, 0.05);   // 轻微阴影
+$shadow-base: 0 2px 12px rgba(0, 0, 0, 0.1);    // 基础阴影
+$shadow-deep: 0 4px 16px rgba(0, 0, 0, 0.15);   // 深度阴影
+
+// 动效
+$animation-duration-fast: 0.2s;                // 快速动效
+$animation-duration-base: 0.3s;                // 基础动效
+$animation-duration-slow: 0.5s;                // 慢速动效
+$animation-timing-function: ease-in-out;       // 动效曲线

+ 126 - 0
src/components/GameCard/index.vue

@@ -0,0 +1,126 @@
+<template>
+    <view class="game-card" @click="handleClick">
+      <view class="game-card__image">
+        <image :src="image" mode="aspectFill" />
+        <view v-if="isNew" class="game-card__tag game-card__tag--new">新</view>
+        <view v-if="isHot" class="game-card__tag game-card__tag--hot">热门</view>
+      </view>
+      <view class="game-card__content">
+        <view class="game-card__title">{{ title }}</view>
+        <view class="game-card__info">
+          <view class="game-card__players">
+            <nut-icon name="people" size="12"></nut-icon>
+            <text>{{ players }}人</text>
+          </view>
+          <view class="game-card__time">
+            <nut-icon name="clock" size="12"></nut-icon>
+            <text>{{ duration }}分钟</text>
+          </view>
+        </view>
+        <view class="game-card__rating">
+          <nut-rate v-model="rating" readonly allow-half size="12"></nut-rate>
+          <text>{{ rating }}</text>
+        </view>
+      </view>
+    </view>
+  </template>
+  
+  <script setup lang="ts">
+  import { defineProps, defineEmits } from 'vue';
+  
+  const props = defineProps({
+    id: { type: String, required: true },
+    title: { type: String, required: true },
+    image: { type: String, required: true },
+    players: { type: String, default: '0' },
+    duration: { type: String, default: '0' },
+    rating: { type: Number, default: 0 },
+    isNew: { type: Boolean, default: false },
+    isHot: { type: Boolean, default: false }
+  });
+  
+  const emit = defineEmits(['click']);
+  
+  const handleClick = () => {
+    emit('click', props.id);
+  };
+  </script>
+  
+  <style lang="scss">
+  .game-card {
+    border-radius: $border-radius-small;
+    background-color: $background-color-light;
+    overflow: hidden;
+    margin-bottom: $spacing-base;
+    box-shadow: $shadow-light;
+    
+    &__image {
+      position: relative;
+      width: 100%;
+      height: 130px;
+      
+      image {
+        width: 100%;
+        height: 100%;
+      }
+    }
+    
+    &__tag {
+      position: absolute;
+      top: $spacing-mini;
+      right: $spacing-mini;
+      padding: 2px $spacing-mini;
+      border-radius: $border-radius-mini;
+      font-size: $font-size-small;
+      color: $text-color-light;
+      
+      &--new {
+        background-color: $tag-color-new;
+      }
+      
+      &--hot {
+        background-color: $tag-color-popular;
+      }
+    }
+    
+    &__content {
+      padding: $spacing-base;
+    }
+    
+    &__title {
+      font-size: $font-size-medium;
+      font-weight: $font-weight-medium;
+      color: $text-color-primary;
+      margin-bottom: $spacing-small;
+      @include text-ellipsis;
+    }
+    
+    &__info {
+      display: flex;
+      justify-content: space-between;
+      margin-bottom: $spacing-small;
+    }
+    
+    &__players, &__time {
+      display: flex;
+      align-items: center;
+      font-size: $font-size-small;
+      color: $text-color-secondary;
+      
+      text {
+        margin-left: 4px;
+      }
+    }
+    
+    &__rating {
+      display: flex;
+      align-items: center;
+      
+      text {
+        margin-left: $spacing-small;
+        font-size: $font-size-small;
+        color: $text-color-secondary;
+      }
+    }
+  }
+  </style>

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

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

+ 131 - 0
src/components/HintCard/index.vue

@@ -0,0 +1,131 @@
+<template>
+    <view class="hint-card" :class="{'hint-card--revealed': isRevealed}">
+      <view class="hint-card__header">
+        <view class="hint-card__title">
+          提示{{ index + 1 }}
+          <text v-if="isRevealed" class="hint-card__time">{{ formatTime(revealTime) }}</text>
+        </view>
+        <view v-if="isHost && !isRevealed" class="hint-card__actions">
+          <nut-button size="mini" type="primary" @click="onReveal">公开</nut-button>
+        </view>
+      </view>
+      <view v-if="isRevealed" class="hint-card__content">
+        {{ content }}
+      </view>
+      <view v-else-if="isHost" class="hint-card__content hint-card__content--preview">
+        {{ content }}
+        <view class="hint-card__mask">仅主持人可见</view>
+      </view>
+      <view v-else class="hint-card__content hint-card__content--hidden">
+        <nut-icon name="lock" size="20"></nut-icon>
+        <text>提示尚未公开</text>
+      </view>
+    </view>
+  </template>
+  
+  <script setup lang="ts">
+  import { defineProps, defineEmits } from 'vue';
+  import { formatDistanceToNow } from 'date-fns';
+  import { zhCN } from 'date-fns/locale';
+  
+  const props = defineProps({
+    index: { type: Number, required: true },
+    content: { type: String, required: true },
+    isRevealed: { type: Boolean, default: false },
+    isHost: { type: Boolean, default: false },
+    revealTime: { type: Date, default: null }
+  });
+  
+  const emit = defineEmits(['reveal']);
+  
+  const formatTime = (date: Date) => {
+    if (!date) return '';
+    return formatDistanceToNow(new Date(date), { 
+      addSuffix: true,
+      locale: zhCN
+    });
+  };
+  
+  const onReveal = () => {
+    emit('reveal', props.index);
+  };
+  </script>
+  
+  <style lang="scss">
+  .hint-card {
+    background-color: $background-color-gray;
+    border-radius: $border-radius-small;
+    padding: $spacing-base;
+    margin-bottom: $spacing-base;
+    transition: all $animation-duration-base;
+    
+    &--revealed {
+      background-color: $background-color-light;
+      box-shadow: $shadow-light;
+    }
+    
+    &__header {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      margin-bottom: $spacing-small;
+    }
+    
+    &__title {
+      font-size: $font-size-small;
+      font-weight: $font-weight-medium;
+      color: $text-color-primary;
+    }
+    
+    &__time {
+      font-size: $font-size-small;
+      color: $text-color-secondary;
+      font-weight: $font-weight-regular;
+      margin-left: $spacing-small;
+    }
+    
+    &__content {
+      font-size: $font-size-base;
+      color: $text-color-primary;
+      line-height: $line-height-base;
+      
+      &--preview {
+        position: relative;
+        filter: blur(1px);
+        opacity: 0.8;
+      }
+      
+      &--hidden {
+        display: flex;
+        flex-direction: column;
+        align-items: center;
+        justify-content: center;
+        padding: $spacing-large 0;
+        color: $text-color-secondary;
+        
+        text {
+          margin-top: $spacing-small;
+          font-size: $font-size-small;
+        }
+      }
+    }
+    
+    &__mask {
+      position: absolute;
+      top: 0;
+      left: 0;
+      right: 0;
+      bottom: 0;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      background-color: rgba(255, 255, 255, 0.5);
+      color: $primary-color;
+      font-size: $font-size-small;
+    }
+    
+    &__actions {
+      display: flex;
+    }
+  }
+  </style>

+ 152 - 0
src/components/PlayerList/index.vue

@@ -0,0 +1,152 @@
+<template>
+    <view class="player-list">
+      <view class="player-list__title">
+        玩家 ({{ players.length }}/{{ maxPlayers }})
+      </view>
+      <view class="player-list__content">
+        <view 
+          v-for="(player, index) in players" 
+          :key="player.id" 
+          class="player-list__item"
+        >
+          <view class="player-list__avatar">
+            <image :src="player.avatarUrl" mode="aspectFill" />
+            <view 
+              v-if="player.role === 'host'" 
+              class="player-list__role player-list__role--host"
+            >
+              主持人
+            </view>
+            <view 
+              v-else 
+              class="player-list__role player-list__role--player"
+            >
+              玩家
+            </view>
+          </view>
+          <view class="player-list__name">{{ player.nickname }}</view>
+          <view v-if="showStatus" class="player-list__status">
+            <view 
+              v-if="player.isReady" 
+              class="player-list__ready player-list__ready--true"
+            >
+              已准备
+            </view>
+            <view 
+              v-else 
+              class="player-list__ready player-list__ready--false"
+            >
+              未准备
+            </view>
+          </view>
+        </view>
+      </view>
+    </view>
+  </template>
+  
+  <script setup lang="ts">
+  import { defineProps } from 'vue';
+  import type { RoomMember } from '@/types/room';
+  
+  defineProps({
+    players: { 
+      type: Array as () => RoomMember[], 
+      required: true 
+    },
+    maxPlayers: { 
+      type: Number, 
+      default: 10 
+    },
+    showStatus: { 
+      type: Boolean, 
+      default: true 
+    }
+  });
+  </script>
+  
+  <style lang="scss">
+  .player-list {
+    margin: $spacing-base 0;
+    
+    &__title {
+      font-size: $font-size-medium;
+      font-weight: $font-weight-medium;
+      color: $text-color-primary;
+      margin-bottom: $spacing-base;
+    }
+    
+    &__content {
+      display: flex;
+      flex-wrap: wrap;
+      gap: $spacing-base;
+    }
+    
+    &__item {
+      display: flex;
+      flex-direction: column;
+      align-items: center;
+      width: 60px;
+    }
+    
+    &__avatar {
+      position: relative;
+      width: 44px;
+      height: 44px;
+      border-radius: $border-radius-circle;
+      overflow: hidden;
+      margin-bottom: $spacing-mini;
+      
+      image {
+        width: 100%;
+        height: 100%;
+      }
+    }
+    
+    &__role {
+      position: absolute;
+      bottom: 0;
+      left: 0;
+      right: 0;
+      font-size: 10px;
+      padding: 1px 0;
+      text-align: center;
+      color: $text-color-light;
+      
+      &--host {
+        background-color: $orange-color;
+      }
+      
+      &--player {
+        background-color: $blue-light-color;
+      }
+    }
+    
+    &__name {
+      font-size: $font-size-small;
+      color: $text-color-primary;
+      max-width: 60px;
+      text-align: center;
+      @include text-ellipsis;
+    }
+    
+    &__status {
+      margin-top: $spacing-mini;
+    }
+    
+    &__ready {
+      font-size: 10px;
+      padding: 1px $spacing-mini;
+      border-radius: $border-radius-mini;
+      
+      &--true {
+        background-color: rgba($green-color, 0.1);
+        color: $green-color;
+      }
+      
+      &--false {
+        background-color: rgba($text-color-secondary, 0.1);
+        color: $text-color-secondary;
+      }
+    }
+  }
+  </style>

+ 136 - 0
src/components/QuestionCard/index.vue

@@ -0,0 +1,136 @@
+<template>
+    <view class="question-card" :class="{'question-card--answered': question.answeredAt}">
+      <view class="question-card__header">
+        <view class="question-card__user">
+          <image :src="question.playerAvatar" mode="aspectFill" class="question-card__avatar" />
+          <text class="question-card__name">{{ question.playerName }}</text>
+        </view>
+        <view class="question-card__time">{{ formatTime(question.createdAt) }}</view>
+      </view>
+      <view class="question-card__content">
+        {{ question.content }}
+      </view>
+      <view v-if="question.answeredAt" class="question-card__answer">
+        <view class="question-card__answer-label">回答</view>
+        <view class="question-card__answer-content">{{ question.answer }}</view>
+      </view>
+      <view v-else-if="isHost" class="question-card__actions">
+        <nut-button size="small" type="primary" @click="onAnswer('是')">是</nut-button>
+        <nut-button size="small" type="primary" @click="onAnswer('否')">否</nut-button>
+        <nut-button size="small" @click="onAnswer('不相关')">不相关</nut-button>
+      </view>
+    </view>
+  </template>
+  
+  <script setup lang="ts">
+  import { defineProps, defineEmits } from 'vue';
+  import type { Question } from '@/types/game';
+  import { formatDistanceToNow } from 'date-fns';
+  import { zhCN } from 'date-fns/locale';
+  
+  interface QuestionWithAvatar extends Question {
+    playerAvatar: string;
+  }
+  
+  const props = defineProps({
+    question: { 
+      type: Object as () => QuestionWithAvatar, 
+      required: true 
+    },
+    isHost: { 
+      type: Boolean, 
+      default: false 
+    }
+  });
+  
+  const emit = defineEmits(['answer']);
+  
+  const formatTime = (date: Date) => {
+    return formatDistanceToNow(new Date(date), { 
+      addSuffix: true,
+      locale: zhCN
+    });
+  };
+  
+  const onAnswer = (answer: string) => {
+    emit('answer', { 
+      questionId: props.question.id, 
+      answer 
+    });
+  };
+  </script>
+  
+  <style lang="scss">
+  .question-card {
+    background-color: $background-color-light;
+    border-radius: $border-radius-small;
+    padding: $spacing-base;
+    margin-bottom: $spacing-base;
+    box-shadow: $shadow-light;
+    
+    &--answered {
+      background-color: $background-color-gray;
+    }
+    
+    &__header {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      margin-bottom: $spacing-small;
+    }
+    
+    &__user {
+      display: flex;
+      align-items: center;
+    }
+    
+    &__avatar {
+      width: 24px;
+      height: 24px;
+      border-radius: $border-radius-circle;
+      margin-right: $spacing-mini;
+    }
+    
+    &__name {
+      font-size: $font-size-small;
+      color: $text-color-primary;
+      font-weight: $font-weight-medium;
+    }
+    
+    &__time {
+      font-size: $font-size-small;
+      color: $text-color-secondary;
+    }
+    
+    &__content {
+      font-size: $font-size-base;
+      color: $text-color-primary;
+      margin-bottom: $spacing-base;
+      line-height: $line-height-base;
+    }
+    
+    &__answer {
+      background-color: rgba($primary-color, 0.05);
+      padding: $spacing-small;
+      border-radius: $border-radius-small;
+      display: flex;
+    }
+    
+    &__answer-label {
+      font-size: $font-size-small;
+      color: $primary-color;
+      font-weight: $font-weight-medium;
+      margin-right: $spacing-small;
+    }
+    
+    &__answer-content {
+      font-size: $font-size-small;
+      color: $text-color-primary;
+    }
+    
+    &__actions {
+      display: flex;
+      gap: $spacing-small;
+    }
+  }
+  </style>

+ 69 - 0
src/components/RoomCode/index.vue

@@ -0,0 +1,69 @@
+<template>
+    <view class="room-code">
+      <view class="room-code__title">房间码</view>
+      <view class="room-code__value">{{ code }}</view>
+      <view class="room-code__actions">
+        <nut-button size="small" type="primary" @click="handleShare">分享房间</nut-button>
+        <nut-button size="small" plain @click="handleCopy">复制</nut-button>
+      </view>
+    </view>
+  </template>
+  
+  <script setup lang="ts">
+  import { defineProps, defineEmits } from 'vue';
+  import Taro from '@tarojs/taro';
+  
+  const props = defineProps({
+    code: { type: String, required: true }
+  });
+  
+  const emit = defineEmits(['share', 'copy']);
+  
+  const handleShare = () => {
+    emit('share', props.code);
+  };
+  
+  const handleCopy = () => {
+    Taro.setClipboardData({
+      data: props.code,
+      success: () => {
+        Taro.showToast({
+          title: '房间码已复制',
+          icon: 'success',
+          duration: 2000
+        });
+        emit('copy', props.code);
+      }
+    });
+  };
+  </script>
+  
+  <style lang="scss">
+  .room-code {
+    background-color: $background-color-gold;
+    border-radius: $border-radius-small;
+    padding: $spacing-base;
+    margin: $spacing-base 0;
+    text-align: center;
+    
+    &__title {
+      font-size: $font-size-small;
+      color: $text-color-secondary;
+      margin-bottom: $spacing-mini;
+    }
+    
+    &__value {
+      font-size: $font-size-xlarge;
+      font-weight: $font-weight-bold;
+      color: $text-color-primary;
+      letter-spacing: 2px;
+      margin-bottom: $spacing-base;
+    }
+    
+    &__actions {
+      display: flex;
+      justify-content: center;
+      gap: $spacing-small;
+    }
+  }
+  </style>

+ 48 - 0
src/components/TabBar/index.vue

@@ -0,0 +1,48 @@
+<template>
+    <nut-tabbar :value="active" @change="onChange">
+      <nut-tabbar-item tab-title="游戏广场" icon="home"></nut-tabbar-item>
+      <nut-tabbar-item tab-title="我的房间" icon="location"></nut-tabbar-item>
+      <nut-tabbar-item tab-title="游戏历史" icon="time"></nut-tabbar-item>
+      <nut-tabbar-item tab-title="我的" icon="my"></nut-tabbar-item>
+    </nut-tabbar>
+  </template>
+  
+  <script setup lang="ts">
+  import { defineProps, defineEmits } from 'vue';
+  import Taro from '@tarojs/taro';
+  
+  const props = defineProps({
+    active: { type: Number, default: 0 }
+  });
+  
+  const emit = defineEmits(['change']);
+  
+  const pages = [
+    '/pages/index/index',
+    '/pages/room/waiting/index',
+    '/pages/history/index',
+    '/pages/profile/index'
+  ];
+  
+  const onChange = (value: number) => {
+    emit('change', value);
+    Taro.switchTab({ url: pages[value] });
+  };
+  </script>
+  
+  <style lang="scss">
+  // 自定义底部导航样式
+  .nut-tabbar {
+    padding-bottom: constant(safe-area-inset-bottom);
+    padding-bottom: env(safe-area-inset-bottom);
+    box-shadow: 0 -1px 5px rgba(0, 0, 0, 0.05);
+    
+    &-item__icon {
+      margin-bottom: $spacing-mini;
+    }
+    
+    &-item__text {
+      margin-top: 0;
+    }
+  }
+  </style>