20 KiB
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