【Go】excelize库实现excel导入导出封装(二),基于map、多个sheet、多级表头、树形结构表头导出,横向、纵向合并单元格导出

前言

大家好,这里是符华~

之前写了一篇 go excelize库封装导入导出 的博客,然后那篇博客还挖了个坑,结果这个坑差点就填不上了🤣还好经过我的不懈努力,总算是把坑给填上了。。。

挖坑

上一篇文章中,我们实现了:通用的导入,普通导出、动态列导出、隔行背景色、自适应行高。这篇文章一开始就列出来要实现哪些功能,然后在结尾处,我还说会弄完那几个未完成的功能。结果我后面开发的时候,才发现,这有个别功能不简单啊,搞了我好久没做出来(死活实现不出来,我想着要不算了。。坑了就坑了😣)。

在这里插入图片描述

其实多个sheet、基于map导出这两个我都很早就实现了,然后因为我一直做不出复杂表头,纵向合并这个功能也就没去动手实现。复杂表头 这块我点名批评,太难搞了,我按照之前的想法死活实现不了😣。也不知道是哪里有问题,看起来思路是对的,但实现效果就是还差点。差点死心放弃了。。

好在前几天,不知道为什么,突然开窍,想到了其他办法,一试,果然可以,就是这样的!然后后面的合并单元格也自然而然地做出来了!!之前就不该死磕一开始那个有问题的方式😂😂。


实现

废话不多说(已经说了挺多废话了),我们直接来看本篇要实现哪些东西:

  • 多个sheet导出
  • 基于map导出
  • 多级表头、树形结构表头导出
  • 横向合并单元格导出
  • 纵向合并单元格导出

多个sheet导出

咱们先从简单的开始,由简入难

// 测试结构体
type Test struct {Id       string `excel:"name:用户账号;"`Name     string `excel:"name:用户姓名;"`Email    string `excel:"name:用户邮箱;width:25;"`Com      string `excel:"name:所属公司;"`Dept     bool   `excel:"name:所在部门;replace:false_超级管理员,true_普通用户;"`RoleName string `excel:"name:角色名称;replace:1_超级管理员,2_普通用户;"`Remark   int    `excel:"name:备注;replace:1_超级管理员,2_普通用户;width:40;"`
}// 要导出的列表
var testList = []Test{{"fuhua", "符华", "fuhua@123.com", "太虚剑派", false, "1", 1},{"baiye", "白夜", "baiye@123.com", "天命科技有限公司", false, "2", 1},{"chiling", "炽翎", "chiling@123.com", "太虚剑派", false, "2", 2},{"yunmo", "云墨", "yunmo@123.com", "太虚剑派", false, "1", 2},{"yuelun", "月轮", "yuelun@123.com", "天命科技有限公司", false, "1", 1},{"xunyu", "迅羽","xunyu@123.com哈哈哈哈哈哈哈哈这里是最大行高测试哈哈哈哈哈哈哈哈这11111111111里是最大行高测试哈哈哈哈哈哈哈哈这里是最大行高测试","天命科技有限公司", true, "2",124},
}// 多个sheet导出
func ExportSheets() {// 获取导出的数据changeHead := map[string]string{"Id": "账号", "Name": "真实姓名"}// 多个sheet导出e := model.ExcelInit()for i := 0; i < 3; i++ {sheet := "Sheet" + fmt.Sprintf("%d", i+1)title := "用户信息" + fmt.Sprintf("%d", i+1)fmt.Println(sheet)// 其实就是相当于普通sheet导出,只不过是每个sheet分别传对应的数据过去err := excel.ExportExcel(sheet, title, "", true, false, testList, changeHead, e)if err != nil {fmt.Println(err)return}}e.F.Path = "C:\\Users\\Administrator\\Desktop\\多个sheet导出.xlsx"if err := e.F.Save(); err != nil {fmt.Println(err)return}
}

实现效果:

在这里插入图片描述

基于map导出

// 基于map的导出
func ExportMap() {// 表头header := []string{"Name", "Age", "City"}// map数据data := []map[string]interface{}{{"Name": "符华", "Email": "fuhua@123.com", "City": "惠州"},{"Name": "陈悦", "Email": "chenyue@qq.com", "City": "深圳"},{"Name": "鹤熙", "Email": "hexi@123.com", "City": "广州"},}f, err := excel.MapExport(header, data, "Sheet1", "", false)if err != nil {fmt.Println("导出失败", err)return}// 保存文件err = f.SaveAs("C:\\Users\\Administrator\\Desktop\\map导出.xlsx")if err != nil {fmt.Println(err)return}fmt.Println("Excel文件已成功导出")
}

map数据导出,构建数据内容,不能用之前这个 normalBuildDataRow 函数构建,需要重新写构建实现。不过构建map数据比构建结构体数据要简单,结构体数据需要反射,map直接遍历就行。

// MapExport map导出
func MapExport(heads interface{}, list []map[string]interface{}, sheet, title string, isGhbj bool) (file *excelize.File, err error) {e, lastRowHead, endColName, dataRow, err := buildCustomHeader(heads, sheet, title)if err != nil {return nil, err}// 构建数据行for _, rowData := range list {startCol := fmt.Sprintf("A%d", dataRow)endCol := fmt.Sprintf("%s%d", endColName, dataRow)row := make([]interface{}, 0)for _, v := range lastRowHead {if val, ok := rowData[v]; ok {row = append(row, val)}}if isGhbj && dataRow%2 == 0 {_ = e.F.SetCellStyle(sheet, startCol, endCol, e.ContentStyle2)} else {_ = e.F.SetCellStyle(sheet, startCol, endCol, e.ContentStyle1)}_ = e.F.SetRowHeight(sheet, dataRow, float64(25)) // 默认行高25if err := e.F.SetSheetRow(sheet, fmt.Sprintf("A%d", dataRow), &row); err != nil {return nil, err}dataRow++}return e.F, nil
}

buildCustomHeader 这个函数,是用来构建表头的,简单表头、复杂表头都可以构建。往下看就知道了。

实现效果:

在这里插入图片描述

复杂表头:多级表头、树形结构表头导出

重点来了,多级表头,按照我之前的想法是:根据表头层级结构,递归表头,计数每级表头有多少个子级,按照子级的数量去合并上级。

在这里插入图片描述

按照这个方法,我是试了又试,合并的单元格总是还差一点,给我搞得没脾气了。。。

后面我突然想到,复杂表头的重点难道不是横向合并单元格吗?!数据行的内容填充肯定是按照最后一级的表头来填充的,最后一级有几列,那数据行就有几列,都是对应的。 而最后一级以上的表头,都是按照相同内容合并的! 那我的表头数据,是不是就不用是树形结构或层级结构? 每级表头只需要按照最后一级表头的数量进行填充就行,相同就相同呗,反正会被合并。

觉得我上面说得绕,那直接看表头数据就明白了:

在这里插入图片描述

这种表头数据结构是有顺序的,必须按照从上往下顺序存储。

也就是说,从一级表头开始到二级、三级…最后一级,每一级表头必须按从上往下顺序存储,最后一级表头必须放在数组最后。

并且每一级表头的列数必须一致,所有表头的列数必须以最后一级表头的列数为准,如果列不够填相同的内容即可,后续会将相同的内容的列合并。

在这里插入图片描述

构建表头时,如果是相同内容,就会合并。这个方法比较麻烦的一点是,假如表头数据是动态的,比如是从数据库查出来的,那要处理成这种格式,还挺麻烦的应该🤐。

// 复杂表头导出
func ExportTree() {// map导出/*header := [][]string{{"一级表头1", "一级表头1", "一级表头1", "一级表头1", "一级表头1", "一级表头1", "一级表头1", "一级表头1", "一级表头2", "一级表头2", "一级表头2", "一级表头2"},{"二级表头1", "二级表头1", "二级表头1", "二级表头1", "二级表头1", "二级表头2", "二级表头2", "二级表头2", "二级表头3", "二级表头3", "二级表头3", "二级表头3"},{"三级表头1", "三级表头1", "三级表头2", "三级表头3", "三级表头3", "三级表头4", "三级表头4", "三级表头5", "三级表头6", "三级表头6", "三级表头6", "三级表头7"},{"四级表头1", "四级表头2", "四级表头3", "四级表头4", "四级表头5", "四级表头6", "四级表头7", "四级表头8", "四级表头9", "四级表头10", "四级表头11", "四级表头12"},}var data = []map[string]interface{}{{"四级表头1": "1", "四级表头2": "1", "四级表头3": "1", "四级表头4": "4", "四级表头5": "5", "四级表头6": "6", "四级表头7": "7", "四级表头8": "8", "四级表头9": "9", "四级表头10": "10", "四级表头11": "11", "四级表头12": "12"},{"四级表头1": "11", "四级表头2": "22", "四级表头3": "33", "四级表头4": "44", "四级表头5": "55", "四级表头6": "66", "四级表头7": "77", "四级表头8": "88", "四级表头9": "99", "四级表头10": "100", "四级表头11": "111", "四级表头12": "122"},{"四级表头1": "111", "四级表头2": "222", "四级表头3": "333", "四级表头4": "444", "四级表头5": "555", "四级表头6": "666", "四级表头7": "777", "四级表头8": "888", "四级表头9": "999", "四级表头10": "1000", "四级表头11": "1111", "四级表头12": "1222"},}f, err := excel.MapExport(header, data, "Sheet1", "这里是标题", false)// 合并表头单元格// 没有title时,表头从第一行开始合并startRowNum=1;// 有title时,表头从第二行开始合并,startRowNum=2;// endRowNum=6,表示内容行开始不再需要合并excel.HorizontalMerge(f, "Sheet1", 2, 6)*/// 结构体导出(自定义表头)header := [][]string{{"基本信息", "基本信息", "基本信息", "基本信息", "基本信息", "其他信息", "其他信息"},{"用户信息", "用户信息", "用户信息", "部门信息", "部门信息", "角色信息", "备注"},{"用户信息", "用户信息", "用户信息", "所属公司", "所在部门", "角色信息", "备注"},{"用户账号", "用户姓名", "用户邮箱", "所属公司", "所在部门", "角色名称", "备注"},}var data = []Test{{"云墨", "云墨", "云墨", "太虚剑派", false, "1", 1},{"fuhua", "炽翎", "炽翎", "炽翎", false, "1", 1},{"月轮", "月轮", "yuelun@123.com", "yuelun@123.com", true, "2", 2},{"admin", "admin", "admin", "admin", false, "1", 2},{"符华", "符华", "admin@123.com", "天命科技有限公司", false, "1", 1},{"chenyue", "chenyue", "chenyue@123.com", "天命科技有限公司", true, "2", 124},{"鹤熙", "鹤熙", "鹤熙", "天命科技有限公司", true, "2", 124},}f, err := excel.CustomHeaderExport("Sheet1", "这里是标题", true, header, data)if err != nil {panic(err)}// 合并表头单元格excel.HorizontalMerge(f, "Sheet1", 2, 5)// 纵向合并数据行内容excel.VerticalMerge(f, "Sheet1", 1, nil)// 保存文件err = f.SaveAs("C:\\Users\\Administrator\\Desktop\\复杂表头导出.xlsx")if err != nil {fmt.Println(err)return}fmt.Println("Excel文件已生成")
}

CustomHeaderExport 函数:

// CustomHeaderExport 自定义表头导出
func CustomHeaderExport(sheet, title string, isGhbj bool, heads interface{}, list interface{}) (file *excelize.File, err error) {e, _, endColName, dataRow, err := buildCustomHeader(heads, sheet, title)if err != nil {return}dataValue := reflect.ValueOf(list)// 判断数据的类型if dataValue.Kind() != reflect.Slice {err = errors.New("invalid data type")return}// 构造数据行err = normalBuildDataRow(e, sheet, endColName, "", dataRow, isGhbj, false, dataValue)return e.F, err
}

构建自定义表头:

// 构建自定义复杂表头
func buildCustomHeader(heads interface{}, sheet, title string) (*model.Excel, []string, string, int, error) {rowsHead := [][]string{}  // 存储多行表头lastRowHead := []string{} // 最后一行表头// 类型断言,判断是单行表头还是多行表头switch heads.(type) {case []string: // 单行表头lastRowHead = heads.([]string)case [][]string: // 复杂表头// 复杂表头规定:从一级表头开始到二级、三级...最后一级,每一级表头必须按从上往下顺序存储,最后一级表头必须放在数组最后// 每一级表头的列数必须一致,也就是说所有表头的列数必须以最后一级表头的列数为准,如果列不够填相同的内容即可,后续会将相同的内容的列合并。// 例如下面这组数据,有四级表头,最后一级有6列,所以一、二、三级表头也需要有6列。然后每一行相同内容的列会合并。/*header := [][]string{{"一级表头1", "一级表头1", "一级表头1", "一级表头1", "一级表头2", "一级表头2"},{"二级表头1", "二级表头1", "二级表头2", "二级表头2", "二级表头3", "二级表头3"},{"三级表头1", "三级表头1", "三级表头2", "三级表头2", "三级表头3", "三级表头4"},{"四级表头1", "四级表头2", "四级表头3", "四级表头4", "四级表头5", "四级表头6"},}*/rowsHead = heads.([][]string)lastRowHead = rowsHead[len(rowsHead)-1] // 在多行表头中,获取最后一行表头default:return nil, nil, "", 0, errors.New("表头格式错误")}e := model.ExcelInit()index, _ := e.F.GetSheetIndex(sheet)if index < 0 { // 如果sheet名称不存在e.F.NewSheet(sheet)}endColName := GetExcelColumnName(len(lastRowHead)) // 根据列数生成 Excel 列名dataRow := 0                                       // 数据行开始的行号,有title时,默认为3(1 为title行,2 为表头行,3 开始就是数据行,包括了3),无title时默认为2(1 为表头行 2 开始就是数据行,包括了2)if len(rowsHead) > 0 {headRowNum := 1 // 第一行表头行号if title != "" {dataRow = 1headRowNum = 2                          // 有标题是,为2buildTitle(e, sheet, title, endColName) // 构建标题}// 当有多行表头时,数据行号就是 表头数量+1,dataRow = dataRow + len(rowsHead) + 1for i, items := range rowsHead {err := buildHeader(e, sheet, endColName, i+headRowNum, &items) // 构建表头if err != nil {fmt.Println(err)return nil, nil, "", 0, err}}} else {dataRow, _ = buildTitleHeader(e, sheet, title, endColName, &lastRowHead) // 构建标题和表头}e.F.SetColWidth(sheet, "A", endColName, float64(20)) // 设置列宽return e, lastRowHead, endColName, dataRow, nil
}

构建标题和表头:

// 构建标题
func buildTitle(e *model.Excel, sheet, title, endColName string) (dataRow int) {dataRow = 2 // 开始的数据行号,默认为1表示一定有一行表头,数据行从第二行开始// 标题默认在第一行if title != "" {dataRow = 3 // 为3表示有一行标题和一行表头,数据行从第三行开始e.F.SetCellValue(sheet, "A1", title)e.F.MergeCell(sheet, "A1", endColName+"1") // 合并标题单元格e.F.SetCellStyle(sheet, "A1", endColName+"1", e.TitleStyle)e.F.SetRowHeight(sheet, 1, float64(30)) // 第一行行高}return
}// 构建表头:headerRowNum 当前表头行行号
func buildHeader(e *model.Excel, sheet, endColName string, headerRowNum int, heads *[]string) (err error) {row := fmt.Sprintf("%d", headerRowNum)e.F.SetRowHeight(sheet, headerRowNum, float64(30))e.F.SetCellStyle(sheet, "A"+row, endColName+row, e.HeadStyle)return e.F.SetSheetRow(sheet, "A"+row, heads)
}// 构建标题和表头:headerRowNum 当前表头行行号
func buildTitleHeader(e *model.Excel, sheet, title, endColName string, heads *[]string) (dataRow int, err error) {dataRow = buildTitle(e, sheet, title, endColName) // 构建标题,获取第一行数据所在的行号// dataRow-1:表头行所在的行号err = buildHeader(e, sheet, endColName, dataRow-1, heads)return
}

效果:

在这里插入图片描述

在这里插入图片描述

横向合并单元格

上面的复杂表头,就是用到了横向合并。

// 横向合并单元格导出
func ExportHorizontal() {// 结构体数据导出var data = []Test{{"云墨", "云墨", "云墨", "太虚剑派", false, "1", 1},                             // A2:C2 , E2:G2{"fuhua", "炽翎", "炽翎", "炽翎", false, "1", 1},                            // B3:D3 , E2:G2{"月轮", "月轮", "yuelun@123.com", "yuelun@123.com", true, "2", 2},        // A4:B4 , C4:D4 , E4:G4{"admin", "admin", "admin", "admin", false, "1", 2},                   // A5:D5 , E5:F5{"符华", "符华", "admin@123.com", "天命科技有限公司", false, "1", 1},              // A6:B6 , E6:G6{"chenyue", "chenyue", "chenyue@123.com", "天命科技有限公司", true, "2", 124}, // A7:B7 , E7:F7{"鹤熙", "鹤熙", "鹤熙", "天命科技有限公司", true, "2", 124},                        // A8:C8 , E8:F8}f, err := excel.NormalDynamicExport("Sheet1", "", "", false, false, data, nil)// map数据导出/*header := []string{"账号", "姓名", "部门", "角色", "备注"}var data = []map[string]interface{}{{"账号": "符华", "姓名": "符华", "部门": "符华", "角色": "太虚剑派", "备注": "太虚剑派"},           // A2:C2 , D2:E2{"账号": "云墨", "姓名": "云墨", "部门": "太虚剑派", "角色": "太虚剑派", "备注": "太虚剑派"},         // A3:B3 , C3:E3{"账号": "月轮", "姓名": "月轮", "部门": "天命科技有限公司", "角色": "天命科技有限公司", "备注": "太虚剑派"}, // A4:B4 , C4:D4{"账号": "鹤熙", "姓名": "天命科技有限公司", "部门": "天命科技有限公司", "角色": "鹤熙", "备注": "鹤熙"},   // B5:C5 , D5:E5}f, err := excel.MapExport(header, data, "Sheet1", "", false)*/if err != nil {fmt.Println(err)return}// 横向合并单元格:没有标题只有一行表头,所以内容从第二行开始合并 startRowNum=2excel.HorizontalMerge(f, "Sheet1", 2, -1) // endRowNum = -1,表示全部每一行都需要合并//excel.HorizontalMerge(f, "Sheet1", 2, 6) // endRowNum = 6,表示第6行开始,后面的行不进行合并(包括第6行)// 保存文件err = f.SaveAs("C:\\Users\\Administrator\\Desktop\\横向合并导出.xlsx")if err != nil {fmt.Println(err)return}fmt.Println("Excel文件已生成")
}

横向合并规定了开始合并的行号、结束合并的行号:

  • startRowNum:开始合并的行号,注意是行号,不是索引,最小从1开始,
  • endRowNum:结束合并的行号,最小从1开始。从endRowNum开始,包括endRowNum这一行不进行合并;不需要停止合并的话传 -1。

endRowNum = -1,表示全部的行只要有相同内容,都需要合并;
endRowNum = 6,表示第6行开始,后面的行不进行合并(包括第6行)。

// 横向合并单元格:startRowNum(开始合并的行号,注意是行号从1开始,不是索引)endRowNum(停止合并的行号,从1开始,从endRowNum开始,包括这一行不进行合并;不需要停止合并的话传-1)
func HorizontalMerge(f *excelize.File, sheet string, startRowNum, endRowNum int) {// startRowNum:比如第一行是标题,第二行是表头,所以从第三行开始合并,startRowNum = 3rows, _ := f.GetRows(sheet) // 获取sheet的所有行,包括 标题、表头行(如果有标题和表头的话)// row 行号,从1开始for row := 1; row <= len(rows); row++ {if row < startRowNum { // 如果当前行号,小于开始合并的行号,则跳过continue}if endRowNum > 0 && row >= endRowNum { // 如果当前行号,大于等于结束合并的行号,退出合并break}prevValue := ""     // 上一单元格的值mergeStartCol := 0  // 开始合并的单元格列索引cols := rows[row-1] // 当前行的列数据(当前行每个单元格的数据)// 遍历单元格时,判断当前单元格和上一单元格的值是否相同,相同继续,不同则判断合并,并且将当前单元格的值和索引,赋值给对应的变量。/** 比如:a,b,b,b,c,c 这六个单元格的值,第一个值 a != "",进入判断, i-mergeStartCol = 0-0,不进行合并,prevValue=a,mergeStartCol=0第二个值 b != a,进入判断,i-mergeStartCol = 1-0,不进行合并,prevValue=b,mergeStartCol=1第三、四个值 不进入 cellValue != prevValue 的判断,i分别为2、3第五个值 c != b,进入判断,i-mergeStartCol = 4-1,合并 B1:D1,prevValue=c,mergeStartCol=4第六个值不进入 cellValue != prevValue 的判断,也结束了for循环,会在 len(cols)-mergeStartCol > 0 这个判断里面进行合并*/for i, col := range cols {cellValue := col // 当前单元格的值// 如果当前单元格的值和上一个单元格的值不相等if cellValue != prevValue {// 当前单元格的列索引 - 开始合并的单元格列索引 大于1,则进行合并if i-mergeStartCol > 1 {// 获取开始合并的单元格startCell := GetExcelColumnName(mergeStartCol+1) + fmt.Sprintf("%d", row)// 获取结束合并的单元格endCell := GetExcelColumnName(i) + fmt.Sprintf("%d", row)//fmt.Print(startCell, ":", endCell, ",")f.MergeCell(sheet, startCell, endCell)}prevValue = cellValuemergeStartCol = i}}// 如果最后一个值和上一个值不同,则肯定会合并前面的单元格;如果最后一个值和上一个值相同,则会在这个判断里面进行合并if len(cols)-mergeStartCol > 0 {startCell := GetExcelColumnName(mergeStartCol+1) + fmt.Sprintf("%d", row)endCell := GetExcelColumnName(len(cols)) + fmt.Sprintf("%d", row)//fmt.Println(startCell, ":", endCell)f.MergeCell(sheet, startCell, endCell)}}
}

效果:

在这里插入图片描述

纵向合并单元格

实现方式和横向合并一个道理,只不过横向合并是 从行到列 ,纵向合并是 从列到行

// 纵向合并单元格导出
func ExportVertical() {// map数据//header := []string{"所属公司", "所属部门", "姓名", "职位", "联系电话"}header := [][]string{{"基本信息", "基本信息", "基本信息", "基本信息", "基本信息"},{"部门信息", "部门信息", "用户信息", "用户信息", "用户信息"},{"所属公司", "所属部门", "姓名", "职位", "联系电话"},}var data = []map[string]interface{}{{"所属公司": "太虚剑派", "所属部门": "开发部", "姓名": "符华", "职位": "部门经理", "联系电话": "1321"},{"所属公司": "太虚剑派", "所属部门": "开发部", "姓名": "炽翎", "职位": "部门经理", "联系电话": "46545"},{"所属公司": "太虚剑派", "所属部门": "开发部", "姓名": "云墨", "职位": "部门主管", "联系电话": "13212"},{"所属公司": "太虚剑派", "所属部门": "财务部", "姓名": "赤鸢", "职位": "部门主管", "联系电话": "13212"},{"所属公司": "太虚剑派", "所属部门": "财务部", "姓名": "华", "职位": "员工", "联系电话": "13212"},{"所属公司": "天命科技", "所属部门": "财务部", "姓名": "白夜", "职位": "组长", "联系电话": "dfgdfg"},{"所属公司": "天命科技", "所属部门": "研发部", "姓名": "月轮", "职位": "组长", "联系电话": "45645"},{"所属公司": "天命科技", "所属部门": "研发部", "姓名": "迅羽", "职位": "组长", "联系电话": "45645"},}f, err := excel.MapExport(header, data, "Sheet1", "", false)if err != nil {panic(err)}needColIndex := []int{1, 2} // 需要合并的列号,比如只需要合并第一列和第二列// 横向合并表头行内容excel.HorizontalMerge(f, "Sheet1", 1, 4)// 纵向合并数据行内容excel.VerticalMerge(f, "Sheet1", 0, needColIndex)// 结构体数据导出/*var data = []Test{{"赤鸢", "云墨", "chiyuan", "太虚剑派", false, "1", 1},{"赤鸢", "炽翎", "chiyuan", "太虚剑派", false, "1", 1},{"赤鸢", "炽翎", "chiyuan", "太虚剑派", true, "2", 2},{"赤鸢", "云墨", "chiyuan", "太虚剑派", false, "1", 2},{"符华", "白夜", "fuhua", "天命科技", false, "1", 1},{"符华", "月轮", "fuhua", "天命科技", true, "2", 121},{"符华", "迅羽", "fuhua", "天命科技", true, "2", 121},}f, err := excel.NormalDynamicExport("Sheet1", "", "", false, false, data, nil)if err != nil {panic(err)}// 纵向合并内容单元格excel.VerticalMerge(f, "Sheet1", 0, nil)*/// 保存文件err = f.SaveAs("C:\\Users\\Administrator\\Desktop\\纵向合并导出.xlsx")if err != nil {fmt.Println(err)return}fmt.Println("Excel文件已生成")
}

纵向合并,有两参数:
headIndex :表头所在索引,一般情况下,不管表头有多少行,只要有 title 标题 headIndex都传1无 title 标题传0
needColIndex :需要合并的列号,注意不是列索引,列号从1开始,如全部列有相同内容都需合并,传nil就行。

// 纵向合并单元格:headIndex 表头所在索引(一般情况下,不管表头有多少行,只要有标题headIndex都传1,无标题传0)
// needColIndex 需要合并的列号(列号从1开始,如全部列都需合并,传nil就行)
func VerticalMerge(f *excelize.File, sheet string, headIndex int, needColIndex []int) {rows, _ := f.GetRows(sheet) // 获取sheet的所以行,包括 标题、表头行(如果有标题和表头的话)// 遍历每一列for colIndex := 1; colIndex <= len(rows[headIndex]); colIndex++ {if len(needColIndex) > 0 && !model.IsContain(needColIndex, colIndex) {continue}startRow := headIndex + 1 // 开始合并的行号endRow := headIndex + 1   // 结束结束的行号prevValue := rows[headIndex][colIndex-1]// 遍历每一行for rowIndex := headIndex; rowIndex < len(rows); rowIndex++ {row := rows[rowIndex]// 因为获取rows时,会忽略空单元格,如果存在空单元格,那每一行的列数并不是相同的,所以需要判断列号是否大于当前行的列数if colIndex <= len(row) {// 判断当前单元格的值和上一个单元格的值是否相同if row[colIndex-1] == prevValue {endRow = rowIndex + 1 // 相同,则更新结束合并的行号} else {if startRow != endRow {colName := GetExcelColumnName(colIndex)f.MergeCell(sheet, colName+fmt.Sprintf("%d", startRow), colName+fmt.Sprintf("%d", endRow))}startRow = rowIndex + 1endRow = rowIndex + 1prevValue = row[colIndex-1]}}}// 处理最后一组相同内容的单元格if startRow != endRow {colName := GetExcelColumnName(colIndex)f.MergeCell(sheet, colName+fmt.Sprintf("%d", startRow), colName+fmt.Sprintf("%d", endRow))}}
}

效果:

在这里插入图片描述

在这里插入图片描述

最后

好歹是把坑给填上了😂😁

除了上面说的几个复杂导出,其实还有一个很重要的导出,那就是用模板导出。然而 excelize 库并不支持类似Java easypoi 的模板指令,这么看来 excelize 库好像不支持用模板导出?得想其他办法实现这个功能,所以下一篇我们来讲讲Go中如何用excel模板导出excel表格。

后续等功能都实现得差不多了,测试完了没什么问题了,我会放出完整代码。

如果大家觉得本篇文章或专栏对你有所帮助或者觉得写得还可以的话,欢迎大家多多给博主 点赞关注 支持一下哦😘你动动手指就是对我莫大的鼓励🥰

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/231207.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

鸿蒙 Window 环境的搭建

鸿蒙操作系统是国内自研的新一代的智能终端操作系统&#xff0c;支持多种终端设备部署&#xff0c;能够适配不同类别的硬件资源和功能需求。是一款面向万物互联的全场景分布式操作系统。 下载、安装与配置 DevEco Studio支持Windows系统和macOS系统 Windows系统配置华为官方推…

LVGL核心部件——弧(arc)控件的介绍

概述 本文介绍LVGL核心部件——弧&#xff08;arc&#xff09;&#xff0c;它由背景和前景弧组成。前景&#xff08;指示器&#xff09;可以进行触摸调整。 LVGL核心部件——弧&#xff08;arc&#xff09;控件 一、部件和样式 LV_PART_MAIN 使用典型的背景样式属性绘制背景&…

基于PHP的校园代购商城系统

有需要请加文章底部Q哦 可远程调试 基于PHP的校园代购商城系统 一 介绍 此校园代购商城系统基于原生PHP开发&#xff0c;数据库mysql&#xff0c;前端bootstrap。系统角色分为用户和管理员。(附带参考设计文档) 技术栈&#xff1a;phpmysqlbootstrapphpstudyvscode 二 功能 …

制造业企业使用SD-WAN的意义

在信息技术和制造业越来越密不可分的背景下&#xff0c;推进智能制造&#xff0c;需要升级网络支撑工业互联网平台的搭建、数字化车间、智能工厂的建设等等。SD-WAN的应用使得制造业企业网络升级更为方便、快捷、低成本。 制造业企业总部、分支机构、工厂一般分布较为分散&…

Android MVP 写法

前言 Model&#xff1a;负责数据逻辑 View&#xff1a;负责视图逻辑 Presenter&#xff1a;负责业务逻辑 持有关系&#xff1a; 1、View 持有 Presenter 2、Model 持有 Presenter 3、Presenter 持有 View 4、Presenter 持有 Model 辅助工具&#xff1a;ViewBinding 执行…

静态网页设计——跑友原创区(HTML+CSS+JavaScript)

前言 声明&#xff1a;该文章只是做技术分享&#xff0c;若侵权请联系我删除。&#xff01;&#xff01; 感谢大佬的视频&#xff1a; https://www.bilibili.com/video/BV1AK411x75x/?vd_source5f425e0074a7f92921f53ab87712357b 使用技术&#xff1a;HTMLCSSJS 主要内容&am…

全网最细,接口+接口自动化测试面试题汇总(附回答)

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 1、我们测试的接口…

02-微服务-Eureka注册中心

Eureka注册中心 假如我们的服务提供者user-service部署了多个实例&#xff0c;如图&#xff1a; 大家思考几个问题&#xff1a; order-service在发起远程调用的时候&#xff0c;该如何得知user-service实例的ip地址和端口&#xff1f;有多个user-service实例地址&#xff0c;…

「Vue3面试系列」Vue3 所采用的 Composition Api 与 Vue2 使用的 Options Api 有什么不同?

文章目录 开始之前正文一、Options Api二、Composition Api三、对比逻辑组织Options APICompostion API 逻辑复用 小结 开始之前 Composition API 可以说是Vue3的最大特点&#xff0c;那么为什么要推出Composition Api&#xff0c;解决了什么问题&#xff1f; 通常使用Vue2开…

杨中科 ASP.NETCore开发效率利器 HOT RELOAD

HOT RELOAD 1、困惑:修改了服务器端的代码&#xff0c;必须重新运行程序。 2、方法1: [启动 (不调试) ] 3、方法2: .NET 6开始的Hot Reload(热重载) 正常修改代码后 不重启&#xff0c;是无法看到新的数据展示在页面 修改 运行结果&#xff1a; 方式一&#xff1a;设置开始…

具有大电流,双通道 12V,短地短电源保护等功能的国产芯片GC8549 可替代ONSEMI的LV8548/LV8549

GC8549 可以工作在 3.8~12V 的电源电压上&#xff0c;每 通道能提供高达 1.5A 持续输出电流或者 2.5A 峰值 电流&#xff0c;睡眠模式下功耗小于 1uA。具有 PWM&#xff08;IN/EN&#xff09;输入接口,与行业标 准器件兼容&#xff0c;并具有过温保护&#xff0c;欠压保护&…

使用 PHP-FFMpeg 操作视频/音频文件

做音频合成的时候找到的一个php操作ffmpeg 的类库。GitHub地址&#xff1a;https://github.com/PHP-FFMpeg/PHP-FFMpeg/。本文的例子大部分都是上面的 在使用之前请安装好 FFMpeg 。如何安装&#xff1f;请看 FFmpeg 安装教程。 使用composer快速安装 > composer require …

Vue3-33-路由-路由的别名配置 alias

别名的作用 路由中的别名配置&#xff0c;可以实现 多个路径 对应 同一个路由。 例如 &#xff1a; 路由的路径是 /a; 配置别名为 &#xff1a; /a2; 则 访问 /a 或 /a2 的时候&#xff0c;都可以访问到 同一个组件。 别名的特点 关键字 &#xff1a; alias 当通过别名进行路由…

UE5 C++(十二)— 委托(代理)、多播委托

这里写目录标题 介绍定义声明委托绑定委托执行委托 单播委托多播委托动态多播代理 介绍 这个官网上有很详细介绍&#xff0c;这里介绍几个点 定义 委托 是一种泛型但类型安全的方式&#xff0c;可在C对象上调用成员函数。可使用委托动态绑定到任意对象的成员函数&#xff0c…

爬虫基础一(持续更新)

爬虫概念&#xff1a; 通过编写程序&#xff0c;模拟浏览器上网&#xff0c;然后让其去互联网上抓取数据的过程 分类&#xff1a; 1&#xff0c;通用爬虫&#xff1a;抓取一整张页面数据 2&#xff0c;聚焦爬虫&#xff1a;抓取页面中的局部内容 3&#xff0c;增量式爬虫&…

星穹铁道1.5版本活动【狐斋志异】,有哪些有趣故事和彩蛋

狐斋志异算是玩梗集大成者&#xff0c;加上剧情内补全了一些故事设定。版本活动名称【狐斋志异】致敬的是清朝小说家蒲松龄创作的《聊斋志异》。聊斋志异被国人所熟悉的莫过于里面的鬼怪故事&#xff0c;因此又称为《鬼狐传》。 这次【狐斋志异】开拓任务也是围绕着开拓者去十王…

鸿鹄电子招投标系统:源码级别解析电子招投标的精髓

招投标管理系统是一个集门户管理、立项管理、采购项目管理、采购公告管理、考核管理、报表管理、评审管理、企业管理、采购管理和系统管理于一体的综合性应用平台。它适用于招标代理、政府采购、企业采购和工程交易等业务的企业&#xff0c;旨在提高项目管理的效率和质量。该系…

Java后端开发——Ajax、jQuery和JSON

Java后端开发——Ajax、jQuery和JSON 概述 Ajax全称是Asynchronous Javascript and XML&#xff0c;即异步的JavaScript和 XML。Ajax是一种Web应用技术&#xff0c;该技术是在JavaScript、DOM、服务器配合下&#xff0c;实现浏览器向服务器发送异步请求。 Ajax异步请求方式不…

Docker中镜像的相关操作

1.辅助操作 docker version&#xff1a;用查看docker客户端引擎和server端引擎版本信息。 docker info&#xff1a;用来查看docker引擎的详细信息。 docker --help&#xff1a;用来查看帮助信息。 2.镜像Image docker images&#xff1a;查看当前本地仓库中存在哪些镜像。 …

速通C语言第十二站 文件操作

系列文章目录 速通C语言系列 速通C语言第一站 一篇博客带你初识C语言 http://t.csdn.cn/N57xl 速通C语言第二站 一篇博客带你搞定分支循环 http://t.csdn.cn/Uwn7W 速通C语言第三站 一篇博客带你搞定函数 http://t.csdn.cn/bfrUM 速通C语言第四站 一篇博客带…