特性:
- 数据驱动,省去一个一个写
el-table-column
标签和属性的烦恼,表格的表头可以存到后端。 - 支持通过拖拽改变列的位置
- 支持通过拖拽改变行的位置
- 支持loading自定义
- 支持具名插槽语法
- 支持可选的复选框列和序号列
- 支持自定义每一列的显示和隐藏,以及是否允许隐藏
- 拖拽的实现依赖于
sortablejs
库
表格使用方式:
import TrsTable from '@/components/normal/TrsTable.vue';
<trs-table
:table="myTable"
:table-label="myTableLabel"
:table-data="items"
@sort="sortByScore"
@lableChanged="lableChanged"
@lableOrder="lableOrdered"
>
<template #trueName="{ scope }">
<a
class="userName"
href="javascript:void(0)"
@click.stop="openDrawer(scope)"
>{{ scope.row.trueName || '--' }}</a>
</template>
<template #position="{ scope }">
{{ scope.row.position&&isJSON(scope.row.position)?JSON.parse(scope.row.position).name:scope.row.position||'--' }}
</template>
<template #percentage="{ scope }">
<el-progress
v-if="scope.row.percentage&&(scope.row.percentage===0 || scope.row.percentage>0)"
:class="{'yydh-large':scope.row.monthlyTaskVolume>0&&scope.row.monthlyKPI > scope.row.monthlyTaskVolume}"
:color="(scope.row.monthlyTaskVolume>0&&scope.row.monthlyKPI>scope.row.monthlyTaskVolume)?colors2:colors"
:percentage="(scope.row.monthlyTaskVolume>0&&scope.row.monthlyKPI>scope.row.monthlyTaskVolume)?Math.ceil(scope.row.monthlyTaskVolume/scope.row.monthlyKPI):Math.ceil(scope.row.percentage*100)||0"
:format="formatlarge(scope.row)"
>
</el-progress>
<div class="contrast rt">{{ scope.row.monthlyKPI || '0' }}/{{ scope.row.monthlyTaskVolume || '0' }}</div>
</template>
</trs-table>
表格组件属性说明
table
table
:表格配置-对象(非必传)
属性 | 类型 | 说明 | 是否必传 | 默认值 |
---|---|---|---|---|
select | Boolean | 是否显示复选框 | 否 | false |
type | Boolean | 是否显示序号列 | 否 | false |
strip | Boolean | 是否显示斑马纹 | 否 | false |
height | String | 表格高度 | 否 | 40px |
labelToogle | Boolean | 能否配置表头各列的显示隐藏 | 否 | false |
rowKey | String | 数据对象主键 | 是 | ‘’ |
colDragble | Boolean | 列是否能拖拽改变顺序 | 否 | false |
rowDragble | Boolean | 行是否能拖拽改变顺序 | 否 | false |
ref | String | 表格的全局引用名称 | 是 | undefined |
loading | Boolean | 表格是否处于loading状态 | 否 | 否 |
loadingText | String | 表格加载loading时的提示文本 | 否 | ‘加载中’ |
typeText | String | 序号列表头文本 | 否 | ‘序号’ |
align | String | 表格对齐方式 | 否 | ‘left’ |
table-label
table-label
: 表头字段配置-数组对象(必传,不可为空)
属性 | 类型 | 说明 | 是否必传 | 默认值 |
---|---|---|---|---|
title | String | 列名称 | 是 | |
fiexd | String | 列固定方向,left | right | |
sort | String | 字段排序设置 | 否 | |
width | String | 列宽度 | 否 | |
prop | String | 列对应表格数据中的字段 | 是 | |
formatter | Function | 自定义格式化方法,参数为当前行与列function(row, column) => {…} | 否 | |
ellipsis | Boolean | 是否文本溢出并在hover时tooltip显示完整文本 | 否 | |
align | String | 列数据对齐方式 | 否 | ‘left’ |
headerAlign | String | 列表头对齐方式 | 否 | 如果不传,则尝试使用列的align设置,如果列的align也没传,尝试使用表格的align设置,如果表格的align也没设置,则默认为’left’; |
visible | Boolean | 是否显示当前列 | 否 | true |
canHide | Boolean | 是否允许隐藏 | 否 | false |
slot | String | 具名插槽名称 | 否 | 不传则采用formatter函数显示数据,如果没有传formatter函数,则直接显示表格数据中的值。 |
示例:
[
{
title: '姓名',// 列名
prop: 'trueName', // 字段名
width: '180', // 规定列宽
ellipsis: true, // 内容超出列宽是否显示省略符号并进行tooltip提示
slot: 'trueName', // 具名插槽名称
align: 'center', // 内容对齐方式
sort: 'custom', // 字段排序设置
formatter: (row, column) => {
return row.monthlySpreadPoint ?? '--';
}, // 内容格式化方法,如果该函数和插槽同时存在,则优先使用该格式化方法
visible: true, // 是否显示该列
order: 1, // 排序序号,按照列从左至右,数字从小到大
headerAlign: 'center' // 该列表头对齐方式,如果不传,与align一致,如未传align,则与表格的对齐一致,如果表格对齐未传,则默认居左
canHide: true, // 是否可以隐藏该列,默认不传则为不可隐藏
fixed: 'left', // 将该列固定在左侧(left)或右侧(right),默认不传则不固定。
},
],
table-data
表格数据, 必传
事件说明
事件名称 | 说明 | 回调参数 |
---|---|---|
sort | 可排序字段表头被点击时的处理方法 | 参数同el-table 的 sort-change |
labelChange | 头字段可见性配置发生变化时的处理方法 | 参数是当前表头配置数组 |
handleSelectionChange | 当前表格数据勾选项发生变化时的处理方法 | 参数同el-table 的selection-change |
colChange | 列拖拽排序后的处理方法 | 拖拽后的列配置 |
rowChange | 行拖动后的处理方法 | 源位置,目标位置 |
表格插槽使用说明
在使用el-table
时,通常我们是像如下方式来使用列插槽的:
<template
slot-scope="scope"
>
<a
class="userName"
href="javascript:void(0)"
@click.stop="openDrawer(scope)"
>{{ scope.row.trueName || '--' }}</a>
</template>
而在trs-table
中,我们使用具名插槽代替了上面的作用域插槽的写法:
<template #trueName="{ scope }">
<a
class="userName"
href="javascript:void(0)"
@click.stop="openDrawer(scope)"
>{{ scope.row.trueName || '--' }}</a>
</template>
可以发先,唯一的不同就是template
标签中,作用域插槽是使用slot-scope
属性来将当前作用域传入插槽的,而具名插槽则是同时#slotName
形式将作用域传入插槽的。
表头可见性配置功能说明
- 可在此处配置列的可见性,勾选的为可见,不勾选的不可见
- 点击此按钮切换配置功能的显示/隐藏,如果在进行取消勾选/勾选操作后,没有点击确定按钮,就点击了该按钮,则在隐藏配置功能同时恢复所有项为打开配置功能时的勾选状态
- 如果在进行取消勾选/勾选操作后,没有点击确定按钮,就点击了该按钮,则在隐藏配置功能同时恢复所有项为打开配置功能时的勾选状态点击此按钮隐藏配置功能,
- 点击确定按钮后,才更新表格个字段的可见性。并且会向父组件吐出一个``lableChanged
事件,如果需要将配置保存到服务端,可在该事件中处理。
表头拖拽排序功能说明
- 可通过用鼠标拖拽列的表头类改变列在表格中的位置
- 可通过用鼠标拖拽行改变数据在表格中的垂直方向的次序。
- 拖拽列时如果有滚动条,拖动到接近左右滚动条滚动位置时,滚动条会自动滚动
- 固定在左侧或右侧的列无法拖拽,无法改变顺序,始终位于最左面或最右面
- 拖动改变顺序后,可见性配置功能中字段的位置也会发生相应变化。
源码
<template>
<div class="swdTable">
<div class="trs-table">
<el-table
:ref="table.ref"
v-loading="table.loading"
:data="tableDataCopy"
:row-key="table.rowKey"
tooltip-effect="dark"
style="width: 100%;"
:stripe="table.strip"
:height="table.height || 40"
:header-cell-style="{
'background-color': '#fafafa'
}"
:element-loading-text="table.loadingText||'加载中'"
@sort-change="handleTableSort"
@selection-change="handleSelectionChange"
>
<el-table-column v-if="table.select" type="selection" width="55"></el-table-column>
<el-table-column v-if="table.type" :label="table.typeText||'序号'" width="60" type="index" align="center"></el-table-column>
<el-table-column
v-for="(col, index) in tableLableShow"
:key="index"
:fixed="col.fixed"
:label="col.title"
:width="tableLableShow1[index].width"
:sortable="col.sort"
:prop="tableLableShow1[index].prop"
:formatter="col.formatter"
:show-overflow-tooltip="col.ellipsis"
:align="col.align"
:column-key="index.toString()"
:header-align="col.headerAlign || col.align || table.align || 'left'"
>
<template slot-scope="scope">
<slot v-if="tableLableShow1[index].slot" :name="tableLableShow1[index].slot" :scope="scope">
</slot>
<span v-else>{{ tableLableShow1[index].formatter? tableLableShow1[index].formatter(scope.row, col) : (scope.row[tableLableShow1[index].prop] || '--') }}</span>
</template>
</el-table-column>
</el-table>
<div v-if="table.labelToggle" ref="trsTableToggle" class="label-toggle">
<span class="lable-toggle-switch" @click="toggleBoxSwitch"><i class="iconfont icon-iconfontcaidan"></i></span>
<div v-if="labelToggle" class="label-toogle-box">
<ul>
<li v-for="(col, index) in tableLableToggle" :key="index">
<el-checkbox
v-model="col.visible"
:disabled="isLabelDisable(col)"
>
{{ col.title }}
</el-checkbox>
</li>
</ul>
<div class="toggle-btns">
<el-button @click="handleLabelChangeCancel">取消</el-button>
<el-button type="danger" @click="handleLabelChange">确定</el-button>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import Sortable from 'sortablejs';
export default {
name: 'TrsTable',
components: {
},
props: {
table: {
type: Object,
default: () => {
return {};
},
},
tableLabel: {
type: Array,
default: () => {
return [];
},
},
tableData: {
type: Array,
default: () => {
return [];
},
},
},
data() {
return {
tableDataCopy: [],
tableLableShow: [],
tableLableShow1: [], // 复制表头,利用下标对应取值
tableLableToggle: [],
labelToggle: false,
scrollSensitivity: 30, // 距离固定列边缘多少距离时开始滚动
};
},
watch: {
tableData: {
// 监听数据变化
handler(newVal, oldVal) {
if (newVal !== oldVal) {
this.tableDataCopy = JSON.parse(JSON.stringify(newVal));
}
// 垂直滚动条回到顶部
this.$nextTick(() => {
if (this.tableLableShow.length) {
this.$refs[this.table.ref].$refs.bodyWrapper.scrollTop = 0;
}
});
},
deep: true,
immediate: false,
},
tableLabel: {
// 监听表头变化
handler(newVal, oldVal) {
this.dealLables();
},
deep: true,
immediate: true,
},
},
created() {
var that = this;
setTimeout(() => {
that.dealLables();
}, 0);
this.tableDataCopy = JSON.parse(JSON.stringify(that.tableData));
},
mounted() {
document.addEventListener('click', this.toggleMenuClose);
setTimeout(() => {
this.columnDrop();
this.rowDrop();
}, 1000);
},
methods: {
toggleMenuClose(e) {
if (this.$refs.trsTableToggle && !this.$refs.trsTableToggle.contains(e.target)) {
if (this.labelToggle === true) {
this.labelToggle = false;
this.handleLabelChangeCancel();
}
}
},
beforeDestroy() {
document.removeEventListener('click', this.toggleMenuClose);
},
// 传入的表头复制一份作为配置数组,并且根据order进行排序
dealLables() {
const labels = [];
for (let i = 0; i < this.tableLabel.length; i++) {
const obj = Object.assign({}, this.tableLabel[i]);
labels[i] = obj;
}
this.tableLableToggle = labels.sort((a, b) => {
const v1 = a.order ?? labels.length;
const v2 = b.order ?? labels.length;
return v1 - v2;
});
this.lableToggleChange();
},
// 勾选项发生变化的回调
handleSelectionChange(val) {
this.$emit('handleSelectionChange', {
multipleSelection: val,
});
},
// 表格排序回调
handleTableSort(item) {
this.$emit('sort', item);
},
// 表头字段可见性发生变化的回调
handleLabelChange() {
this.lableToggleChange();
this.$emit('lableChange', this.tableLableToggle);
},
// 表头设置改变
lableToggleChange() {
this.tableLableShow = this.tableLableToggle.filter(lable => lable.visible !== false);
this.tableLableShow1 = this.tool.deepClone(this.tableLableShow);
this.labelToggle = false;
},
// 更新表头字段可见性配置列表
updateLabelToogle({ start, end }) {
const moveCol = this.tableLableShow1[start];
const targetCol = this.tableLableShow1[end];
const moveColIndex = this.getIndexOfToggleLabel(moveCol);
const targetColIndex = this.getIndexOfToggleLabel(targetCol);
const removeCol = this.tableLableToggle.splice(moveColIndex, 1)[0];
this.tableLableToggle.splice(targetColIndex, 0, removeCol);
},
// 获取给定列在表头字段可见性配置列表中的位置索引
getIndexOfToggleLabel(col) {
let index;
for (let i = 0; i < this.tableLableToggle.length; i++) {
if (this.tableLableToggle[i].title === col.title) {
index = i;
}
}
return index || this.tableLableToggle.length - 1;
},
// 切换表头字段配置列表的收缩展开
toggleBoxSwitch() {
this.labelToggle = !this.labelToggle;
if (!this.labelToggle) {
this.handleLabelChangeCancel();
}
},
// 取消对表头字段可见性配置列表的更改
handleLabelChangeCancel() {
this.tableLableToggle = this.tableLableToggle.map((label) => {
const labelShow = this.tableLableShow.filter((item) => item.title === label.title);
if (!labelShow.length) {
label.visible = false;
} else {
label.visible = true;
}
return label;
});
this.labelToggle = false;
},
isLabelDisable(col) {
if (col.fixed || !col.canHide) {
return true;
}
return false;
},
/**
* [columnDrop description] 列拖拽
* @return {[type]} [return description]
*/
columnDrop() {
const wrapperTr = document.querySelector('.el-table__header-wrapper tr');
this.sortable = Sortable.create(wrapperTr, {
animation: 180,
delay: 0,
dragoverBubble: true,
disabled: !this.table.colDragble,
forceFallback: true,
onEnd: evt => {
const oldItem = this.tableLableShow1[evt.oldIndex];
this.updateLabelToogle({
start: evt.oldIndex,
end: evt.newIndex,
});
this.tableLableShow1.splice(evt.oldIndex, 1);
this.tableLableShow1.splice(evt.newIndex, 0, oldItem);
this.$emit('colChange', this.tableLableToggle);
},
onChange: (evt) => {
const leftFixDom = document.querySelector('.el-table__fixed');
const rightFixDom = document.querySelector('.el-table__fixed-right');
this.autoScroll(leftFixDom, rightFixDom, evt);
},
});
},
rowDrop() {
const tbody = document.querySelector('.el-table__body-wrapper tbody');
const _this = this;
Sortable.create(tbody, {
animation: 180,
delay: 0,
disabled: !this.table.rowDragble,
onEnd({ newIndex, oldIndex }) {
const currRow = _this.tableDataCopy.splice(oldIndex, 1)[0];
_this.tableDataCopy.splice(newIndex, 0, currRow);
_this.$emit('rowChange', oldIndex, newIndex);
},
});
},
autoScroll(leftFixDom, rightFixDom, evt) {
this.autoScrollLeft(leftFixDom, evt);
this.autoScrollRight(rightFixDom, evt);
},
autoScrollLeft(leftFixDom, evt) {
const leftDomRect = leftFixDom.getBoundingClientRect();
if (!evt.draggedRect) {
evt.draggedRect = evt.item.getBoundingClientRect();
}
if (evt.draggedRect.left - this.scrollSensitivity < leftDomRect.right) {
this.$refs[this.table.ref].$refs.bodyWrapper.scrollLeft -= evt.draggedRect.width * 2;
}
},
autoScrollRight(rightFixDom, evt) {
const rightFixRect = rightFixDom.getBoundingClientRect();
if (!evt.draggedRect) {
evt.draggedRect = evt.item.getBoundingClientRect();
}
if (evt.draggedRect.right + this.scrollSensitivity > rightFixRect.left) {
this.$refs[this.table.ref].$refs.bodyWrapper.scrollLeft += evt.draggedRect.width * 2;
}
},
},
};
</script>
<style lang="less" scoped>
.swdTable {
width: 100%;
.trs-table {
position: relative;
.label-toggle {
position: absolute;
right: -10px;
top: 10px;
width: 200px;
background: #fff;
z-index: 100; // 因为列的拖动用的虚线框层级是99,所以这里要比虚线框高一层,否则无法点击到
.lable-toggle-switch {
position: absolute;
right: 0;
top: 0;
width: 30px;
height: 40px;
cursor: pointer;
}
.label-toogle-box {
border: 1px solid #cecece;
padding-bottom: 15px;
ul {
max-height: 50vh;
overflow-y: auto;
padding: 20px;
padding-right: 5px;
li {
width: 100%;
display: block;
height: 30px;
line-height: 30px;
}
}
.toggle-btns {
display: flex;
align-items: center;
justify-content: space-around;
}
}
}
/deep/ .el-table {
th {
padding: 0;
.thead-cell {
padding: 0;
display: inline-flex;
flex-direction: column;
align-items: left;
cursor: pointer;
overflow: initial;
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
}
.virtual {
position: fixed;
display: none;
width: 0;
z-index: 99;
background: none;
border: 1px solid #aeaeae;
opacity: 0.5;
margin-top: -10px;
}
li {
list-style: none;
height: 40px;
line-height: 40px;
}
}
&.darg_active_left {
.thead-cell {
.virtual {
border-left: 2px dotted #666;
}
}
}
&.darg_active_right {
.thead-cell {
.virtual {
border-right: 2px dotted #666;
}
}
}
}
/* stylelint-disable */
.el-table__fixed-body-wrapper {
top: 40px !important;
}
.el-table__header-wrapper {
height: 40px; // 动态渲染表头需要一开始就规定表头高度,否则表格高度计算会有bug
.cell {
white-space: nowrap;
overflow: hidden;
}
}
.el-table__fixed-header-wrapper {
.cell {
white-space: nowrap;
overflow: hidden;
}
}
.el-table__body-wrapper, .el-table__empty-block {
min-width: 85vw;
height: calc(100% - 40px) !important;
}
/* stylelint-enable */
.el-table__fixed-right {
right: 0;
&::before {
height: 0;
}
}
}
}
.trs-table_moving {
/* stylelint-disable */
/deep/ .el-table {
th {
.thead-cell {
cursor: move !important;
}
}
}
/* stylelint-enable */
/deep/ .el-table__fixed {
cursor: not-allowed;
}
}
}
</style>