基于vxe-table实现翻转表格、固定列、展开收缩行、行列样式定制、合并行和列

2,919 阅读5分钟

起因

前阵子处理了个需求,做出来的效果如下图:

image.png

需求分析

我们的表格都是基于vxe-table实现,整理了下一共需要处理以下几点:

  • table数据行列倒置:按照一般逻辑,接口返回的数据是以时间为统计维度,但是页面的展示需要以类别为统计维度,所以需要用到行列倒置。
  • 行列合并:如上图,<账本1>、<账本2>做了行合并,第一、五行中的类型<支出>、<收入>以及最后一行的<合计>做了列合并
  • 展开收缩行
  • 特殊行、列样式定义:如合计行,类型列 需要定制样式
  • fixed固定列

需求处理

按照难易程度,先把问题具体化,然后按顺序依次解决

table数据行列倒置

共需要完成以下:

  1. 定义原始的表头数据

     data() {
         return {
             // 未进行翻转时的原始表头数据
             originalTitle:[
                 {field: 'outAll', title:'支出', sub: '支出'},
                 {field: 'transferOut', title:'支出', sub: ' 其中:转账'},
                 {field: 'redbagOut', title:'支出', sub: '其中:红包'},
                 {field: 'shopping', title:'支出', sub: '其中:购物'},
                 
                 {field: 'inAll', title:'收入', sub: '收入'},
                 {field: 'wages', title:'收入', sub: ' 其中:工资'},
                 {field: 'transferIn', title:'收入', sub: ' 其中:转账'},
                 {field: 'redbagIn', title:'收入', sub: ' 其中:红包'},
                 
                 {field: 'all', title:'合计', sub: '合计'},
             ],
         }
     }
    
  2. 根据表头数据以及接口返回的原始数据,计算拿到页面要展示的表格渲染参数以及表体数据

    表体数据:遍历原始表头数据,将title放在col0列,sub放在col1列,之后遍历接口拿到的数据,按顺序将field字段中的数据存储到对象中 表格渲染参数:前两列固定写死col0col1,剩下的遍历原始数据生成

     // 表格翻转
     reverseTable(table) {
         // 构建初始化表格数据
         // 表体参数 
         const buildData = this.originalTitle.map(col => {
             const item = {col0: col.title, col1: col.sub}
             table.forEach((row, index) => {
                 item[`col${index+2}`] = row[col.field]
             })
             return item
         })
         // 表格渲染参数 
         const buildColumns = [{
             field: 'col0',
             width: 80,
             headerAlign: 'center',
             title: ''
         }, {
             field: 'col1',
             width: 120,
             headerAlign: 'center',
             title: '类型',
         }]
         table.forEach((item,index) => {
             buildColumns.push({
                 field: `col${index+2}`,
                 minWidth: 180,
                 align: 'right',
                 title: item.title
             })
         })
         this.tableData = buildData;
         this.dataColumn = buildColumns;
     },
    
  3. template中使用高级表格,渲染看看

     <vxe-grid 
         border 
         show-overflow 
         resizable
         max-height="670"
         :show-header="true" 
         :columns="dataColumn" 
         :data="tableData"
         :auto-resize="true"
         :sync-resize="true"
     ></vxe-grid>
    

行列合并

vxe-table 行列合并的实现是通过:span-method="rowSpanMethod"rowSpanMethod中计算行列数

  • 只对第一、二列进行计算
  • 列合并的处理:判断第一列和第二列的值是否相同,若相同,其中一个列数设为2,另一个列数设为0
  • 行合并的处理:判断当前行和下一行,若值相同,行数+1(该循环跑至下一行的值与当前行值不同,结束循环)
  • template中增加配置:span-method="rowSpanMethod" 代码如下:
    // 计算合并行列
    rowSpanMethod({row, _rowIndex, column, visibleData}) {
        // 合并行和列 范围
        const fields = ['col0', 'col1'];
        const cellValue = row[column.property];
        if(cellValue && fields.includes(column.property)) {
            // 处理列合并
            if(column.property === 'col0' && row['col1'] === cellValue) {
                return { rowspan:0, colspan:0 }
            } else if(column.property === 'col1' && row['col0'] === cellValue) {
                return { rowspan:1, colspan:2 }
            }
            // 处理行合并
            let prevRow = visibleData[_rowIndex - 1];
            let nextRow = visibleData[_rowIndex + 1];
            if(prevRow && prevRow[column.property] === cellValue) {
                return { rowspan:1, colspan:1 }
            } else {
                let countRow = 1; // 用于计算下一行
                while(nextRow && nextRow[column.property] === cellValue) {
                    nextRow = visibleData[++countRow + _rowIndex]
                }
                return {rowspan:countRow, colspan:1}
            }
        }
    },

展开收缩行

vxe-table 的展开收缩核心是通过tree-config来配置

  • 数据处理:将要展开收缩的数据放到父级的children中,这里的children命名是可配置,在原先的数据处理中,追加以下逻辑:
      // 处理收缩展开行
      buildData[0].children = buildData.slice(1, 4)
      buildData.splice(1, 3);
      
      buildData[1].children = buildData.slice(2, 5)
      buildData.splice(2, 3);
    
  • template中增加配置:tree-config="{children:'children', toggleMethod: toggleMethod, indent: 0}"
  • 增加了展开收缩后,行列合并也需要对应处理下
    1. 手动给展开行打上标记:展开时expaned: true/false
    // 手动给展开行打标记 最后要有return
    toggleMethod(e) {
        e.row.expanded = e.expanded;
        return true;
    },
    
    1. 行列合并时,兼容展开收缩功能。
    • 列合并不影响
    • 行合并处理:基于原先行合并的逻辑,通过debugger了解到:
      1. 展开行的_rowIndex都为-1
      2. 通过expaned来判断是否为展开行
      3. 若为展开行,通过children.length来追加跨行数
    // 处理行合并
    let prevRow = visibleData[_rowIndex - 1];
    let nextRow = visibleData[_rowIndex + 1];
    // 处理展开行,展开的子行 _rowIndex 都为-1
    if(_rowIndex === -1) {
        if(column.property === 'col0') return { rowspan:0, colspan:0 };
        else return { rowspan:1, colspan:1 }
    } else if(prevRow && prevRow[column.property] === cellValue) {
        return { rowspan:1, colspan:1 }
    } else {
        let countRow = 1; // 用于计算下一行
        let rowspan = 1; // 用于计算实际跨行数目
        if(row.expanded && row.children?.length && column.property === 'col0') {
            rowspan += row.children.length
        }
        while(nextRow && nextRow[column.property] === cellValue) {
            if(nextRow.expanded && nextRow.children?.length && nextRow.property === 'col0') {
                rowspan += nextRow.children.length
            }
            nextRow = visibleData[++countRow + _rowIndex]
            rowspan++
        }
        return {rowspan:rowspan, colspan:1}
    }
    

特殊行、列样式定义

行列样式定义的,就很简单了,通过:cell-style="cellStyle"配置即可,cellStyle方法如下:

// 计算行列样式
cellStyle({row, column}) {
    // 合计行 样式调整
    if(row.col1 === '合计') {
        return {
            backgroundColor: '#ffffcc',
            color: '#606266',
            fontWeight: 'bold'
        }
    }
    // 固定展示的两列 样式定制
    if(['col0', 'col1'].includes(column.property)) {
        return {
            backgroundColor:'#f8f8f9',
            color:'#606266'
        }
    }
}

fixed固定列

最后一个,在表格渲染参数中,对应列增加fixed: left即可

最后,附上以上功能的核心代码,感兴趣的话可以理解下逻辑,copy下来改改就能跑通啦

核心代码如下:

<vxe-grid 
    border 
    show-overflow 
    resizable
    max-height="670"
    :tree-config="{children:'children', toggleMethod: toggleMethod, indent: 0}"
    :show-header="true" 
    :span-method="rowSpanMethod"
    :columns="dataColumn" 
    :data="tableData"
    :auto-resize="true"
    :sync-resize="true"
    :cell-style="cellStyle"
></vxe-grid>
data() {
    return {
        // 计算后的表头
        dataColumn:[], 
        // 计算后的表格数据
        tableData: [], 
        // 未进行翻转时的原始表头数据
        originalTitle:[
            {field: 'outAll', title:'支出', sub: '支出'},
            {field: 'transferOut', title:'支出', sub: ' 其中:转账'},
            {field: 'redbagOut', title:'支出', sub: '其中:红包'},
            {field: 'shopping', title:'支出', sub: '其中:购物'},
            
            {field: 'inAll', title:'收入', sub: '收入'},
            {field: 'wages', title:'收入', sub: ' 其中:工资'},
            {field: 'transferIn', title:'收入', sub: ' 其中:转账'},
            {field: 'redbagIn', title:'收入', sub: ' 其中:红包'},
            
            {field: 'all', title:'合计', sub: '合计'},
        ],
    }
},
methods: {
    // 列表查询
    async getList() {
        let { data } = await handleGetList();
        this.reverseTable(data.list)
    },
    
    // 表格翻转
    reverseTable(table) {
        // 构建初始化表格数据
        const buildData = this.originalTitle.map(col => {
            const item = {col0: col.title, col1: col.sub}
            table.forEach((row, index) => {
                item[`col${index+2}`] = row[col.field]
            })
            return item
        })
        // 处理收缩展开行
        buildData[0].children = buildData.slice(1, 4)
        buildData.splice(1, 3);
        
        buildData[1].children = buildData.slice(2, 5)
        buildData.splice(2, 3);
        
        const buildColumns = [{
            field: 'col0',
            fixed: 'left', // 定义固定列
            width: 80,
            headerAlign: 'center',
            title: ''
        }, {
            field: 'col1',
            fixed: 'left', // 定义固定列
            width: 120,
            headerAlign: 'center',
            title: '类型',
            treeNode: true // 定义展开收缩行
        }]
        table.forEach((item,index) => {
            buildColumns.push({
                field: `col${index+2}`,
                minWidth: 180,
                align: 'right',
                title: item.title,
                formatter:this.formatterMoney
            })
        })
        
        this.tableData = buildData;
        this.dataColumn = buildColumns;
    },
    // 手动给展开行打标记 最后要有return
    toggleMethod(e) {
        e.row.expanded = e.expanded;
        return true;
    },
    // 计算合并行列
    rowSpanMethod({row, _rowIndex, column, visibleData}) {
        // 合并行和列 范围
        const fields = ['col0', 'col1'];
        const cellValue = row[column.property];
        if(cellValue && fields.includes(column.property)) {
            // 处理列合并
            if(column.property === 'col0' && row['col1'] === cellValue) {
                return { rowspan:0, colspan:0 }
            } else if(column.property === 'col1' && row['col0'] === cellValue) {
                return { rowspan:1, colspan:2 }
            }
            // 处理行合并
            let prevRow = visibleData[_rowIndex - 1];
            let nextRow = visibleData[_rowIndex + 1];
            // 处理展开行,展开的子行 _rowIndex 都为-1
            if(_rowIndex === -1) {
                if(column.property === 'col0') return { rowspan:0, colspan:0 };
                else return { rowspan:1, colspan:1 }
            } else if(prevRow && prevRow[column.property] === cellValue) {
                return { rowspan:1, colspan:1 }
            } else {
                let countRow = 1; // 用于计算下一行
                let rowspan = 1; // 用于计算实际跨行数目
                if(row.expanded && row.children?.length && column.property === 'col0') {
                    rowspan += row.children.length
                }
                while(nextRow && nextRow[column.property] === cellValue) {
                    if(nextRow.expanded && nextRow.children?.length && nextRow.property === 'col0') {
                        rowspan += nextRow.children.length
                    }
                    nextRow = visibleData[++countRow + _rowIndex]
                    rowspan++
                }
                return {rowspan:rowspan, colspan:1}
            }
        }
    },
    // 计算行列样式
    cellStyle({row, column}) {
       // 合计行 样式调整
       if(row.col1 === '合计') {
           return {
               backgroundColor: '#ffffcc',
               color: '#606266',
               fontWeight: 'bold'
           }
       }
       // 固定展示的两列 样式定制
       if(['col0', 'col1'].includes(column.property)) {
           return {
               backgroundColor:'#f8f8f9',
               color:'#606266'
           }
       }
    }
}