前言
表格是前端非常常用的一个控件,但是每次都使用v-for
指令手动绘制tr/th/td这些元素是非常麻烦的。同时,基础的 table
样式通常也是不满足需求的,因此一个好的表格封装就显得比较重要了。
最基础的表格封装
最基础基础的表格封装所要做的事情就是让用户只关注行和列的数据,而不需要关注 DOM
结构是怎样的,我们可以参考 AntDesign
,columns
dataSource
这两个属性是必不可少的,代码如下:
import { defineComponent } from 'vue'
import type { PropType } from 'vue'
interface Column {title: string;dataIndex: string;slotName?: string;
}
type TableRecord = Record<string, unknown>;
export const Table = defineComponent({props: {columns: {type: Array as PropType<Column[]>,required: true,},dataSource: {type: Array as PropType<TableRecord[]>,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 (<table><tr>{props.columns.map(column => {const { title, dataIndex } = columnreturn <th key={dataIndex}>{title}</th>})}</tr>{props.dataSource.map((record, index) => {return (<tr key={getRowKey(record, index)}>{props.columns.map((column, i) => {const { dataIndex, slotName } = columnconst text = record[dataIndex]return (<td key={dataIndex}>{getTdContent(text, record, i, slotName)}</td>)})}</tr>)})}</table>)}}
})
需要关注一下的是 Column
中有一个 slotName
属性,这是为了能够自定义该列的所需要渲染的内容(在 AntDesign
中是通过 TableColumn
组件实现的,这里为了方便直接使用 slotName
)。
实现复制功能
首先我们可以手动选中表格复制尝试一下,发现表格是支持选中复制的,那么实现思路也就很简单了,通过代码选中表格再执行复制命令就可以了,代码如下:
export const Table = defineComponent({props: {// ...},setup(props, { slots, expose }) {// 新增,存储table节点const tableRef = ref<HTMLTableElement | null>(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<HTMLTableElement | null>(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测一下功能是否正常,代码如下:
<template><button @click="handleCopy">点击按钮复制表格</button><c-table:columns="columns":data-source="dataSource"border="1"style="margin-top: 10px;"ref="table"><template #status><img class="status-icon" :src="arrowUpIcon" /></template><template #statusOnCopy>→</template></c-table>
</template>
<script setup lang="ts"> import { ref } from 'vue'
import { Table as CTable } from '../components'
import arrowUpIcon from '../assets/arrow-up.svg'
const columns = [{ title: '序号', dataIndex: 'serial' },{ title: '班级', dataIndex: 'class' },{ title: '姓名', dataIndex: 'name' },{ title: '状态', dataIndex: 'status', slotName: 'status', slotNameOnCopy: 'statusOnCopy' }
]
const dataSource = [{ serial: 1, class: '三年级1班', name: '张三' },{ serial: 2, class: '三年级2班', name: '李四' },{ serial: 3, class: '三年级3班', name: '王五' },{ serial: 4, class: '三年级4班', name: '赵六' },{ serial: 5, class: '三年级5班', name: '宋江' },{ serial: 6, class: '三年级6班', name: '卢俊义' },{ serial: 7, class: '三年级7班', name: '吴用' },{ serial: 8, class: '三年级8班', name: '公孙胜' },
]
const table = ref<InstanceType<typeof CTable> | null>(null)
const handleCopy = () => {table.value?.copy()
} </script>
<style scoped> .status-icon {width: 20px;height: 20px;
} </style>
附上完整代码:
import { defineComponent, ref, nextTick } from 'vue'
import type { PropType } from 'vue'
interface Column {title: string;dataIndex: string;slotName?: string;slotNameOnCopy?: string;
}
type TableRecord = Record<string, unknown>;
export const Table = defineComponent({props: {columns: {type: Array as PropType<Column[]>,required: true,},dataSource: {type: Array as PropType<TableRecord[]>,default: () => [],},rowKey: {type: Function as PropType<(record: TableRecord) => string>,}},setup(props, { slots, expose }) {const tableRef = ref<HTMLTableElement | null>(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 (<table ref={tableRef}><tr>{props.columns.map(column => {const { title, dataIndex } = columnreturn <th key={dataIndex}>{title}</th>})}</tr>{props.dataSource.map((record, index) => {return (<tr key={getRowKey(record, index)}>{props.columns.map((column, i) => {const { dataIndex, slotName, slotNameOnCopy } = columnconst text = record[dataIndex]return (<td key={dataIndex}>{getTdContent(text, record, i, slotName, slotNameOnCopy)}</td>)})}</tr>)})}</table>)}) as unknown as { copy: typeof copy }}
})
最后
最近还整理一份JavaScript与ES的笔记,一共25个重要的知识点,对每个知识点都进行了讲解和分析。能帮你快速掌握JavaScript与ES的相关知识,提升工作效率。
有需要的小伙伴,可以点击下方卡片领取,无偿分享