🎨 Element Plus 组件库实战技巧与踩坑记录
分享我在Vue 3项目中使用Element Plus的经验技巧和踩坑记录
前言
Element Plus是Vue 3生态中最流行的UI组件库之一,提供了丰富的组件和良好的设计。在开发博客项目的过程中,我积累了很多使用Element Plus的经验和技巧,也踩过一些坑。本文将分享这些实战经验。
快速上手
1. 安装与配置
# 安装Element Plus
npm install element-plus
# 安装图标库
npm install @element-plus/icons-vue
// main.ts
import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import App from './App.vue'
const app = createApp(App)
// 注册所有组件
app.use(ElementPlus)
// 注册所有图标
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
app.mount('#app')
2. 按需引入(推荐)
为了减小包体积,建议按需引入组件:
# 安装按需引入插件
npm install -D unplugin-vue-components unplugin-auto-import
// 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'
export default defineConfig({
plugins: [
vue(),
AutoImport({
resolvers: [ElementPlusResolver()],
}),
Components({
resolvers: [ElementPlusResolver()],
}),
],
})
这样配置后,使用组件时会自动按需引入,无需手动import。
常用组件技巧
1. 表单组件
el-form深度验证
<template>
<el-form
ref="formRef"
:model="formData"
:rules="rules"
label-width="120px"
>
<el-form-item label="标题" prop="title">
<el-input v-model="formData.title" />
</el-form-item>
<el-form-item label="邮箱" prop="email">
<el-input v-model="formData.email" />
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input
v-model="formData.password"
type="password"
show-password
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSubmit">
提交
</el-button>
<el-button @click="handleReset">
重置
</el-button>
</el-form-item>
</el-form>
</template>
<script setup lang="ts">
import type { FormInstance, FormRules } from 'element-plus'
const formRef = ref<FormInstance>()
const formData = reactive({
title: '',
email: '',
password: ''
})
const rules = reactive<FormRules>({
title: [
{ required: true, message: '请输入标题', trigger: 'blur' },
{ min: 2, max: 50, message: '长度在 2 到 50 个字符', trigger: 'blur' }
],
email: [
{ required: true, message: '请输入邮箱地址', trigger: 'blur' },
{ type: 'email', message: '请输入正确的邮箱地址', trigger: ['blur', 'change'] }
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 6, message: '密码长度不能少于 6 位', trigger: 'blur' }
]
})
const handleSubmit = async () => {
if (!formRef.value) return
await formRef.value.validate((valid, fields) => {
if (valid) {
// 验证通过,提交表单
console.log('提交:', formData)
} else {
console.log('验证失败:', fields)
}
})
}
const handleReset = () => {
formRef.value?.resetFields()
}
</script>
动态表单
<template>
<el-form :model="formData">
<el-form-item
v-for="(item, index) in formData.items"
:key="index"
:label="'项目 ' + (index + 1)"
>
<el-input v-model="item.value" />
<el-button
@click="removeItem(index)"
icon="Delete"
type="danger"
>
删除
</el-button>
</el-form-item>
<el-button @click="addItem" icon="Plus">
添加项目
</el-button>
</el-form>
</template>
<script setup lang="ts">
const formData = reactive({
items: [{ value: '' }]
})
const addItem = () => {
formData.items.push({ value: '' })
}
const removeItem = (index: number) => {
formData.items.splice(index, 1)
}
</script>
2. 表格组件
表格排序和筛选
<template>
<el-table
:data="filteredData"
:default-sort="{ prop: 'date', order: 'descending' }"
@sort-change="handleSortChange"
>
<el-table-column prop="title" label="标题" sortable />
<el-table-column
prop="category"
label="分类"
:filters="categoryFilters"
:filter-method="filterCategory"
/>
<el-table-column prop="views" label="浏览量" sortable />
<el-table-column prop="date" label="日期" sortable />
</el-table>
</template>
<script setup lang="ts">
const articles = ref<Article[]>([])
const filteredData = computed(() => {
return articles.value
})
const categoryFilters = [
{ text: 'Vue', value: 'Vue' },
{ text: 'React', value: 'React' },
{ text: 'TypeScript', value: 'TypeScript' }
]
const filterCategory = (value: string, row: Article) => {
return row.category === value
}
const handleSortChange = (sort: any) => {
console.log('排序改变:', sort)
}
</script>
表格分页
<template>
<el-table :data="paginatedData">
<!-- 列定义 -->
</el-table>
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:total="total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</template>
<script setup lang="ts">
const currentPage = ref(1)
const pageSize = ref(10)
const total = ref(0)
const paginatedData = computed(() => {
const start = (currentPage.value - 1) * pageSize.value
const end = start + pageSize.value
return articles.value.slice(start, end)
})
const handleSizeChange = (size: number) => {
pageSize.value = size
}
const handleCurrentChange = (page: number) => {
currentPage.value = page
}
</script>
3. 弹窗组件
对话框嵌套
<template>
<el-button @click="showDialog = true">打开对话框</el-button>
<el-dialog v-model="showDialog" title="父对话框">
<p>这是父对话框的内容</p>
<el-button @click="showChildDialog = true">
打开子对话框
</el-button>
<el-dialog
v-model="showChildDialog"
title="子对话框"
append-to-body
>
<p>这是子对话框的内容</p>
</el-dialog>
</el-dialog>
</template>
<script setup lang="ts">
const showDialog = ref(false)
const showChildDialog = ref(false)
</script>
注意:嵌套对话框时,子对话框需要添加append-to-body属性。
4. 树形组件
异步加载树
<template>
<el-tree
:props="defaultProps"
:load="loadNode"
lazy
show-checkbox
/>
</template>
<script setup lang="ts">
const defaultProps = {
label: 'name',
children: 'children',
isLeaf: 'leaf'
}
const loadNode = async (node: Node, resolve: (data: TreeData[]) => void) => {
if (node.level === 0) {
// 加载根节点
const data = await loadRootNodes()
resolve(data)
} else {
// 加载子节点
const data = await loadChildNodes(node.data.id)
resolve(data)
}
}
const loadRootNodes = async () => {
// 异步加载数据
return [
{ name: '节点1', id: 1 },
{ name: '节点2', id: 2 }
]
}
</script>
主题定制
1. 使用CSS变量
// styles/theme.scss
:root {
--el-color-primary: #409eff;
--el-color-success: #67c23a;
--el-color-warning: #e6a23c;
--el-color-danger: #f56c6c;
--el-color-info: #909399;
}
// 使用自定义主题
$--color-primary: var(--el-color-primary);
2. SCSS变量覆盖
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import ElementPlus from 'unplugin-element-plus/vite'
export default defineConfig({
plugins: [
vue(),
ElementPlus({
// 使用scss样式
useSource: true
})
]
})
// styles/element-variables.scss
/* 改变主题色变量 */
$--color-primary: #1890ff;
$--color-success: #52c41a;
$--color-warning: #faad14;
$--color-danger: #f5222d;
$--color-info: #909399;
/* 改变icon字体路径变量,必需 */
$--font-path: '~element-plus/lib/theme-chalk/fonts';
@import "~element-plus/packages/theme-chalk/src/index";
3. 暗黑模式
<template>
<el-switch
v-model="isDark"
@change="toggleDark"
inline-prompt
active-text="暗"
inactive-text="亮"
/>
</template>
<script setup lang="ts">
const isDark = ref(false)
const toggleDark = (value: boolean) => {
if (value) {
document.documentElement.classList.add('dark')
} else {
document.documentElement.classList.remove('dark')
}
}
</script>
<style>
/* 暗黑模式样式 */
html.dark {
--el-bg-color: #141414;
--el-text-color-primary: #e5eaf3;
--el-border-color: #4c4d4f;
--el-border-color-light: #414243;
}
</style>
性能优化
1. 图标按需加载
// utils/icons.ts
import { registerIcons } from 'element-plus/es/components/icon'
// 只注册需要的图标
export function lazyRegisterIcons() {
const icons = [
'Edit',
'Delete',
'View',
'Download',
'Share',
'Star',
'Plus',
'Search',
'Home'
]
// 使用requestIdleCallback在空闲时注册
const idleCallback = window.requestIdleCallback || window.setTimeout
idleCallback(() => {
registerIcons(icons)
})
}
// main.ts
import { lazyRegisterIcons } from './utils/icons'
lazyRegisterIcons()
2. 虚拟滚动
<template>
<el-virtual-list
:data="items"
:height="400"
:item-size="50"
>
<template #default="{ item, index }">
<div class="item">
{{ index }} - {{ item.name }}
</div>
</template>
</el-virtual-list>
</template>
<script setup lang="ts">
import { ElVirtualList } from 'element-plus'
// 生成大量数据
const items = Array.from({ length: 10000 }, (_, i) => ({
id: i,
name: `Item ${i}`
}))
</script>
踩坑记录
1. Dialog关闭不触发事件
问题:点击遮罩层关闭Dialog时,没有触发关闭事件。
解决:使用before-close属性:
<el-dialog
v-model="visible"
:before-close="handleClose"
>
<template #header>
<span>标题</span>
</template>
</el-dialog>
<script setup lang="ts">
const handleClose = (done: () => void) => {
// 执行关闭前的逻辑
done()
}
</script>
2. Table固定列错位
问题:表格固定列在滚动时出现错位。
解决:监听窗口大小变化,调用doLayout方法:
<template>
<el-table
ref="tableRef"
:data="tableData"
>
<el-table-column prop="date" label="日期" fixed />
<el-table-column prop="name" label="姓名" />
</el-table>
</template>
<script setup lang="ts">
const tableRef = ref()
onMounted(() => {
window.addEventListener('resize', () => {
tableRef.value?.doLayout()
})
})
</script>
3. Select下拉框显示位置错误
问题:Select组件的下拉框在页面滚动后显示位置错误。
解决:使用popper-options配置:
<el-select
v-model="value"
:popper-options="{
modifiers: [
{
name: 'flip',
options: {
fallbackPlacements: ['bottom-start', 'top-start']
}
}
]
}"
>
<el-option
v-for="item in options"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
4. DatePicker时间格式问题
问题:DatePicker返回的日期格式不符合预期。
解决:使用value-format属性:
<el-date-picker
v-model="date"
type="datetime"
value-format="YYYY-MM-DD HH:mm:ss"
placeholder="选择日期时间"
/>
5. Upload组件上传失败
问题:Upload组件在某些情况下上传失败。
解决:正确处理on-success和on-error回调:
<el-upload
action="/api/upload"
:on-success="handleSuccess"
:on-error="handleError"
:before-upload="beforeUpload"
>
<el-button type="primary">上传文件</el-button>
</el-upload>
<script setup lang="ts">
const handleSuccess = (response: any, file: any) => {
if (response.code === 200) {
ElMessage.success('上传成功')
} else {
ElMessage.error(response.message)
}
}
const handleError = (error: any) => {
ElMessage.error('上传失败:' + error.message)
}
const beforeUpload = (file: File) => {
const isJPG = file.type === 'image/jpeg' || file.type === 'image/png'
const isLt2M = file.size / 1024 / 1024 < 2
if (!isJPG) {
ElMessage.error('只能上传JPG/PNG图片!')
}
if (!isLt2M) {
ElMessage.error('图片大小不能超过2MB!')
}
return isJPG && isLt2M
}
</script>
最佳实践
1. 统一配置
// config/element-plus.ts
import { ElConfigProvider } from 'element-plus'
import zhCn from 'element-plus/es/locale/lang/zh-cn'
export default {
locale: zhCn,
size: 'default',
zIndex: 3000
}
<!-- App.vue -->
<template>
<el-config-provider :locale="locale">
<router-view />
</el-config-provider>
</template>
<script setup lang="ts">
import zhCn from 'element-plus/es/locale/lang/zh-cn'
const locale = zhCn
</script>
2. 封装常用组件
<!-- components/SearchInput.vue -->
<template>
<el-input
v-model="searchText"
:placeholder="placeholder"
clearable
@clear="handleClear"
@input="handleInput"
>
<template #prefix>
<el-icon><Search /></el-icon>
</template>
<template #suffix>
<el-button
v-if="searchText"
link
icon="Close"
@click="handleClear"
/>
</template>
</el-input>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
interface Props {
modelValue: string
placeholder?: string
}
const props = withDefaults(defineProps<Props>(), {
placeholder: '请输入搜索内容'
})
const emit = defineEmits<{
(e: 'update:modelValue', value: string): void
(e: 'search', value: string): void
}>()
const searchText = ref(props.modelValue)
watch(() => props.modelValue, (val) => {
searchText.value = val
})
watch(searchText, (val) => {
emit('update:modelValue', val)
})
const handleClear = () => {
searchText.value = ''
emit('search', '')
}
const handleInput = debounce((value: string) => {
emit('search', value)
}, 300)
</script>
3. 全局样式覆盖
// styles/element-overrides.scss
// 全局修改el-button样式
.el-button {
border-radius: 4px;
font-weight: 500;
&--primary {
background-color: #1890ff;
border-color: #1890ff;
&:hover {
background-color: #40a9ff;
border-color: #40a9ff;
}
}
}
// 修改el-dialog样式
.el-dialog {
border-radius: 8px;
overflow: hidden;
.el-dialog__header {
padding: 20px 20px 10px;
border-bottom: 1px solid #f0f0f0;
}
.el-dialog__body {
padding: 20px;
}
}
总结
Element Plus是一个功能强大、设计优秀的UI组件库,掌握以下要点可以更好地使用它:
- 按需引入 - 减小包体积
- 主题定制 - 符合项目风格
- 性能优化 - 图标懒加载、虚拟滚动
- 踩坑经验 - 了解常见问题和解决方案
- 最佳实践 - 封装常用组件、统一配置
希望这些经验能帮助你在Vue 3项目中更好地使用Element Plus!
标签:#ElementPlus #Vue3 #UI组件库 #前端 #实战技巧
点赞❤️ + 收藏⭐️ + 评论💬,你的支持是我创作的动力!