34 KiB
Aether 设计系统 v2.3
基于 shadcn/ui 和书本纸张主题的完整前端设计规范
版本: 2.3.0 最后更新: 2025-11-18
概述
本文档描述了 Aether 前端项目的设计系统,基于 shadcn/ui 和自定义主题构建。所有组件均已实现并在生产环境中使用。
核心理念
- 一致性优先 - 所有组件遵循统一的视觉语言和交互模式
- 响应式设计 - 组件自适应不同屏幕尺寸(移动端、平板、桌面)
- 可访问性 - 遵循 WCAG 2.1 标准,支持键盘导航和屏幕阅读器
- 性能优化 - 轻量级组件,按需加载,优化渲染性能
- 开发体验 - TypeScript 类型安全,清晰的 API 设计,完善的文档
色彩体系
项目使用书本纸张主题色:
- book-cloth - 书籍封面布料色 (#cc785c / #d4a27f)
- kraft - 牛皮纸色 (#b97847 / #c9a26f)
- manilla - 马尼拉纸色 (#e8ddc5 / #d4c5a9)
- cloud - 云白色 (#f5f3ed / #2a2723)
详细配置见 src/config/theme.ts
技术栈
- Vue 3 - Composition API
- TypeScript - 类型安全
- Tailwind CSS - 原子化 CSS
- shadcn/ui - 基础组件库
- lucide-vue-next - 图标库
- Vite - 构建工具
主题系统
主题配置
主题配置位于 src/config/theme.ts,包含:
export const theme = {
colors: themeColors, // 颜色系统
spacing, // 间距系统(基于 8px 网格)
radius, // 圆角系统
shadows, // 阴影系统
typography, // 字体系统
animations, // 动画系统
breakpoints, // 响应式断点
zIndex, // 层级管理
components: componentDefaults // 组件默认配置
}
CSS 变量
全局 CSS 变量定义在 src/assets/index.css,使用 HSL 色彩空间:
: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/:
布局组件
- 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/,所有组件支持从 @/components/layout 统一导入:
import { PageHeader, PageContainer, Section, CardSection, Grid, StatCard } from '@/components/layout'
PageHeader
页面头部组件,支持标题、描述、图标和操作按钮。
使用示例:
<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
页面容器,提供响应式的最大宽度和内边距。
使用示例:
<template>
<PageContainer maxWidth="2xl" padding="md">
<!-- 页面内容 -->
</PageContainer>
</template>
Props:
maxWidth?: 'sm' | 'md' | 'lg' | 'xl' | '2xl' | 'full'- 最大宽度(默认: '2xl')padding?: 'none' | 'sm' | 'md' | 'lg'- 内边距(默认: 'md')
Section
区块容器,用于分隔页面不同区域。
使用示例:
<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 组件的增强版。
使用示例:
<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
响应式网格布局。
使用示例:
<template>
<Grid :cols="{ sm: 1, md: 2, lg: 3 }" gap="md">
<Card>项目 1</Card>
<Card>项目 2</Card>
<Card>项目 3</Card>
</Grid>
</template>
StatCard
统计卡片,用于展示关键指标。
使用示例:
<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)
1. PageLayout
页面布局容器,集成标题、筛选、分页等功能。
使用示例:
<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
响应式数据表格,桌面端显示表格,移动端自动切换为卡片视图。
使用示例:
<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):
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
智能搜索输入框,支持防抖、清除、建议列表。
使用示例:
<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
筛选栏容器,集成搜索和筛选条件。
使用示例:
<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
分页组件,支持页码导航和每页数量选择。
使用示例:
<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
空状态组件,支持多种类型和自定义内容。
使用示例:
<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
状态徽章组件,用于显示不同状态。
使用示例:
<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
加载状态组件,支持多种加载样式。
使用示例:
<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
带确认对话框的按钮组件,简化危险操作的确认流程。
使用示例:
<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
操作菜单下拉组件,用于集中展示多个操作选项。
使用示例:
<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 接口:
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)
useBreakpoints
响应式断点检测,用于实现响应式布局。
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 通知接口。
import { useToast } from '@/composables/useToast'
const { success, error, warning, info } = useToast()
// 成功消息(5秒后自动消失)
success('操作成功')
success('数据保存成功', '提示')
// 错误消息(8秒后自动消失)
error('操作失败')
error('保存失败,请检查网络连接', '错误')
// 警告消息(8秒后自动消失)
warning('该操作可能影响其他数据', '警告')
// 信息消息(5秒后自动消失)
info('系统将在 5 分钟后进行维护', '系统通知')
接口定义:
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
确认对话框,用于危险操作确认。
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
类名工具函数,简化条件类名的生成。
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
<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)
合理使用插槽
<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>
统一错误处理
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 类
<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 变量
<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>
响应式设计
<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. 性能优化
按需导入组件
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 优化列表
<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 条的列表,使用虚拟滚动:
npm install vue-virtual-scroller
<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
<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>
键盘导航
<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 属性
<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 的页面
- Dashboard.vue
- Users.vue
- Settings.vue
- SystemSettings.vue
- Profile.vue
- ActivityLogs.vue
- Announcements.vue
- ApiKeys.vue
- AuditLogs.vue
- MyApiKeys.vue
- Usage.vue
- ProviderList.vue
- MyProviders.vue
- CacheMonitoring.vue
- 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 - 支持更简洁的组件导入方式
导入方式优化:
// 旧版导入 (繁琐)
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为待废弃组件
迁移指南:
// 旧版导入 (已废弃)
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)
- 基础设计系统搭建
- 实现所有核心业务组件
- 建立主题系统