element-ui是国内最流行的的vue开源框架,el-table组件在element-ui整个框架中是最复杂、最重要的部分。其中涉及到的知识有JSX,render渲染函数,组件间的状态管理等等。出于好奇和挑战,在网上受教于el的源代码以及网上相关内容资料。完成了一个简单基础的table组件 就叫 sd-table吧。
支持功能: 自定义列、支持自定义插槽、支持自定义排序、支持全选、多选、分页回调、宽高样式等。
el-table 源码传送门:element: Element 是一套为开发者、设计师和产品经理准备的基于 Vue 2.0 的桌面端组件库,提供了配套设计资源,帮助你的网站快速成型。由饿了么公司前端团队开源。 - Gitee.com
1.效果及使用方法
1.1 使用方法:
使用方法如下,看看是不是特别熟悉?与 el-table是否类似呢?
<sd-table :data = "tableData"
ref="sdTable"
:width="'800'"
:height="'500'"
:total="1000"
:current-page="currentPage"
@selection-change="handleSelectionChange"
@loadMore = 'loadMore'
>
<sd-table-column type="selection" width="20">
</sd-table-column>
<sd-table-column width="100" label="solt列">
<template slot-scope="scope">
<img src="../assets/vip-icon-hui-5.png" style="width: 25px">
<span>{{ scope.row.name }}</span>
</template>
</sd-table-column>
<sd-table-column prop="name" label="姓名" width="200" sortable></sd-table-column>
<sd-table-column prop="date" label="日期" width="200" sortable></sd-table-column>
<sd-table-column prop="address" label="地址" ></sd-table-column>
</sd-table>
1.2效果图
2.代码
2.1 sd-table-body.js
sd-table-body 主要是渲染整体表格,使用render 函数 循环二维数组 遍历tr行 遍历 td列。
export default {
name:'sd-table-body',
computed:{
table(){
return this.$parent
}
},
props:{
store: {
require: true
}
},
methods:{
getBodyCellStyle(column){
return {
width: column.width + 'px',
'text-align': column.align
}
},
},
render(h) {
let tableData = this.table.tableData ;
let columns = this.store.getColumns() ;
let tableWidth = this.store.getTableWidth() ;
return (
<table cellspacing="0" border='0' cellpadding="0" style={{width: tableWidth + 'px'}}>
{tableData.map((row, index) =>
<tr class={['sd-table-tr',row.$checked?'sd-table-tr-choose':'']}>
{columns.map(column => <td class={'sd-table-body__td'} style={this.getBodyCellStyle(column)}>
<div>{column.renderCell(row, index)}</div>
</td>)}
</tr>)
}
</table>
)
}
}
2.2 sd-table-column.js
sd-table-column.js 此组件主要是渲染 列相关内容,判断type 是selection或 index 或者是自定义插槽。若type=“selection” 为多选框,type=“index” 为 行序号 自定义插槽则直接显示自定义插槽内容,插槽内容可以为组件或html。
代码如下:
let columnIdSeed = 0 ;
export default {
name: 'sd-table-column',
computed:{
tableColumnId(){
return 'columnId-' + (columnIdSeed++)
},
table(){
return this.$parent
},
},
props:{
label: String,
prop: String,
width: String,
align: String,
type: String,
sortable:{
type:Boolean,
default:false,
}
},
data(){
return{
column:{},
value_2: false,
}
},
methods:{
renderCell(data,index){
let curIndex = index + 1 ;
if(this.type ==='index') {
return curIndex ;
}
if(this.type ==='selection') {
this.column.renderCell = (data,index) => (
<div class="cell">
<input type="checkbox" checked={data.$checked} on-click={ ($event) => this.selectionChange($event, data) } id={index}/>
</div>
);
return ;
}
if(this.prop) return data[this.prop]
return this.$scopedSlots.default({
$index: curIndex,
row: data
})
},
selectionChange(event,row){
let isExist = false ;
if(row.$checked){
//row.$checked = !row.$checked
this.$set(row,'$checked',!row.$checked);
}else{
this.$set(row,'$checked',true);
//row.$checked = true ;
}
if(row.$checked){
this.table.selectRowData.forEach((item)=>{
if(item == row){
isExist = true ;
}
})
if(!isExist){
this.table.selectRowData.push(row) ;
}
}else{
for(let i = 0 ; i < this.table.selectRowData.length ; i ++){
if(this.table.selectRowData[i] == row){
this.table.selectRowData.splice(i,1);
}
}
}
this.table.$emit("selection-change",this.table.selectRowData) ;
},
test(event,data){
console.log(event.target.id);
}
},
watch:{
value_2(newVal){
console.log(newVal)
}
},
created(){
this.column = {
tableId: this.table.tableId,
columnId: this.tableColumnId,
label: this.label,
prop: this.prop,
originWidth: this.width,
width: 0 ,
slots: this.$slots.default,
align: this.align,
renderCell: this.renderCell,
type: this.type,
sortable:this.sortable,
}
this.table.store.insertColumn(this.column)
},
render(h){
return h('div', this.$slots.default) ;
}
}
2.3 sd-table-head.js
此组件是表头内容,组件拿到columns 这个表头数组,通过render函数内遍历可得到表头行,这里涉及到一个chekbox和排序。 不涉及复杂表头,复杂表头以后研究。
export default {
name: 'sd-table-head',
data(){
return{
columns:[],
// 实现排序功能
"asc-n": (a, b) => a - b,
"desc-n": (a, b) => b - a,
"asc-s": (a, b) => a.localeCompare(b),
"desc-s": (a, b) => b.localeCompare(a),
}
},
computed:{
table(){
return this.$parent
}
},
props:{
store:{
require: true
}
},
methods:{
getHeaderCellStyle(column){
return {
width: column.width + 'px'
}
},
handleSortClick(event, column, givenOrder) {
event.stopPropagation();
if (!column.sortable) return;
this.$set(this.table,'order',givenOrder);
console.log(this.table.order)
this.sort(givenOrder,column) ;
this.$emit('sort-change',givenOrder);
},
sort(type,column) {
// 是否排序
if (column.sortable) {
// 对父父亲元素进行排序
this.$parent.tableData.sort((a, b) => {
// 调用排序规则,传入排序方式asc、desc,然后判断数据类型进行拼接后调用方法实现排序
return this[type + this.getSortType(a[column.prop])](
a[column.prop],
b[column.prop]
);
});
}
},
selectAll(){
this.table.isSelectedAll = !this.table.isSelectedAll;
if(this.table.isSelectedAll ){
this.table.toggleRowSelection(this.table.tableData) ;
}else{
this.table.clearSelection();
}
},
// 处理排序类型
getSortType(val) {
// 返回标识串进行拼接
return typeof val === "string" ? "-s" : "-n";
},
},
created(){
this.columns = this.store.getColumns() ;
},
render(h) {
let columns = this.store.getColumns() ;
let tableWidth = this.store.getTableWidth() ;
return (
<table cellspacing="0" border="0" cellpadding="0" style={{width: tableWidth+'px'}}>
{
<tr>{columns.map(column=>
<td style={this.getHeaderCellStyle(column)}>
{
(column.type =='selection')?
(<input type='checkbox' checked={this.table.isSelectedAll} on-click={ ($event) => this.selectAll() }/>):
(column.label)
}
{
column.sortable ? (<span
class="caret-wrapper">
<i class={['sort-caret ascending',this.table.order=='asc'?'ascending ':'']}
on-click={ ($event) => this.handleSortClick($event, column, 'asc') }>
</i>
<i class={['sort-caret descending',this.table.order=='desc'?'descending':'']}
on-click={ ($event) => this.handleSortClick($event, column, 'desc') }>
</i>
</span>) : ''
}
</td>
)}
</tr>
}
</table>
);
},
}
2.4 sd-table-store.js
store 主要是保存列数据以及表的基本数据信息,方便以上各个组件初始化使用
export default class SdTableStore {
constructor(tableId) {
this.storeid = 'store-' + tableId ;
this.columns = [] ;
this.columnLabelMap = {} ;
this.realTableWidth = 0 ;
}
insertColumn (column){
this.columns.push(column) ;
this.columnLabelMap[column.columnId] = column.label
}
getColumns(){
return this.columns ;
}
updateTableWidth (width) {
this.realTableWidth = width ;
}
getTableWidth () {
return this.realTableWidth ;
}
}
2.5 util.js
此工具方法为了计算列宽。
function calcColumnWidth (columns, table) {
let bodyWidth = table.$el.clientWidth - 24
let tableWidth = 0
let flexColumns = []
flexColumns = columns.filter(column => {
if (typeof column.originWidth !== 'string') return column
})
for (let column of columns) {
column.width = column.originWidth || 80
tableWidth += parseInt(column.width)
}
// 宽度有富余
if (tableWidth < bodyWidth) {
// 富余宽度
let flexWidth = bodyWidth - tableWidth
let flexSpaceWidth = parseInt(flexWidth / flexColumns.length)
flexColumns[0].width += flexWidth - flexSpaceWidth * (flexColumns.length - 1)
for (let i = 1; i < flexColumns.length; i++) {
flexColumns[i].width += flexSpaceWidth
}
table.store.updateTableWidth(bodyWidth)
} else {
table.store.updateTableWidth(tableWidth)
}
return columns ;
}
export {calcColumnWidth}
2.6 sd-table.vue
此组件是以上组件的父组件,负责组织和调用以上组件。
代码如下:
<template>
<div class="sd-table"
:class="[{
'sd-table--border': border
}]"
:style="tableStyle">
<!-- 隐藏列 -->
<div class="hidden-columns"><slot></slot></div>
<div class="pos-relative">
<div class="header-warp" :ref="headRef">
<sd-table-head class="sd-table-head" :store="store"></sd-table-head>
</div>
<!-- 遮挡竖直滚动条 -->
<div class="hiddenBlock"></div>
</div>
<!-- <div v-loading="loadingFlag" class="pos-relative" :class="textAlign">-->
<div class="pos-relative" :class="textAlign">
<div :style="bodyWrapperStyle" class="body-wrap" :ref="bodyRef" @scroll="handleScroll">
<sd-table-body :store="store"></sd-table-body>
</div>
<div class="noData flex-midcenter" v-show="!data.length">暂无数据</div>
</div>
</div>
</template>
<script>
import SdTableStore from "./sd-table-store";
import SdTableHead from "./sd-table-head";
import SdTableBody from "./sd-table-body";
import {calcColumnWidth} from '@/utils/util';
let tableIdSeed = 0 ;
export default {
name: "sd-table",
data () {
return {
store: null,
curPage: 1,
allPages: 0,
bodyWrapperStyle: {
height: this.height+'px',
'text-align': this.align,
overflow: 'auto'
},
loadingFlag: true,
tableStyle: {
width: this.width
},
fixedHeight: 1,
tableData: [],
selectRowData:[],
isSelectedAll:false,
order:'asc',
}
},
computed: {
tableId () {
return 'tableId-' + (tableIdSeed++)
},
bodyRef () {
return 'tableBody-' + tableIdSeed
},
tBody () {
return this.$refs[this.bodyRef]
},
headRef () {
return 'tableHead-' + tableIdSeed
},
tHead () {
return this.$refs[this.headRef]
},
'textAlign' () {
return 'align-' + this.align
}
},
components:{
SdTableHead,
SdTableBody,
},
props: {
data: {
type: Array,
default () {
return []
}
},
width: {
type: String,
default: '100%'
},
total: {
type: [Number, String],
default: 0
},
align: {
type: String,
default: 'center'
},
height: {
type: String,
default: '300'
},
border: Boolean,
currentPage: {
type: Number,
default: 1
}
},
methods: {
initData () {
this.curPage = 1
this.tableData = []
this.allPages = Math.ceil(parseInt(this.total) / this.data.length)
//this.tBody.scrollTop = 0
},
handleScroll (e) {
this.tHead.scrollLeft = this.tBody.scrollLeft
// fixedHeight修正由水平滚动条带来的高度计算误差
if ((this.tBody.scrollTop + this.tBody.clientHeight + this.fixedHeight >= this.tBody.scrollHeight) && (this.curPage < this.allPages)) {
this.loadingFlag = true
this.$emit('loadMore', ++this.curPage)
}
},
toggleRowSelection(rows){
// 交集
this.tableData.forEach((item)=>{
rows.forEach((itm)=>{
if(item == itm){
this.$set(item,"$checked",true);
}
})
})
this.selectRowData = rows ;
console.log(this.selectRowData) ;
},
clearSelection(){
this.selectRowData = [] ;
this.isSelectedAll = false;
this.tableData.forEach((item)=>{
this.$set(item,"$checked",false);
})
},
doLayout () {
calcColumnWidth(this.store.columns, this)
}
},
watch: {
data: {
deep: true,
immediate: true,
handler (n, o) {
this.loadingFlag = false
if (this.currentPage === 1) {
this.initData()
}
this.tableData = [...this.tableData, ...n]
}
}
},
created () {
let store = new SdTableStore(this.tableId)
this.store = store;
},
mounted () {
if (!this.data.length) {
this.loadingFlag = false
}
let self = this;
self.doLayout()
window.addEventListener('resize', function () {
self.doLayout()
})
},
}
</script>
<style>
.sd-table{
width: 100%;
padding: 5px 20px 5px 20px;
display: flex;
flex-direction: column;
box-sizing: border-box;
}
.sd-table .header-warp{
width: 100%;
display: flex;
flex-direction: row;
align-items: center;
font-size: 12px;
padding: 10px 0 10px 0;
}
.body-wrap {
position: relative;
}
.sd-table-tr{
height: 60px;
background-color: white;
border-top: 1px solid #eeeeee;
display: flex;
align-items: center;
font-size: 14px;
transition: border-color .3s,background-color .3s,color .3s;
flex-shrink: 0;
}
.sd-table-tr:hover{
padding: 0 0 0 0;
background-color: #e8f8f7;
}
/**当选中,item背景状态**/
.sd-table-tr-choose{
padding: 0 0 0 0;
background-color: #e8f8f7 !important;
}
.checkboxshow{
visibility:visible !important;
}
.sd-table .item input[type=checkbox]{
visibility:visible;
}
.sd-table .item:hover .share{
display: block;
}
.sd-table .item:hover .itesd-time>span{
display: none;
}
.hiddenBlock {
position: absolute;
background: $head-bg;
width: 25px;
height: calc(100% - 1px);
top: 0;
right: 0;
border-bottom: $border;
}
.forbidden-child-pointer-events >* {
pointer-events: none;
}
.sd-table .caret-wrapper {
display: inline-flex;
flex-direction: column;
align-items: center;
height: 34px;
width: 24px;
vertical-align: middle;
overflow: initial;
position: relative;
}
.sort-caret.ascending {
border-bottom-color: #c0c4cc;
top: 5px;
}
.sort-caret-ascending {
border-bottom-color: #409eff;
}
.sort-caret.descending {
border-top-color: #409eff;
}
.sort-caret.descending {
border-top-color: #c0c4cc;
bottom: 7px;
}
.sort-caret {
width: 0;
height: 0;
border: 5px solid transparent;
position: absolute;
left: 7px;
}
</style>
2.6 test-table.vue
test-table.vue 用于调用测试表格使用。mbutton 为我自定义的组件,请自行去掉或替换。
我们可以看到基本与el-table 的使用用法一致。当然目前功能不完善,功能性不如element-ui。但是自定义插槽、多选排序以及分页功能是我们经常用到的,足够用了。
<template>
<div>
<sd-table :data = "tableData"
ref="sdTable"
:width="'800'"
:height="'500'"
:total="1000"
:current-page="currentPage"
@selection-change="handleSelectionChange"
@loadMore = 'loadMore'
>
<sd-table-column type="selection" width="20">
</sd-table-column>
<sd-table-column width="100" label="solt列">
<template slot-scope="scope">
<img src="../assets/vip-icon-hui-5.png" style="width: 25px">
<span>{{ scope.row.name }}</span>
</template>
</sd-table-column>
<sd-table-column prop="name" label="姓名" width="200" sortable></sd-table-column>
<sd-table-column prop="date" label="日期" width="200" sortable></sd-table-column>
<sd-table-column prop="address" label="地址" ></sd-table-column>
</sd-table>
<button style="width: 100px;height: 50px" @click="add">添加数据</button>
<button style="width: 100px;height: 50px" @click="figureChoose">指定选择</button>
<button style="width: 100px;height: 50px" @click="cancel">取消选择</button>
</div>
</template>
<script>
import SdTable from "../components/table/sd-table";
import SdTableColumn from "../components/table/sd-table-column";
import Mbutton from "../components/mbutton";
export default {
name: "test-table",
components: {Mbutton, SdTableColumn, SdTable},
data(){
return {
tableData: [
{
no: 0,
date: "2022-01-01",
name: "徐凤年",
address: "北凉国",
},
{
no: 1,
date: "2022-01-02",
name: "姜泥",
address: "楚国",
},
{
no: 2,
date: "2022-01-03",
name: "王仙芝",
address: "武帝城",
},
{
no: 3,
date: "2022-01-04",
name: "李淳罡",
address: "剑仙一键开天门",
}
]
,
currentPage: 1,
}
},methods:{
add(){
let one = {
no: 5,
date: "2022-01-05",
name: "呵呵姑娘",
address: "不详",
};
this.tableData.push(one);
},
figureChoose(){
this.$refs.sdTable.toggleRowSelection([this.tableData[1],this.tableData[2]])
},
handleSelectionChange(val){
console.log("handleSelectionChange",val);
},
cancel(){
this.$refs.sdTable.clearSelection();
console.log("cancel");
},loadMore(val){
console.log(val);
this.currentPage = val ;
}
}
}
</script>
<style scoped>
</style>
感谢阅读!