Element Plus 组件库
大约 16 分钟约 4787 字
Element Plus 组件库
简介
Element Plus 是 Vue3 生态中最流行的 UI 组件库,提供了丰富的企业级组件(表格、表单、弹窗、导航等)。基于 Element Plus 可以快速构建管理后台、数据看板等 B 端应用,无需从零设计 UI,大幅提升开发效率。
Element Plus 是 Element UI 的 Vue 3 版本,完全使用 TypeScript 重写,全面拥抱 Composition API。它不仅继承了 Element UI 的设计理念和组件生态,还在性能、可访问性和国际化方面做了大量改进。
Element Plus 核心数据:
- 60+ 高质量组件,覆盖表单、数据展示、导航、反馈等场景
- 完整的 TypeScript 类型支持
- 支持 Vue 3 的 Composition API 和 Options API
- 内置暗色模式
- 支持按需引入,Tree-shaking 友好
- 覆盖数十种语言的国际化支持
特点
安装配置
项目集成
// 完整引入
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
app.use(ElementPlus)
// 按需引入(推荐)
// npm install -D unplugin-vue-components unplugin-auto-import
// vite.config.ts
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
export default defineConfig({
plugins: [
AutoImport({ resolvers: [ElementPlusResolver()] }),
Components({ resolvers: [ElementPlusResolver()] })
]
})
// 主题定制
// styles/element-override.scss
:root {
--el-color-primary: #0078D4;
--el-color-primary-light-3: #4da3e0;
--el-color-primary-light-5: #80bce8;
--el-color-primary-dark-2: #0060aa;
--el-border-radius-base: 6px;
--el-font-size-base: 14px;
}完整的 Vite 配置
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
import { resolve } from 'path'
export default defineConfig({
plugins: [
vue(),
// 自动导入 Element Plus 的 API(如 ElMessage, ElMessageBox)
AutoImport({
resolvers: [ElementPlusResolver()],
imports: ['vue', 'vue-router', 'pinia'],
dts: 'src/auto-imports.d.ts',
}),
// 自动注册 Element Plus 组件
Components({
resolvers: [ElementPlusResolver()],
dts: 'src/components.d.ts',
}),
],
resolve: {
alias: {
'@': resolve(__dirname, 'src'),
},
},
css: {
preprocessorOptions: {
scss: {
additionalData: `@use "@/styles/variables.scss" as *;`,
},
},
},
})主题定制深入
// 方式 1:CSS 变量覆盖(推荐,运行时动态切换)
// styles/element-variables.scss
:root {
// 主色
--el-color-primary: #409eff;
--el-color-primary-light-3: #79bbff;
--el-color-primary-light-5: #a0cfff;
--el-color-primary-light-7: #c6e2ff;
--el-color-primary-light-8: #d9ecff;
--el-color-primary-light-9: #ecf5ff;
--el-color-primary-dark-2: #337ecc;
// 功能色
--el-color-success: #67c23a;
--el-color-warning: #e6a23c;
--el-color-danger: #f56c6c;
--el-color-info: #909399;
// 文字
--el-font-size-base: 14px;
--el-font-size-small: 13px;
--el-font-size-extra-small: 12px;
// 圆角
--el-border-radius-base: 4px;
--el-border-radius-small: 2px;
--el-border-radius-round: 20px;
--el-border-radius-circle: 100%;
// 边框
--el-border-color: #dcdfe6;
--el-border-color-light: #e4e7ed;
--el-border-color-lighter: #ebeef5;
}// 方式 2:SCSS 变量覆盖(编译时确定,更彻底)
// styles/element-theme.scss
@forward 'element-plus/theme-chalk/src/common/var.scss' with (
$colors: (
'primary': (
'base': #0078D4,
),
'success': (
'base': #52c41a,
),
),
$border-radius: (
'base': 6px,
'small': 4px,
'round': 16px,
),
$font-size: (
'base': 14px,
'small': 13px,
'extra-small': 12px,
),
);// 方式 3:使用 Element Plus 的主题编辑器
// npm install @element-plus/theme-chalk -D
// node_modules/@element-plus/theme-chalk/src/ 目录下有所有 SCSS 源文件
// 可以直接修改后编译
// 运行时切换主题(配合 CSS 变量)
function toggleTheme(isDark: boolean) {
const root = document.documentElement
if (isDark) {
root.classList.add('dark')
root.style.setProperty('--el-bg-color', '#141414')
root.style.setProperty('--el-text-color-primary', '#E5EAF3')
root.style.setProperty('--el-border-color', '#4c4d4f')
} else {
root.classList.remove('dark')
root.style.setProperty('--el-bg-color', '#ffffff')
root.style.setProperty('--el-text-color-primary', '#303133')
root.style.setProperty('--el-border-color', '#dcdfe6')
}
}常用组件
表格
<template>
<el-table :data="tableData" stripe border style="width: 100%"
@selection-change="handleSelection">
<el-table-column type="selection" width="50" />
<el-table-column prop="name" label="姓名" width="120" sortable />
<el-table-column prop="email" label="邮箱" />
<el-table-column prop="role" label="角色" width="100">
<template #default="{ row }">
<el-tag :type="row.role === 'admin' ? 'danger' : 'info'">
{{ row.role }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="80">
<template #default="{ row }">
<el-switch v-model="row.active" @change="toggleStatus(row)" />
</template>
</el-table-column>
<el-table-column label="操作" width="180" fixed="right">
<template #default="{ row }">
<el-button size="small" @click="editRow(row)">编辑</el-button>
<el-popconfirm title="确定删除?" @confirm="deleteRow(row)">
<template #reference>
<el-button size="small" type="danger">删除</el-button>
</template>
</el-popconfirm>
</template>
</el-table-column>
</el-table>
<el-pagination
v-model:current-page="page"
v-model:page-size="pageSize"
:total="total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next"
@change="loadData"
/>
</template>表格高级用法
<template>
<!-- 虚拟滚动表格(大数据量) -->
<el-table-v2
:columns="columns"
:data="largeData"
:width="800"
:height="600"
:row-height="50"
fixed
/>
<!-- 可展开行表格 -->
<el-table :data="users" row-key="id">
<el-table-column type="expand">
<template #default="{ row }">
<div class="p-4">
<p>详细地址:{{ row.address }}</p>
<p>备注:{{ row.remark }}</p>
</div>
</template>
</el-table-column>
<el-table-column prop="name" label="姓名" />
<el-table-column prop="email" label="邮箱" />
</el-table>
<!-- 合并行列表头 -->
<el-table :data="tableData">
<el-table-column prop="date" label="日期" width="150" />
<el-table-column label="配送信息">
<el-table-column prop="name" label="姓名" width="120" />
<el-table-column label="地址">
<el-table-column prop="province" label="省份" width="120" />
<el-table-column prop="city" label="市区" width="120" />
<el-table-column prop="address" label="地址" width="300" />
<el-table-column prop="zip" label="邮编" width="120" />
</el-table-column>
</el-table-column>
</el-table>
<!-- 自定义排序和筛选 -->
<el-table :data="tableData" :default-sort="{ prop: 'date', order: 'descending' }">
<el-table-column prop="date" label="日期" sortable :sort-method="customSort" />
<el-table-column prop="tag" label="标签" :filters="tagFilters" :filter-method="filterTag">
<template #default="{ row }">
<el-tag>{{ row.tag }}</el-tag>
</template>
</el-table-column>
</el-table>
</template>
<script setup lang="ts">
// 表格列定义(el-table-v2)
import type { Column } from 'element-plus'
const columns: Column<any>[] = [
{ key: 'id', dataKey: 'id', title: 'ID', width: 80 },
{ key: 'name', dataKey: 'name', title: '姓名', width: 150 },
{ key: 'email', dataKey: 'email', title: '邮箱', width: 250 },
{ key: 'role', dataKey: 'role', title: '角色', width: 120 },
]
// 自定义排序
function customSort(a: any, b: any) {
return new Date(a.date).getTime() - new Date(b.date).getTime()
}
// 筛选标签
const tagFilters = [
{ text: '首页', value: '首页' },
{ text: '产品', value: '产品' },
{ text: '关于', value: '关于' },
]
function filterTag(value: string, row: any) {
return row.tag === value
}
</script>表单
<template>
<el-form :model="form" :rules="rules" ref="formRef" label-width="100px">
<el-form-item label="用户名" prop="username">
<el-input v-model="form.username" placeholder="请输入用户名" />
</el-form-item>
<el-form-item label="邮箱" prop="email">
<el-input v-model="form.email" type="email" />
</el-form-item>
<el-form-item label="角色" prop="role">
<el-select v-model="form.role" placeholder="请选择">
<el-option label="管理员" value="admin" />
<el-option label="编辑" value="editor" />
<el-option label="查看者" value="viewer" />
</el-select>
</el-form-item>
<el-form-item label="部门" prop="department">
<el-tree-select v-model="form.department" :data="deptTree" check-strictly />
</el-form-item>
<el-form-item label="备注">
<el-input v-model="form.remark" type="textarea" :rows="3" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitForm">提交</el-button>
<el-button @click="resetForm">重置</el-button>
</el-form-item>
</el-form>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue'
import type { FormInstance, FormRules } from 'element-plus'
const formRef = ref<FormInstance>()
const form = reactive({
username: '',
email: '',
role: '',
department: '',
remark: ''
})
const rules: FormRules = {
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 3, max: 20, message: '3-20 个字符', trigger: 'blur' }
],
email: [
{ required: true, message: '请输入邮箱', trigger: 'blur' },
{ type: 'email', message: '邮箱格式不正确', trigger: 'blur' }
],
role: [{ required: true, message: '请选择角色', trigger: 'change' }]
}
const submitForm = async () => {
await formRef.value?.validate()
await api.createUser(form)
}
</script>表单高级用法
<template>
<!-- 动态表单项 -->
<el-form :model="dynamicForm" ref="dynamicFormRef" label-width="100px">
<el-form-item
v-for="(domain, index) in dynamicForm.domains"
:key="domain.key"
:label="'域名 ' + (index + 1)"
:prop="'domains.' + index + '.value'"
:rules="domainRules"
>
<el-input v-model="domain.value" />
<el-button @click.prevent="removeDomain(domain)">删除</el-button>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitDynamicForm">提交</el-button>
<el-button @click="addDomain">新增域名</el-button>
</el-form-item>
</el-form>
<!-- 分步表单 -->
<el-steps :active="activeStep" finish-status="success">
<el-step title="基本信息" />
<el-step title="详细配置" />
<el-step title="完成" />
</el-steps>
<el-form
v-show="activeStep === 0"
:model="stepForm"
:rules="step1Rules"
ref="step1Ref"
label-width="100px"
>
<el-form-item label="项目名称" prop="name">
<el-input v-model="stepForm.name" />
</el-form-item>
<el-form-item label="项目描述" prop="description">
<el-input v-model="stepForm.description" type="textarea" />
</el-form-item>
<el-button type="primary" @click="nextStep">下一步</el-button>
</el-form>
</template>
<script setup lang="ts">
// 动态表单
const dynamicFormRef = ref<FormInstance>()
const dynamicForm = reactive({
domains: [{ key: 1, value: '' }] as { key: number; value: string }[]
})
let domainId = 1
const addDomain = () => {
dynamicForm.domains.push({ key: ++domainId, value: '' })
}
const removeDomain = (item: any) => {
const index = dynamicForm.domains.indexOf(item)
if (index > -1) dynamicForm.domains.splice(index, 1)
}
const domainRules = [{ required: true, message: '请输入域名', trigger: 'blur' }]
// 自定义校验器
const validatePhone = (rule: any, value: string, callback: any) => {
if (!/^1[3-9]\d{9}$/.test(value)) {
callback(new Error('请输入正确的手机号'))
} else {
callback()
}
}
const rules: FormRules = {
phone: [
{ required: true, message: '请输入手机号', trigger: 'blur' },
{ validator: validatePhone, trigger: 'blur' }
]
}
// 异步校验(检查用户名是否已存在)
const validateUsername = async (rule: any, value: string, callback: any) => {
try {
const exists = await api.checkUsername(value)
if (exists) {
callback(new Error('用户名已存在'))
} else {
callback()
}
} catch {
callback(new Error('校验失败'))
}
}
// 手动触发单个字段的校验
const formRef = ref<FormInstance>()
async function validateField(field: string) {
try {
await formRef.value?.validateField(field)
console.log(`${field} 校验通过`)
} catch (error) {
console.log(`${field} 校验失败`, error)
}
}
// 清除校验结果
function clearValidation() {
formRef.value?.clearValidate()
// 或清除指定字段
formRef.value?.clearValidate(['username', 'email'])
}
</script>消息与通知
import { ElMessage, ElMessageBox, ElNotification } from 'element-plus'
// 消息提示
ElMessage.success('操作成功')
ElMessage.error('操作失败')
ElMessage.warning('请注意')
ElMessage.info('提示信息')
// 确认对话框
await ElMessageBox.confirm('确定删除该用户?', '确认', {
type: 'warning',
confirmButtonText: '确定',
cancelButtonText: '取消'
})
// 用户确认后执行删除
// 通知
ElNotification({
title: '系统通知',
message: '您有 3 条新消息',
type: 'info',
duration: 5000
})消息与通知进阶
import { ElMessage, ElMessageBox, ElNotification, ElLoading } from 'element-plus'
// 自定义 Message 持续时间和分组
ElMessage({
message: '文件上传中...',
type: 'info',
duration: 0, // 不自动关闭
grouping: true, // 相同消息合并
showClose: true, // 显示关闭按钮
offset: 80, // 距离顶部的偏移量
})
// 可关闭的 HTML 消息
ElMessage({
dangerouslyUseHTMLString: true,
message: '<strong>这是 <i>HTML</i> 内容</strong>',
})
// MessageBox 的多种形式
// Alert
await ElMessageBox.alert('内容已保存', '提示', { type: 'success' })
// Prompt(输入框)
const { value } = await ElMessageBox.prompt('请输入备注', '编辑备注', {
confirmButtonText: '确定',
cancelButtonText: '取消',
inputPattern: /^.{1,50}$/,
inputErrorMessage: '备注长度 1-50 个字符',
})
// 自定义 MessageBox 内容
await ElMessageBox({
title: '删除确认',
message: h('div', null, [
h('p', '确定要删除以下用户?'),
h('p', { style: 'color: red; font-weight: bold' }, user.name),
]),
showCancelButton: true,
confirmButtonText: '删除',
cancelButtonText: '取消',
type: 'warning',
beforeClose: (action, instance, done) => {
if (action === 'confirm') {
instance.confirmButtonLoading = true
instance.confirmButtonText = '删除中...'
deleteUser(user.id)
.then(() => done())
.catch(() => {
instance.confirmButtonLoading = false
ElMessage.error('删除失败')
})
} else {
done()
}
},
})
// Notification 自定义位置和持续时间
ElNotification({
title: '审批通知',
message: '张三提交了一个审批请求,请及时处理',
type: 'warning',
position: 'bottom-right',
duration: 0, // 不自动关闭
onClick: () => {
router.push('/approval/1')
},
})
// 全局 Loading
const loadingInstance = ElLoading.service({
lock: true,
text: '加载中...',
background: 'rgba(0, 0, 0, 0.7)',
})
try {
await fetchData()
} finally {
loadingInstance.close()
}弹窗与抽屉
<template>
<!-- 基础弹窗 -->
<el-dialog v-model="dialogVisible" title="用户编辑" width="500px" destroy-on-close>
<el-form :model="editForm" label-width="80px">
<el-form-item label="姓名">
<el-input v-model="editForm.name" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSave">确定</el-button>
</template>
</el-dialog>
<!-- 抽屉 -->
<el-drawer v-model="drawerVisible" title="用户详情" size="40%">
<p>姓名:{{ user.name }}</p>
<p>邮箱:{{ user.email }}</p>
</el-drawer>
<!-- 嵌套弹窗 -->
<el-dialog v-model="outerVisible" title="外层弹窗">
<el-button @click="innerVisible = true">打开内层弹窗</el-button>
<el-dialog v-model="innerVisible" title="内层弹窗" append-to-body>
<p>这是内层弹窗</p>
</el-dialog>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue'
// 弹窗最佳实践:使用 v-model 控制显示
const dialogVisible = ref(false)
const drawerVisible = ref(false)
const editForm = reactive({
name: '',
email: '',
})
// 打开弹窗并填充数据
function openEditDialog(row: any) {
Object.assign(editForm, { name: row.name, email: row.email })
dialogVisible.value = true
}
// 关闭时清理
function handleClose() {
// destroy-on-close 已经自动清理
}
// 使用 Teleport 将弹窗渲染到 body(默认行为)
// append-to-body 解决嵌套弹窗的层级问题
</script>导航组件
<template>
<!-- 顶部导航 -->
<el-menu
:default-active="activeIndex"
mode="horizontal"
:ellipsis="false"
router
>
<el-menu-item index="/">首页</el-menu-item>
<el-menu-item index="/users">用户管理</el-menu-item>
<el-sub-menu index="/settings">
<template #title>系统设置</template>
<el-menu-item index="/settings/profile">个人设置</el-menu-item>
<el-menu-item index="/settings/system">系统配置</el-menu-item>
</el-sub-menu>
</el-menu>
<!-- 侧边栏导航 -->
<el-menu
:default-active="route.path"
:collapse="isCollapse"
router
class="sidebar-menu"
>
<el-menu-item index="/dashboard">
<el-icon><Odometer /></el-icon>
<template #title>仪表盘</template>
</el-menu-item>
<el-sub-menu index="/system">
<template #title>
<el-icon><Setting /></el-icon>
<span>系统管理</span>
</template>
<el-menu-item index="/system/users">用户管理</el-menu-item>
<el-menu-item index="/system/roles">角色管理</el-menu-item>
<el-menu-item index="/system/menus">菜单管理</el-menu-item>
</el-sub-menu>
</el-menu>
<!-- 面包屑 -->
<el-breadcrumb separator="/">
<el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
<el-breadcrumb-item>系统管理</el-breadcrumb-item>
<el-breadcrumb-item>用户管理</el-breadcrumb-item>
</el-breadcrumb>
<!-- 标签页 -->
<el-tabs v-model="activeTab" type="card" closable @tab-remove="removeTab">
<el-tab-pane
v-for="tab in tabs"
:key="tab.name"
:label="tab.title"
:name="tab.name"
/>
</el-tabs>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { Odometer, Setting } from '@element-plus/icons-vue'
const route = useRoute()
const router = useRouter()
const activeIndex = computed(() => route.path)
const isCollapse = ref(false)
// 标签页管理
const tabs = ref([
{ name: '/dashboard', title: '仪表盘' },
{ name: '/users', title: '用户管理' },
])
const activeTab = ref('/dashboard')
function removeTab(name: string) {
const index = tabs.value.findIndex(t => t.name === name)
if (index > -1) {
tabs.value.splice(index, 1)
if (activeTab.value === name) {
const next = tabs.value[index] || tabs.value[index - 1]
if (next) router.push(next.name)
}
}
}
</script>数据录入组件
<template>
<!-- 日期选择 -->
<el-date-picker
v-model="dateRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
:shortcuts="dateShortcuts"
/>
<!-- 数字输入 -->
<el-input-number v-model="count" :min="1" :max="100" :step="2" />
<!-- 级联选择 -->
<el-cascader
v-model="selectedArea"
:options="areaOptions"
:props="{ checkStrictly: true }"
placeholder="请选择地区"
/>
<!-- 穿梭框 -->
<el-transfer
v-model="transferValue"
:data="transferData"
:titles="['可选列表', '已选列表']"
filterable
/>
<!-- 上传组件 -->
<el-upload
action="/api/upload"
:headers="{ Authorization: `Bearer ${token}` }"
:before-upload="beforeUpload"
:on-success="handleUploadSuccess"
:on-error="handleUploadError"
:file-list="fileList"
:limit="5"
:on-exceed="handleExceed"
accept=".jpg,.png,.pdf"
>
<el-button type="primary">点击上传</el-button>
<template #tip>
<div class="el-upload__tip">支持 jpg/png/pdf 格式,最多 5 个文件</div>
</template>
</el-upload>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { ElMessage } from 'element-plus'
import type { UploadProps, UploadRawFile } from 'element-plus'
// 日期快捷选项
const dateRange = ref<string[]>([])
const dateShortcuts = [
{
text: '最近一周',
value: () => {
const end = new Date()
const start = new Date()
start.setTime(start.getTime() - 7 * 24 * 3600 * 1000)
return [start, end]
},
},
{
text: '最近一个月',
value: () => {
const end = new Date()
const start = new Date()
start.setMonth(start.getMonth() - 1)
return [start, end]
},
},
]
// 上传限制
const beforeUpload: UploadProps['beforeUpload'] = (rawFile: UploadRawFile) => {
const maxSize = 5 * 1024 * 1024 // 5MB
if (rawFile.size > maxSize) {
ElMessage.error('文件大小不能超过 5MB')
return false
}
return true
}
const handleExceed: UploadProps['onExceed'] = () => {
ElMessage.warning('最多上传 5 个文件')
}
</script>封装通用业务组件
<!-- ProTable.vue —— 通用表格组件 -->
<template>
<div class="pro-table">
<!-- 搜索区域 -->
<el-form
v-if="searchFields.length"
:model="searchForm"
inline
class="search-form"
>
<el-form-item
v-for="field in searchFields"
:key="field.prop"
:label="field.label"
>
<el-input
v-if="field.type === 'input'"
v-model="searchForm[field.prop]"
:placeholder="`请输入${field.label}`"
clearable
/>
<el-select
v-else-if="field.type === 'select'"
v-model="searchForm[field.prop]"
:placeholder="`请选择${field.label}`"
clearable
>
<el-option
v-for="opt in field.options"
:key="opt.value"
:label="opt.label"
:value="opt.value"
/>
</el-select>
<el-date-picker
v-else-if="field.type === 'date'"
v-model="searchForm[field.prop]"
type="daterange"
range-separator="至"
start-placeholder="开始"
end-placeholder="结束"
value-format="YYYY-MM-DD"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">搜索</el-button>
<el-button @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
<!-- 工具栏 -->
<div class="toolbar">
<slot name="toolbar" />
</div>
<!-- 表格 -->
<el-table
v-loading="loading"
:data="tableData"
border
stripe
@selection-change="handleSelectionChange"
>
<el-table-column v-if="showSelection" type="selection" width="50" />
<el-table-column
v-for="col in columns"
:key="col.prop"
:prop="col.prop"
:label="col.label"
:width="col.width"
:sortable="col.sortable"
:fixed="col.fixed"
>
<template #default="scope">
<slot :name="col.prop" v-bind="scope">
{{ scope.row[col.prop] }}
</slot>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
v-model:current-page="pagination.page"
v-model:page-size="pagination.pageSize"
:total="pagination.total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
class="pagination"
@change="fetchData"
/>
</div>
</template>
<script setup lang="ts" generic="T extends Record<string, any}">
import { ref, reactive, onMounted, watch } from 'vue'
interface Column {
prop: string
label: string
width?: number | string
sortable?: boolean
fixed?: string | boolean
}
interface SearchField {
prop: string
label: string
type: 'input' | 'select' | 'date'
options?: { label: string; value: any }[]
}
interface Props {
columns: Column[]
searchFields?: SearchField[]
fetchDataApi: (params: any) => Promise<{ data: T[]; total: number }>
showSelection?: boolean
immediate?: boolean
}
const props = withDefaults(defineProps<Props>(), {
searchFields: () => [],
showSelection: false,
immediate: true,
})
const loading = ref(false)
const tableData = ref<T[]>([]) as Ref<T[]>
const searchForm = reactive<Record<string, any>>({})
const selectedRows = ref<T[]>([])
const pagination = reactive({
page: 1,
pageSize: 20,
total: 0,
})
async function fetchData() {
loading.value = true
try {
const params = {
page: pagination.page,
pageSize: pagination.pageSize,
...searchForm,
}
const res = await props.fetchDataApi(params)
tableData.value = res.data
pagination.total = res.total
} finally {
loading.value = false
}
}
function handleSearch() {
pagination.page = 1
fetchData()
}
function handleReset() {
Object.keys(searchForm).forEach(key => {
searchForm[key] = undefined
})
handleSearch()
}
function handleSelectionChange(rows: T[]) {
selectedRows.value = rows
}
onMounted(() => {
if (props.immediate) fetchData()
})
defineExpose({
refresh: fetchData,
selectedRows,
searchForm,
})
</script>优点
缺点
总结
Element Plus 是 Vue3 B 端开发的首选 UI 库。核心组件:el-table(表格)、el-form(表单)、el-dialog(弹窗)、el-menu(导航)。按需引入用 unplugin-vue-components 自动导入。表单验证用 rules + validate()。消息提示用 ElMessage/ElMessageBox。
最佳实践清单:
- 始终使用按需引入,避免完整引入增大包体积
- 封装通用业务组件(ProTable、ProForm),统一交互模式
- 表单校验使用 rules 声明式 + 自定义 validator,避免手动校验
- 主题定制优先使用 CSS 变量方案,支持运行时切换
- 弹窗使用 destroy-on-close 避免状态残留
- 大数据量表格使用 el-table-v2 虚拟滚动
关键知识点
- 先判断主题更偏浏览器原理、框架机制、工程化还是性能优化。
- 前端问题很多看似是页面问题,实际源头在构建、缓存、状态流或接口协作。
- 真正成熟的前端方案一定同时考虑首屏、交互、可维护性和线上诊断。
- 前端主题最好同时看浏览器原理、框架机制和工程化约束。
项目落地视角
- 把组件边界、状态归属、网络层规范和错误处理先定下来。
- 上线前检查包体积、缓存命中、接口失败路径和关键交互降级策略。
- 如果主题和性能有关,最好用 DevTools、Lighthouse 或埋点验证。
- 对关键页面先建立状态流和数据流,再考虑组件拆分。
常见误区
- 只盯框架 API,不理解浏览器和运行时成本。
- 把状态、请求和 UI 更新混成一层,后期难维护。
- 线上问题出现时没有日志、埋点和性能基线可对照。
- 只追框架新特性,不分析实际渲染成本。
进阶路线
- 继续补齐 SSR、边缘渲染、设计系统和监控告警能力。
- 把主题和后端接口约定、CI/CD、缓存策略一起思考。
- 沉淀组件规范、页面模板和性能基线,减少团队差异。
- 继续补齐设计系统、SSR/边缘渲染、监控告警和组件库治理。
适用场景
- 当你准备把《Element Plus 组件库》真正落到项目里时,最适合先在一个独立模块或最小样例里验证关键路径。
- 适合中后台应用、门户站点、组件库和实时交互页面。
- 当需求涉及状态流、路由、网络缓存、SSR/CSR 或性能治理时,这类主题很关键。
落地建议
- 先定义组件边界和状态归属,再落地 UI 细节。
- 对核心页面做首屏、体积、缓存和错误路径检查。
- 把安全、兼容性和可访问性纳入默认交付标准。
排错清单
- 先用浏览器 DevTools 看请求、性能面板和控制台错误。
- 检查依赖版本、构建配置、环境变量和静态资源路径。
- 如果是线上问题,优先确认缓存、CDN 和构建产物是否一致。
复盘问题
- 如果把《Element Plus 组件库》放进你的当前项目,最先要验证的输入、输出和失败路径分别是什么?
- 《Element Plus 组件库》最容易在什么规模、什么边界条件下暴露问题?你会用什么指标或日志去确认?
- 相比默认实现或替代方案,采用《Element Plus 组件库》最大的收益和代价分别是什么?
