最近因为项目需要,利用vue开发了一套利于扩展的表格组件,可选择分页展示,带有排序功能支持,支持自定义操作按钮与class以及自定义render渲染。效果如下:
使用简单:
html:
vue实例中传入基本的列信息:
完整代码:
<html>
<head>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="http://code.jquery.com/jquery-1.4.1.min.js"></script>
<style>
table { width: 100%; margin-bottom: 24px; border-collapse: collapse; border-spacing: 0; empty-cells: show; border: 1px solid #e9e9e9;}
table th { font: bold 14px "Trebuchet MS", Verdana, Arial, Helvetica, sans-serif;background: #CAE8EA; color: #5c6b77; font-weight: 600; white-space: nowrap; border-top: 1px solid #C1DAD7;}
table td, table th { padding: 8px 16px; text-align: left;border-right: 1px solid #C1DAD7; border-bottom: 1px solid #C1DAD7;}
table th a { display: inline-block; margin: 0 4px; cursor: pointer;}
table th a.on {color: #3399ff;}
table th a:hover {color: #3399ff;}
.row{display: flex;-ms-flex-wrap: wrap; flex-wrap: wrap; margin-right: -12px; margin-left: -12px;}
.row .col-4{ -webkit-box-flex: 0; -ms-flex: 0 0 33.33333%; flex: 0 0 33.33333%; max-width: 20%; position: relative; width: 100%; padding-right: 12px; padding-left: 12px;}
.pagination{list-style: none;display: -webkit-box;display: -ms-flexbox;border-radius: .25rem;}
.col-4{-webkit-box-flex: 0;flex: 0 0 33.33333%;max-width: 20%;}
.pagination-sm{margin: 0;}
button{outline: none;}
.pagination-sm .page-link{padding: .25rem .5rem;font-size: .74375rem;line-height: 1.5;margin: 0;}
.pagination-sm .active .page-link{color: red;}
.btn-confirm {position: absolute; top: 50%; left: 70%; transform: translate(-50%, -50%); width: 80px; height: 30px; line-height: 20px; text-align: center; color: #fff;text-transform: uppercase; text-decoration: none; font-family: sans-serif; box-sizing: border-box; background: linear-gradient(90deg, #03a9f4, #f441a5, #ffeb3b, #03a9f4); background-size: 400%; border-radius: 60px; z-index: 1; }
.btn-confirm:hover { animation: animate 8s linear infinite; }
@keyframes animate { 0% { background-position: 0%; } 100% { background-position: 400%; } }
.btn-confirm::before { content: ''; position: absolute; top: -5px; left: -5px; right: -5px; bottom: -5px; z-index: -1; background: linear-gradient(90deg, #03a9f4, #f441a5, #ffeb3b, #03a9f4); background-size: 400%; border-radius: 40px; opacity: 0; transition: 0.5s; }
.btn-confirm:hover::before { filter: blur(20px); opacity: 1; animation: animate 8s linear infinite; }
</style>
</head>
<body>
<div id="app">
<tt-table :datas="datas" :columns="columns" pageSize=2 :rows="rows"></tt-table>
</div>
<script>
/**分页组件嵌入到表格中*/
Vue.component('page-split', {
props: {
totalRows: {
required: true,
type: Number
},
pageSize: {
type: Number,
default: 8
},
pageNumAmount: {
type: Number,
default: 3
},
pageNum: {
type: Number,
default: 1
}
},
model: {
prop: 'pageNum',
event: 'switch-page'
},
data: function data() {
return {
totalPage: 0,
inputPage: '',
showPageList: []
};
},
created: function() {
this.calShowPageNums();
},
watch: {
totalRows: function totalRows() {
this.calShowPageNums();
}
},
methods: {
invokeAjaxMethod: function(){
this.$emit('switch-page', this.pageNum);
this.calShowPageNums();
},
toSpecialPage: function toSpecialPage() {
if (!/^\d{1,10}$/.test(this.inputPage)) {
this.inputPage = '';
return false;
}
if (this.inputPage > this.totalPage || this.inputPage < 1) {
this.inputPage = '';
return false;
}
this.pageNum = this.inputPage;
this.invokeAjaxMethod();
},
prePage: function prePage() {
if (this.pageNum < 2) {
return false;
}
this.pageNum--;
this.invokeAjaxMethod();
},
goPage: function goPage(pageNo) {
this.pageNum = pageNo;
this.invokeAjaxMethod();
},
nextPage: function nextPage() {
if (this.pageNum == this.totalPage) {
return false;
}
this.pageNum++;
this.invokeAjaxMethod();
},
//计算应该展示出的页号
calShowPageNums: function calShowPageNums() {
this.totalPage = this.getNumMultiple(this.totalRows, this.pageSize);
var showPageNum = 1,
showListIndex = 0;
if (this.totalPage > this.pageNumAmount) {
showPageNum = this.pageNum - (this.pageNumAmount >> 1); //显示的第一个页号
if (this.totalPage - showPageNum < this.pageNumAmount) {
//末尾几页页号数不够的时候
showPageNum = this.totalPage - this.pageNumAmount + 1;
}
showPageNum = showPageNum < 1 ? 1 : showPageNum;
}
this.showPageList = []; //先清空原来的数据
for (var currentNum = 0; currentNum < this.pageNumAmount; ++currentNum, showPageNum++) {
if (showPageNum > this.totalPage) {
break;
}
this.showPageList[showListIndex++] = showPageNum;
}
if (this.pageNum > this.totalPage && this.pageNum > 1){//删除最后一页的最后一条数据后,pageNum需要更改过来
if (this.totalPage > 0) {
this.pageNum = this.totalPage;
this.invokeAjaxMethod();
} else {//删除了所有数据
this.pageNum = 0;
}
}
},
getNumMultiple(rawNum, baseNum, greedy = true) {
var muti = parseInt(rawNum / baseNum);
if (greedy && rawNum % baseNum > 0) {
muti++;
}
return muti;
}
},
template:
'<div class="row">' +
'<div class="col-4">' +
'<div class="dataTables_info">共{{totalRows}}条记录</div>' +
'</div>' +
'<div class="col-4" style="white-space: nowrap;">' +
'<nav aria-label="..." class="float-right">' +
'<ul class="pagination pagination-sm">' +
'<li class="page-item">' +
'<button :class="pageNum <= 1 ? \'btn-disabled\': \'\'" class="page-link" @click="prePage()">上一页</button>' +
'</li>' +
'<li v-for="pageItemNo in showPageList" class="page-item" :class="pageItemNo == pageNum ? \'active\': \'\'">' +
'<a class="page-link" @click="goPage(pageItemNo)" href="javascript:void(0)">{{pageItemNo}}<span class="sr-only"></span></a>' +
'</li>' +
'<li class="page-item">' +
'<button :class="pageNum >= totalPage ? \'btn-disabled\' : \'\'" class="page-link" @click="nextPage()">下一页</button>' +
'</li>' +
'<span style="line-height: 27px;">共{{totalPage}}页</span>' +
'</ul>' +
'</nav>' +
'</div>' +
'<div class="col-4">' +
'<div style="display: inline-flex;">' +
'<label style="line-height: 27px;">到第</label>' +
'<input v-model="inputPage" class="form-control form-control-sm" type="text" style="width: 80px;" maxlength="10"/>' +
'<label style="line-height: 27px;">页</label>' +
'<button style="height: 30px;line-height: 15px;" @click="toSpecialPage" class="btn btn-confirm">确定</button>' +
'</div>' +
'</div>' +
'</div>'
});
//单元格组件
Vue.component('table-column', {
props: {
label: {//td展示的内容
default: ""
},
view: {//是否展示
type: Boolean,
default: true
},
render: {
type: Function
},
item: {
type: Object,
required: true
}
},
data(){
return {
itemStyle: ''
}
},
created() {
if (!this.view && "undefined" != typeof this.view) {
this.itemStyle += "display: none";
}
},
render(h) {
const that = this;
if (that.render){
return that.render(h, that.item);
}
return h("td", {
style: that.itemStyle,
class: 'table-td',
domProps: {
innerHTML:that.label
}
});
}
});
//操作按钮组件
Vue.component('table-button', {
props: {
item: {//该条内容
type: Object,
required: true
}
},
data(){
return {
cls: this.item.cls,
label: this.item.label
}
},
created(){
if (!this.cls) {
this.cls = "btn btn-outline-info btn-sm"
}
},
methods: {
clickEvt(){
this.$emit("handel-click", this.item);
}
},
template: `<button v-text="label" :class="cls" @click="clickEvt"></button>`
});
//表格组件
Vue.component("tt-table", {
props: {
pageSize: {
type: Number,
default: 2
},
rows: {//总数量
default: 0
},
pageNo: {//当前页
type: Number,
default: 1
},
paging: {
default: true
},
datas: {
type: Array,
required: true
},
columns: {
type: Array,
required: true
}
},
data(){
return {
needPaging: this.paging,
showList: []//当前页展示的内容
}
},
created(){
if (this.needPaging == "false"){
this.needPaging = false;//转换成boolean
} else {
if (this.needPaging && this.needPaging != "true"){
this.needPaging = true;
}
}
this.handlePaging(1);
},
render(h){
const that = this;
var cols = new Array();
var heads = new Array();
//组装column
that.columns.forEach((item, index) => {
var colWidth = item.width;
if (!colWidth) {
colWidth = "15%";
}
let columnStyle = {
width: colWidth
};
if (!item.view && typeof item.view != "undefined"){
columnStyle.display = "none";
}
cols.push(h("col", {
style: columnStyle
}));
if (item.sort){
heads.push(h("th", {
style: columnStyle
}, [
h("label", item.label),
h("span", {
on: {
click: function(){
that.handleOrder(item.code, true);
}
}
}, "↑"),
h("span", {
on: {
click: function(){
that.handleOrder(item.code, false);
}
}
}, "↓")]));
} else {
heads.push(h("th", {
style: columnStyle
}, item.label));
}
});
var tbodys = new Array();
that.showList.forEach((item, index) =>{
var tds = new Array();
that.columns.forEach((columnItem, columnIndex) =>{
if (columnItem.code == 'opt') {
//组装操作按钮
let optBtns = [];
columnItem.datas.forEach((btnItem) => {
optBtns.push(h("table-button", {
props: {
item: btnItem
},
on: {
'handel-click': function(obj){
btnItem.clickHander(obj);
}
}
}));
});
tds.push(h("td",{
props:{
label: ""
}
}, optBtns));
} else {
tds.push(h("table-column", {
props: {
label: item[ columnItem.code ],
item: item,
render: columnItem.render,
view: columnItem.view//是否展示
}
}));
}
});
tbodys.push(h("tr", tds));
});
if (that.needPaging) {
return h("div", [
h("div", {
class: "table-responsive",
style: "margin-top: 10px;"
}, [
h("table", {
class: "table mb-0 table-striped"
}, [
h("colgroup", cols),
h("thead", [
h('tr', heads)
]),
h("tbody", tbodys)
])
]),
h("page-split", {
props: {
totalRows: window.parseInt( that.rows ),
pageNum: that.pageNo,
pageSize: that.pageSize
},
domProps: {
pageNum: that.pageNo,
},
on: {
'switch-page': function(thisPage){
that.pageNo = thisPage;
that.handlePaging(thisPage);
}
}
})
]);
} else {
return h("div", {
class: "table-responsive",
style: "margin-top: 10px;"
}, [
h("table", {
class: "table mb-0 table-striped"
}, [
h("colgroup", cols),
h("thead", [
h('tr', heads)
]),
h("tbody", tbodys)
])
]);
}
},
methods: {
/**
* 处理排序
*/
handleOrder(sortedCode, isAsc){
//如果是数字或者时间则按照大小排序,字符串则按照长短进行排序
//找出一个非空的列
var firstSortVal = '';
for (let key in this.showList) {
if (this.showList[key][sortedCode]) {
firstSortVal = this.showList[key][sortedCode];
break;
}
}
if (!firstSortVal) {
return false;
}
//判断类型
var columnType = 0;//0-2分别标识string,number,date类型
if (typeof window.parseFloat(firstSortVal) == "number") {
columnType = 1;
} else {
let tempDate = new Date(firstSortVal);
if (tempDate instanceof Date) {
columnType = 2;
}
}
var sortedFunction = '';
switch (columnType){
case 0:
if (isAsc) {
sortedFunction = function(a, b){
return a[sortedCode].length < b[sortedCode].length ? -1 : 1;
}
} else {
sortedFunction = function(a, b){
return a[sortedCode].length > b[sortedCode].length ? -1 : 1;
}
}
break;
case 1:
if (isAsc) {
sortedFunction = function(a, b){
return window.parseFloat(a[sortedCode]) < window.parseFloat(b[sortedCode]) ? -1 : 1;
}
} else {
sortedFunction = function(a, b){
return window.parseFloat(a[sortedCode]) > window.parseFloat(b[sortedCode]) ? -1 : 1;
}
}
break;
case 2:
if (isAsc) {
sortedFunction = function(a, b){
return new Date( a[sortedCode] ) < new Date( b[sortedCode] ) ? -1 : 1;
}
} else {
sortedFunction = function(a, b){
return new Date( a[sortedCode] ) > new Date( b[sortedCode] ) ? -1 : 1;
}
}
break;
}
this.showList.sort(sortedFunction);
},
/**处理分页*/
handlePaging(pageNo){
var showList = this.datas.slice((pageNo -1) * this.pageSize, pageNo * this.pageSize);
this.showList = [].concat(showList);
}
}
});
//挂载实例:
new Vue({
el: "#app",
data: {
label: "hello world from vue inst",
rows: 0,
columns: [
{
code: "id",
label: "ID",
sort: true
},{
code: "name",
label: "姓名",
render: function(h, item){
const that = this;
return h({
template: "<td>" + that.label + "</td>"
});
}
},{
code: "age",
label: "年龄"
},
{
code: "opt",
label: "操作",
datas: [
{
label: "查看",
cls: 'class',
clickHander(item){
window.alert("click view:" + JSON.stringify(item));
}
},{
label: "编辑",
cls: 'class',
clickHander(item){
window.alert("click edit:" + JSON.stringify(item));
}
}
]
}
],
datas: [
{
id: "1",
name: "name1",
age: 21
},{
id: "2",
name: "name2",
age: 22
},
{
id: "3",
name: "name3",
age: 33
},{
id: "4",
name: "name4",
age: 44
},
{
id: "5",
name: "name5",
age: 55
},{
id: "6",
name: "name6",
age: 66
}
]
},
created(){
this.rows = this.datas.length;
}
});
</script>
</body>
</html>