完美升级!将ElTree生硬文本提示替换为优雅的ElEmpty组件

为什么满足于单调的"暂无数据"文字?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 性能优化建议

对于频繁切换状态的场景,建议:

  1. 使用 v-show 替代 v-if 避免频繁的DOM创建销毁
  2. 图片预加载 提前加载可能用到的空状态图片
  3. 组件缓存 使用 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 组件,不仅解决了样式调整的烦恼,更重要的是提供了:

  1. 专业的用户体验:符合现代UI设计规范
  2. 丰富的功能扩展:支持图标、按钮、自定义内容
  3. 统一的视觉语言:与Element Plus生态系统完美融合
  4. 便捷的维护方式:通过封装实现一次定义,多处使用

无论你是开发后台管理系统、数据展示平台还是复杂的企业应用,使用 el-empty 作为空状态解决方案都能显著提升产品的专业度和用户体验。立即尝试这种升级方案,让你的应用在细节处展现专业品质!

行动建议 :从今天开始,在所有使用 el-tree 的项目中,用 el-empty 替换所有的 empty-text。你的用户会感谢你提供的更友好、更专业的空状态体验。

相关推荐
光影少年2 小时前
react和vue多个组件在一个页面展示不同内容都是请求一个接口,如何优化提升率性能
前端·vue.js·react.js
匠心网络科技2 小时前
前端框架-Vue双向绑定核心机制全解析(二)
前端·javascript·vue.js·前端框架
肖。35487870942 小时前
窗口半初始化导致的BadTokenException闪退!解决纯Java开发的安卓软件开局闪退!具体表现为存储中的缓存为0和数据为0。
android·java·javascript·css·html
我是伪码农10 小时前
Vue 1.23
前端·javascript·vue.js
wqwqweee10 小时前
Flutter for OpenHarmony 看书管理记录App实战:搜索功能实现
开发语言·javascript·python·flutter·harmonyos
HIT_Weston12 小时前
107、【Ubuntu】【Hugo】搭建私人博客:模糊搜索 Fuse.js(三)
linux·javascript·ubuntu
henujolly15 小时前
ethers.js读取合约信息
开发语言·javascript·区块链
wuhen_n16 小时前
高阶函数与泛型函数的类型体操
前端·javascript·typescript
POLITE316 小时前
Leetcode 437. 路径总和 III (Day 16)JavaScript
javascript·算法·leetcode