为什么满足于单调的"暂无数据"文字?Element Plus 早已为你准备了专业、美观且功能强大的空状态组件------
el-empty。今天,我们将彻底告别empty-text的样式烦恼,实现无缝升级。
引言:为什么选择 el-empty?
el-empty 是 Element Plus 专门为空状态设计的组件,它相比简单的文本提示 (empty-text) 具有以下压倒性优势:
- 内置精美布局:图标、文字、操作按钮的完美垂直居中排列
- 丰富的预设样式:提供默认、错误、无权限等多种场景图标
- 完全可自定义:可替换图标、图片,添加任意自定义内容
- 专业视觉设计:遵循 Element Plus 设计规范,与整个UI系统和谐统一
- 响应式支持:自动适配不同屏幕尺寸
一、基础实现:快速替换方案
1.1 使用 #empty 插槽集成 el-empty
这是最直接、最推荐的方式,通过 el-tree 的 #empty 插槽完全控制空状态的渲染。
vue
<template>
<div class="tree-container">
<el-tree
:data="treeData"
:props="defaultProps"
node-key="id"
:expand-on-click-node="false"
>
<!-- 使用 #empty 插槽替换默认空状态 -->
<template #empty>
<el-empty description="暂无数据" />
</template>
</el-tree>
</div>
</template>
<script setup>
import { ref } from 'vue'
const treeData = ref([]) // 空数据示例
const defaultProps = {
children: 'children',
label: 'label',
}
</script>
<style scoped>
.tree-container {
padding: 20px;
border: 1px solid #e4e7ed;
border-radius: 6px;
background: #fff;
min-height: 400px;
}
</style>
1.2 效果对比
| 特性 | empty-text 文本提示 |
el-empty 组件 |
|---|---|---|
| 视觉美观度 | ⭐☆☆☆☆ (需自定义样式) | ⭐⭐⭐⭐⭐ (开箱即用) |
| 布局居中 | 需额外CSS调整 | 自动完美居中 |
| 图标支持 | 不支持 | 内置多种图标 |
| 操作按钮 | 不支持 | 支持添加按钮 |
| 响应式 | 需手动实现 | 自动响应式 |
| 维护成本 | 高 | 低 |
二、进阶用法:充分利用 el-empty 特性
2.1 不同场景的空状态
el-empty 支持多种预设模式,满足不同业务场景需求:
vue
<template>
<div class="tree-with-empty">
<!-- 场景选择器 -->
<div class="scene-selector">
<el-radio-group v-model="emptyScene" @change="changeScene">
<el-radio label="noData">无数据</el-radio>
<el-radio label="error">加载失败</el-radio>
<el-radio label="noPermission">无权限</el-radio>
<el-radio label="searchEmpty">搜索无结果</el-radio>
<el-radio label="custom">完全自定义</el-radio>
</el-radio-group>
</div>
<el-tree
:data="treeData"
:props="defaultProps"
>
<template #empty>
<!-- 根据不同场景显示不同的空状态 -->
<el-empty
v-if="emptyScene === 'noData'"
description="暂无数据"
:image="emptyImages[emptyScene]"
/>
<el-empty
v-else-if="emptyScene === 'error'"
description="数据加载失败"
:image="emptyImages[emptyScene]"
>
<el-button type="primary" @click="retryLoad">重新加载</el-button>
</el-empty>
<el-empty
v-else-if="emptyScene === 'noPermission'"
description="您没有查看权限"
:image="emptyImages[emptyScene]"
>
<el-button type="primary" @click="applyPermission">申请权限</el-button>
</el-empty>
<el-empty
v-else-if="emptyScene === 'searchEmpty'"
description="未找到相关内容"
:image="emptyImages[emptyScene]"
>
<div class="search-suggestions">
<p>建议:</p>
<ul>
<li>检查搜索词拼写</li>
<li>尝试更通用的关键词</li>
<li>清空筛选条件</li>
</ul>
</div>
</el-empty>
<!-- 完全自定义 -->
<el-empty v-else description="完全自定义示例">
<template #image>
<div class="custom-image">
<svg width="100" height="100" viewBox="0 0 100 100">
<circle cx="50" cy="50" r="40" fill="#f0f9ff" stroke="#409eff" stroke-width="3"/>
<text x="50" y="55" text-anchor="middle" font-size="14" fill="#409eff">空</text>
</svg>
</div>
</template>
<div class="custom-actions">
<el-button type="primary" @click="importData">
<el-icon><Upload /></el-icon>
导入数据
</el-button>
<el-button @click="useTemplate">
<el-icon><DocumentCopy /></el-icon>
使用模板
</el-button>
</div>
</el-empty>
</template>
</el-tree>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import { Upload, DocumentCopy } from '@element-plus/icons-vue'
const emptyScene = ref('noData')
const treeData = ref([])
const defaultProps = {
children: 'children',
label: 'label',
}
// 不同场景的图片配置(可以使用本地图片或在线图片)
const emptyImages = computed(() => ({
noData: '', // 使用默认图标
error: '', // 使用默认错误图标
noPermission: '', // 使用默认无权限图标
searchEmpty: '', // 使用默认搜索无结果图标
}))
const changeScene = (scene) => {
console.log(`切换到场景: ${scene}`)
}
const retryLoad = () => {
console.log('重新加载数据')
}
const applyPermission = () => {
console.log('申请权限')
}
const importData = () => {
console.log('导入数据')
}
const useTemplate = () => {
console.log('使用模板')
}
</script>
<style scoped>
.tree-with-empty {
padding: 20px;
}
.scene-selector {
margin-bottom: 20px;
padding: 15px;
background: #f5f7fa;
border-radius: 4px;
}
.search-suggestions {
text-align: left;
margin-top: 10px;
padding: 10px;
background: #f8f9fa;
border-radius: 4px;
font-size: 13px;
color: #606266;
}
.search-suggestions p {
margin-bottom: 5px;
font-weight: 500;
}
.search-suggestions ul {
margin: 0;
padding-left: 20px;
}
.search-suggestions li {
margin-bottom: 3px;
}
.custom-image {
margin-bottom: 15px;
}
.custom-actions {
display: flex;
gap: 10px;
justify-content: center;
margin-top: 15px;
}
</style>
2.2 动态空状态:加载与空数据分离
在实际应用中,我们经常需要区分"加载中"、"加载失败"和"真正无数据"的状态:
vue
<template>
<div class="dynamic-empty-tree">
<!-- 加载控制 -->
<div class="controls">
<el-button-group>
<el-button @click="setLoading">加载中</el-button>
<el-button @click="setEmpty">无数据</el-button>
<el-button @click="setError">加载失败</el-button>
<el-button @click="setData">有数据</el-button>
</el-button-group>
</div>
<!-- 树组件 -->
<div class="tree-wrapper">
<el-tree
v-loading="loading"
element-loading-text="正在加载树数据..."
element-loading-spinner="el-icon-loading"
element-loading-background="rgba(255, 255, 255, 0.7)"
:data="treeData"
:props="defaultProps"
>
<!-- 空状态插槽 -->
<template #empty>
<!-- 根据状态显示不同的空状态 -->
<div v-if="!loading" class="dynamic-empty">
<el-empty
:description="emptyDescription"
:image="emptyImage"
>
<!-- 根据状态显示不同的操作按钮 -->
<div v-if="isEmptyState" class="empty-actions">
<el-button type="primary" @click="addRootNode">
添加根节点
</el-button>
<el-button @click="refreshData">
刷新
</el-button>
</div>
<div v-else-if="isErrorState" class="empty-actions">
<el-button type="primary" @click="retryLoad">
重试
</el-button>
<el-button @click="contactSupport">
联系支持
</el-button>
</div>
</el-empty>
<!-- 额外信息 -->
<div v-if="isEmptyState" class="extra-info">
<el-divider content-position="center">或者</el-divider>
<p class="help-text">您还可以:</p>
<div class="quick-actions">
<el-link type="primary" :underline="false" @click="importFromFile">
<el-icon><Upload /></el-icon>
从文件导入
</el-link>
<el-link type="primary" :underline="false" @click="useSampleData">
<el-icon><MagicStick /></el-icon>
使用示例数据
</el-link>
</div>
</div>
</div>
</template>
</el-tree>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import { Upload, MagicStick } from '@element-plus/icons-vue'
// 状态控制
const loading = ref(false)
const treeData = ref([])
const currentState = ref('empty') // 'empty', 'error', 'data'
const defaultProps = {
children: 'children',
label: 'label',
}
// 计算属性
const isEmptyState = computed(() => currentState.value === 'empty' && !loading.value)
const isErrorState = computed(() => currentState.value === 'error' && !loading.value)
const emptyDescription = computed(() => {
if (currentState.value === 'empty') return '暂无数据'
if (currentState.value === 'error') return '数据加载失败'
return '暂无数据'
})
const emptyImage = computed(() => {
if (currentState.value === 'error') return '' // 使用默认错误图标
return '' // 使用默认空数据图标
})
// 状态设置方法
const setLoading = () => {
loading.value = true
treeData.value = []
currentState.value = 'empty'
setTimeout(() => {
loading.value = false
}, 2000)
}
const setEmpty = () => {
loading.value = false
treeData.value = []
currentState.value = 'empty'
}
const setError = () => {
loading.value = false
treeData.value = []
currentState.value = 'error'
}
const setData = () => {
loading.value = false
currentState.value = 'data'
treeData.value = [
{
id: 1,
label: '根节点',
children: [
{ id: 2, label: '子节点1' },
{ id: 3, label: '子节点2' }
]
}
]
}
// 操作方法
const addRootNode = () => {
console.log('添加根节点')
}
const refreshData = () => {
console.log('刷新数据')
}
const retryLoad = () => {
console.log('重试加载')
}
const contactSupport = () => {
console.log('联系支持')
}
const importFromFile = () => {
console.log('从文件导入')
}
const useSampleData = () => {
console.log('使用示例数据')
}
</script>
<style scoped>
.dynamic-empty-tree {
padding: 20px;
}
.controls {
margin-bottom: 20px;
padding: 15px;
background: #f5f7fa;
border-radius: 4px;
}
.tree-wrapper {
min-height: 400px;
border: 1px solid #e4e7ed;
border-radius: 6px;
overflow: hidden;
}
.dynamic-empty {
padding: 40px 20px;
}
.empty-actions {
display: flex;
gap: 10px;
justify-content: center;
margin-top: 20px;
}
.extra-info {
margin-top: 30px;
text-align: center;
}
.help-text {
color: #909399;
font-size: 13px;
margin-bottom: 10px;
}
.quick-actions {
display: flex;
gap: 20px;
justify-content: center;
}
</style>
三、企业级最佳实践
3.1 封装可复用的空状态组件
在企业项目中,建议将空状态封装成可复用的组件:
vue
<!-- components/EmptyState.vue -->
<template>
<el-empty
:description="description"
:image="computedImage"
>
<!-- 默认插槽用于自定义内容 -->
<slot>
<!-- 默认操作按钮 -->
<div v-if="showDefaultActions" class="default-actions">
<el-button v-if="showRefresh" type="primary" @click="handleRefresh">
{{ refreshText }}
</el-button>
<el-button v-if="showCreate" @click="handleCreate">
{{ createText }}
</el-button>
</div>
</slot>
<!-- 底部额外信息插槽 -->
<template #bottom>
<slot name="bottom">
<div v-if="extraText" class="extra-text">
{{ extraText }}
</div>
</slot>
</template>
</el-empty>
</template>
<script setup>
import { computed } from 'vue'
const props = defineProps({
// 基础属性
type: {
type: String,
default: 'noData', // 'noData', 'error', 'noPermission', 'searchEmpty', 'custom'
validator: (value) => ['noData', 'error', 'noPermission', 'searchEmpty', 'custom'].includes(value)
},
description: {
type: String,
default: ''
},
image: {
type: String,
default: ''
},
// 操作按钮配置
showDefaultActions: {
type: Boolean,
default: true
},
showRefresh: {
type: Boolean,
default: true
},
showCreate: {
type: Boolean,
default: true
},
refreshText: {
type: String,
default: '刷新'
},
createText: {
type: String,
default: '创建'
},
// 额外信息
extraText: {
type: String,
default: ''
}
})
const emit = defineEmits(['refresh', 'create'])
// 根据类型计算使用的图片
const computedImage = computed(() => {
if (props.image) return props.image
const imageMap = {
noData: '',
error: '',
noPermission: '',
searchEmpty: '',
custom: ''
}
return imageMap[props.type] || ''
})
// 根据类型计算默认描述
const computedDescription = computed(() => {
if (props.description) return props.description
const descriptionMap = {
noData: '暂无数据',
error: '加载失败',
noPermission: '无访问权限',
searchEmpty: '未找到相关内容',
custom: ''
}
return descriptionMap[props.type] || '暂无数据'
})
// 事件处理
const handleRefresh = () => {
emit('refresh')
}
const handleCreate = () => {
emit('create')
}
</script>
<style scoped>
.default-actions {
display: flex;
gap: 10px;
justify-content: center;
margin-top: 20px;
}
.extra-text {
margin-top: 15px;
color: #909399;
font-size: 13px;
text-align: center;
}
</style>
3.2 在 el-tree 中使用封装组件
vue
<template>
<div class="enterprise-tree">
<el-tree
:data="treeData"
:props="defaultProps"
node-key="id"
>
<template #empty>
<EmptyState
:type="emptyType"
:description="emptyDescription"
@refresh="handleRefresh"
@create="handleCreate"
>
<!-- 自定义底部内容 -->
<template #bottom>
<div class="enterprise-bottom">
<p>需要帮助?</p>
<div class="help-links">
<el-link type="primary" :underline="false" @click="viewDocument">
查看文档
</el-link>
<el-divider direction="vertical" />
<el-link type="primary" :underline="false" @click="contactSupport">
联系支持
</el-link>
<el-divider direction="vertical" />
<el-link type="primary" :underline="false" @click="suggestFeature">
功能建议
</el-link>
</div>
</div>
</template>
</EmptyState>
</template>
</el-tree>
</div>
</template>
<script setup>
import { ref } from 'vue'
import EmptyState from '@/components/EmptyState.vue'
const treeData = ref([])
const emptyType = ref('noData')
const emptyDescription = ref('当前目录为空')
const defaultProps = {
children: 'children',
label: 'label',
}
const handleRefresh = () => {
console.log('刷新数据')
// 实际项目中这里应该调用API
}
const handleCreate = () => {
console.log('创建新项')
}
const viewDocument = () => {
console.log('查看文档')
}
const contactSupport = () => {
console.log('联系支持')
}
const suggestFeature = () => {
console.log('功能建议')
}
</script>
<style scoped>
.enterprise-tree {
padding: 20px;
min-height: 500px;
}
.enterprise-bottom {
margin-top: 20px;
padding: 15px;
background: #f8f9fa;
border-radius: 6px;
}
.enterprise-bottom p {
margin-bottom: 10px;
color: #606266;
font-size: 14px;
text-align: center;
}
.help-links {
display: flex;
justify-content: center;
align-items: center;
gap: 5px;
}
</style>
四、常见问题与解决方案
4.1 样式覆盖问题
如果 el-empty 的样式被全局样式影响,可以使用深度选择器进行修复:
css
/* 确保 el-empty 在 tree 中正确显示 */
.tree-container :deep(.el-empty) {
padding: 40px 20px !important;
}
.tree-container :deep(.el-empty__image) {
width: 120px !important;
height: 120px !important;
margin-bottom: 20px !important;
}
.tree-container :deep(.el-empty__description) {
margin-top: 0 !important;
color: #606266 !important;
font-size: 14px !important;
}
4.2 性能优化建议
对于频繁切换状态的场景,建议:
- 使用
v-show替代v-if避免频繁的DOM创建销毁 - 图片预加载 提前加载可能用到的空状态图片
- 组件缓存 使用
KeepAlive缓存空状态组件
vue
<template>
<el-tree :data="treeData">
<template #empty>
<!-- 使用 keep-alive 缓存空状态组件 -->
<keep-alive>
<component
:is="emptyComponent"
v-bind="emptyProps"
/>
</keep-alive>
</template>
</el-tree>
</template>
五、总结
将 el-tree 的默认文本提示替换为 el-empty 组件,不仅解决了样式调整的烦恼,更重要的是提供了:
- 专业的用户体验:符合现代UI设计规范
- 丰富的功能扩展:支持图标、按钮、自定义内容
- 统一的视觉语言:与Element Plus生态系统完美融合
- 便捷的维护方式:通过封装实现一次定义,多处使用
无论你是开发后台管理系统、数据展示平台还是复杂的企业应用,使用 el-empty 作为空状态解决方案都能显著提升产品的专业度和用户体验。立即尝试这种升级方案,让你的应用在细节处展现专业品质!
行动建议 :从今天开始,在所有使用 el-tree 的项目中,用 el-empty 替换所有的 empty-text。你的用户会感谢你提供的更友好、更专业的空状态体验。