feat: 优化

This commit is contained in:
2026-03-16 00:35:45 +08:00
parent c8c62a1427
commit 6639a751bc
7 changed files with 1752 additions and 559 deletions

View File

@@ -0,0 +1,211 @@
---
name: antd-to-shadcn
description: |
Vue 3 项目从 Ant Design Vue 迁移到 shadcn-vue 的专业技能。
当用户请求以下操作时使用此技能:
- 将 Ant Design 组件替换为 shadcn 组件
- 迁移表单、表格、弹窗等复杂组件
- 保持业务功能不变的前提下现代化 UI
- 颜色解耦和主题配置
- 组件间距和布局优化
触发词迁移、shadcn、antd 替换、组件升级、UI 现代化
---
# Ant Design Vue → shadcn-vue 迁移技能
将 Vue 3 + Ant Design Vue 项目迁移到 shadcn-vue + TailwindCSS。
## 核心原则
1. **业务功能不变** - 只改 UI 层,不改业务逻辑
2. **现代化 UI** - 采用 shadcn 设计语言,简洁现代
3. **颜色解耦** - 使用 CSS 变量,支持主题切换
4. **渐进式迁移** - 逐个组件替换,保持可运行
## 设计品质要求
### 抵制过时设计
**拒绝老气 UI**
- ❌ 粗重边框、多重边框嵌套
- ❌ 灰暗沉闷的配色
- ❌ 过度装饰的渐变和阴影
- ❌ 拥挤无呼吸感的布局
- ❌ 间距混乱、缺乏层级
**拥抱现代设计**
- ✅ 轻量边框或无边框设计
- ✅ 明亮有活力的色彩
- ✅ 克制的阴影shadow-sm 为主)
- ✅ 充足留白,呼吸感
- ✅ 清晰的视觉层级8px 间距递进)
### 颜色设计原则
**年轻活力感**
- 主色饱和度适中oklch chroma 0.14-0.18
- 避免过于灰暗的中间色
- 使用微妙渐变增加质感
- 深色模式保持足够对比度
**发现优秀配色时**
1. 提取关键颜色值
2. 添加到 `style.css` 设计令牌
3. 使用语义化命名(如 `--color-accent-blue`
4. 在组件中通过变量引用
### 间距层级规范
```
紧密gap-1 (4px) - 图标与文字
标准gap-2 (8px) - 同组元素
舒适gap-3 (12px) - 表单项之间
宽松gap-4 (16px) - 卡片内容
分区gap-6 (24px) - 不同区块
```
## 项目上下文
```
前端目录: frontend/app/web-gold/
组件目录: src/components/ui/ # shadcn 组件
主题文件: src/theme.css # shadcn 主题变量
样式文件: src/style.css # 设计令牌
```
## 迁移工作流
### Step 1: 识别 Ant Design 组件
扫描文件中的 Ant Design 导入:
```vue
// 需要替换的模式
import { Button, Input, Form, Table, Modal, ... } from 'ant-design-vue'
import { IconName } from '@ant-design/icons-vue'
import { message, notification } from 'ant-design-vue'
```
### Step 2: 组件映射
参见 [COMPONENT_MAP.md](references/COMPONENT_MAP.md) 获取完整映射表。
快速参考:
| Ant Design | shadcn-vue |
|------------|------------|
| a-button | Button |
| a-input | Input |
| a-form | Form |
| a-table | Table |
| a-modal | Dialog |
| a-select | Select |
| a-message | Sonner (toast) |
| a-icon | Iconify lucide |
### Step 3: 迁移模式
参见 [MIGRATION_PATTERNS.md](references/MIGRATION_PATTERNS.md) 获取详细代码示例。
### Step 4: 样式迁移
参见 [STYLES.md](references/STYLES.md) 获取样式迁移指南。
## 执行迁移
### 1. 表单组件迁移
```vue
<!-- Before: Ant Design -->
<a-form :model="form" :rules="rules" ref="formRef">
<a-form-item name="field" label="标签">
<a-input v-model:value="form.field" />
</a-form-item>
</a-form>
<!-- After: shadcn-vue -->
<Form v-model="form" :schema="schema" @submit="onSubmit">
<FormField name="field" v-slot="{ componentField }">
<FormItem>
<FormLabel>标签</FormLabel>
<FormControl>
<Input v-bind="componentField" />
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</Form>
```
### 2. 表格组件迁移
```vue
<!-- Before: Ant Design -->
<a-table :columns="columns" :data-source="data" />
<!-- After: shadcn-vue -->
<Table>
<TableHeader>
<TableRow>
<TableHead v-for="col in columns" :key="col.key">{{ col.title }}</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow v-for="row in data" :key="row.id">
<TableCell v-for="col in columns" :key="col.key">{{ row[col.dataIndex] }}</TableCell>
</TableRow>
</TableBody>
</Table>
```
### 3. 消息提示迁移
```javascript
// Before
import { message } from 'ant-design-vue'
message.success('操作成功')
// After
import { toast } from 'vue-sonner'
toast.success('操作成功')
```
### 4. 图标迁移
```vue
<!-- Before -->
import { UserOutlined } from '@ant-design/icons-vue'
<UserOutlined />
<!-- After -->
import { Icon } from '@iconify/vue'
<Icon icon="lucide:user" />
```
## 间距规范
使用 Tailwind 间距类替代固定值:
- `p-4` = 16px
- `gap-3` = 12px
- `space-y-4` = 子元素间 16px 间距
## 颜色使用
使用语义化 CSS 变量:
```css
color: var(--foreground) /* 主文字 */
color: var(--muted-foreground) /* 次要文字 */
background: var(--background) /* 背景 */
background: var(--primary) /* 主色 */
border-color: var(--border) /* 边框 */
```
## 检查清单
迁移完成后验证:
- [ ] 所有 a- 前缀组件已替换
- [ ] ant-design-vue 导入已移除
- [ ] 图标已迁移到 Iconify
- [ ] message/notification 已迁移到 Sonner
- [ ] 样式使用 Tailwind 类或 CSS 变量
- [ ] 业务功能测试通过

View File

@@ -0,0 +1,162 @@
# 组件映射表
完整的 Ant Design Vue → shadcn-vue 组件映射。
## 基础组件
| Ant Design | shadcn-vue | 说明 |
|------------|------------|------|
| `a-button` | `Button` | 按钮 |
| `a-input` | `Input` | 输入框 |
| `a-input-number` | `NumberField` | 数字输入 |
| `a-input-password` | `Input type="password"` | 密码输入 |
| `a-textarea` | `Textarea` | 多行文本 |
| `a-select` | `Select` | 下拉选择 |
| `a-radio` | `RadioGroup` + `RadioGroupItem` | 单选 |
| `a-checkbox` | `Checkbox` | 复选框 |
| `a-switch` | `Switch` | 开关 |
| `a-slider` | `Slider` | 滑动条 |
| `a-rate` | 自定义 | 评分(需自定义) |
| `a-upload` | 自定义 | 上传(需自定义) |
## 表单组件
| Ant Design | shadcn-vue | 说明 |
|------------|------------|------|
| `a-form` | `Form` | 表单容器 |
| `a-form-item` | `FormItem` | 表单项 |
| `a-form-provider` | 自定义 | 表单上下文 |
| `a-range-picker` | `RangeCalendar` + `Popover` | 日期范围 |
| `a-date-picker` | `Calendar` + `Popover` | 日期选择 |
| `a-time-picker` | 自定义 | 时间选择 |
## 数据展示
| Ant Design | shadcn-vue | 说明 |
|------------|------------|------|
| `a-table` | `Table` | 表格 |
| `a-list` | 自定义 | 列表 |
| `a-card` | `Card` | 卡片 |
| `a-descriptions` | 自定义 | 描述列表 |
| `a-statistic` | 自定义 | 统计数值 |
| `a-tree` | 自定义 | 树形控件 |
| `a-avatar` | `Avatar` | 头像 |
| `a-badge` | `Badge` | 徽标 |
| `a-tag` | `Badge` variant | 标签 |
| `a-timeline` | 自定义 | 时间轴 |
| `a-image` | `Img` 或自定义 | 图片 |
| `a-empty` | `Empty` | 空状态 |
## 导航组件
| Ant Design | shadcn-vue | 说明 |
|------------|------------|------|
| `a-menu` | `NavigationMenu` / `SidebarMenu` | 菜单 |
| `a-dropdown` | `DropdownMenu` | 下拉菜单 |
| `a-pagination` | `Pagination` | 分页 |
| `a-steps` | `Stepper` | 步骤条 |
| `a-breadcrumb` | `Breadcrumb` | 面包屑 |
| `a-tabs` | `Tabs` | 标签页 |
## 反馈组件
| Ant Design | shadcn-vue | 说明 |
|------------|------------|------|
| `a-modal` | `Dialog` | 对话框 |
| `a-drawer` | `Drawer` | 抽屉 |
| `a-message` | `toast` (vue-sonner) | 全局提示 |
| `a-notification` | `toast` (vue-sonner) | 通知 |
| `a-popconfirm` | `AlertDialog` | 确认对话框 |
| `a-popover` | `Popover` | 气泡卡片 |
| `a-tooltip` | `Tooltip` | 文字提示 |
| `a-progress` | `Progress` | 进度条 |
| `a-spin` | `Spinner` | 加载中 |
| `a-skeleton` | `Skeleton` | 骨架屏 |
| `a-alert` | `Alert` | 警告提示 |
## 布局组件
| Ant Design | shadcn-vue | 说明 |
|------------|------------|------|
| `a-layout` | 自定义 | 布局 |
| `a-layout-sider` | `Sidebar` | 侧边栏 |
| `a-layout-header` | 自定义 | 头部 |
| `a-layout-content` | 自定义 | 内容 |
| `a-layout-footer` | 自定义 | 底部 |
| `a-row` / `a-col` | Tailwind `grid` / `flex` | 栅格 |
| `a-space` | `div` + Tailwind `gap-*` | 间距 |
| `a-divider` | `Separator` | 分割线 |
## 图标映射
| Ant Design Icons | Iconify (lucide) |
|------------------|------------------|
| `UserOutlined` | `lucide:user` |
| `SettingOutlined` | `lucide:settings` |
| `SearchOutlined` | `lucide:search` |
| `PlusOutlined` | `lucide:plus` |
| `DeleteOutlined` | `lucide:trash-2` |
| `EditOutlined` | `lucide:pencil` |
| `CloseOutlined` | `lucide:x` |
| `CheckOutlined` | `lucide:check` |
| `EyeOutlined` | `lucide:eye` |
| `EyeInvisibleOutlined` | `lucide:eye-off` |
| `LoadingOutlined` | `lucide:loader-2` |
| `ExclamationCircleOutlined` | `lucide:alert-circle` |
| `InfoCircleOutlined` | `lucide:info` |
| `QuestionCircleOutlined` | `lucide:help-circle` |
| `PhoneOutlined` | `lucide:phone` |
| `MailOutlined` | `lucide:mail` |
| `SafetyOutlined` | `lucide:shield` |
| `UploadOutlined` | `lucide:upload` |
| `DownloadOutlined` | `lucide:download` |
| `CopyOutlined` | `lucide:copy` |
| `ReloadOutlined` | `lucide:refresh-cw` |
| `FilterOutlined` | `lucide:filter` |
| `MoreOutlined` | `lucide:more-horizontal` |
| `MenuOutlined` | `lucide:menu` |
| `HomeOutlined` | `lucide:home` |
| `LeftOutlined` | `lucide:chevron-left` |
| `RightOutlined` | `lucide:chevron-right` |
| `UpOutlined` | `lucide:chevron-up` |
| `DownOutlined` | `lucide:chevron-down` |
## 图标使用方式
```vue
<script setup>
import { Icon } from '@iconify/vue'
</script>
<template>
<!-- 使用 Icon 组件 -->
<Icon icon="lucide:user" class="w-4 h-4" />
<!-- 带颜色 -->
<Icon icon="lucide:settings" class="w-5 h-5 text-primary" />
<!-- 在按钮中 -->
<Button>
<Icon icon="lucide:plus" class="w-4 h-4 mr-2" />
添加
</Button>
</template>
```
## 表单验证迁移
| Ant Design | shadcn-vue (VeeForm) |
|------------|----------------------|
| `rules` prop | `zod` schema + `toTypedSchema` |
| `validateFields()` | `form.validate()` |
| `validateTrigger` | schema 配置 |
| `hasFeedback` | `FormMessage` 组件 |
## 事件映射
| Ant Design | shadcn-vue |
|------------|------------|
| `@change` | `@update:modelValue` |
| `@pressEnter` | `@keydown.enter` |
| `@search` | 自定义 + `@keydown.enter` |
| `@select` | `@update:modelValue` (Select) |

View File

@@ -0,0 +1,662 @@
# 迁移模式
详细的代码迁移示例,从 Ant Design Vue 到 shadcn-vue。
## 目录
1. [表单迁移](#表单迁移)
2. [表格迁移](#表格迁移)
3. [弹窗迁移](#弹窗迁移)
4. [消息提示迁移](#消息提示迁移)
5. [下拉选择迁移](#下拉选择迁移)
6. [分页迁移](#分页迁移)
7. [标签页迁移](#标签页迁移)
8. [抽屉迁移](#抽屉迁移)
---
## 表单迁移
### 简单表单
```vue
<!-- ========== Before: Ant Design ========== -->
<script setup>
import { ref, reactive } from 'vue'
const formRef = ref()
const form = reactive({
username: '',
password: ''
})
const rules = {
username: [{ required: true, message: '请输入用户名' }],
password: [{ required: true, message: '请输入密码' }]
}
async function handleSubmit() {
await formRef.value.validateFields()
// 提交逻辑
}
</script>
<template>
<a-form ref="formRef" :model="form" :rules="rules" layout="vertical">
<a-form-item name="username" label="用户名">
<a-input v-model:value="form.username" />
</a-form-item>
<a-form-item name="password" label="密码">
<a-input-password v-model:value="form.password" />
</a-form-item>
<a-form-item>
<a-button type="primary" @click="handleSubmit">登录</a-button>
</a-form-item>
</a-form>
</template>
<!-- ========== After: shadcn-vue ========== -->
<script setup>
import { toTypedSchema } from '@vee-validate/zod'
import { useForm } from 'vee-validate'
import * as z from 'zod'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage
} from '@/components/ui/form'
const formSchema = toTypedSchema(z.object({
username: z.string().min(1, '请输入用户名'),
password: z.string().min(1, '请输入密码')
}))
const { handleSubmit } = useForm({
validationSchema: formSchema,
initialValues: { username: '', password: '' }
})
const onSubmit = handleSubmit((values) => {
// 提交逻辑values 已验证
})
</script>
<template>
<Form @submit="onSubmit" class="space-y-4">
<FormField v-slot="{ componentField }" name="username">
<FormItem>
<FormLabel>用户名</FormLabel>
<FormControl>
<Input placeholder="请输入用户名" v-bind="componentField" />
</FormControl>
<FormMessage />
</FormItem>
</FormField>
<FormField v-slot="{ componentField }" name="password">
<FormItem>
<FormLabel>密码</FormLabel>
<FormControl>
<Input type="password" placeholder="请输入密码" v-bind="componentField" />
</FormControl>
<FormMessage />
</FormItem>
</FormField>
<Button type="submit">登录</Button>
</Form>
</template>
```
### 表单 + 验证码(带按钮状态)
```vue
<!-- ========== After: shadcn-vue ========== -->
<script setup>
import { ref } from 'vue'
import { toTypedSchema } from '@vee-validate/zod'
import { useForm } from 'vee-validate'
import * as z from 'zod'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Form, FormField, FormItem, FormLabel, FormControl, FormMessage } from '@/components/ui/form'
const countdown = ref(0)
const sendingCode = ref(false)
const formSchema = toTypedSchema(z.object({
mobile: z.string().regex(/^1[3-9]\d{9}$/, '请输入正确的手机号'),
code: z.string().length(4, '验证码为4位数字')
}))
const { handleSubmit, validateField } = useForm({
validationSchema: formSchema
})
async function sendCode() {
const { valid } = await validateField('mobile')
if (!valid) return
sendingCode.value = true
// 发送验证码 API
countdown.value = 60
const timer = setInterval(() => {
countdown.value--
if (countdown.value <= 0) clearInterval(timer)
}, 1000)
sendingCode.value = false
}
const onSubmit = handleSubmit((values) => {
// 登录逻辑
})
</script>
<template>
<Form @submit="onSubmit" class="space-y-4">
<FormField v-slot="{ componentField }" name="mobile">
<FormItem>
<FormLabel>手机号</FormLabel>
<FormControl>
<Input placeholder="请输入手机号" v-bind="componentField" />
</FormControl>
<FormMessage />
</FormItem>
</FormField>
<FormField v-slot="{ componentField }" name="code">
<FormItem>
<FormLabel>验证码</FormLabel>
<div class="flex gap-2">
<FormControl>
<Input placeholder="请输入验证码" v-bind="componentField" class="flex-1" />
</FormControl>
<Button
type="button"
variant="outline"
:disabled="countdown > 0 || sendingCode"
@click="sendCode"
>
{{ countdown > 0 ? `${countdown}s` : '获取验证码' }}
</Button>
</div>
<FormMessage />
</FormItem>
</FormField>
<Button type="submit" class="w-full">登录</Button>
</Form>
</template>
```
---
## 表格迁移
```vue
<!-- ========== Before: Ant Design ========== -->
<a-table
:columns="columns"
:data-source="data"
:loading="loading"
:pagination="pagination"
@change="handleTableChange"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'action'">
<a-button type="link" @click="edit(record)">编辑</a-button>
<a-popconfirm title="确定删除?" @confirm="del(record)">
<a-button type="link" danger>删除</a-button>
</a-popconfirm>
</template>
</template>
</a-table>
<!-- ========== After: shadcn-vue ========== -->
<script setup>
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
import { Button } from '@/components/ui/button'
import { Spinner } from '@/components/ui/spinner'
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogTrigger
} from '@/components/ui/alert-dialog'
const columns = [
{ key: 'name', title: '名称' },
{ key: 'status', title: '状态' },
{ key: 'action', title: '操作' }
]
</script>
<template>
<div class="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead v-for="col in columns" :key="col.key">
{{ col.title }}
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<!-- 加载状态 -->
<TableRow v-if="loading">
<TableCell :colspan="columns.length" class="h-24 text-center">
<Spinner class="mx-auto" />
</TableCell>
</TableRow>
<!-- 空状态 -->
<TableRow v-else-if="data.length === 0">
<TableCell :colspan="columns.length" class="h-24 text-center text-muted-foreground">
暂无数据
</TableCell>
</TableRow>
<!-- 数据行 -->
<TableRow v-else v-for="record in data" :key="record.id">
<TableCell>{{ record.name }}</TableCell>
<TableCell>{{ record.status }}</TableCell>
<TableCell>
<div class="flex gap-2">
<Button variant="link" size="sm" @click="edit(record)">编辑</Button>
<AlertDialog>
<AlertDialogTrigger as-child>
<Button variant="link" size="sm" class="text-destructive">删除</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogTitle>确定删除</AlertDialogTitle>
<AlertDialogDescription>此操作不可撤销</AlertDialogDescription>
<AlertDialogFooter>
<AlertDialogCancel>取消</AlertDialogCancel>
<AlertDialogAction @click="del(record)">确定</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
</TableCell>
</TableRow>
</TableBody>
</Table>
</div>
<!-- 分页独立组件 -->
<div class="mt-4 flex justify-end">
<Pagination
:current="pagination.current"
:total="pagination.total"
:page-size="pagination.pageSize"
@change="handlePageChange"
/>
</div>
</template>
```
---
## 弹窗迁移
```vue
<!-- ========== Before: Ant Design ========== -->
<a-modal
v-model:open="visible"
title="编辑"
@ok="handleOk"
@cancel="handleCancel"
>
<a-form>...</a-form>
</a-modal>
<!-- ========== After: shadcn-vue ========== -->
<script setup>
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle
} from '@/components/ui/dialog'
import { Button } from '@/components/ui/button'
const visible = ref(false)
</script>
<template>
<Dialog v-model:open="visible">
<DialogContent class="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>编辑</DialogTitle>
<DialogDescription>修改以下信息</DialogDescription>
</DialogHeader>
<!-- 表单内容 -->
<div class="py-4">
<Form>...</Form>
</div>
<DialogFooter>
<Button variant="outline" @click="visible = false">取消</Button>
<Button @click="handleOk">确定</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</template>
```
---
## 消息提示迁移
```javascript
// ========== Before: Ant Design ==========
import { message, notification } from 'ant-design-vue'
message.success('操作成功')
message.error('操作失败')
message.warning('警告信息')
message.loading('加载中...')
notification.success({
message: '成功',
description: '操作已完成'
})
// ========== After: shadcn-vue (vue-sonner) ==========
import { toast } from 'vue-sonner'
toast.success('操作成功')
toast.error('操作失败')
toast.warning('警告信息')
toast.loading('加载中...')
// 带描述的通知
toast.success('操作已完成', {
description: '数据已保存'
})
// 自定义时长
toast.success('操作成功', { duration: 3000 })
// Promise 状态
toast.promise(saveData(), {
loading: '保存中...',
success: '保存成功',
error: '保存失败'
})
```
---
## 下拉选择迁移
```vue
<!-- ========== Before: Ant Design ========== -->
<a-select
v-model:value="selected"
:options="options"
placeholder="请选择"
allow-clear
show-search
/>
<!-- ========== After: shadcn-vue ========== -->
<script setup>
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue
} from '@/components/ui/select'
const selected = ref('')
const options = [
{ value: '1', label: '选项一' },
{ value: '2', label: '选项二' }
]
</script>
<template>
<Select v-model="selected">
<SelectTrigger class="w-[180px]">
<SelectValue placeholder="请选择" />
</SelectTrigger>
<SelectContent>
<SelectItem
v-for="opt in options"
:key="opt.value"
:value="opt.value"
>
{{ opt.label }}
</SelectItem>
</SelectContent>
</Select>
</template>
```
---
## 分页迁移
```vue
<!-- ========== Before: Ant Design ========== -->
<a-pagination
v-model:current="page"
:total="total"
:page-size="pageSize"
show-size-changer
@change="handleChange"
/>
<!-- ========== After: shadcn-vue ========== -->
<script setup>
import {
Pagination,
PaginationContent,
PaginationEllipsis,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious
} from '@/components/ui/pagination'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue
} from '@/components/ui/select'
const page = ref(1)
const pageSize = ref(10)
const total = ref(100)
const totalPages = computed(() => Math.ceil(total.value / pageSize.value))
</script>
<template>
<div class="flex items-center justify-between">
<Pagination v-model:page="page" :total="totalPages" @update:page="handleChange">
<PaginationContent>
<PaginationItem>
<PaginationPrevious />
</PaginationItem>
<PaginationItem v-for="p in totalPages" :key="p">
<PaginationLink :isActive="page === p" @click="page = p">
{{ p }}
</PaginationLink>
</PaginationItem>
<PaginationItem>
<PaginationNext />
</PaginationItem>
</PaginationContent>
</Pagination>
<Select v-model="pageSize" @update:modelValue="handlePageSizeChange">
<SelectTrigger class="w-[100px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="10">10/</SelectItem>
<SelectItem value="20">20/</SelectItem>
<SelectItem value="50">50/</SelectItem>
</SelectContent>
</Select>
</div>
</template>
```
---
## 标签页迁移
```vue
<!-- ========== Before: Ant Design ========== -->
<a-tabs v-model:activeKey="activeKey">
<a-tab-pane key="1" tab="标签一">内容一</a-tab-pane>
<a-tab-pane key="2" tab="标签二">内容二</a-tab-pane>
</a-tabs>
<!-- ========== After: shadcn-vue ========== -->
<script setup>
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
const activeKey = ref('1')
</script>
<template>
<Tabs v-model="activeKey">
<TabsList>
<TabsTrigger value="1">标签一</TabsTrigger>
<TabsTrigger value="2">标签二</TabsTrigger>
</TabsList>
<TabsContent value="1">内容一</TabsContent>
<TabsContent value="2">内容二</TabsContent>
</Tabs>
</template>
```
---
## 抽屉迁移
```vue
<!-- ========== Before: Ant Design ========== -->
<a-drawer
v-model:open="visible"
title="详情"
placement="right"
:width="400"
>
内容
</a-drawer>
<!-- ========== After: shadcn-vue ========== -->
<script setup>
import {
Drawer,
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerFooter,
DrawerHeader,
DrawerTitle
} from '@/components/ui/drawer'
import { Button } from '@/components/ui/button'
const visible = ref(false)
</script>
<template>
<Drawer v-model:open="visible">
<DrawerContent class="right-0 left-auto top-0 bottom-0 h-full w-[400px] mt-0 rounded-none">
<DrawerHeader>
<DrawerTitle>详情</DrawerTitle>
<DrawerDescription>查看详细信息</DrawerDescription>
</DrawerHeader>
<div class="p-4 flex-1 overflow-auto">
内容
</div>
<DrawerFooter>
<DrawerClose as-child>
<Button variant="outline">关闭</Button>
</DrawerClose>
</DrawerFooter>
</DrawerContent>
</Drawer>
</template>
```
---
## 常见问题
### Q: 如何处理 a-form 的 layout 属性?
```vue
<!-- layout="vertical" -->
<Form class="space-y-4">
<!-- layout="horizontal" -->
<Form class="flex gap-4">
<FormField class="flex items-center gap-2">
<FormLabel class="w-24 shrink-0">
<FormControl class="flex-1">
</FormField>
</Form>
<!-- layout="inline" -->
<Form class="flex flex-wrap gap-4">
```
### Q: 如何处理自定义验证?
```javascript
// Ant Design
const rules = {
field: [{
validator: async (rule, value) => {
if (!value) throw new Error('必填')
if (value.length < 3) throw new Error('太短')
}
}]
}
// shadcn-vue (zod)
const formSchema = toTypedSchema(z.object({
field: z.string()
.min(1, '必填')
.refine(val => val.length >= 3, '太短')
.refine(async (val) => {
// 异步验证
const exists = await checkExists(val)
return !exists
}, '已存在')
}))
```
### Q: 如何处理表单初始值?
```javascript
// Ant Design
const form = reactive({ name: '初始值' })
// shadcn-vue
const { resetForm } = useForm({
validationSchema: formSchema,
initialValues: { name: '初始值' }
})
// 重置到初始值
resetForm()
```

View File

@@ -0,0 +1,484 @@
# 样式迁移指南
从 Ant Design 样式系统迁移到 TailwindCSS + CSS 变量。
## 目录
1. [现代设计原则](#现代设计原则)
2. [颜色系统](#颜色系统)
3. [间距系统](#间距系统)
4. [字体系统](#字体系统)
5. [阴影系统](#阴影系统)
6. [圆角系统](#圆角系统)
7. [响应式布局](#响应式布局)
8. [状态样式](#状态样式)
---
## 现代设计原则
### 抵制过时设计模式
**❌ 拒绝老气 UI**
- 粗重边框、多重边框嵌套(边框重叠)
- 灰暗沉闷的配色(缺乏活力)
- 过度装饰的渐变和重阴影
- 拥挤无呼吸感的布局
- 间距混乱、缺乏层级感
- 老式的表单样式(密集排列)
**✅ 拥抱现代设计**
- 轻量边框或无边框设计
- 明亮有活力的色彩
- 克制精致的阴影shadow-sm 为主)
- 充足留白,呼吸感
- 清晰的视觉层级4px 间距递进)
- 现代表单(宽松布局,清晰标签)
### 颜色活力设计
**年轻感配色特征**
```css
/* 主色:中等饱和度,避免过深或过浅 */
--primary: oklch(0.55 0.18 254.604); /* 活力蓝,非深蓝 */
/* 强调色:高饱和度,用于吸引注意 */
--accent-bright: oklch(0.65 0.22 254.604);
/* 渐变:柔和过渡,非跳跃式 */
--gradient-primary: linear-gradient(135deg,
oklch(0.68 0.16 254.604),
oklch(0.45 0.16 254.604)
);
```
**发现优秀配色时**
1. 提取关键颜色值oklch 格式)
2. 添加到 `style.css` 设计令牌
3. 使用语义化命名
4.`theme.css` 中同步 shadcn 变量
### 间距层级感
```
紧凑gap-2 (8px) - 同组元素
舒适gap-3 (12px) - 表单项之间
宽松gap-4 (16px) - 卡片内容
分区gap-6 (24px) - 不同区块
隔离gap-8 (32px) - 模块分隔
```
---
## 颜色系统
### 语义化颜色变量
项目使用 `oklch` 色彩空间,定义在 `theme.css` 中:
```css
/* theme.css 中的变量 */
--background /* 页面背景 */
--foreground /* 主文字 */
--card /* 卡片背景 */
--card-foreground /* 卡片文字 */
--popover /* 弹出层背景 */
--popover-foreground
--primary /* 主色(品牌蓝) */
--primary-foreground
--secondary /* 次要色 */
--secondary-foreground
--muted /* 静音背景 */
--muted-foreground /* 静音文字 */
--accent /* 强调背景 */
--accent-foreground
--destructive /* 危险色 */
--destructive-foreground
--border /* 边框 */
--input /* 输入框边框 */
--ring /* 焦点环 */
```
### 使用方式
```vue
<template>
<!-- 使用 Tailwind -->
<div class="bg-background text-foreground">
<p class="text-muted-foreground">次要文字</p>
<Button class="bg-primary text-primary-foreground">主按钮</Button>
<span class="text-destructive">错误提示</span>
</div>
</template>
<style scoped>
/* 在 CSS 中使用变量 */
.custom-element {
color: var(--foreground);
background: var(--card);
border: 1px solid var(--border);
}
</style>
</template>
```
### Ant Design 颜色迁移
| Ant Design | shadcn/Tailwind |
|------------|-----------------|
| `@primary-color` | `var(--primary)``text-primary bg-primary` |
| `@success-color` | `text-green-500` 或自定义 |
| `@warning-color` | `text-yellow-500` 或自定义 |
| `@error-color` | `var(--destructive)``text-destructive` |
| `@text-color` | `var(--foreground)``text-foreground` |
| `@text-color-secondary` | `var(--muted-foreground)` |
| `@border-color-base` | `var(--border)``border-border` |
| `@disabled-color` | `text-muted-foreground` |
---
## 间距系统
Tailwind 使用 4px 基准:
| Tailwind | 像素值 |
|----------|--------|
| `p-1` / `m-1` | 4px |
| `p-2` / `m-2` | 8px |
| `p-3` / `m-3` | 12px |
| `p-4` / `m-4` | 16px |
| `p-5` / `m-5` | 20px |
| `p-6` / `m-6` | 24px |
| `p-8` / `m-8` | 32px |
| `p-10` / `m-10` | 40px |
| `p-12` / `m-12` | 48px |
### 常见场景
```vue
<!-- 表单间距 -->
<Form class="space-y-4">
<FormField class="space-y-2">...</FormField>
</Form>
<!-- 按钮组 -->
<div class="flex gap-2">
<Button>取消</Button>
<Button>确定</Button>
</div>
<!-- 卡片内边距 -->
<Card class="p-6">
<CardHeader class="pb-4">...</CardHeader>
<CardContent>...</CardContent>
</Card>
<!-- 页面边距 -->
<div class="p-6 lg:p-8">
...
</div>
```
### Ant Design 间距迁移
| Ant Design | Tailwind |
|------------|----------|
| `margin: 16px` | `m-4` |
| `padding: 24px` | `p-6` |
| `gap: 8px` | `gap-2` |
| `space-between` | `justify-between` |
| `align-center` | `items-center` |
---
## 字体系统
### 字体大小
| Tailwind | 像素值 | 用途 |
|----------|--------|------|
| `text-xs` | 12px | 辅助文字 |
| `text-sm` | 14px | 正文、标签 |
| `text-base` | 16px | 正文 |
| `text-lg` | 18px | 小标题 |
| `text-xl` | 20px | 标题 |
| `text-2xl` | 24px | 大标题 |
| `text-3xl` | 30px | 页面标题 |
### 字重
```vue
<template>
<span class="font-normal">常规</span>
<span class="font-medium">中等</span>
<span class="font-semibold">半粗</span>
<span class="font-bold">粗体</span>
</template>
```
### 行高
```vue
<template>
<p class="leading-tight">紧凑行高</p>
<p class="leading-normal">正常行高</p>
<p class="leading-relaxed">宽松行高</p>
</template>
```
---
## 阴影系统
定义在 `theme.css`
```css
--shadow-sm /* 微阴影 */
--shadow /* 基础阴影 */
--shadow-md /* 中等阴影 */
--shadow-lg /* 大阴影 */
--shadow-xl /* 超大阴影 */
--shadow-2xl /* 巨大阴影 */
```
### 使用
```vue
<template>
<Card class="shadow-sm">卡片</Card>
<Dialog class="shadow-lg">弹窗</Dialog>
<DropdownMenu class="shadow-md">下拉菜单</DropdownMenu>
</template>
```
### Ant Design 阴影迁移
| Ant Design | Tailwind |
|------------|----------|
| `box-shadow: 0 2px 8px rgba(0,0,0,0.15)` | `shadow-md` |
| Modal 阴影 | `shadow-xl` |
| Popover 阴影 | `shadow-lg` |
---
## 圆角系统
定义在 `theme.css`
```css
--radius-sm /* 小圆角 */
--radius-md /* 中圆角 */
--radius-lg /* 大圆角 */
--radius-xl /* 超大圆角 */
```
### 使用
```vue
<template>
<!-- 使用 Tailwind -->
<Button class="rounded-md">按钮</Button>
<Card class="rounded-lg">卡片</Card>
<Input class="rounded-md" />
<!-- 自定义圆角 -->
<div class="rounded-[var(--radius-lg)]">自定义</div>
</template>
```
### Ant Design 圆角迁移
| Ant Design | Tailwind |
|------------|----------|
| `border-radius: 2px` | `rounded-sm` |
| `border-radius: 4px` | `rounded` |
| `border-radius: 8px` | `rounded-lg` |
| `border-radius: 12px` | `rounded-xl` |
---
## 响应式布局
### 断点
| 断点 | 最小宽度 |
|------|----------|
| `sm:` | 640px |
| `md:` | 768px |
| `lg:` | 1024px |
| `xl:` | 1280px |
| `2xl:` | 1536px |
### 响应式示例
```vue
<template>
<!-- 响应式网格 -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<Card v-for="item in items">...</Card>
</div>
<!-- 响应式隐藏 -->
<div class="hidden md:block">桌面端显示</div>
<div class="md:hidden">移动端显示</div>
<!-- 响应式间距 -->
<div class="p-4 md:p-6 lg:p-8">
...
</div>
<!-- 响应式文字 -->
<h1 class="text-2xl md:text-3xl lg:text-4xl">标题</h1>
</template>
```
### 栅格迁移
```vue
<!-- Ant Design 栅格 -->
<a-row :gutter="16">
<a-col :span="12">...</a-col>
<a-col :span="12">...</a-col>
</a-row>
<!-- Tailwind 网格 -->
<div class="grid grid-cols-2 gap-4">
<div>...</div>
<div>...</div>
</div>
<!-- 或使用 Flex -->
<div class="flex gap-4">
<div class="flex-1">...</div>
<div class="flex-1">...</div>
</div>
```
---
## 状态样式
### 悬停
```vue
<template>
<Button class="hover:bg-primary/90">按钮</Button>
<Card class="hover:shadow-md transition-shadow">卡片</Card>
</template>
```
### 焦点
```vue
<template>
<Input class="focus:ring-2 focus:ring-primary" />
<Button class="focus-visible:ring-2 focus-visible:ring-primary">
按钮
</Button>
</template>
```
### 禁用
```vue
<template>
<Button class="disabled:opacity-50 disabled:cursor-not-allowed">
按钮
</Button>
<Input class="disabled:bg-muted disabled:cursor-not-allowed" />
</template>
```
### 加载中
```vue
<template>
<Button class="relative" disabled>
<Spinner v-if="loading" class="absolute" />
<span :class="{ 'opacity-0': loading }">提交</span>
</Button>
</template>
```
---
## 过渡动画
### 基础过渡
```vue
<template>
<!-- 颜色过渡 -->
<Button class="transition-colors hover:bg-primary">按钮</Button>
<!-- 阴影过渡 -->
<Card class="transition-shadow hover:shadow-lg">卡片</Card>
<!-- 全属性过渡 -->
<div class="transition-all hover:scale-105">元素</div>
</template>
```
### 动画时长
```vue
<template>
<div class="transition-colors duration-150">快速</div>
<div class="transition-colors duration-200">正常</div>
<div class="transition-colors duration-300">慢速</div>
</template>
```
---
## 常见样式模式
### 卡片
```vue
<template>
<div class="rounded-lg border bg-card p-6 shadow-sm">
<h3 class="text-lg font-semibold">标题</h3>
<p class="text-muted-foreground mt-2">描述文字</p>
</div>
</template>
```
### 表单项
```vue
<template>
<div class="space-y-2">
<Label class="text-sm font-medium">标签</Label>
<Input class="h-10" />
<p class="text-xs text-muted-foreground">提示文字</p>
</div>
</template>
```
### 操作栏
```vue
<template>
<div class="flex items-center justify-between py-4">
<div class="text-sm text-muted-foreground"> {{ total }} </div>
<div class="flex gap-2">
<Button variant="outline" size="sm">导出</Button>
<Button size="sm">添加</Button>
</div>
</div>
</template>
```
### 空状态
```vue
<template>
<div class="flex flex-col items-center justify-center py-12 text-center">
<Icon icon="lucide:inbox" class="h-12 w-12 text-muted-foreground/50" />
<h3 class="mt-4 text-lg font-medium">暂无数据</h3>
<p class="mt-2 text-sm text-muted-foreground">点击添加按钮创建</p>
<Button class="mt-4">添加</Button>
</div>
</template>
```