Files
gamegroup_fe/doc/design/03-组件设计文档.md
2026-01-28 14:35:43 +08:00

20 KiB

GameGroup 组件设计文档

项目名称: GameGroup 前端系统 文档版本: v1.0 更新时间: 2026-01-28


📋 目录


1. 组件概述

1.1 组件分类

组件层级
├── 布局组件 (Layout)
│   ├── MainLayout        # 主布局
│   ├── EmptyLayout       # 空白布局
│   └── AuthLayout        # 认证布局
│
├── 容器组件 (Container)
│   ├── AppHeader         # 顶部导航
│   ├── AppSidebar        # 侧边栏
│   └── AppFooter         # 页脚
│
├── 业务组件 (Business)
│   ├── UserCard          # 用户卡片
│   ├── GroupCard         # 小组卡片
│   ├── GameCard          # 游戏卡片
│   ├── AppointmentCard   # 预约卡片
│   └── HonorBadge        # 荣誉徽章
│
└── 基础组件 (Base)
    ├── Button            # 按钮
    ├── Input             # 输入框
    ├── Select            # 下拉选择
    ├── Modal             # 弹窗
    ├── Drawer            # 抽屉
    ├── Table             # 表格
    ├── Form              # 表单
    ├── Upload            # 上传
    └── ...

1.2 命名规范

  • 单文件组件: PascalCase (UserCard.vue)
  • 组件注册: PascalCase
  • props: camelCase
  • events: kebab-case

1.3 组件结构

<template>
  <!-- 模板内容 -->
</template>

<script setup lang="ts">
// 1. 导入
import { ref, computed } from 'vue'

// 2. Props定义
interface Props {
  // ...
}
const props = withDefaults(defineProps<Props>(), {
  // ...
})

// 3. Emits定义
interface Emits {
  // ...
}
const emit = defineEmits<Emits>()

// 4. 响应式数据
const state = ref()

// 5. 计算属性
const computed = computed(() => {})

// 6. 方法
const method = () => {}

// 7. 生命周期
onMounted(() => {})
</script>

<style scoped lang="scss">
// 样式
</style>

2. 基础组件

2.1 Button 按钮

功能特性

  • 支持多种类型 (primary, secondary, text, danger)
  • 支持多种尺寸 (large, medium, small)
  • 支持图标
  • 支持加载状态
  • 支持禁用状态
  • 支持按钮组

API

interface ButtonProps {
  type?: 'primary' | 'secondary' | 'text' | 'danger'
  size?: 'large' | 'medium' | 'small'
  icon?: string
  loading?: boolean
  disabled?: boolean
  long?: boolean  /* 长按钮,宽度100% */
  nativeType?: 'button' | 'submit' | 'reset'
}

interface ButtonEmits {
  click: [event: MouseEvent]
}

使用示例

<template>
  <!-- 基础用法 -->
  <Button type="primary">主要按钮</Button>
  <Button type="secondary">次要按钮</Button>
  <Button type="text">文字按钮</Button>

  <!-- 尺寸 -->
  <Button size="large">大按钮</Button>
  <Button size="medium">默认按钮</Button>
  <Button size="small">小按钮</Button>

  <!-- 图标 -->
  <Button icon="plus">添加</Button>

  <!-- 加载状态 -->
  <Button loading>加载中...</Button>

  <!-- 按钮组 -->
  <ButtonGroup>
    <Button></Button>
    <Button></Button>
    <Button></Button>
  </ButtonGroup>
</template>

实现代码

<!-- components/base/Button/Button.vue -->
<template>
  <button
    :class="buttonClass"
    :disabled="disabled || loading"
    :type="nativeType"
    @click="handleClick"
  >
    <Icon v-if="loading" name="loading" class="is-loading" />
    <Icon v-else-if="icon" :name="icon" />
    <span v-if="$slots.default"><slot /></span>
  </button>
</template>

<script setup lang="ts">
import { computed } from 'vue'
import type { ButtonProps, ButtonEmits } from './types'

const props = withDefaults(defineProps<ButtonProps>(), {
  type: 'default',
  size: 'medium',
  nativeType: 'button'
})

const emit = defineEmits<ButtonEmits>()

const buttonClass = computed(() => [
  'g-button',
  `g-button--${props.type}`,
  `g-button--${props.size}`,
  {
    'is-disabled': props.disabled,
    'is-loading': props.loading,
    'is-long': props.long
  }
])

const handleClick = (e: MouseEvent) => {
  if (!props.disabled && !props.loading) {
    emit('click', e)
  }
}
</script>

<style scoped lang="scss">
.g-button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  border: none;
  border-radius: 8px;
  font-weight: 600;
  cursor: pointer;
  transition: all 0.2s;

  &:active {
    transform: scale(0.98);
  }

  // 类型
  &--primary {
    background: var(--color-primary-500);
    color: white;

    &:hover:not(.is-disabled) {
      background: var(--color-primary-600);
    }
  }

  &--secondary {
    background: transparent;
    color: var(--color-primary-500);
    border: 2px solid var(--color-primary-500);

    &:hover:not(.is-disabled) {
      background: var(--color-primary-50);
    }
  }

  // 尺寸
  &--large {
    height: 48px;
    padding: 0 24px;
    font-size: 16px;
  }

  &--medium {
    height: 40px;
    padding: 0 20px;
    font-size: 14px;
  }

  &--small {
    height: 32px;
    padding: 0 16px;
    font-size: 12px;
  }

  // 状态
  &.is-disabled {
    opacity: 0.6;
    cursor: not-allowed;
  }

  &.is-loading {
    pointer-events: none;
  }

  &.is-long {
    width: 100%;
  }
}
</style>

2.2 Input 输入框

功能特性

  • 支持前缀/后缀图标
  • 支持清除按钮
  • 支持字数统计
  • 支持搜索模式
  • 支持多行文本域
  • 支持密码显示切换

API

interface InputProps {
  modelValue: string | number
  type?: 'text' | 'password' | 'textarea' | 'search'
  placeholder?: string
  disabled?: boolean
  clearable?: boolean
  showPassword?: boolean
  prefixIcon?: string
  suffixIcon?: string
  maxlength?: number
  rows?: number  /* textarea行数 */
  size?: 'large' | 'medium' | 'small'
}

interface InputEmits {
  'update:modelValue': [value: string]
  'change': [value: string]
  'focus': [event: FocusEvent]
  'blur': [event: FocusEvent]
  'clear': []
}

使用示例

<template>
  <!-- 基础用法 -->
  <Input v-model="value" placeholder="请输入内容" />

  <!-- 可清除 -->
  <Input v-model="value" clearable />

  <!-- 密码框 -->
  <Input v-model="password" type="password" show-password />

  <!-- 带图标 -->
  <Input v-model="email" prefix-icon="email" />

  <!-- 文本域 -->
  <Input v-model="content" type="textarea" :rows="4" />

  <!-- 字数限制 -->
  <Input v-model="text" :maxlength="100" show-word-limit />
</template>

2.3 Modal 弹窗

功能特性

  • 支持自定义头部/底部
  • 支持多种尺寸
  • 支持全屏
  • 支持遮罩层点击关闭
  • 支持键盘ESC关闭
  • 支持嵌套弹窗

API

interface ModalProps {
  visible?: boolean
  title?: string
  width?: string | number
  fullscreen?: boolean
  top?: string  /* 距离顶部距离 */
  modal?: boolean  /* 是否显示遮罩 */
  lockScroll?: boolean  /* 是否锁定滚动 */
  closeOnClickModal?: boolean
  closeOnPressEscape?: boolean
  showClose?: boolean
  beforeClose?: (done: () => void) => void
}

interface ModalEmits {
  'update:visible': [value: boolean]
  open: []
  opened: []
  close: []
  closed: []
}

interface ModalSlots {
  header?: () => VNode
  default?: () => VNode
  footer?: () => VNode
}

使用示例

<template>
  <Modal
    v-model:visible="visible"
    title="提示"
    width="500px"
    :before-close="handleBeforeClose"
  >
    <p>这是一段内容</p>
    <template #footer>
      <Button @click="visible = false">取消</Button>
      <Button type="primary" @click="handleConfirm">确定</Button>
    </template>
  </Modal>
</template>

<script setup lang="ts">
import { ref } from 'vue'

const visible = ref(false)

const handleBeforeClose = (done: () => void) => {
  // 关闭前确认
  done()
}

const handleConfirm = () => {
  visible.value = false
}
</script>

2.4 Card 卡片

功能特性

  • 支持阴影配置
  • 支持边框配置
  • 支持图片封面
  • 支持悬停效果
  • 支持可点击

API

interface CardProps {
  shadow?: 'always' | 'hover' | 'never'
  bodyStyle?: object  /* body样式 */
  header?: string  /* 标题 */
  cover?: string  /* 封面图 */
  clickable?: boolean  /* 可点击 */
  hoverable?: boolean  /* 悬停效果 */
}

interface CardSlots {
  header?: () => VNode
  default?: () => VNode
  cover?: () => VNode
  extra?: () => VNode  /* 头部额外内容 */
}

使用示例

<template>
  <!-- 基础卡片 -->
  <Card>
    <p>卡片内容</p>
  </Card>

  <!-- 带标题 -->
  <Card header="卡片标题">
    <p>卡片内容</p>
    <template #extra>
      <Button size="small">更多</Button>
    </template>
  </Card>

  <!-- 图片卡片 -->
  <Card :cover="imageUrl" hoverable>
    <template #cover>
      <img :src="imageUrl" alt="cover" />
    </template>
    <h4>卡片标题</h4>
    <p>卡片描述</p>
  </Card>
</template>

2.5 Table 表格

功能特性

  • 支持自定义列
  • 支持排序
  • 支持选择
  • 支持分页
  • 支持加载状态
  • 支持展开行

API

interface TableProps<T = any> {
  data: T[]
  columns: Column[]
  stripe?: boolean  /* 斑马纹 */
  border?: boolean
  showHeader?: boolean
  highlightCurrentRow?: boolean
  rowKey?: string | ((row: T) => string)
  defaultSort?: { prop: string; order: 'ascending' | 'descending' }
}

interface Column {
  prop: string
  label: string
  width?: number | string
  minWidth?: number | string
  align?: 'left' | 'center' | 'right'
  sortable?: boolean
  fixed?: 'left' | 'right'
  formatter?: (row: any, column: Column, value: any) => string
  slots?: { default: (scope: { row: any; $index: number }) => VNode }
}

使用示例

<template>
  <Table :data="tableData" :columns="columns" stripe border>
    <template #action="{ row }">
      <Button size="small" @click="handleEdit(row)">编辑</Button>
      <Button size="small" type="danger" @click="handleDelete(row)">删除</Button>
    </template>
  </Table>
</template>

<script setup lang="ts">
import { ref } from 'vue'

interface User {
  id: string
  name: string
  email: string
}

const tableData = ref<User[]>([])

const columns = [
  { prop: 'name', label: '姓名', width: 120 },
  { prop: 'email', label: '邮箱' },
  { prop: 'action', label: '操作', width: 200, slots: { default: 'action' } }
]
</script>

3. 业务组件

3.1 UserCard 用户卡片

功能特性

  • 显示用户头像、昵称、角色
  • 显示会员状态
  • 显示在线状态
  • 支持快捷操作
  • 支持点击跳转

API

interface UserCardProps {
  user: {
    id: string
    username: string
    nickname?: string
    avatar: string
    role?: string
    isMember?: boolean
    isOnline?: boolean
  }
  showActions?: boolean  /* 显示操作按钮 */
  clickable?: boolean  /* 可点击 */
}

interface UserCardEmits {
  click: [user: User]
  chat: [userId: string]
}

使用示例

<template>
  <UserCard
    :user="userInfo"
    :show-actions="true"
    @click="handleUserClick"
    @chat="handleChat"
  />
</template>

<script setup lang="ts">
import { ref } from 'vue'

const userInfo = ref({
  id: '1',
  username: 'johndoe',
  nickname: 'John',
  avatar: 'https://...',
  isMember: true,
  isOnline: true
})

const handleUserClick = (user: User) => {
  // 跳转到用户详情
}

const handleChat = (userId: string) => {
  // 打开聊天
}
</script>

3.2 GroupCard 小组卡片

功能特性

  • 显示小组头像、名称
  • 显示成员数量
  • 显示用户角色
  • 支持快速加入
  • 显示小组标签

API

interface GroupCardProps {
  group: {
    id: string
    name: string
    avatar: string
    description?: string
    currentMembers: number
    maxMembers: number
    myRole?: 'owner' | 'admin' | 'member'
    tags?: string[]
  }
  showJoinButton?: boolean
  showMemberCount?: boolean
}

interface GroupCardEmits {
  click: [group: Group]
  join: [groupId: string]
}

3.3 GameCard 游戏卡片

功能特性

  • 显示游戏封面
  • 显示游戏名称
  • 显示游戏类型标签
  • 显示人数范围
  • 显示平台信息

API

interface GameCardProps {
  game: {
    id: string
    name: string
    coverUrl: string
    description?: string
    tags: string[]
    minPlayers: number
    maxPlayers: number
    platform: string
  }
  showCover?: boolean
  showTags?: boolean
  clickable?: boolean
}

3.4 AppointmentCard 预约卡片

功能特性

  • 显示预约时间
  • 显示参与人数
  • 显示游戏信息
  • 显示预约状态
  • 支持快速加入/退出

API

interface AppointmentCardProps {
  appointment: {
    id: string
    title: string
    startTime: string
    endTime: string
    currentParticipants: number
    maxParticipants: number
    status: 'open' | 'full' | 'cancelled' | 'finished'
    game: Game
    isParticipant: boolean
    isCreator: boolean
  }
  showActions?: boolean
}

interface AppointmentCardEmits {
  join: [appointmentId: string]
  leave: [appointmentId: string]
}

3.5 HonorBadge 荣誉徽章

功能特性

  • 显示荣誉图标
  • 显示荣誉名称
  • 显示获得者
  • 动画效果

API

interface HonorBadgeProps {
  honor: {
    id: string
    title: string
    type: 'YEARLY' | 'MONTHLY' | 'WEEKLY'
    year?: number
    icon?: string
  }
  size?: 'small' | 'medium' | 'large'
  animated?: boolean
}

4. 布局组件

4.1 MainLayout 主布局

功能特性

  • 响应式布局
  • 侧边栏折叠
  • 顶部导航栏
  • 内容区域

结构

┌─────────────────────────────────────────┐
│              AppHeader                  │
├──────────┬──────────────────────────────┤
│          │                              │
│          │                              │
│   App    │        RouterView            │
│ Sidebar  │                              │
│          │                              │
│          │                              │
└──────────┴──────────────────────────────┘

使用示例

<template>
  <MainLayout>
    <RouterView />
  </MainLayout>
</template>

4.2 AppHeader 顶部导航

功能特性

  • Logo展示
  • 导航菜单
  • 搜索框
  • 用户信息
  • 消息通知

API

interface AppHeaderProps {
  showLogo?: boolean
  showSearch?: boolean
  showNotification?: boolean
  fixed?: boolean
}

4.3 AppSidebar 侧边栏

功能特性

  • 可折叠
  • 菜单导航
  • 用户信息卡片
  • 小组快捷入口

API

interface AppSidebarProps {
  collapsed?: boolean
  menuItems: MenuItem[]
}

interface MenuItem {
  path: string
  icon?: string
  label: string
  children?: MenuItem[]
}

5. 组件通信

5.1 Props / Emits

父子组件通信:

<!-- 父组件 -->
<template>
  <ChildComponent
    :message="parentMessage"
    @update="handleUpdate"
  />
</template>

<!-- 子组件 -->
<script setup lang="ts">
interface Props {
  message: string
}

interface Emits {
  update: [value: string]
}

const props = defineProps<Props>()
const emit = defineEmits<Emits>()

// 发送事件
emit('update', 'new value')
</script>

5.2 Provide / Inject

跨层级组件通信:

<!-- 祖先组件 -->
<script setup lang="ts">
import { provide } from 'vue'

provide('theme', {
  color: 'purple',
  size: 'large'
})
</script>

<!-- 后代组件 -->
<script setup lang="ts">
import { inject } from 'vue'

const theme = inject('theme', {
  color: 'blue',
  size: 'medium'
})
</script>

5.3 Slots

插槽传递:

<!-- 父组件 -->
<template>
  <ChildComponent>
    <template #header>
      <h1>自定义标题</h1>
    </template>

    <p>默认内容</p>

    <template #footer="{ close }">
      <Button @click="close">关闭</Button>
    </template>
  </ChildComponent>
</template>

<!-- 子组件 -->
<template>
  <div>
    <slot name="header" />
    <slot />
    <slot name="footer" :close="handleClose" />
  </div>
</template>

5.4 Event Bus

兄弟组件通信:

// utils/eventBus.ts
import { ref } from 'vue'

type EventBus = Record<string, any[]>

const bus = ref<EventBus>({})

export function useEventBus() {
  const emit = (event: string, ...args: any[]) => {
    if (bus.value[event]) {
      bus.value[event].forEach((fn) => fn(...args))
    }
  }

  const on = (event: string, callback: Function) => {
    if (!bus.value[event]) {
      bus.value[event] = []
    }
    bus.value[event].push(callback)
  }

  const off = (event: string, callback?: Function) => {
    if (!callback) {
      delete bus.value[event]
    } else {
      bus.value[event] = bus.value[event].filter((fn) => fn !== callback)
    }
  }

  return { emit, on, off }
}

6. 组件性能优化

6.1 懒加载

动态导入组件:

<script setup lang="ts">
import { defineAsyncComponent } from 'vue'

const HeavyComponent = defineAsyncComponent(() =>
  import('./HeavyComponent.vue')
)
</script>

6.2 KeepAlive

缓存组件状态:

<template>
  <KeepAlive :include="['GroupList', 'GameList']">
    <RouterView />
  </KeepAlive>
</template>

6.3 v-once

静态内容只渲染一次:

<template>
  <div v-once>
    <h1>{{ staticTitle }}</h1>
    <p>{{ staticContent }}</p>
  </div>
</template>

6.4 computed缓存

使用计算属性缓存结果:

<script setup lang="ts">
import { ref, computed } from 'vue'

const list = ref([...])

// 使用computed
const filteredList = computed(() => {
  return list.value.filter(item => item.active)
})

// 而不是methods
const getFilteredList = () => {
  return list.value.filter(item => item.active)
}
</script>

6.5 虚拟滚动

长列表使用虚拟滚动:

<template>
  <VirtualList
    :items="largeList"
    :item-height="50"
    :visible-height="600"
  >
    <template #default="{ item }">
      <div>{{ item.name }}</div>
    </template>
  </VirtualList>
</template>

7. 组件测试

7.1 单元测试

// components/base/Button/__tests__/Button.test.ts
import { mount } from '@vue/test-utils'
import { describe, it, expect } from 'vitest'
import Button from '../Button.vue'

describe('Button', () => {
  it('renders properly', () => {
    const wrapper = mount(Button, {
      slots: { default: 'Click me' }
    })
    expect(wrapper.text()).toBe('Click me')
  })

  it('emits click event', async () => {
    const wrapper = mount(Button)
    await wrapper.trigger('click')
    expect(wrapper.emitted('click')).toBeTruthy()
  })

  it('does not emit click when disabled', async () => {
    const wrapper = mount(Button, {
      props: { disabled: true }
    })
    await wrapper.trigger('click')
    expect(wrapper.emitted('click')).toBeFalsy()
  })
})

8. 总结

本组件设计文档确保:

  • 可复用性: 组件可在多个场景使用
  • 可维护性: 清晰的代码结构和文档
  • 一致性: 统一的API和设计风格
  • 类型安全: 完整的TypeScript支持
  • 性能优化: 合理的优化策略

文档维护: 随组件库演进持续更新 最后更新: 2026-01-28