以前做的组件,留下来欣赏,这里用了scss和阿里矢量图标,可以自己用其他方式替代
<template>
<div class="FitTable" :style="tableStyle">
<div class="progress" v-if="loading">
<ScaleLoader class="progress-spinner" :loading="loading" :color="loadingColor"></ScaleLoader>
</div>
<div class="FitTable-header" ref="header" :style="headerStyle">
<div class="FitTable-header__content">
<div v-if="ifSelect" class="cell-select" :style="{height: headerHeight + 'px'}">
<div class="cell-select-icon"></div>
</div>
<div v-if="selectData" class="FitTable-cell select-cell" :style="{width: headerHeight + 'px'}">
<div @click.stop="changeHeaderSelectStatus(selectHeaderStatus)" class="select-icon" :class="selectHeaderStatus ? ['icon-aliduihao1', 'selected']:['no-select']"></div>
</div>
<div v-for="(item, index) in columns" :key="item.unique || index" class="FitTable-cell"
:style="{minWidth: typeof item.minWidth === 'number'?item.minWidth + 'px':item.minWidth,
width: item.width,
textAlign: item.textAlign || 'center',
height: headerHeight + 'px'}"
:title="item.title">
<slot :name="item.slotHeader" :data="{...item,index}">
<div class="header-title">{{item.title}}
<div v-show="item.sort !== void(0)" class="table-sort">
<div class="table-sort-asc" :style="{opacity:(item.sort!=='desc')?1:0.3}" @click.stop="changeSort(item,'asc')"></div>
<div class="table-sort-desc" :style="{opacity:(item.sort!=='asc')?1:0.3}" @click.stop="changeSort(item,'desc')"></div>
</div>
</div>
</slot>
</div>
</div>
</div>
<div :style="tbodyHeight" ref="tableTbody">
<FitScroll>
<div class="FitTable-body" :style="{tableLayout:tableLayout}">
<NoData :message="message" v-if="!loading&&!data.length"></NoData>
<div class="FitTable-body-row">
<template v-for="(item,index) in data">
<slot :name="`index-header${index}`">
</slot>
<div class="FitTable-body-row_line" :key="(dataKey && item[dataKey]) || index" :style="{height: lineHeight}">
<div class="cell-select" v-if="ifSelect">
<div class="cell-select-icon"></div>
</div>
<div v-if="selectData" class="FitTable-cell select-cell" :style="{width: headerHeight + 'px'}">
<div @click.stop="changeSelectStatus(item[selectKey], selectDataSet.has(item[selectKey]))" class="select-icon" :class="selectDataSet.has(item[selectKey]) ? ['icon-aliduihao1', 'selected']:['no-select']"></div>
</div>
<div v-for="(_item, _index) in columns" :key="_item.unique || _index" class="FitTable-cell"
:style="{minWidth: typeof _item.minWidth === 'number'?_item.minWidth + 'px':_item.minWidth,
width: _item.width,
textAlign: _item.textAlign || 'center'}"
@click.stop="showExpand(index, _item.expand)">
<div class="FitTable-cell-content">
<div class="FitTable-cell-content-data">
<slot :name="_item.slot" :data="{...item,index, data: item}">
<span :title="item[_item.key] || _item.defaultVal" style="color: #DBE5E8;" v-if="!_item.expand">
{{item[_item.key] || _item.defaultVal}}
</span>
<span class="expand-icon" :class="expands.includes(index)?'icon-alileft':'icon-alibottom-triangle'" v-else></span>
</slot>
</div>
</div>
</div>
</div>
<div class="expand" :key="`expand-${(dataKey && item[dataKey]) || index}`" :style="expandStyle" v-show="expands.includes(index)">
<slot name="expand" :data="{...item,index, data: item}">
</slot>
</div>
<slot :name="`index-footer${index}`"></slot>
</template>
</div>
</div>
</FitScroll>
</div>
</div>
</template>
<script>
/* eslint-disable */
import ScaleLoader from 'vue-spinner/src/ScaleLoader.vue'
import NoData from '../NoData'
import resize from '@/mixins/eventResize';
export default {
name: "FitTable",
components: {
ScaleLoader,
NoData
},
props: {
maxHeight: {
type: String,
default: null
},
// 表格宽度,默认100%
width: {
type: String,
default: '100%'
},
height: {
type: Number | String,
default: '100%'
},
ifSelect: {
type: Boolean,
default: false
},
// 起始索引
// startIndex:{
// type: Number,
// default: 0
// },
/* TODO columns接受值: 对象数组
title: 表头名称
slotHeader: 表头插槽名称
slot: 表体插槽
defaultVal: 数据不存在,判断为false时的值,默认值
unique: 默认索引值,必须唯一,便于vue优化
key: 对应prop:{data}中数据的键值
width:列固定宽度
minWidth: 对应列最小宽度
textAlign:文本对齐方式
expand:展开expand插槽的信息
onlySort: 设置为true,该字段排序时,其他字段排序重置
sort: 默认排序方式 空字符为不排序,asc为升序, desc为降序
*/
columns: {
type: Array,
default: () => []
},
data: {
type: Array,
default: () => []
},
// 唯一值,防止不必要的错误发生
selectKey: {
type: String,
default: null
},
// 选择框
selectData: {
type: Array,
default: null
},
message:{
type: String,
default: '查询不到数据'
},
headerHeight: {
type: Number,
default: 35
},
backgroundColor: {
type: String,
// default: '#3c6180',
default: 'rgba(60,97,128,0.3)'
},
lineHeight: {
type: String,
default: ''
},
// prop:{data}中唯一的值,不传则通过索引优化表格
dataKey: {
type: String,
default: null
},
loading: {
type: Boolean,
default: false
},
loadingColor: {
type: String,
default: '#45d1ea'
},
tableLayout: {
type: String,
default: 'fixed'
}
},
mixins: [resize],
computed: {
tableStyle: {
get () {
return {width: this.width,maxHeight: this.maxHeight,height:typeof this.height === 'number' ?this.height + 'px':this.height};
}
},
headerStyle () {
return {width:this.tbodyWidth + 'px', backgroundColor: this.backgroundColor,tableLayout: this.tableLayout};
},
tbodyHeight () {
const height = this.tableHeight > (this.maxHeight || Infinity)?this.maxHeight:this.tableHeight;
return { height: height -this.headerHeight + 'px',overflowY: 'auto' };
},
expandStyle () {
return {
width: this.tbodyWidth + 'px'
}
},
selectDataSet () {
return this.selectData && new Set(this.selectData);
},
selectHeaderStatus () {
if (this.selectDataSet && this.selectKey) {
return this.selectDataSet.size === this.data.length;
} else {
return false;
}
}
},
watch: {
height () {
if (String(this.height).indexOf('%') !== -1) {
this.tableHeight = this.$el.offsetHeight * parseInt(this.height) / 100
} else {
this.tableHeight = this.height;
}
}
},
data: () => ({
rowWidth: null,
// 将自定义的列width值收集起来的和
customWidth: { count: 0, value: 0 },
tbodyWidth: 0,
tableHeight: 0,
theaderHeight: 0,
expands: []
}),
mounted() {
// const width = this.$el.offsetWidth;
// this.theaderHeight = this.$refs['header'].offsetHeight;
this.$nextTick(() => {
if (String(this.height).indexOf('%') !== -1) {
this.tableHeight = this.$el.offsetHeight * parseInt(this.height) / 100;
} else {
this.tableHeight = this.height;
}
const getTbodyWidth = () => {
requestAnimationFrame(() => {
if (!this.$refs['tableTbody']) {
return getTbodyWidth();
}
this.tbodyWidth = this.$refs['tableTbody'].clientWidth;
if (!this.tbodyWidth) {
getTbodyWidth();
}
})
};
getTbodyWidth();
});
// this.rowWidth = (width - this.customWidth.value) / (this.columns.length - this.customWidth.count) + 'px';
// // eslint-disable-next-line no-console
window.addEventListener('resize', this.resize);
this.$once('hook:beforeDestroy', () => window.removeEventListener('resize', this.resize))
},
methods: {
changeSort (item, type) {
if (item.sort === 'asc' || item.sort === 'desc') {
item.sort = ''
} else {
if (type === 'asc') {
item.sort = 'desc'
} else {
item.sort = 'asc'
}
}
if (item.onlySort) {
this.columns.forEach(e => {
if (e !== item && e.sort !== void (0)) {
e.sort = ''
}
})
}
this.$emit('changeSort', item)
},
resize(el) {
// if (this.width.indexOf('%') !== -1) {
// const width = this.$el.offsetWidth;
// this.rowWidth = (width - this.customWidth.value) / (this.columns.length - this.customWidth.count) + 'px'
// }
setTimeout(() => {
this.theaderHeight = this.$refs['header'].offsetHeight;
if (String(this.height).indexOf('%') !== -1) {
// console.log(this.$el.offsetHeight,this,document.getElementsByClassName('FitTable')[0].offsetHeight, 8899)
this.tableHeight = this.$el.offsetHeight * parseInt(this.height) / 100
}
this.tbodyWidth = this.$refs['tableTbody'].clientWidth;
});
},
changeHeaderSelectStatus (status) {
if (status) {
this.selectData.splice(0, this.selectData.length);
} else {
this.data.forEach(e => {
if (!this.selectDataSet.has(e[this.selectKey])) {
this.selectData.push(e[this.selectKey]);
}
});
}
},
changeSelectStatus (unique, status) {
if (status) {
this.selectData.reverse().forEach((e, index) => {
if (e === unique) {
this.selectData.splice(index, 1);
}
})
} else {
this.selectData.push(unique);
}
},
showExpand (index, expand) {
if (!expand) {
return;
}
const findIndex = this.expands.findIndex(e => e === index);
let ifExpand = true;
if (~findIndex) {
this.expands.splice(findIndex, 1);
ifExpand = false;
} else {
this.expands.push(index);
}
this.$nextTick(() => {
this.$emit('expand', {index, ifExpand});
});
}
}
}
</script>
<style lang="scss" scoped>
@import "../../styles/_scroll";
$sortWidth: 0.5vw;
$sortColor: #ccc;
$tableCell: table-cell;
.FitTable {
position: relative;
.select-icon {
margin: 0 auto;
width: 15px;
height: 15px;
border-radius: 2px;
}
.selected {
display: flex;
justify-content: center;
align-items: center;
font-size: 1rem;
color: white;
background-color: #3A97E2;
}
.no-select {
border: 1px solid white;
}
&-header {
display: table;
box-sizing: border-box;
// border-bottom: 1px solid rgba(255,255,255,0.32);
&__content {
display: table-header-group;
border-bottom: 1px solid rgba(255,255,255,0.3);
.cell-select {
width: 35px;
display: $tableCell;
position: relative;
&-icon {
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
margin: auto;
height: 15px;
width: 15px;
border: 1px solid white;
}
}
.FitTable-cell {
display: $tableCell;
vertical-align: middle;
> div {
//position: relative;
//top: 50%;
//transform: translateY(-50%);
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.header-title {
margin: 1px 5px;
color: #DBE5E8;
display: inline-flex;
align-items: center;
.table-sort {
margin-left: 0.2vw;
display: inline-block;
&-asc {
cursor: pointer;
border-top: $sortWidth solid transparent;
border-bottom: $sortWidth solid $sortColor;
border-left: calc(#{$sortWidth} * 2 / 3) solid transparent;
border-right: calc(#{$sortWidth} * 2 / 3) solid transparent;
margin-bottom: 0.15vh;
}
&-desc {
cursor: pointer;
border-top: $sortWidth solid $sortColor;
border-bottom: $sortWidth solid transparent;
border-left: calc(#{$sortWidth} * 2 / 3) solid transparent;
border-right: calc(#{$sortWidth} * 2 / 3) solid transparent;
margin-top: 0.15vh;
}
}
}
}
}
}
&-body {
width: 100%;
display: table;
box-sizing: border-box;
&-row {
display: table-row-group;
&_line {
display: table-row;
.cell-select {
width: 35px;
display: $tableCell;
position: relative;
&-icon {
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
margin: auto;
height: 15px;
width: 15px;
border: 1px solid white;
}
}
&:nth-child(odd) {
background-color: transparent;
}
&:nth-child(even) {
background-color: rgba(158,158,158,0.1);
}
&:hover {
background-color: rgba(158, 158, 158, 0.1);
}
.FitTable-cell {
display: $tableCell;
vertical-align: middle;
&-content {
height: 30px;
padding: 1px 5px;
&-data {
position:relative;
top: 15px;
transform: translateY(-50%);
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
.expand-icon {
color: white;
}
}
}
}
}
}
}
}
.progress {
position: absolute;
text-align: center;
width: 100%;
height: 100%;
// z-index: -1;
&-spinner{
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
}
</style>