使用Vue3实现一个可复制的表格
创始人
2024-05-26 08:53:54

前言

表格是前端非常常用的一个控件,但是每次都使用v-for指令手动绘制tr/th/td这些元素是非常麻烦的。同时,基础的 table 样式通常也是不满足需求的,因此一个好的表格封装就显得比较重要了。

最基础的表格封装

最基础基础的表格封装所要做的事情就是让用户只关注行和列的数据,而不需要关注 DOM 结构是怎样的,我们可以参考 AntDesigncolumns dataSource 这两个属性是必不可少的,代码如下:

import { defineComponent } from 'vue'
import type { PropType } from 'vue'interface Column {title: string;dataIndex: string;slotName?: string;
}
type TableRecord = Record;export const Table = defineComponent({props: {columns: {type: Array as PropType,required: true,},dataSource: {type: Array as PropType,default: () => [],},rowKey: {type: Function as PropType<(record: TableRecord) => string>,}},setup(props, { slots }) {const getRowKey = (record: TableRecord, index: number) => {if (props.rowKey) {return props.rowKey(record)}return record.id ? String(record.id) : String(index)}const getTdContent = ( text: any,record: TableRecord,index: number,slotName?: string ) => {if (slotName) {return slots[slotName]?.(text, record, index)}return text}return () => {return ({props.columns.map(column => {const { title, dataIndex } = columnreturn })}{props.dataSource.map((record, index) => {return ({props.columns.map((column, i) => {const { dataIndex, slotName } = columnconst text = record[dataIndex]return ()})})})}
{title}
{getTdContent(text, record, i, slotName)}
)}} })

需要关注一下的是 Column 中有一个 slotName 属性,这是为了能够自定义该列的所需要渲染的内容(在 AntDesign 中是通过 TableColumn 组件实现的,这里为了方便直接使用 slotName)。

实现复制功能

首先我们可以手动选中表格复制尝试一下,发现表格是支持选中复制的,那么实现思路也就很简单了,通过代码选中表格再执行复制命令就可以了,代码如下:

export const Table = defineComponent({props: {// ...},setup(props, { slots, expose }) {// 新增,存储table节点const tableRef = ref(null)// ...// 复制的核心方法const copy = () => {if (!tableRef.value) returnconst range = document.createRange()range.selectNode(tableRef.value)const selection = window.getSelection()if (!selection) returnif (selection.rangeCount > 0) {selection.removeAllRanges()}selection.addRange(range)document.execCommand('copy')}// 将复制方法暴露出去以供父组件可以直接调用expose({ copy })return (() => {return (// ...)}) as unknown as { copy: typeof copy } // 这里是为了让ts能够通过类型校验,否则调用`copy`方法ts会报错}
}) 

这样复制功能就完成了,外部是完全不需要关注如何复制的,只需要调用组件暴露出去的 copy 方法即可。

处理表格中的不可复制元素

虽然复制功能很简单,但是这也仅仅是复制文字,如果表格中有一些不可复制元素(如图片),而复制时需要将这些替换成对应的文字符号,这种该如何实现呢?

解决思路就是在组件内部定义一个复制状态,调用复制方法时把状态设置为正在复制,根据这个状态渲染不同的内容(非复制状态时渲染图片,复制状态是渲染对应的文字符号),代码如下:

export const Table = defineComponent({props: {// ...},setup(props, { slots, expose }) {const tableRef = ref(null)// 新增,定义复制状态const copying = ref(false)// ...const getTdContent = ( text: any,record: TableRecord,index: number,slotName?: string,slotNameOnCopy?: string ) => {// 如果处于复制状态,则渲染复制状态下的内容if (copying.value && slotNameOnCopy) {return slots[slotNameOnCopy]?.(text, record, index)}if (slotName) {return slots[slotName]?.(text, record, index)}return text}const copy = () => {copying.value = true// 将复制行为放到 nextTick 保证复制到正确的内容nextTick(() => {if (!tableRef.value) returnconst range = document.createRange()range.selectNode(tableRef.value)const selection = window.getSelection()if (!selection) returnif (selection.rangeCount > 0) {selection.removeAllRanges()}selection.addRange(range)document.execCommand('copy')// 别忘了把状态重置回来copying.value = false})}expose({ copy })return (() => {return (// ...)}) as unknown as { copy: typeof copy }}
}) 

测试

最后我们可以写一个demo测一下功能是否正常,代码如下:

 

附上完整代码:

import { defineComponent, ref, nextTick } from 'vue'
import type { PropType } from 'vue'interface Column {title: string;dataIndex: string;slotName?: string;slotNameOnCopy?: string;
}
type TableRecord = Record;export const Table = defineComponent({props: {columns: {type: Array as PropType,required: true,},dataSource: {type: Array as PropType,default: () => [],},rowKey: {type: Function as PropType<(record: TableRecord) => string>,}},setup(props, { slots, expose }) {const tableRef = ref(null)const copying = ref(false)const getRowKey = (record: TableRecord, index: number) => {if (props.rowKey) {return props.rowKey(record)}return record.id ? String(record.id) : String(index)}const getTdContent = ( text: any,record: TableRecord,index: number,slotName?: string,slotNameOnCopy?: string ) => {if (copying.value && slotNameOnCopy) {return slots[slotNameOnCopy]?.(text, record, index)}if (slotName) {return slots[slotName]?.(text, record, index)}return text}const copy = () => {copying.value = truenextTick(() => {if (!tableRef.value) returnconst range = document.createRange()range.selectNode(tableRef.value)const selection = window.getSelection()if (!selection) returnif (selection.rangeCount > 0) {selection.removeAllRanges()}selection.addRange(range)document.execCommand('copy')copying.value = false})}expose({ copy })return (() => {return ({props.columns.map(column => {const { title, dataIndex } = columnreturn })}{props.dataSource.map((record, index) => {return ({props.columns.map((column, i) => {const { dataIndex, slotName, slotNameOnCopy } = columnconst text = record[dataIndex]return ()})})})}
{title}
{getTdContent(text, record, i, slotName, slotNameOnCopy)}
)}) as unknown as { copy: typeof copy }} })

最后

最近还整理一份JavaScript与ES的笔记,一共25个重要的知识点,对每个知识点都进行了讲解和分析。能帮你快速掌握JavaScript与ES的相关知识,提升工作效率。



有需要的小伙伴,可以点击下方卡片领取,无偿分享

相关内容

热门资讯

中国平安:拟于9月10日实施半...   9月2日金融一线消息,中国平安公告称,公司拟实施2026年半年度利润分配,以方案实施前总股本18...
临床前也值15亿美金?罗氏杀疯... (来源:关注行业动态的 医药地理)近日,先声药业集团旗下先声再明宣布已...
恒指公司:希音将快速纳入恒生综... 9月2日,恒生指数公司公告,希音-W符合快速纳入规则要求,将于9月14日收市后被纳入恒生综合指数及其...
【电视栏目】《聚焦文圣》第19... (来源:幸福文圣)本周内容提要:1.全省移风易俗小戏小品优秀作品巡演暨2026年辽阳市文化科技卫生“...
欢喜传媒:8月股份无变动 符合... 欢喜传媒公告称,截至2026年8月31日,公司法定/注册股本为5.00亿港元,已发行普通股43.84...