| <!-- |
| ~ Licensed to the Apache Software Foundation (ASF) under one or more |
| ~ contributor license agreements. See the NOTICE file distributed with |
| ~ this work for additional information regarding copyright ownership. |
| ~ The ASF licenses this file to You under the Apache License, Version 2.0 |
| ~ (the "License"); you may not use this file except in compliance with |
| ~ the License. You may obtain a copy of the License at |
| ~ |
| ~ http://www.apache.org/licenses/LICENSE-2.0 |
| ~ |
| ~ Unless required by applicable law or agreed to in writing, software |
| ~ distributed under the License is distributed on an "AS IS" BASIS, |
| ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| ~ See the License for the specific language governing permissions and |
| ~ limitations under the License. |
| --> |
| |
| <template> |
| <div style="position:relative;height:100%"> |
| <div |
| class="table-div" |
| :style="{height:`${tableHeightCalc}px`}" |
| > |
| <div |
| id="topDiv" |
| class="table-top" |
| :style="{right:`${(scrollBarWidthCalc())}px`}" |
| @dblclick="headDblclick" |
| > |
| <table |
| id="topTable" |
| cellpadding="0" |
| cellspacing="0" |
| border="0" |
| style="width:100%;table-layout:fixed;" |
| > |
| <tr> |
| <td |
| v-for="(top,index) in columns" |
| class="top-td" |
| :key="index" |
| :data-col-index="index" |
| :style="{width: top.width? `${top.width}px` : 'auto'}" |
| > |
| <span :data-col-index="index" :title="top.colHeadHoverTitle">{{top.title?top.title:""}}</span> |
| <span |
| :class="'sort-icon'" |
| v-if="top.sortable" |
| > |
| <i class="ivu-icon ivu-icon-md-arrow-dropup" |
| :class="{'on': sortType.index === index && sortType.type === 'asc'}" @click="handleSort(index, 'asc')"></i> |
| <i class="ivu-icon ivu-icon-md-arrow-dropdown" |
| :class="{'on': sortType.index === index && sortType.type === 'desc'}" @click="handleSort(index, 'desc')"></i> |
| </span> |
| </td> |
| </tr> |
| </table> |
| </div> |
| <div |
| id="bottomDiv" |
| class="table-bottom" |
| @scroll.stop="handleScroll" |
| > |
| <div :style="{height:`${dataTop}px`}"></div> |
| <table |
| id="bottomTable" |
| v-if="showTableList.length" |
| cellpadding="0" |
| cellspacing="0" |
| border="0" |
| style="width:100%;table-layout:fixed;overflow:hidden" |
| :style="{height:`${loadedNum*tdHeight}px`}" |
| > |
| <tbody> |
| <tr |
| v-for="(items,indexs) in showTableList" |
| @click="rowClick(items,indexs+dataTop/tdHeight)" |
| @dblclick="rowDblclick(items,indexs+dataTop/tdHeight+1)" |
| :key="indexs" |
| :style="{'line-height':`${tdHeight}px`}" |
| :class="selectIndex==indexs?'trselect':'trhover'" |
| > |
| <td |
| class="bottom-td" |
| v-if="columns[0].type=='index'" |
| :style="{width: columns[0].width? `${columns[0].width}px` : 'auto', height:`${tdHeight}px`}" |
| > |
| {{indexs+dataTop/tdHeight+1}}</td> |
| <td |
| class="bottom-td" |
| v-if="columns[0].type=='select'" |
| ></td> |
| <template v-for="(item,index) in columnsBottom"> |
| <td |
| v-if="item.key" |
| :key="`${index}-${indexs}`" |
| class="bottom-td" |
| :class="[hasLineBreak(item.logic==undefined?items[item.key]:item.logic(items)) ? '' : 'hint--right hint--rounded']" |
| :style="{width: item.width?`${item.width}px`:'auto'}" |
| :aria-label="item.logic==undefined?items[item.key]:item.logic(items)" |
| > |
| <p v-if="hasLineBreak(item.logic==undefined?items[item.key]:item.logic(items))" style="width: 100%" class="wrap">{{item.logic==undefined?items[item.key]:item.logic(items)}}</p> |
| <div v-else class="scroll" :aria-label="item.logic==undefined?items[item.key]:item.logic(items)" :style="{width: '100%', height:`${tdHeight}px`}">{{item.logic==undefined?items[item.key]:item.logic(items)}}</div> |
| |
| </td> |
| <td |
| v-if="item.slot" |
| class="bottom-td" |
| :key="index" |
| :style="{width: item.width?`${item.width}px`:'auto', height:`${tdHeight}px`}" |
| > |
| <slot :name="item.slot" :row="items" :index="indexs+dataTop/tdHeight+1"></slot> |
| </td> |
| </template> |
| </tr> |
| </tbody> |
| </table> |
| <div v-show="showTableList.length < 1" class="no-data-tip" :style="noDataStyle"> |
| 暂无数据 |
| </div> |
| <div :style="{height:`${tableOtherBottom}px`}"></div> |
| </div> |
| </div> |
| <div |
| class="table-bottom-load" |
| v-show="showLoad" |
| :style="{right:`${(scrollBarWidthCalc())}px`,top:'40px',height:`${tableHeightCalc-40}px`}" |
| > |
| <svg |
| class="icon loading " |
| aria-hidden="true" |
| > |
| <use xlink:href="#icon-jiazai"></use> |
| </svg> |
| <div class="msg">加载中,请稍候</div> |
| </div> |
| </div> |
| </template> |
| |
| <script> |
| export default { |
| name: "WbTable", |
| props: { |
| loadNum: { |
| //Default number of rows to load(默认加载行数) |
| type: [Number, String], |
| default() { |
| return 50; |
| } |
| }, |
| tdHeight: { |
| //table row height(表格行高) |
| type: [Number, String], |
| default() { |
| return 40; |
| } |
| }, |
| tableHeight: { |
| //table height(表格高度) |
| type: [Number, String], |
| }, |
| tableList: { |
| //All tabular data(所有表格数据) |
| type: Array, |
| default() { |
| return []; |
| } |
| }, |
| columns: { |
| //All table matching rules(所有表格匹配规则) |
| type: Array, |
| default() { |
| return []; |
| } |
| }, |
| showHeader: { |
| type: Boolean, |
| default: true |
| }, |
| highlightRow: {}, |
| outerSort: {}, // If external sorting, clicking the sorting icon in the header only triggers the event(若外部排序则点击表头排序图标只触发事件) |
| scrollBarWidth: { |
| type: Number, |
| default(){ |
| return 8 |
| } |
| } |
| }, |
| data() { |
| return { |
| showLoad: false, |
| showTableList: [], //table data actually displayed(实际显示的表格数据) |
| loadedNum: 0, //The amount of data actually rendered(实际渲染的数据数量) |
| dataTotal: 0, //total data(总数据条数) |
| dataTop: 0, //The height of the top of the rendered data(渲染数据顶部的高度) |
| scrollTop: 0, //scroll up and down distance(滚动上下的距离) |
| scrollHeight: 0, //The height of the data scroll(数据滚动的高度) |
| handleScroll: null, |
| selectIndex: -1, //selected row(选择的行) |
| sortType: {}, |
| noDataStyle: { |
| width: '100%' |
| } |
| }; |
| }, |
| computed: { |
| tableOtherBottom() { |
| //The bottom height of the remaining data in the table(表格剩余数据底部高度) |
| return ( |
| this.dataTotal * this.tdHeight - |
| this.dataTop - |
| this.loadedNum * this.tdHeight |
| ); |
| }, |
| columnsBottom() { |
| if (this.columns[0].type != undefined) { |
| return this.columns.slice(1, this.columns.length); |
| } else { |
| return this.columns; |
| } |
| }, |
| // Calculate the height when the actual data of the table is lower than the number of rendered bars(计算表格实际数据低于渲染条数时的高度) |
| tableHeightCalc() { |
| let dataHeightt = (this.tdHeight + 1) * this.loadedNum + 43;// 43 is the header and reserved height, +1 is the border(43为表头和预留高度, +1 为border) |
| if(this.tableHeight) return this.tableHeight; |
| if(this.tableList.length <= 0) return 100; |
| return dataHeightt |
| } |
| }, |
| methods: { |
| /** |
| * @typedef {Object} Options -configuration item(配置项) |
| * @property {Boolean} leading -Whether an additional trigger is required to start(开始是否需要额外触发一次) |
| * @property {this} context -context(上下文) |
| **/ |
| //Use Proxy to implement function anti-shake(使用Proxy实现函数防抖) |
| proxy( |
| func, |
| time, |
| options = { |
| leading: true, |
| context: null |
| } |
| ) { |
| let timer; |
| let _this = this; |
| let handler = { |
| apply(target, _, args) { |
| //proxy function call(代理函数调用) |
| let bottomScroll = document.getElementById("bottomDiv"); |
| let topScroll = document.getElementById("topDiv"); |
| if (bottomScroll.scrollTop == _this.scrollTop) { |
| //scroll left and right(左右滚动) |
| _this.handleScrollLeft(topScroll, bottomScroll); |
| return; |
| } |
| // The same as the core logic of closure implementation(和闭包实现核心逻辑相同) |
| if (!options.leading) { |
| if (timer) return; |
| timer = setTimeout(() => { |
| timer = null; |
| Reflect.apply(func, options.context, args); |
| }, time); |
| } else { |
| if (timer) { |
| _this.needLoad(bottomScroll); |
| clearTimeout(timer); |
| } |
| timer = setTimeout(() => { |
| Reflect.apply(func, options.context, args); |
| }, time); |
| } |
| } |
| }; |
| return new Proxy(func, handler); |
| }, |
| //Whether to show loading(是否显示加载中) |
| needLoad(bottomScroll) { |
| if ( |
| Math.abs(bottomScroll.scrollTop - this.scrollTop) > |
| this.tdHeight * this.loadNum |
| ) { |
| this.showLoad = true; //show loading(显示加载中) |
| this.scrollTop = bottomScroll.scrollTop; |
| } |
| }, |
| //scroll processing(滚动处理) |
| scrollProcessing() { |
| // const last = $event && $event.last; |
| const bottomScroll = document.getElementById("bottomDiv"); |
| // const topScroll = document.getElementById("topDiv"); |
| const direction = bottomScroll.scrollTop >= this.scrollTop; //scroll direction(滚动方向) |
| // if(this.needLoad(last,bottomScroll))return; |
| //Record the last scroll down position(记录上一次向下滚动的位置) |
| this.scrollTop = bottomScroll.scrollTop; |
| direction ? this.handleScrollBottom() : this.handleScrollTop(); |
| this.showLoad = false; |
| }, |
| //scroll bar up(滚动条向上滚动) |
| handleScrollTop() { |
| if (this.dataTop < this.scrollTop) { |
| //scroll down should be called if the last scroll position is above the data(如果最后滚动位置在数据上方应该调用向下滚动) |
| this.handleScrollBottom(); |
| return; |
| } |
| //If the loaded data is less than the default amount of data loaded(如果加载的数据小于默认加载的数据量) |
| if (this.dataTotal > this.loadNum) { |
| const computeHeight = this.dataTop; //The height at which the data needs to be processed(数据需要处理的时候的高度) |
| const maxHeigth = computeHeight - this.loadNum * this.tdHeight; //No need to clear all data height(不需要清除所有数据的高度) |
| if (this.scrollTop < computeHeight && this.scrollTop >= maxHeigth) { |
| //If the total data is greater than the already rendered data(如果数据总数大于已经渲染的数据) |
| const dataTopNum = parseInt(this.dataTop / this.tdHeight); //Number of top bars of data(数据顶部条数) |
| dataTopNum - this.loadNum >= 0 |
| ? this.dataProcessing( |
| this.loadNum, |
| this.loadedNum - this.loadNum, |
| "top" |
| ) |
| : this.dataProcessing(dataTopNum, dataTopNum, "top"); |
| } else if (this.scrollTop < maxHeigth) { |
| const scrollNum = parseInt(this.scrollTop / this.tdHeight); //滚动的位置在第几条数据 |
| scrollNum - this.loadNum >= 0 |
| ? this.dataProcessing(this.loadNum * 2, scrollNum, "topAll") |
| : this.dataProcessing( |
| scrollNum + this.loadNum, |
| scrollNum, |
| "topAll" |
| ); |
| } |
| } |
| }, |
| hasLineBreak(content) { |
| if (content.split(/\n/).length > 1) { |
| return true |
| } |
| return false |
| }, |
| |
| //scroll bar scroll down(滚动条向下滚动) |
| handleScrollBottom() { |
| if (this.dataTop > this.scrollTop) { |
| this.handleScrollTop(); |
| return; |
| } |
| const computeHeight = |
| this.dataTop + this.loadedNum * this.tdHeight - this.tableHeightCalc; //The height at which the data needs to be processed(数据需要处理的时候的高度) |
| const maxHeight = computeHeight + this.tdHeight * this.loadNum; //No need to clear all data height(不需要清除所有数据的高度) |
| if (this.scrollTop > computeHeight && this.scrollTop <= maxHeight) { |
| //If the scroll height reaches the bottom height of the data display(如果滚动高度到达数据显示底部高度) |
| if (this.dataTotal > this.loadedNum) { |
| const dataTopNum = parseInt(this.dataTop / this.tdHeight); //Number of top bars of data(数据顶部条数) |
| const total = dataTopNum + this.loadedNum + this.loadNum; |
| const otherTotal = this.dataTotal - (dataTopNum + this.loadedNum); |
| total <= this.dataTotal |
| ? this.dataProcessing( |
| this.loadedNum - this.loadNum, |
| this.loadNum, |
| "bottom" |
| ) |
| : this.dataProcessing(otherTotal, otherTotal, "bottom"); |
| } |
| } else if (this.scrollTop > maxHeight) { |
| let scrollNum = parseInt(this.scrollTop / this.tdHeight); //The scroll position is in the first few data(滚动的位置在第几条数据) |
| scrollNum + this.loadNum <= this.dataTotal |
| ? this.dataProcessing(scrollNum, this.loadNum * 2, "bottomAll") |
| : this.dataProcessing( |
| scrollNum, |
| this.dataTotal - scrollNum + this.loadNum, |
| "bottomAll" |
| ); |
| } |
| }, |
| //scroll bar scroll left and right(滚动条左右滚动) |
| handleScrollLeft(topScroll, bottomScroll) { |
| //The top header scrolls with the bottom(顶部表头跟随底部滚动) |
| topScroll.scrollTo(bottomScroll.scrollLeft, topScroll.pageYOffset); |
| }, |
| //Data processing when scrolling up and down(上下滚动时数据处理) |
| dataProcessing(topNum, bottomNum, type) { |
| const topPosition = parseInt(this.dataTop / this.tdHeight); |
| if (type === "top") { |
| this.showTableList.splice(this.loadedNum - bottomNum, bottomNum); //减去底部数据 |
| for (let i = 1; i <= topNum; i++) { |
| //plus top data(加上顶部数据) |
| const indexNum = topPosition - i; |
| this.showTableList.unshift(this.tableList[indexNum]); |
| } |
| this.loadedNum = this.loadedNum + topNum - bottomNum; //Recalculate the actual number of rendered data(重新计算实际渲染数据条数) |
| this.dataTop = this.dataTop - topNum * this.tdHeight; //Recalculate the height of the rendered data(重新计算渲染数据的高度) |
| document.getElementById("bottomDiv").scrollTop = |
| document.getElementById("bottomDiv").scrollTop + |
| bottomNum * this.tdHeight; |
| // this.scrollTop = document.getElementById("bottomDiv").scrollTop; |
| } else if (type == "bottom") { |
| this.showTableList.splice(0, topNum); //minus top data(减去顶部数据) |
| for (let i = 0; i < bottomNum; i++) { |
| //plus bottom data(加上底部数据) |
| const indexNum = topPosition + this.loadedNum + i; |
| this.showTableList.push(this.tableList[indexNum]); |
| } |
| this.loadedNum = this.loadedNum - topNum + bottomNum; //Recalculate the actual number of rendered data(重新计算实际渲染数据条数) |
| this.dataTop = this.dataTop + topNum * this.tdHeight; //Recalculate the height of the rendered data(重新计算渲染数据的高度) |
| document.getElementById("bottomDiv").scrollTop = |
| document.getElementById("bottomDiv").scrollTop - |
| topNum * this.tdHeight; |
| // this.scrollTop = document.getElementById("bottomDiv").scrollTop; |
| } else if (type == "bottomAll") { |
| this.showTableList = []; //minus top data(减去顶部数据) |
| let scrollNum = topNum; |
| for (let i = 0; i < bottomNum; i++) { |
| //plus bottom data(加上底部数据) |
| let indexNum = scrollNum - this.loadNum + i; |
| this.showTableList.push(this.tableList[indexNum]); |
| } |
| this.loadedNum = bottomNum; //Recalculate the actual number of rendered data(重新计算实际渲染数据条数) |
| this.dataTop = (scrollNum - this.loadNum) * this.tdHeight; //Recalculate the height of the rendered data(重新计算渲染数据的高度) |
| // this.scrollTop = document.getElementById("bottomDiv").scrollTop; |
| } else if (type == "topAll") { |
| this.showTableList = []; //minus top data(减去顶部数据) |
| let scrollNum = bottomNum; |
| for (let i = 0; i < topNum; i++) { |
| //plus bottom data(加上底部数据) |
| let indexNum = scrollNum - topNum + this.loadNum + i; |
| this.showTableList.push(this.tableList[indexNum]); |
| } |
| this.loadedNum = topNum; //Recalculate the actual number of rendered data(重新计算实际渲染数据条数) |
| this.dataTop = (scrollNum - topNum + this.loadNum) * this.tdHeight; //Recalculate the height of the rendered data(重新计算渲染数据的高度) |
| // this.scrollTop = document.getElementById("bottomDiv").scrollTop; |
| } |
| this.showLoad = false; |
| }, |
| rowClick(item, index) { |
| if (this.highlightRow !== undefined) { |
| this.selectIndex = index; |
| } |
| this.$emit("on-current-change", item, index); |
| }, |
| rowDblclick(index, item) { |
| this.$emit("on-row-dblclick", item, index); |
| }, |
| headDblclick(e) { |
| if (e && e.target && e.target.dataset.colIndex) { |
| let index = e.target.dataset.colIndex |
| this.$emit("on-head-dblclick", this.columns[index], index); |
| const selection = window.getSelection(); |
| selection.selectAllChildren(e.target); |
| } else { |
| window.getSelection().removeAllRanges(); |
| } |
| }, |
| //sort(排序) |
| handleSort(index, type) { |
| let column = this.columns[index]; |
| let key = column.key; |
| if (this.sortType.type === type && this.sortType.index === index) { |
| type = 'normal'; |
| } |
| this.sortType = { |
| index, |
| type |
| }; |
| if (this.outerSort !== undefined) { |
| this.$emit("on-sort-change", { |
| column, |
| key, |
| order: type |
| }); |
| return |
| } |
| |
| this.tableList.sort((a, b) => { |
| if (column.sortMethod) { |
| return column.sortMethod(a[key], b[key], type); |
| } else { |
| if (type === "asc") { |
| return a[key] > b[key] ? 1 : -1; |
| } else if (type === "desc") { |
| return a[key] < b[key] ? 1 : -1; |
| } |
| } |
| }); |
| }, |
| initTable() { |
| document.getElementById("bottomDiv") && |
| (document.getElementById("bottomDiv").scrollTop = 0); |
| // Later, if you want to switch the tab to save the scroll bar state, you have to record the scroll distance and assign it again.(后面如果要做切换tab保存滚动条状态,得记录滚动距离再赋值) |
| // document.getElementById("bottomDiv") && |
| // (document.getElementById("bottomDiv").scrollLeft = 0); |
| this.loadedNum = 0; //The amount of data actually rendered(实际渲染的数据数量) |
| this.dataTotal = 0; //total data(总数据条数) |
| this.dataTop = 0; //The height of the top of the rendered data(渲染数据顶部的高度) |
| this.scrollTop = 0; //scroll up and down distance(滚动上下的距离) |
| this.showTableList = []; |
| if (this.tableList.length > 0) { |
| this.dataTotal = this.tableList.length; //get data length(获取数据长度) |
| if (this.dataTotal >= this.loadNum) { |
| //Determine whether the data length is greater than the set length of each rendering(判断数据长度是否大于设置的每次渲染长度) |
| this.loadedNum = this.loadNum; //Set the actual number of rendered bars(设置实际渲染条数) |
| for (var i = 0; i < this.loadNum; i++) { |
| let data = this.tableList[i]; |
| this.showTableList.push(data); |
| } |
| } else if (this.dataTotal < this.loadNum) { |
| this.loadedNum = this.dataTotal; |
| for (let i = 0; i < this.dataTotal; i++) { |
| let data = this.tableList[i]; |
| this.showTableList.push(data); |
| } |
| } |
| } |
| if(this.showTableList.length < 1) { |
| this.$nextTick(()=>{ |
| this.getNoDataStyle() |
| }) |
| } |
| |
| }, |
| getNoDataStyle() { |
| let topTable = document.getElementById("topTable"); |
| if (topTable) { |
| this.noDataStyle.width = topTable.clientWidth + 'px' |
| } |
| }, |
| // Determine whether a vertical scroll bar appears(判断是否出现竖直滚动条) |
| scrollBarWidthCalc() { |
| let bottomDiv = document.getElementById("bottomDiv"); |
| return bottomDiv && bottomDiv.scrollHeight > bottomDiv.clientHeight ? this.scrollBarWidth : 0 |
| }, |
| }, |
| created() { |
| this.handleScroll = this.proxy(this.scrollProcessing, 240, { |
| leading: true, |
| context: this |
| }); |
| }, |
| mounted() { |
| this.initTable(); |
| }, |
| watch: { |
| tableList: { |
| handler(newValue, oldValue) { |
| this.initTable(); |
| if (oldValue) { |
| this.scrollProcessing(); |
| } |
| } |
| }, |
| tableHeight(newValue) { |
| if (newValue) { |
| this.scrollProcessing(); //table height change recalculation(表格高度改变重新计算) |
| } |
| } |
| } |
| }; |
| </script> |
| <style scoped> |
| @import "./table.css"; |
| </style> |