Files
Aether/frontend/DESIGN_SYSTEM.md
2025-12-10 20:52:44 +08:00

1533 lines
34 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Aether 设计系统 v2.3
> 基于 shadcn/ui 和书本纸张主题的完整前端设计规范
**版本**: 2.3.0
**最后更新**: 2025-11-18
---
## 概述
本文档描述了 Aether 前端项目的设计系统,基于 shadcn/ui 和自定义主题构建。所有组件均已实现并在生产环境中使用。
### 核心理念
1. **一致性优先** - 所有组件遵循统一的视觉语言和交互模式
2. **响应式设计** - 组件自适应不同屏幕尺寸(移动端、平板、桌面)
3. **可访问性** - 遵循 WCAG 2.1 标准,支持键盘导航和屏幕阅读器
4. **性能优化** - 轻量级组件,按需加载,优化渲染性能
5. **开发体验** - TypeScript 类型安全,清晰的 API 设计,完善的文档
### 色彩体系
项目使用书本纸张主题色:
- **book-cloth** - 书籍封面布料色 (#cc785c / #d4a27f)
- **kraft** - 牛皮纸色 (#b97847 / #c9a26f)
- **manilla** - 马尼拉纸色 (#e8ddc5 / #d4c5a9)
- **cloud** - 云白色 (#f5f3ed / #2a2723)
详细配置见 [src/config/theme.ts](src/config/theme.ts)
---
## 技术栈
- **Vue 3** - Composition API
- **TypeScript** - 类型安全
- **Tailwind CSS** - 原子化 CSS
- **shadcn/ui** - 基础组件库
- **lucide-vue-next** - 图标库
- **Vite** - 构建工具
---
## 主题系统
### 主题配置
主题配置位于 [src/config/theme.ts](src/config/theme.ts),包含:
```ts
export const theme = {
colors: themeColors, // 颜色系统
spacing, // 间距系统(基于 8px 网格)
radius, // 圆角系统
shadows, // 阴影系统
typography, // 字体系统
animations, // 动画系统
breakpoints, // 响应式断点
zIndex, // 层级管理
components: componentDefaults // 组件默认配置
}
```
### CSS 变量
全局 CSS 变量定义在 `src/assets/index.css`,使用 HSL 色彩空间:
```css
:root {
--background: 0 0% 100%;
--foreground: 20 14.3% 4.1%;
--primary: 15 55% 58%;
--border: 20 5.9% 90%;
--muted: 60 4.8% 95.9%;
--muted-foreground: 25 5.3% 44.7%;
/* ... 更多变量 */
}
.dark {
--background: 20 14.3% 4.1%;
--foreground: 0 0% 95%;
--primary: 15 45% 68%;
/* ... 暗色模式变量 */
}
```
---
## 组件库
### 基础组件 (shadcn/ui)
所有基础组件位于 [src/components/ui/](src/components/ui/)
#### 布局组件
- **Card** - 卡片容器
- 变体:`default``outline``ghost``interactive`
- **Separator** - 分隔线(水平/垂直)
- **Tabs** - 选项卡容器
#### 表单组件
- **Button** - 按钮
- 变体:`default``destructive``outline``secondary``ghost``link`
- 大小:`sm``md``lg``icon`
- **Input** - 输入框
- **Textarea** - 多行文本框
- **Select** - 下拉选择框
- **Checkbox** - 复选框
- **Switch** - 开关
- **Label** - 表单标签
#### 反馈组件
- **Badge** - 徽章标签
- **Skeleton** - 骨架屏
- **Toast** - 消息提示
- **Dialog** - 对话框/模态框
- **Alert** - 警告提示
#### 数据展示
- **Table** 系列 - 表格组件
- Table、TableHeader、TableBody、TableRow、TableHead、TableCell
- **Avatar** - 头像
- **Progress** - 进度条
---
### 布局组件 (Layout Components)
位于 [src/components/layout/](src/components/layout/),所有组件支持从 `@/components/layout` 统一导入:
```ts
import { PageHeader, PageContainer, Section, CardSection, Grid, StatCard } from '@/components/layout'
```
#### PageHeader
页面头部组件,支持标题、描述、图标和操作按钮。
**使用示例:**
```vue
<script setup lang="ts">
import { PageHeader } from '@/components/layout'
import { Settings } from 'lucide-vue-next'
</script>
<template>
<PageHeader
title="系统设置"
description="管理系统级别的配置和参数"
:icon="Settings"
>
<template #actions>
<Button @click="save">保存配置</Button>
</template>
</PageHeader>
</template>
```
**Props:**
- `title: string` - 页面标题(必填)
- `description?: string` - 页面描述
- `icon?: Component` - 图标组件
**Slots:**
- `icon` - 自定义图标区域
- `actions` - 右侧操作按钮
---
#### PageContainer
页面容器,提供响应式的最大宽度和内边距。
**使用示例:**
```vue
<template>
<PageContainer maxWidth="2xl" padding="md">
<!-- 页面内容 -->
</PageContainer>
</template>
```
**Props:**
- `maxWidth?: 'sm' | 'md' | 'lg' | 'xl' | '2xl' | 'full'` - 最大宽度(默认: '2xl')
- `padding?: 'none' | 'sm' | 'md' | 'lg'` - 内边距(默认: 'md')
---
#### Section
区块容器,用于分隔页面不同区域。
**使用示例:**
```vue
<template>
<Section
title="用户信息"
description="管理用户基本资料"
spacing="md"
>
<template #actions>
<Button size="sm">编辑</Button>
</template>
<!-- 区块内容 -->
</Section>
</template>
```
**Props:**
- `title?: string` - 区块标题
- `description?: string` - 区块描述
- `spacing?: 'none' | 'sm' | 'md' | 'lg'` - 底部间距(默认: 'md')
**Slots:**
- `header` - 自定义头部
- `actions` - 右侧操作按钮
- `default` - 主内容
---
#### CardSection
卡片区块,基于 Card 组件的增强版。
**使用示例:**
```vue
<template>
<CardSection
title="系统配置"
description="配置系统默认参数"
variant="elevated"
padding="lg"
>
<template #actions>
<Button size="sm" variant="ghost">重置</Button>
</template>
<template #footer>
<Button>保存</Button>
</template>
<!-- 卡片内容 -->
</CardSection>
</template>
```
**Props:**
- `title?: string` - 卡片标题
- `description?: string` - 卡片描述
- `variant?: 'default' | 'elevated' | 'glass'` - 卡片样式(默认: 'default')
- `padding?: 'none' | 'sm' | 'md' | 'lg'` - 内边距(默认: 'md')
**Slots:**
- `header` - 自定义头部
- `actions` - 头部右侧操作
- `default` - 主内容
- `footer` - 底部内容
---
#### Grid
响应式网格布局。
**使用示例:**
```vue
<template>
<Grid :cols="{ sm: 1, md: 2, lg: 3 }" gap="md">
<Card>项目 1</Card>
<Card>项目 2</Card>
<Card>项目 3</Card>
</Grid>
</template>
```
---
#### StatCard
统计卡片,用于展示关键指标。
**使用示例:**
```vue
<script setup lang="ts">
import { StatCard } from '@/components/layout'
import { Users } from 'lucide-vue-next'
</script>
<template>
<StatCard
title="总用户数"
:value="1234"
:icon="Users"
trend="up"
:trendValue="12.5"
trendLabel="较上月"
/>
</template>
```
---
#### ShellHeader (待废弃)
旧版页面头部组件,建议迁移到 `PageHeader`
---
### 业务组件 (Common Components)
位于 [src/components/common/](src/components/common/)
#### 1. PageLayout
页面布局容器,集成标题、筛选、分页等功能。
**使用示例:**
```vue
<script setup lang="ts">
import PageLayout from '@/components/common/PageLayout.vue'
import DataTable from '@/components/common/DataTable.vue'
const searchQuery = ref('')
const currentPage = ref(1)
const pageSize = ref(20)
const roleFilter = ref('')
</script>
<template>
<PageLayout
title="用户管理"
subtitle="管理系统用户和权限"
:showFilters="true"
:showPagination="true"
v-model:searchQuery="searchQuery"
v-model:currentPage="currentPage"
v-model:pageSize="pageSize"
:total="totalUsers"
spacing="normal"
maxWidth="full"
>
<template #toolbar>
<Button @click="openAddDialog">添加用户</Button>
</template>
<template #filters>
<Select v-model="roleFilter">
<SelectTrigger><SelectValue placeholder="角色筛选" /></SelectTrigger>
<SelectContent>
<SelectItem value="admin">管理员</SelectItem>
<SelectItem value="user">普通用户</SelectItem>
</SelectContent>
</Select>
</template>
<DataTable :columns="columns" :data="users" />
</PageLayout>
</template>
```
**主要 Props**
| 属性 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `title` | `string` | - | 页面标题(必填) |
| `subtitle` | `string` | - | 页面副标题 |
| `showHeader` | `boolean` | `true` | 是否显示页面头部 |
| `showBackButton` | `boolean` | `false` | 是否显示返回按钮 |
| `maxWidth` | `'sm' \| 'md' \| 'lg' \| 'xl' \| 'full'` | `'full'` | 内容最大宽度 |
| `spacing` | `'tight' \| 'normal' \| 'relaxed'` | `'normal'` | 内容间距 |
| `showFilters` | `boolean` | `false` | 是否显示筛选栏 |
| `showPagination` | `boolean` | `false` | 是否显示分页 |
**主要 Slots**
- `toolbar` - 页面右上角工具栏
- `headerExtra` - 头部额外内容
- `filters` - 筛选条件
- `filterLeft` / `filterRight` - 筛选栏左右插槽
- `default` - 主内容区
- `footer` - 页面底部
---
#### 2. DataTable
响应式数据表格,桌面端显示表格,移动端自动切换为卡片视图。
**使用示例:**
```vue
<script setup lang="ts">
import DataTable, { type DataTableColumn } from '@/components/common/DataTable.vue'
import StatusBadge from '@/components/common/StatusBadge.vue'
const columns: DataTableColumn[] = [
{
key: 'name',
label: '名称',
sortable: true,
width: '200px',
showOnMobile: true
},
{
key: 'email',
label: '邮箱',
align: 'left',
showOnMobile: true
},
{
key: 'status',
label: '状态',
align: 'center',
showOnMobile: true
},
{
key: 'created_at',
label: '创建时间',
formatter: (value) => new Date(value).toLocaleDateString(),
showOnMobile: false
},
{
key: 'actions',
label: '操作',
align: 'right',
showOnMobile: true
}
]
const handleRowClick = (row, index) => {
console.log('点击行:', row)
}
const handleSort = (sortBy, sortOrder) => {
// 处理排序
}
</script>
<template>
<DataTable
:columns="columns"
:data="tableData"
:loading="loading"
:clickable="true"
rowKey="id"
@rowClick="handleRowClick"
@sort="handleSort"
>
<template #cell-status="{ value }">
<StatusBadge :status="value" />
</template>
<template #cell-actions="{ row }">
<div class="flex gap-2">
<Button size="sm" variant="outline" @click.stop="editRow(row)">
编辑
</Button>
<Button size="sm" variant="destructive" @click.stop="deleteRow(row)">
删除
</Button>
</div>
</template>
<template #mobile-card="{ row }">
<!-- 自定义移动端卡片布局 -->
<div class="p-4 space-y-2">
<div class="font-semibold">{{ row.name }}</div>
<div class="text-sm text-muted-foreground">{{ row.email }}</div>
<StatusBadge :status="row.status" />
</div>
</template>
<template #empty>
<EmptyState
type="empty"
title="暂无用户"
description="点击右上角按钮添加第一个用户"
/>
</template>
</DataTable>
</template>
```
**主要 Props**
| 属性 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `columns` | `DataTableColumn[]` | - | 列配置(必填) |
| `data` | `T[]` | - | 数据源(必填) |
| `rowKey` | `string` | `'id'` | 行唯一标识字段 |
| `loading` | `boolean` | `false` | 是否加载中 |
| `clickable` | `boolean` | `false` | 是否可点击行 |
| `emptyTitle` | `string` | `'暂无数据'` | 空状态标题 |
**列配置DataTableColumn**
```ts
interface DataTableColumn<T = any> {
key: string // 列标识(对应数据字段)
label: string // 列标题
width?: string // 列宽度
align?: 'left' | 'center' | 'right' // 对齐方式
sortable?: boolean // 是否可排序
formatter?: (value: any, row: T, index: number) => string // 值格式化
headerClass?: string // 表头样式类
cellClass?: string // 单元格样式类
showOnMobile?: boolean // 是否在移动端显示(默认 true
}
```
**主要 Events**
- `rowClick(row, index)` - 行点击事件
- `sort(sortBy, sortOrder)` - 排序事件
**主要 Slots**
- `cell-{key}` - 自定义单元格内容(接收 `{ row, column, index, value }` 参数)
- `mobile-card` - 自定义移动端卡片布局(接收 `{ row, index }` 参数)
- `empty` - 自定义空状态
- `footer` - 表格底部内容
---
#### 3. SearchInput
智能搜索输入框,支持防抖、清除、建议列表。
**使用示例:**
```vue
<script setup lang="ts">
const searchQuery = ref('')
const searchSuggestions = ['用户名', '邮箱', 'ID']
const searching = ref(false)
const handleSearch = async (value: string) => {
searching.value = true
try {
await performSearch(value)
} finally {
searching.value = false
}
}
</script>
<template>
<SearchInput
v-model="searchQuery"
placeholder="搜索用户名、邮箱..."
:suggestions="searchSuggestions"
:loading="searching"
:debounce="500"
size="md"
@search="handleSearch"
@clear="searchQuery = ''"
/>
</template>
```
**主要 Props**
| 属性 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `modelValue` | `string` | - | 输入值(必填) |
| `placeholder` | `string` | `'搜索...'` | 占位符 |
| `clearable` | `boolean` | `true` | 是否显示清除按钮 |
| `loading` | `boolean` | `false` | 是否显示加载图标 |
| `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | 大小 |
| `suggestions` | `string[]` | `[]` | 搜索建议列表 |
| `debounce` | `number` | `300` | 防抖延迟(毫秒) |
---
#### 4. FilterBar
筛选栏容器,集成搜索和筛选条件。
**使用示例:**
```vue
<template>
<FilterBar
v-model:searchQuery="searchQuery"
:showSearch="true"
:hasActiveFilters="hasFilters"
searchPlaceholder="搜索..."
@searchChange="handleSearch"
@reset="resetFilters"
>
<template #filters>
<Select v-model="statusFilter">
<SelectTrigger class="w-36">
<SelectValue placeholder="状态" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">全部</SelectItem>
<SelectItem value="active">活跃</SelectItem>
<SelectItem value="inactive">禁用</SelectItem>
</SelectContent>
</Select>
<Select v-model="roleFilter">
<SelectTrigger class="w-36">
<SelectValue placeholder="角色" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">全部</SelectItem>
<SelectItem value="admin">管理员</SelectItem>
<SelectItem value="user">用户</SelectItem>
</SelectContent>
</Select>
</template>
</FilterBar>
</template>
```
---
#### 5. Pagination
分页组件,支持页码导航和每页数量选择。
**使用示例:**
```vue
<script setup lang="ts">
const currentPage = ref(1)
const pageSize = ref(20)
const totalRecords = ref(1000)
const loadData = () => {
// 重新加载数据
}
</script>
<template>
<Pagination
v-model:currentPage="currentPage"
v-model:pageSize="pageSize"
:total="totalRecords"
:showPageSizeSelector="true"
:pageSizeOptions="[10, 20, 50, 100]"
@pageChange="loadData"
@pageSizeChange="loadData"
/>
</template>
```
**主要 Props**
| 属性 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `currentPage` | `number` | `1` | 当前页码 |
| `pageSize` | `number` | `20` | 每页显示数量 |
| `total` | `number` | `0` | 总记录数 |
| `showPageSizeSelector` | `boolean` | `true` | 是否显示页面大小选择器 |
| `pageSizeOptions` | `number[]` | `[10, 20, 50, 100]` | 每页数量选项 |
---
#### 6. EmptyState
空状态组件,支持多种类型和自定义内容。
**使用示例:**
```vue
<script setup lang="ts">
import { RefreshCw, Plus } from 'lucide-vue-next'
</script>
<template>
<!-- 搜索无结果 -->
<EmptyState
type="search"
title="未找到结果"
description="尝试使用不同的关键词搜索"
actionText="清空筛选"
:actionIcon="RefreshCw"
@action="resetSearch"
/>
<!-- 筛选无结果 -->
<EmptyState
type="filter"
title="无匹配结果"
description="没有符合当前筛选条件的数据"
actionText="重置筛选"
@action="resetFilters"
/>
<!-- 空数据 -->
<EmptyState
type="empty"
size="lg"
title="暂无用户"
description="点击下方按钮添加第一个用户"
actionText="添加用户"
:actionIcon="Plus"
actionVariant="default"
@action="openAddDialog"
/>
<!-- 加载错误 -->
<EmptyState
type="error"
title="加载失败"
description="数据加载过程中出现错误,请稍后重试"
actionText="重新加载"
@action="retry"
/>
</template>
```
**类型type**
- `default` - 默认空状态
- `search` - 搜索无结果
- `filter` - 筛选无结果
- `error` - 加载错误
- `empty` - 空空如也
- `notFound` - 未找到资源
**主要 Props**
| 属性 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `type` | `EmptyStateType` | `'default'` | 空状态类型 |
| `title` | `string` | - | 标题(自动根据类型设置) |
| `description` | `string` | - | 描述(自动根据类型设置) |
| `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | 大小 |
| `actionText` | `string` | - | 操作按钮文本 |
| `actionIcon` | `Component` | - | 操作按钮图标 |
| `actionVariant` | `ButtonVariant` | `'default'` | 按钮变体 |
---
#### 7. StatusBadge
状态徽章组件,用于显示不同状态。
**使用示例:**
```vue
<template>
<!-- 成功状态 -->
<StatusBadge status="success" label="已激活" :showIcon="true" />
<!-- 错误状态 -->
<StatusBadge status="error" label="已禁用" variant="solid" />
<!-- 警告状态 -->
<StatusBadge status="warning" label="待审核" variant="soft" />
<!-- 信息状态 -->
<StatusBadge status="info" label="处理中" />
<!-- 待处理 -->
<StatusBadge status="pending" label="排队中" />
<!-- 活跃 -->
<StatusBadge status="active" label="在线" />
<!-- 未激活 -->
<StatusBadge status="inactive" label="离线" />
</template>
```
**状态类型status**
| 状态 | 颜色 | 图标 | 用途 |
|------|------|------|------|
| `success` | 绿色 | CheckCircle2 | 成功、完成、已激活 |
| `error` | 红色 | XCircle | 错误、失败、已禁用 |
| `warning` | 黄色 | AlertCircle | 警告、待审核、需注意 |
| `info` | 蓝色 | Info | 信息、提示 |
| `pending` | 灰色 | Clock | 待处理、排队中 |
| `neutral` | 灰色 | Minus | 中性状态 |
| `active` | 主题色 | CheckCircle2 | 活跃、在线 |
| `inactive` | 灰色 | Minus | 未激活、离线 |
**变体variant**
- `solid` - 实心背景
- `soft` - 柔和背景(默认)
- `outline` - 描边样式
---
#### 8. LoadingState
加载状态组件,支持多种加载样式。
**使用示例:**
```vue
<template>
<!-- 旋转加载器 -->
<LoadingState
variant="spinner"
message="加载中,请稍候..."
size="md"
/>
<!-- 骨架屏 -->
<LoadingState
variant="skeleton"
size="lg"
:fullHeight="true"
/>
<!-- 脉冲点 -->
<LoadingState
variant="pulse"
message="正在加载数据..."
/>
</template>
```
**变体variant**
- `spinner` - 旋转加载器(默认)
- `skeleton` - 骨架屏
- `pulse` - 脉冲点动画
---
#### 9. ConfirmButton
带确认对话框的按钮组件,简化危险操作的确认流程。
**使用示例:**
```vue
<script setup lang="ts">
import { ConfirmButton } from '@/components/common'
import { Trash2 } from 'lucide-vue-next'
const handleDelete = async () => {
await deleteItem()
console.log('删除成功')
}
</script>
<template>
<!-- 危险操作确认 -->
<ConfirmButton
variant="destructive"
:icon="Trash2"
confirm-type="danger"
confirm-title="确认删除"
confirm-message="此操作不可撤销确定要删除吗?"
@confirmed="handleDelete"
>
删除
</ConfirmButton>
<!-- 警告确认 -->
<ConfirmButton
confirm-type="warning"
confirm-title="重置配置"
confirm-message="将重置所有配置到默认值"
@confirmed="resetConfig"
>
重置
</ConfirmButton>
<!-- 无需确认直接执行 -->
<ConfirmButton
:require-confirm="false"
@click="handleClick"
>
普通按钮
</ConfirmButton>
</template>
```
**主要 Props:**
| 属性 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `text` | `string` | - | 按钮文本 |
| `variant` | `ButtonVariant` | `'default'` | 按钮样式 |
| `size` | `ButtonSize` | `'md'` | 按钮大小 |
| `icon` | `Component` | - | 图标组件 |
| `disabled` | `boolean` | `false` | 是否禁用 |
| `confirmTitle` | `string` | `'确认操作'` | 确认对话框标题 |
| `confirmMessage` | `string` | `'确定要执行此操作吗?'` | 确认消息 |
| `confirmType` | `'default' \| 'danger' \| 'warning'` | `'default'` | 确认类型 |
| `requireConfirm` | `boolean` | `true` | 是否需要确认 |
**Events:**
- `click` - 不需要确认时触发
- `confirmed` - 确认后触发
- `cancelled` - 取消确认时触发
---
#### 10. ActionMenu
操作菜单下拉组件,用于集中展示多个操作选项。
**使用示例:**
```vue
<script setup lang="ts">
import { ActionMenu, type ActionMenuItem } from '@/components/common'
import { Edit, Copy, Trash, MoreVertical } from 'lucide-vue-next'
const menuItems: ActionMenuItem[] = [
{
label: '编辑',
icon: Edit,
onClick: () => handleEdit()
},
{
label: '复制',
icon: Copy,
onClick: () => handleCopy()
},
{ separator: true },
{
label: '删除',
icon: Trash,
variant: 'destructive',
onClick: async () => {
await handleDelete()
}
}
]
</script>
<template>
<ActionMenu
:items="menuItems"
:trigger-icon="MoreVertical"
trigger-variant="ghost"
placement="bottom-end"
/>
</template>
```
**ActionMenuItem 接口:**
```ts
interface ActionMenuItem {
label?: string // 菜单项标签
icon?: Component // 图标
badge?: string | number // 徽章
variant?: 'default' | 'destructive' // 样式变体
disabled?: boolean // 是否禁用
separator?: boolean // 是否为分隔线
onClick?: () => void | Promise<void> // 点击回调
}
```
**主要 Props:**
| 属性 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `items` | `ActionMenuItem[]` | - | 菜单项列表(必填) |
| `triggerText` | `string` | - | 触发按钮文本 |
| `triggerIcon` | `Component` | - | 触发按钮图标 |
| `triggerVariant` | `ButtonVariant` | `'outline'` | 触发按钮样式 |
| `triggerSize` | `ButtonSize` | `'sm'` | 触发按钮大小 |
| `showChevron` | `boolean` | `true` | 是否显示下拉箭头 |
| `placement` | `'bottom-start' \| 'bottom-end' \| 'top-start' \| 'top-end'` | `'bottom-end'` | 菜单位置 |
---
## 工具函数 (Composables)
位于 [src/composables/](src/composables/)
### useBreakpoints
响应式断点检测,用于实现响应式布局。
```ts
import { useBreakpoints } from '@/composables/useBreakpoints'
const {
windowWidth, // 窗口宽度
isSm, // >= 640px
isMd, // >= 768px
isLg, // >= 1024px
isXl, // >= 1280px
is2Xl, // >= 1536px
current, // 当前断点 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl'
isMobile, // < 768px
isTablet, // 768px ~ 1024px
isDesktop // >= 1024px
} = useBreakpoints()
// 示例:根据屏幕大小显示不同内容
<div v-if="isMobile">移动端视图</div>
<div v-else-if="isTablet">平板视图</div>
<div v-else>桌面视图</div>
```
---
### useToast
消息提示管理,统一的 Toast 通知接口。
```ts
import { useToast } from '@/composables/useToast'
const { success, error, warning, info } = useToast()
// 成功消息5秒后自动消失
success('操作成功')
success('数据保存成功', '提示')
// 错误消息8秒后自动消失
error('操作失败')
error('保存失败,请检查网络连接', '错误')
// 警告消息8秒后自动消失
warning('该操作可能影响其他数据', '警告')
// 信息消息5秒后自动消失
info('系统将在 5 分钟后进行维护', '系统通知')
```
**接口定义:**
```ts
interface UseToast {
toasts: Ref<Toast[]>
success(message: string, title?: string): string
error(message: string, title?: string): string
warning(message: string, title?: string): string
info(message: string, title?: string): string
showToast(options: Omit<Toast, 'id'>): string
removeToast(id: string): void
clearAll(): void
}
interface Toast {
id: string
title?: string
message?: string
variant?: 'success' | 'error' | 'warning' | 'info'
duration?: number
}
```
---
### useConfirm
确认对话框,用于危险操作确认。
```ts
import { useConfirm } from '@/composables/useConfirm'
const { confirm, confirmDanger, confirmWarning } = useConfirm()
// 普通确认
const ok = await confirm('确定要删除吗?', '确认删除')
if (ok) {
await deleteItem()
}
// 危险操作确认(红色按钮)
const ok = await confirmDanger(
'此操作不可撤销,确定继续吗?',
'删除确认'
)
// 警告确认(黄色主题)
const ok = await confirmWarning(
'该操作可能影响其他用户,是否继续?',
'警告'
)
```
---
### useClasses
类名工具函数,简化条件类名的生成。
```ts
import { useClasses } from '@/composables/useClasses'
const { cn, conditional, fromObject } = useClasses()
// 合并类名(过滤 falsy 值)
const className = cn(
'base-class',
isActive && 'active',
error && 'error',
'another-class'
)
// 结果: 'base-class active another-class' (假设 isActive=true, error=false)
// 条件类名
const className = conditional(isActive, 'bg-primary', 'bg-muted')
// 结果: isActive ? 'bg-primary' : 'bg-muted'
// 从对象生成类名
const className = fromObject({
'text-red-500': hasError,
'font-bold': isImportant,
'underline': isLink
})
// 结果: 只包含值为 true 的键
```
---
## 最佳实践
### 1. 组件开发规范
#### 使用 TypeScript
```vue
<script setup lang="ts">
interface Props {
title: string
count?: number
items?: string[]
}
interface Emits {
(e: 'update', value: string): void
(e: 'delete', id: string): void
}
const props = withDefaults(defineProps<Props>(), {
count: 0,
items: () => []
})
const emit = defineEmits<Emits>()
</script>
```
#### 遵循命名规范
- **组件文件**: PascalCase (如 `UserCard.vue``DataTable.vue`
- **Composables**: camelCase + `use` 前缀 (如 `useAuth.ts``useBreakpoints.ts`
- **工具函数**: camelCase (如 `formatDate.ts``validateEmail.ts`
- **常量**: SCREAMING_SNAKE_CASE (如 `API_BASE_URL``MAX_FILE_SIZE`
#### 合理使用插槽
```vue
<template>
<Card>
<!-- 具名插槽 + 默认内容 -->
<template #header>
<slot name="header">
<h3 class="text-lg font-semibold">默认标题</h3>
</slot>
</template>
<!-- 默认插槽 -->
<slot>默认内容</slot>
<!-- 作用域插槽 -->
<template #footer>
<slot name="footer" :count="items.length" :total="total" />
</template>
</Card>
</template>
```
#### 统一错误处理
```ts
import { useToast } from '@/composables/useToast'
import { apiClient } from '@/api/client'
const { error: showError, success: showSuccess } = useToast()
async function saveData() {
try {
await apiClient.post('/users', userData)
showSuccess('用户创建成功')
} catch (err: any) {
const message = err.response?.data?.detail || err.message || '操作失败'
showError(message, '错误')
console.error('Failed to create user:', err)
}
}
```
---
### 2. 样式规范
#### 优先使用 Tailwind 类
```vue
<template>
<div class="flex items-center gap-4 p-6 rounded-lg bg-card border border-border hover:shadow-md transition-shadow">
<Avatar :src="user.avatar" />
<div class="flex-1 min-w-0">
<h3 class="text-lg font-semibold truncate">{{ user.name }}</h3>
<p class="text-sm text-muted-foreground">{{ user.role }}</p>
</div>
<Badge :variant="user.active ? 'success' : 'neutral'">
{{ user.active ? '在线' : '离线' }}
</Badge>
</div>
</template>
```
#### 使用主题 CSS 变量
```vue
<template>
<div class="custom-card">
<!-- 内容 -->
</div>
</template>
<style scoped>
.custom-card {
background-color: hsl(var(--card));
color: hsl(var(--card-foreground));
border: 1px solid hsl(var(--border));
border-radius: var(--radius);
}
.custom-card:hover {
background-color: hsl(var(--muted));
}
</style>
```
#### 响应式设计
```vue
<template>
<!-- 移动端 1 平板 2 桌面 3 -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<Card v-for="item in items" :key="item.id">
{{ item.name }}
</Card>
</div>
<!-- 使用 composable 实现条件渲染 -->
<div v-if="isMobile">移动端布局</div>
<div v-else>桌面端布局</div>
</template>
<script setup lang="ts">
import { useBreakpoints } from '@/composables/useBreakpoints'
const { isMobile } = useBreakpoints()
</script>
```
---
### 3. 性能优化
#### 按需导入组件
```ts
import { defineAsyncComponent } from 'vue'
// 异步加载重型组件
const HeavyChart = defineAsyncComponent(() =>
import('./components/HeavyChart.vue')
)
// 带加载状态的异步组件
const HeavyTable = defineAsyncComponent({
loader: () => import('./components/HeavyTable.vue'),
loadingComponent: LoadingState,
delay: 200,
errorComponent: ErrorState,
timeout: 3000
})
```
#### 使用 v-memo 优化列表
```vue
<template>
<div
v-for="item in largeList"
:key="item.id"
v-memo="[item.updated_at, item.status]"
>
<!-- 仅当 updated_at status 变化时重新渲染 -->
<ItemCard :item="item" />
</div>
</template>
```
#### 虚拟滚动
对于超过 100 条的列表,使用虚拟滚动:
```bash
npm install vue-virtual-scroller
```
```vue
<script setup lang="ts">
import { RecycleScroller } from 'vue-virtual-scroller'
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
</script>
<template>
<RecycleScroller
:items="items"
:item-size="64"
key-field="id"
v-slot="{ item }"
>
<UserCard :user="item" />
</RecycleScroller>
</template>
```
---
### 4. 可访问性 (a11y)
#### 语义化 HTML
```vue
<template>
<nav aria-label="主导航">
<ul role="list">
<li><RouterLink to="/dashboard">仪表盘</RouterLink></li>
<li><RouterLink to="/users">用户管理</RouterLink></li>
</ul>
</nav>
<main>
<h1>页面标题</h1>
<section aria-labelledby="section-title">
<h2 id="section-title">区块标题</h2>
<!-- 内容 -->
</section>
</main>
</template>
```
#### 键盘导航
```vue
<template>
<button
@click="handleClick"
@keydown.enter="handleClick"
@keydown.space.prevent="handleClick"
:aria-label="buttonLabel"
>
<Icon :name="iconName" aria-hidden="true" />
{{ text }}
</button>
<!-- 可聚焦的非按钮元素 -->
<div
role="button"
tabindex="0"
@click="handleAction"
@keydown.enter="handleAction"
@keydown.space.prevent="handleAction"
>
自定义按钮
</div>
</template>
```
#### ARIA 属性
```vue
<template>
<!-- 对话框 -->
<div
role="dialog"
aria-labelledby="dialog-title"
aria-describedby="dialog-description"
aria-modal="true"
>
<h2 id="dialog-title">对话框标题</h2>
<p id="dialog-description">对话框描述</p>
</div>
<!-- 表单 -->
<form>
<label for="username">用户名</label>
<input
id="username"
type="text"
aria-required="true"
aria-invalid="false"
aria-describedby="username-error"
/>
<span id="username-error" role="alert" aria-live="polite">
<!-- 错误信息 -->
</span>
</form>
</template>
```
---
## 组件迁移检查清单
### 已完全迁移到 shadcn 的页面
- [x] Dashboard.vue
- [x] Users.vue
- [x] Settings.vue
- [x] SystemSettings.vue
- [x] Profile.vue
- [x] ActivityLogs.vue
- [x] Announcements.vue
- [x] ApiKeys.vue
- [x] AuditLogs.vue
- [x] MyApiKeys.vue
- [x] Usage.vue
- [x] ProviderList.vue
- [x] MyProviders.vue
- [x] CacheMonitoring.vue
- [x] ProviderDetailNew.vue
### 部分迁移或自定义样式
- [ ] Home.vue - 使用大量自定义动画和样式(不建议迁移)
---
## 更新日志
### v2.3.0 (2025-11-18)
**新增组件:**
- `ConfirmButton` - 带确认对话框的按钮组件
- `ActionMenu` - 操作菜单下拉组件
**优化导入系统:**
- 创建 `@/components/ui/index.ts` 统一导出所有 shadcn UI 组件
- 完善 `@/components/layout/index.ts``@/components/common/index.ts`
- 支持更简洁的组件导入方式
**导入方式优化:**
```ts
// 旧版导入 (繁琐)
import Button from '@/components/ui/button.vue'
import Input from '@/components/ui/input.vue'
import Card from '@/components/ui/card.vue'
// 新版导入 (推荐)
import { Button, Input, Card } from '@/components/ui'
import { PageHeader, Section, CardSection } from '@/components/layout'
import { DataTable, ConfirmButton, ActionMenu } from '@/components/common'
```
**文档:**
- 添加 `ConfirmButton``ActionMenu` 组件完整文档
- 更新组件导入最佳实践
### v2.2.0 (2025-11-18)
**重构:**
- 统一布局组件目录: 将 `layout-v2` 合并到 `layout`
- 所有布局组件现在从 `@/components/layout` 统一导入
- 删除冗余的 `layout-v2` 目录
**文档:**
- 添加完整的布局组件文档和使用示例
- 标记 `ShellHeader` 为待废弃组件
**迁移指南:**
```ts
// 旧版导入 (已废弃)
import { PageHeader } from '@/components/layout-v2'
// 新版导入 (推荐)
import { PageHeader } from '@/components/layout'
```
### v2.1.0 (2025-11-18)
**新增:**
- 完善所有组件文档和使用示例
- 添加完整的 TypeScript 类型定义
- 统一 Toast 工具函数接口useToast
- 完善响应式支持useBreakpoints
**修复:**
- 修复 CacheMonitoring.vue 的 toast 调用
- 统一所有页面的组件使用
**文档:**
- 更新所有组件的使用示例
- 添加最佳实践章节
- 添加性能优化指南
- 添加可访问性指南
### v2.0.0 (2025-11-17)
- 基础设计系统搭建
- 实现所有核心业务组件
- 建立主题系统
---
## 参考资源
- [shadcn/ui 官方文档](https://ui.shadcn.com/)
- [Tailwind CSS 文档](https://tailwindcss.com/docs)
- [Vue 3 文档](https://vuejs.org/)
- [Lucide Icons](https://lucide.dev/)
- [WCAG 2.1 标准](https://www.w3.org/WAI/WCAG21/quickref/)
- [主题配置文件](src/config/theme.ts)