下载工作台
前端框架精通

UI 组件库:Element Plus 与 Ant Design

试读上半部分 · 解锁后可读全文

第 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 PlusAnt Design 5
自动按需unplugin-vue-components默认 ESM 较好
样式sass 变量 / CSS 变量ConfigProvider theme
避免全量 import ElementPlusimport 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-radius8px卡片、按钮圆角
--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.tsimport './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-loadingdestroy-on-close 防表单脏数据、row-key 优化表格更新。


以下内容需解锁后阅读

试读已结束。解锁本章 ¥5.00,或开通年度会员畅读全部教程。
年度会员 ¥199.00/年; 小紫 AI 工作台有效会员 ¥99.00/年

正文仅在服务端鉴权后下发,未付费无法获取下半部分内容。