第 11 章 · UI 组件库:Element Plus 与 Ant Design
本章目标:判断何时用 UI 库、掌握按需引入与主题定制;用 Element Plus + Vue 完成 Table、Form、Dialog 完整后台页;用 Ant Design + React 实现 Layout、ProTable 概念、表单;建立与贤紫商城后台对齐的设计令牌;了解无障碍(a11y)与国际化(i18n) 要点。
学时建议:5~6 小时(含 3 小时跟练)
前置:本模块 ch10(Vue TS)或 ch10(React TS);了解后台 CRUD 基本流程。
11.1 何时用 UI 组件库
| 场景 | 用 UI 库 | 自建 / 轻量 CSS |
|---|---|---|
| 商户后台表格+表单+弹窗 | ✅ | 成本高 |
| academy-demo 教学首页 | 可选 | ✅ 原生 tokens |
| 营销 H5 | 慎用全库 | ✅ 或局部组件 |
| 数据大屏 | 图表库为主 | AntV / ECharts |
| 内网工具 3 页以内 | 看团队 | 可能够用 |
收益:统一交互、内置 a11y 基础、减少 CSS 维护、表格排序筛选开箱即用。
代价:包体积、视觉同质化、深度定制需学主题 API、与品牌色对齐要配置 Design Token。
选型简图
后台 CRUD 密集?
├─ Vue 团队 → Element Plus(国内文档、表格表单强)
└─ React 团队 → Ant Design(生态 + ProComponents)
仅 C 端品牌页?
└─ 设计系统自研或 Tailwind + Headless UI
行业案例 · 贤紫优选商城:商户后台统一 Vue 3 + Element Plus;运营数据中台子应用 React + Ant Design;C 端不用全库,仅用 tokens.css 保持主色#e91e8c一致。UI 库版本锁定在package.json,大版本升级每半年评估一次。
11.2 按需引入与包体积
11.2.1 Element Plus(Vite)
npm install element-plus
npm install -D unplugin-vue-components unplugin-auto-import
// vite.config.ts
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
export default defineConfig({
plugins: [
vue(),
Components({
resolvers: [ElementPlusResolver({ importStyle: 'sass' })]
})
]
})
模板中直接使用 <el-button>,构建时自动 import,无需全量 app.use(ElementPlus)。
11.2.2 Ant Design(Vite + React)
npm install antd
// 按路由懒加载页面即可;组件级:
import Button from 'antd/es/button'
import 'antd/es/button/style/css'
或使用 vite-plugin-imp 自动拆分样式。Ant Design 5 用 CSS-in-JS,Tree-shaking 依赖 ES Module 导入。
| 策略 | Element Plus | Ant Design 5 |
|---|---|---|
| 自动按需 | unplugin-vue-components | 默认 ESM 较好 |
| 样式 | sass 变量 / CSS 变量 | ConfigProvider theme |
| 避免 | 全量 import ElementPlus | import antd 整体 |
目标:后台首屏 gzip 控制在 250KB 以内(含路由+一个列表页组件)。
11.3 主题定制与贤紫设计令牌
11.3.1 贤紫后台 Design Token(与 academy tokens 对齐)
| Token | 值 | 用途 |
|---|---|---|
--xz-primary | #e91e8c | 主色、链接、主按钮 |
--xz-primary-hover | #d0167a | 悬停 |
--xz-success | #10b981 | 成功、已支付 |
--xz-warning | #f59e0b | 警告、待审核 |
--xz-danger | #ef4444 | 错误、删除 |
--xz-text | #1f2937 | 正文 |
--xz-text-secondary | #6b7280 | 次要说明 |
--xz-border | #e5e7eb | 表格线、输入框 |
--xz-radius | 8px | 卡片、按钮圆角 |
--xz-font | "PingFang SC", system-ui, sans-serif | 后台字体 |
src/assets/tokens.css(Vue/React 共用可复制):
:root {
--xz-primary: #e91e8c;
--xz-primary-hover: #d0167a;
--xz-radius: 8px;
--xz-font: "PingFang SC", system-ui, sans-serif;
}
body {
font-family: var(--xz-font);
color: var(--xz-text, #1f2937);
}
11.3.2 Element Plus 主题
// styles/element/index.scss
@forward 'element-plus/theme-chalk/src/common/var.scss' with (
$colors: (
'primary': (
'base': #e91e8c,
),
),
$border-radius: (
'base': 8px,
)
);
@use 'element-plus/theme-chalk/src/index.scss' as *;
main.ts:import './styles/element/index.scss'
11.3.3 Ant Design ConfigProvider
import { ConfigProvider } from 'antd'
const xianziTheme = {
token: {
colorPrimary: '#e91e8c',
borderRadius: 8,
fontFamily: 'PingFang SC, system-ui, sans-serif'
},
components: {
Table: { headerBg: '#fafafa' },
Button: { primaryShadow: 'none' }
}
}
export function AppProviders({ children }: { children: React.ReactNode }) {
return (
<ConfigProvider theme={xianziTheme}>
{children}
</ConfigProvider>
)
}
验收:截图对比贤紫商户后台与 demo 页主按钮色、表格头背景,色差肉眼不可辨。
11.4 Element Plus + Vue:完整后台页
场景:学院课程管理 — 列表(Table)、筛选(Form)、新建/编辑(Dialog + Form)。
11.4.1 页面结构
src/views/admin/
CourseAdminView.vue # 列表 + 弹窗
src/api/
courses.ts # 请求封装
src/types/
course.ts # ch08 类型
11.4.2 列表与筛选
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import type { Course, Level } from '@/types/course'
import type { FormInstance } from 'element-plus'
const loading = ref(false)
const tableData = ref<Course[]>([])
const query = reactive({
keyword: '',
level: '' as Level | ''
})
const dialogVisible = ref(false)
const editing = ref<Course | null>(null)
const formRef = ref<FormInstance>()
const formModel = reactive({
id: '',
title: '',
desc: '',
hours: 8,
level: 'beginner' as Level,
tags: [] as string[]
})
async function loadData() {
loading.value = true
const res = await fetch('/mock/courses.json')
tableData.value = await res.json()
loading.value = false
}
function onSearch() {
// 实际项目调 API;演示可前端 filter
loadData()
}
function openCreate() {
editing.value = null
Object.assign(formModel, {
id: '',
title: '',
desc: '',
hours: 8,
level: 'beginner',
tags: []
})
dialogVisible.value = true
}
function openEdit(row: Course) {
editing.value = row
Object.assign(formModel, { ...row, tags: [...row.tags] })
dialogVisible.value = true
}
async function onSubmit() {
await formRef.value?.validate()
// POST /api/admin/courses
dialogVisible.value = false
await loadData()
}
onMounted(loadData)
</script>
<template>
<div class="course-admin">
<el-form :inline="true" :model="query" @submit.prevent="onSearch">
<el-form-item label="关键词">
<el-input v-model="query.keyword" clearable placeholder="课程名" />
</el-form-item>
<el-form-item label="难度">
<el-select v-model="query.level" clearable placeholder="全部">
<el-option label="入门" value="beginner" />
<el-option label="进阶" value="intermediate" />
<el-option label="高级" value="advanced" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="onSearch">查询</el-button>
<el-button @click="openCreate">新建课程</el-button>
</el-form-item>
</el-form>
<el-table v-loading="loading" :data="tableData" row-key="id" border>
<el-table-column prop="id" label="ID" width="100" />
<el-table-column prop="title" label="课程名" min-width="180" />
<el-table-column prop="hours" label="课时" width="80" />
<el-table-column prop="level" label="难度" width="100" />
<el-table-column label="操作" width="160" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="openEdit(row)">编辑</el-button>
<el-button link type="danger">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-dialog
v-model="dialogVisible"
:title="editing ? '编辑课程' : '新建课程'"
width="520px"
destroy-on-close
>
<el-form
ref="formRef"
:model="formModel"
label-width="80px"
:rules="{
title: [{ required: true, message: '请输入课程名', trigger: 'blur' }],
hours: [{ required: true, type: 'number', min: 1, message: '课时≥1' }]
}"
>
<el-form-item label="课程名" prop="title">
<el-input v-model="formModel.title" />
</el-form-item>
<el-form-item label="简介" prop="desc">
<el-input v-model="formModel.desc" type="textarea" rows="3" />
</el-form-item>
<el-form-item label="课时" prop="hours">
<el-input-number v-model="formModel.hours" :min="1" />
</el-form-item>
<el-form-item label="难度" prop="level">
<el-select v-model="formModel.level">
<el-option label="入门" value="beginner" />
<el-option label="进阶" value="intermediate" />
<el-option label="高级" value="advanced" />
</el-select>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="onSubmit">保存</el-button>
</template>
</el-dialog>
</div>
</template>
<style scoped>
.course-admin {
padding: 16px;
background: #fff;
border-radius: var(--xz-radius, 8px);
}
</style>
要点:FormInstance 类型、v-loading、destroy-on-close 防表单脏数据、row-key 优化表格更新。