Vue3 ElementPlus 二次封装常用表格展示组件

本文介绍了如何在vue3环境下,结合element-plus库,对el-table进行二次封装,创建一个通用表格组件,以简化开发过程,减少代码重复,提升开发效率。主要涵盖基础文本展示、枚举类型处理、文本省略、鼠标悬浮和操作区域等功能。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

vue3通用表格组件的由来,前面讲过后台管理页面中查询展示表格数据基本上是必备功能。前面我发布过一篇使用vue2+element-ui封装的通用页面组件。原文可访问这篇文章 还在写重复的增删改查的重复代码?还在重复写Ajax请求?Vue2+Element-ui实现常规后台查询展示页面来了

在vue3普及的现在。这篇文章将使用vue3和element-plus。在el-table组件的基础上二次封装表格的常用展示形式。简化我们的代码使用方式。减少重复代码量,提高一定的开发效率。下面我们来一起看看我们的实现思路。

首先我们来初始化一个vue3的项目

初始化vue3的项目我们使用vite+vue3+ts,这里就不多叙述了,大家可以看这篇文章: 初始化

我们先来看看表格的基本效果:

我们来列举下表格的几个常用功能

1.基础文本展示

2.枚举类型展示 如:(状态,分类,标签,等根据status或type等字段展示对应的内容)

3.文本过长超出省略号

4.鼠标悬浮展示

5.操作区域 如:(查看,编辑,删除等操作)

根据以上的功能我们来在el-table的基础上把他封装到一个通用组件中

这里我们使用tsx函数组件的方式来实现,这样避免在template模版中使用大量的if else 哦我

我们先来定义一个table组件 在components文件中定义一个mijiTable.tsx的组件

首先我们在组件中引入element-plus的el-table、el-table-column组件

在这个组件中我们使用defineComponent来定义一个组件,使用组合式api setup来实现内部逻辑

这个组件我们接受两个值,一个是用来渲染el-table-column的数据 columns,一个是表格要渲染的数据 dataSource

我们来看看columns字段都包含了什么:

其实columns的内容字段就是 el-table-column组件的属性我们在这个基础上扩展一个slot字段用来自定义单元格内容

[{
    prop: '',
    label: '操作',
    type: '', // 
    slot: 'options',
    width: 120,
    fixed: 'right'
}]

在element-plus中 type属性默认值改成了default我们这里的type不用做el-table-column的type属性。而是用做区分表格列是什么格式。目前我们实现的有以下几种

常规 text、index序号、枚举、和操作栏

我们来看看如何动态渲染el-table-colunm

​
import {
  ElTable,
  ElTableColumn
} from 'element-plus'

export default defineComponent({
  props: {
    columns: {
      type: Array,
      default: () => []
    },
    dataSource: {
      type: Array,
      default: () => []
    }
  },
  setup(props, { slots }: any) {
    const { columns, dataSource } = props
    console.log('渲染表格列的数据', columns)
    console.log('表格数据:', dataSource);

    const tableData = ref()
    watch(() => props.dataSource, (now) => {
      tableData.value = now
    })
    
    const initTable = () => {
      return <ElTable
        data={tableData.value}
        style={{ width: "100%" }}>
      </ElTable>
    }

    return () => initTable()
  }
})

​

基础的文本渲染没有什么特殊的还是使用el-table-column组件就好

​
<ElTableColumn
    prop={col.prop}
    label={col.label}
    className={col.className}
    width={colWidth}
    min-width={minWidth}
    show-overflow-tooltip={showOverflowTooltip}
    fixed={fixed}
>
    {{ ...tableCellSlot }}
</ElTableColumn>

​

这里我就不列举官方的这些属性了,大家可以去官方文档查看: el-table-column属性

下面我们看看如何动态渲染不同类型的table-column

我们在el-table中写如下代码:利用数组的map去生成el-table-column组件,在map中我们来判断当前的column是什么类型的 然后动态的返回就好。

​
      {
          columns.map((col: any) => {
            const colWidth = col.width || '';
            const minWidth = col.minWidth || '';
            const fixed = col.fixed || false;
            const showOverflowTooltip = col.showOverflowTooltip || false
            const length = col.options && col.options.length || null
            const headerSlot = genHeaderSlot(col); // 自定义表头
            const defaultSlot = genDefaultSlot(col); // 自定义表格单元格
            const tableCellSlot = {
              ...headerSlot,
              ...defaultSlot,
            }
            // 序号
            if (col.prop === 'index') {
              return (<ElTableColumn
                label={col.label}
                type="index"
                className={col.className}
                width={55}
              />);
            }

            switch (col.type) {
              // 操作栏
              case 'options':
                return (
                  <ElTableColumn
                    prop={col.prop}
                    label={col.label}
                    className={col.className}
                    width={colWidth || (length > 3 ? '180px' : '160px')}
                    min-width={minWidth}
                    show-overflow-tooltip={showOverflowTooltip}
                    fixed={fixed}
                    formatter={(row) => {
                      let btns, moreBtn
                      if (length > 3) {
                        btns = col.options.slice(0, 2)
                        moreBtn = col.options.slice(2, length)
                      } else {
                        btns = col.options
                      }
                      return length > 3 ? renderOptionsAndMore(btns, moreBtn, row) : renderOptions(btns, row)
                    }}
                  >
                    {{ ...tableCellSlot }}
                  </ElTableColumn>
                );
              // 枚举类型的表格单元格
              case 'map':
                if (col.typeMap) { // 状态枚举
                  return (<ElTableColumn
                    prop={col.prop}
                    label={col.label}
                    className={col.className}
                    width={colWidth}
                    min-width={minWidth}
                    show-overflow-tooltip={showOverflowTooltip}
                    fixed={fixed}
                    formatter={(row): any => {
                      return renderTableMapCell(row, col);
                    }}
                  >
                    {{ ...tableCellSlot }}
                  </ElTableColumn>);
                } else { // 普通枚举
                  return (<ElTableColumn
                    prop={col.prop}
                    label={col.label}
                    className={col.className}
                    width={colWidth}
                    min-width={minWidth}
                    show-overflow-tooltip={showOverflowTooltip}
                    fixed={fixed}
                    formatter={(row) => {
                      return col.map[row[col.prop]];
                    }}
                  >
                    {{ ...tableCellSlot }}
                  </ElTableColumn>);
                }
              default:
                return (
                  <ElTableColumn
                    prop={col.prop}
                    label={col.label}
                    className={col.className}
                    width={colWidth}
                    min-width={minWidth}
                    show-overflow-tooltip={showOverflowTooltip}
                    fixed={fixed}
                  >
                    {{ ...tableCellSlot }}
                  </ElTableColumn>
                );
            }
          })
        }

​

index类型就不过多解释了,来看看column中的options类型,也就是表格的操作栏,

操作栏动态计算高度或者通过传入的宽度来设置宽度,这里的按钮我们通过 options 类型中的options数组来渲染

​
    const buttonEvents = (v: any, row: any) => {
      if (v.clickEvent) {
        v.clickEvent(row)
      } else {
        alert('请传入clickEvent事件参数')
      }
    }
    const renderButton = (v: any, row: any) => {
      return <ElButton
        text
        type={v.type}
        disabled={v.disabled && v.disabled(row)}
        class={v.className}
        style={{ padding: '5px' }}
        onClick={() => buttonEvents(v, row)}
      >
        {v.text}
      </ElButton>
    }
    // 渲染按钮
    const renderOptions = (btns: any, row: any) => {
      return btns && btns.map((v: any) => {
        return renderButton(v, row)
      })
    }
    // 渲染更多按钮
    const renderOptionsAndMore = (btns: any, moreBtn: any, row: any) => {
      return (<div>
        {renderOptions(btns, row)}
        {
          <ElDropdown v-slots={{
            dropdown: () => <ElDropdownMenu>
              {moreBtn.map((v: any) => {
                return <ElDropdownItem>
                  {renderButton(v, row)}
                </ElDropdownItem>
              })}
            </ElDropdownMenu>
          }}>
            <ElButton style={{ marginLeft: '10px', padding: '5px' }} type="primary" text>
              更多<ElIcon><ArrowDownBold /></ElIcon>
            </ElButton>
          </ElDropdown>
        }
      </div>)
    }

​

除了options类型还有枚举和插槽这两种形式,插槽的就相对简单了。

​
     // 根据自定义表头的配置,动态生成需要的scopedSlot对象
    const genHeaderSlot = (col: any) => {
      if (col.headerSlot) {
        return {
          header: () => {
            return slots.headerSlot && slots.headerSlot();
          }
        };
      }

      return {};
    }
    // 自定义表格单元格的slot显示
    const genDefaultSlot = (col: any) => {
      if (col.slot) {
        return {
          // scope 是当前渲染单元格的数据
          default: (scope: any) => {
            return slots[col.slot] && slots[col.slot](scope);
          }
        }
      }
    }
    
    
    在table-column 中我们这样使用的
    
    const headerSlot = genHeaderSlot(col); // 自定义表头
    const defaultSlot = genDefaultSlot(col); // 自定义表格单元格
    const tableCellSlot = {
        ...headerSlot,
        ...defaultSlot,
    }

<ElTableColumn
    prop={col.prop}
    label={col.label}
>
    {{ ...tableCellSlot }} //这里展开插槽的对象默认渲染的 是 default: () => col.slot()
</ElTableColumn>

​

枚举的形式我们以普通的和带徽标的为例,枚举的我们要额外接收两个参数,map和typeMap,map为当前数据所要映射的内容,typeMap为带徽标组件的类型'primary' | 'success' | 'warning' | 'danger' | 'info' 你们也可以自行拓展其他的组件,如tag等展示型组件

​
    // 渲染 type: map 的单元格
    const renderTableMapCell = (row: any, col: any) => {
      if (col.map[row[col.prop]]) {
        return <ElBadge
          is-dot={true}
          type={col.typeMap[row[col.prop]]}
          dot-position="left-middle"
        >
          {col.map[row[col.prop]]}
        </ElBadge>;
      }

      return null;
    }


​

到这里我们基础的使用列已经封装好了,下面我们来看看具体的使用方式。

使用方法

可以全局引入注册全局组件,也可以在单独页面中引入。这里我们看下全局组件

​
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'

import router from './router/index'

import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
// 引入表格组件
import MiJiTable from './components/mijiTable'

const app = createApp(App)
// 这里通过app.component注册表格组件为全局组件
app.component(MiJiTable.name, MiJiTable)

app.use(router).use(ElementPlus).mount('#app')

​

这里MiJiTable.name就是组件中的name属性。miji-table

这样我们在页面中就可以直接使用<miji-table></miji-table>

​
<template>
  <miji-table :columns="columns" :dataSource="dataSource"></miji-table>
</template>

<script setup lang="ts">
import { onMounted, ref } from "vue";
// 我们初始化的时候定义一个columns 定义好要渲染表格的列有哪些。
const columns = ref([
  { prop: "index", label: "序号", width: "80px" },
  { prop: "date", label: "Date" },
  { prop: "name", label: "Name" },
  {
    prop: "state",
    label: "State",
  },
  { prop: "city", label: "City", minWidth: "120px" },
  { prop: "address", label: "Address", minWidth: "120px" },
  { prop: "zip", label: "Zip", minWidth: "120px" },
  { prop: "tag", label: "Tag", minWidth: "120px" },
  {
    prop: "option",
    label: "操作",
    fixed: "right",
    type: "options",
    options: [
      {
        type: "primary",
        text: "查看",
        clickEvent: (row) => optionsFunction(row),
      }
    ],
  },
]);
// 定义 表格数据的变量
const dataSource = ref([]);

const optionsFunction = (row) => {
  console.log(row);
};

onMounted(() => {
  setTimeout(() => {
    dataSource.value = [] // 这里做请求数据填充
  }, 1000);
});
</script>

​

完整代码:如下

​
import { watch, defineComponent, ref } from 'vue'
import {
  ElTable,
  ElTableColumn,
  ElButton,
  ElDropdown,
  ElDropdownMenu,
  ElDropdownItem,
  ElBadge,
  ElIcon
} from 'element-plus'

import { ArrowDownBold } from '@element-plus/icons-vue'

import { tableProps } from './props'

export default defineComponent({
  props: tableProps,
  setup(props, { slots }: any) {
    const { columns, dataSource } = props
    console.log('表格数据:', dataSource);
    const tableData = ref()
    watch(() => props.dataSource, (now) => {
      tableData.value = now
    })

    const buttonEvents = (v: any, row: any) => {
      if (v.clickEvent) {
        v.clickEvent(row)
      } else {
        alert('请传入clickEvent事件参数')
      }
    }
    const renderButton = (v: any, row: any) => {
      return <ElButton
        text
        type={v.type}
        disabled={v.disabled && v.disabled(row)}
        class={v.className}
        style={{ padding: '5px' }}
        onClick={() => buttonEvents(v, row)}
      >
        {v.text}
      </ElButton>
    }
    // 渲染按钮
    const renderOptions = (btns: any, row: any) => {
      return btns && btns.map((v: any) => {
        return renderButton(v, row)
      })
    }
    // 渲染更多按钮
    const renderOptionsAndMore = (btns: any, moreBtn: any, row: any) => {
      return (<div>
        {renderOptions(btns, row)}
        {
          <ElDropdown v-slots={{
            dropdown: () => <ElDropdownMenu>
              {moreBtn.map((v: any) => {
                return <ElDropdownItem>
                  {renderButton(v, row)}
                </ElDropdownItem>
              })}
            </ElDropdownMenu>
          }}>
            <ElButton style={{ marginLeft: '10px', padding: '5px' }} type="primary" text>
              更多<ElIcon><ArrowDownBold /></ElIcon>
            </ElButton>
          </ElDropdown>
        }
      </div>)
    }

    // 渲染 type: map 的单元格
    const renderTableMapCell = (row: any, col: any) => {
      if (col.map[row[col.prop]]) {
        return <ElBadge
          is-dot={true}
          type={col.typeMap[row[col.prop]]}
          dot-position="left-middle"
        >
          {col.map[row[col.prop]]}
        </ElBadge>;
      }

      return null;
    }

    // 根据自定义表头的配置,动态生成需要的scopedSlot对象
    const genHeaderSlot = (col: any) => {
      if (col.headerSlot) {
        return {
          header: () => {
            return slots.headerSlot && slots.headerSlot();
          }
        };
      }

      return {};
    }
    // 自定义表格单元格的slot显示
    const genDefaultSlot = (col: any) => {
      if (col.slot) {
        return {
          // scope 是当前渲染单元格的数据
          default: (scope: any) => {
            return slots[col.slot] && slots[col.slot](scope);
          }
        }
      }
    }

    const initTable = () => {
      return <ElTable
        data={tableData.value}
        style={{ width: "100%" }}>
        {
          columns.map((col: any) => {
            const colWidth = col.width || '';
            const minWidth = col.minWidth || '';
            const fixed = col.fixed || false;
            const showOverflowTooltip = col.showOverflowTooltip || false
            const length = col.options && col.options.length || null
            const headerSlot = genHeaderSlot(col); // 自定义表头
            const defaultSlot = genDefaultSlot(col); // 自定义表格单元格
            const tableCellSlot = {
              ...headerSlot,
              ...defaultSlot,
            }
            // 序号
            if (col.prop === 'index') {
              return (<ElTableColumn
                label={col.label}
                type="index"
                className={col.className}
                width={55}
              />);
            }

            switch (col.type) {
              // 操作栏
              case 'options':
                return (
                  <ElTableColumn
                    prop={col.prop}
                    label={col.label}
                    className={col.className}
                    width={colWidth || (length > 3 ? '180px' : '160px')}
                    min-width={minWidth}
                    show-overflow-tooltip={showOverflowTooltip}
                    fixed={fixed}
                    formatter={(row) => {
                      let btns, moreBtn
                      if (length > 3) {
                        btns = col.options.slice(0, 2)
                        moreBtn = col.options.slice(2, length)
                      } else {
                        btns = col.options
                      }
                      return length > 3 ? renderOptionsAndMore(btns, moreBtn, row) : renderOptions(btns, row)
                    }}
                  >
                    {{ ...tableCellSlot }}
                  </ElTableColumn>
                );
              // 枚举类型的表格单元格
              case 'map':
                if (col.typeMap) { // 状态枚举
                  return (<ElTableColumn
                    prop={col.prop}
                    label={col.label}
                    className={col.className}
                    width={colWidth}
                    min-width={minWidth}
                    show-overflow-tooltip={showOverflowTooltip}
                    fixed={fixed}
                    formatter={(row): any => {
                      return renderTableMapCell(row, col);
                    }}
                  >
                    {{ ...tableCellSlot }}
                  </ElTableColumn>);
                } else { // 普通枚举
                  return (<ElTableColumn
                    prop={col.prop}
                    label={col.label}
                    className={col.className}
                    width={colWidth}
                    min-width={minWidth}
                    show-overflow-tooltip={showOverflowTooltip}
                    fixed={fixed}
                    formatter={(row) => {
                      return col.map[row[col.prop]];
                    }}
                  >
                    {{ ...tableCellSlot }}
                  </ElTableColumn>);
                }
              default:
                return (
                  <ElTableColumn
                    prop={col.prop}
                    label={col.label}
                    className={col.className}
                    width={colWidth}
                    min-width={minWidth}
                    show-overflow-tooltip={showOverflowTooltip}
                    fixed={fixed}
                  >
                    {{ ...tableCellSlot }}
                  </ElTableColumn>
                );
            }
          })
        }
      </ElTable>
    }
    return () => initTable()
  }
})

​

以上就是常规功能表格的二次封装,下篇文章我会在这个基础上封装一个常规后台页面级别的组件,

欢迎大家私信留言,多多点评

可以扫描下方二维码关注我的公众号或添加我的微信联系、沟通。

<think>好的,我现在需要解决用户在使用Vue3ElementPlus和PrintJS打印多页数据时,只有从第二页开始渲染的问题。首先,我得理解问题的具体情况。用户可能在打印时,第一页的内容没有正确显示,而第二页及之后的内容正常。这可能与分页处理或打印样式有关。 首先,我需要考虑PrintJS的工作原理。PrintJS通常会将指定的DOM元素内容转换为打印页面。如果分页处理不当,可能导致第一页的内容被截断或未正确加载。可能的原因包括CSS分页设置不正确,或者动态加载的数据在打印时未完全渲染到第一页。 接下来,用户提到的ElementPlus组件,比如el-table,可能在处理大量数据时使用了虚拟滚动或其他优化技术,这可能会影响打印时的DOM结构。例如,虚拟滚动可能只渲染可见区域的内容,而打印时需要所有数据都渲染出来。因此,在打印前需要确保所有数据都被正确渲染到DOM中,而不仅仅是当前可视区域的部分。 参考引用[2]中提到,ElementPlus的el-table二次封装组件新增了虚拟滚动功能。虚拟滚动可能会延迟渲染非可见区域的数据,导致打印时第一页的数据未被完全加载。因此,解决方案可能需要临时禁用虚拟滚动,或在打印前强制渲染所有数据。 另一个可能的问题是CSS的分页控制。在打印样式中,可能需要使用page-break-before或page-break-after属性来控制分页。如果这些样式未正确应用,可能导致第一页内容为空,而数据从第二页开始。需要检查打印时的CSS样式,确保分页逻辑正确。 此外,PrintJS的配置也可能影响结果。例如,如果打印配置中设置了延迟或异步加载,可能在打印时第一页的数据还未准备好。需要确保在调用PrintJS之前,所有数据已经渲染完成,并且DOM已经更新。 可能的解决方案步骤包括: 1. 在打印前禁用虚拟滚动,确保所有数据渲染到DOM中。 2. 调整CSS分页属性,确保内容正确分页。 3. 使用Vue的nextTick或setTimeout确保DOM更新后再执行打印。 4. 检查PrintJS的配置,确保正确处理分页和样式。 另外,参考引用[1]中提到处理大量数据时的卡顿问题,解决方案可能涉及优化渲染性能。但在打印场景下,可能需要暂时牺牲性能以确保所有数据渲染,因此可以临时关闭虚拟滚动,打印完成后再恢复。 综上所述,我需要指导用户如何在这些方面进行调整,比如修改CSS样式、处理虚拟滚动、确保DOM更新后打印等。同时,可能需要提供代码示例,比如如何在打印前操作虚拟滚动的启用状态,或添加打印专用的CSS样式。</think>针对Vue3中使用ElementPlus和PrintJS打印多页数据时出现的第二页开始渲染的问题,以下是分步解决方案: --- ### 一、问题根源分析 1. **虚拟滚动未完全渲染** Element-Plus的表格组件(如`el-table`)在数据量大时可能开启虚拟滚动优化,导致打印时仅渲染可视区域内容[^2] 2. **CSS分页控制缺失** 未正确设置打印分页样式导致内容切割异常 3. **异步渲染未完成** 打印时DOM更新未完成即触发打印操作 --- ### 二、解决方案步骤 #### 步骤1:强制完整渲染数据 ```vue <script setup> import { ref, nextTick } from &#39;vue&#39; // 临时禁用虚拟滚动 const disableVirtualScroll = () => { document.querySelectorAll(&#39;.el-table__body-wrapper&#39;).forEach(el => { el.style.overflowY = &#39;visible&#39; // 关闭滚动条 el.style.height = &#39;auto&#39; // 取消固定高度 }) } // 打印前处理 const handlePrint = async () => { await nextTick() // 等待DOM更新 disableVirtualScroll() // 延迟确保渲染完成 setTimeout(() => { printJS({ printable: &#39;print-area&#39;, type: &#39;html&#39;, css: &#39;@media print { @page { size: auto; margin: 20mm 0; } }&#39; // 自定义打印样式 }) // 打印后恢复虚拟滚动(可选) // restoreVirtualScroll() }, 500) } </script> ``` #### 步骤2:添加打印专用CSS样式 ```css /* 打印分页控制 */ @media print { .print-page { page-break-after: always; break-after: page; } /* 隐藏非必要元素 */ .no-print { display: none !important; } /* 表格完整显示 */ .el-table__body-wrapper { overflow: visible !important; height: auto !important; } } ``` #### 步骤3:优化模板结构 ```html <template> <div id="print-area"> <div v-for="(page, index) in pages" :key="index" class="print-page"> <el-table :data="page.data" style="width: 100%"> <!-- 表格列定义 --> </el-table> <div class="page-footer">第 {{ index+1 }} 页</div> </div> </div> </template> ``` --- ### 三、关键配置说明 | 配置项 | 说明 | |----------------|--------------------------------------------------------------------| | `nextTick()` | 确保Vue完成DOM更新后再执行打印操作 | | `setTimeout` | 给予浏览器足够时间完成渲染(根据数据量调整延迟时间) | | `@media print` | 定义打印专用样式,建议设置`margin: 0`避免浏览器默认页边距导致内容截断[^1] | --- ### 四、补充优化建议 1. **分页预处理** 将大数据分割为多页数组: ```js const pageSize = 50; // 每页行数 const pages = computed(() => { return chunk(rawData.value, pageSize); }); ``` 2. **内存释放** 打印完成后主动清除临时DOM节点: ```js const cleanPrintArea = () => { const container = document.getElementById(&#39;print-area&#39;); container.innerHTML = &#39;&#39;; } ``` --- ### 五、典型错误排查 | 现象 | 解决方法 | |----------------------|------------------------------------| | 首页空白 | 检查`@media print`的`margin`设置 | | 表格列宽异常 | 添加`table-layout: fixed`样式 | | 分页位置错误 | 使用`page-break-inside: avoid` | | 图片未加载完成 | 添加`onload`事件监听延迟打印 | ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

糖葫芦加冰

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值