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

news2024/11/26 10:29:31

前言

大家好,这里是符华~

之前写了一篇 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)) // 默认行高25
		if 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 = 1
			headRowNum = 2                          // 有标题是,为2
			buildTitle(e, sheet, title, endColName) // 构建标题
		}
		// 当有多行表头时,数据行号就是 表头数量+1,
		dataRow = dataRow + len(rowsHead) + 1
		for 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=2
	excel.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 = 3
	rows, _ := 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 = cellValue
				mergeStartCol = 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 + 1
					endRow = rowIndex + 1
					prevValue = 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.coloradmin.cn/o/1357606.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

使用fabric.js实现对图片涂鸦、文字编辑、平移缩放与保存功能

文章目录 背景1.初始化画布1.创建画布2.设置画布大小 2.渲染图片3.功能&#xff1a;开启涂鸦4.功能&#xff1a;添加文字5.旋转图片6.画布平移7.画布缩放8.保存图片9.上传图片10.销毁实例11.总结 背景 项目中有个需求&#xff0c;需要对图片附件进行简单的编辑操作&#xff0c…

网络故障问题一般性检查排查思路

一、基本连通性检查 在网络中ping是一个十分强大的TCP/IP工具。它可以用来检测网络的连通情况和分析网络速度、也可以ping网址根据域名得到服务器IP、同时我们根据ping返回的TTL值来判断对方所使用的操作系统及数据包经过路由器数量。 ping 网址&#xff0c;有几种输出情况&a…

【OpenCV】在MacOS上使用OpenCvSharp

前言 OpenCV是一个基于Apache2.0许可&#xff08;开源&#xff09;发行的跨平台计算机视觉和机器学习软件库&#xff0c;它具有C&#xff0c;Python&#xff0c;Java和MATLAB接口&#xff0c;并支持Windows&#xff0c;Linux&#xff0c;Android和Mac OS。OpenCvSharp是一个Op…

Flume基础知识(七):Flume 事务与 Flume Agent 内部原理

1. Flume 事务详解 2. Flume Agent 内部原理 重要组件&#xff1a; 1&#xff09;ChannelSelector ChannelSelector 的作用就是选出 Event 将要被发往哪个 Channel。其共有两种类型&#xff0c; 分别是 Replicating&#xff08;复制&#xff09;和 Multiplexing&#xff08;多…

如何计算非线性负载的功率需求?

非线性负载的功率需求计算是一个相对复杂的过程&#xff0c;因为非线性负载的电流和电压之间的关系不是简单的正比关系。在计算非线性负载的功率需求时&#xff0c;需要考虑负载的特性、工作状态以及电源电压等因素。 确定负载的类型&#xff1a;首先需要了解负载的具体类型&am…

MS713/MS713T:CMOS 低压、4Ω四路单刀单掷开关,替代ADG713

产品简述 MS713/MS713T 是一款单芯片 CMOS 4 路可选择开关&#xff0c;具有低 功耗、高开关速度、低导通阻抗、低漏电和高带宽特性。其工作 电压范围是 1.8V 到 5.5V &#xff0c;可以广泛应用在电池供电仪器仪表、新 一代的模数转换和数模转换系统中。其高带宽特性可用在 …

接口测试基础知识总结

一、HTTP 1、http请求头和响应头包含那些内容&#xff1f; 请求头信息 请求报头允许客户端向服务器端传递请求的附加信息以及客户端自身的信息。 2、常用的请求报头如下&#xff1a; Accept&#xff1a;浏览器可接受的MIME类型。 l MIME用于设定某种扩展名的文件用哪种应…

ubuntu远程桌面连接之vnc

一、前言 ubuntu安装图形化桌面以后,有些时候出于需要会想要进行远程桌面连接。ubuntu想要进行远程桌面连接就需要vnc服务的支持,安装vnc的方法有很多,博主也试过一些方式,但是安装完后使用vnc连接工具发现是花屏,无法正常使用。后来发现一种简单的方式即可配置好vnc连接,…

计算机毕业设计 基于Java的供应商管理系统的设计与实现 Java实战项目 附源码+文档+视频讲解

博主介绍&#xff1a;✌从事软件开发10年之余&#xff0c;专注于Java技术领域、Python人工智能及数据挖掘、小程序项目开发和Android项目开发等。CSDN、掘金、华为云、InfoQ、阿里云等平台优质作者✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精…

【赠书第16期】码上行动:用ChatGPT学会Python编程

文章目录 前言 1 ChatGPT简介 2 Python编程简介 3 使用ChatGPT学习Python编程 4 如何使用ChatGPT学习Python编程 5 推荐图书 6 粉丝福利 前言 随着人工智能技术的不断发展&#xff0c;聊天机器人已经成为我们日常生活和工作中不可或缺的一部分。其中&#xff0c;ChatGP…

MEW-UNet:医学图像分割中的频域多轴表示学习

文章目录 摘要1、简介2、相关研究2.1、医学图像分割2.2、基于ViT的技术 3、我们的方法3.1、准备工作3.2、多轴外部权重块3.3、外部权重生成器 4、实验4.1、数据集4.2、实现细节4.3、与当前最佳方法的比较4.4、消融实验4.5、可视化 5、结论声明 摘要 https://arxiv.org/pdf/231…

uniapp 无限级树形结构面包屑、单选-多选、搜索、移除功能插件,基于【虚拟列表】高性能渲染海量数据,加入动态高度、缓冲区

hyq-tree-vtw 无限级树形结构面包屑、单选-多选、搜索、移除功能 示例项目 单选-user 单选-任意一项 多选-关联下级 多选-任意一项 已选择数据弹框 说明 本插件需要使用uni-popup、uni-transition用于已选择数据弹框&#xff0c;因此需要有这些依赖,请自行导入本插件基于【虚…

Maven多模块项目架构配置介绍和实战

项目采用的是Maven多模块架构&#xff0c;项目的部分子模块的pom.xml中重复引用了相同的JAR包。很明显&#xff0c;当初在配置Maven模块的时候&#xff0c;没有考虑清楚各个模块的架构职责&#xff0c;同时也不了解Maven模块依赖的传递性。主要介绍一下Maven多模块的配置思路和…

基于深度学习大模型实现离线翻译模型私有化部署使用,通过docker打包开源翻译模型,可到内网或者无网络环境下运行使用,可以使用一千多个翻译模型语言模型进行翻译

基于深度学习大模型实现离线翻译模型私有化部署使用,通过docker打包开源翻译模型,可到内网或者无网络环境下运行使用,可以使用一千多个翻译模型语言模型进行翻译,想要什么语种直接进行指定和修改就行。 环境要求,电脑内存低于8G建议不要尝试了,有无GPU都可以运行,但是有…

geemap学习笔记041:Landsat Collection2系列数据去云算法总结

前言 去云算法是进行数据处理中所要进行一步重要操作&#xff0c;Sentinal-2数据中已经提供了去云算法&#xff0c;但是Landsat Collection2系列数据中并没有提供去云算法&#xff0c;下面就以Landsat 8 Collection2为例进行介绍。 1 导入库并显示地图 import ee import gee…

http 503 错误

503错误是一种HTTP状态码&#xff0c;表示你请求的网站或服务暂时不可用&#xff0c;通常是因为服务器过载或维护&#xff0c;你可能会看到类似这样的提示&#xff1a;503 Service Unavailable、503 Service Temporarily Unavailable、HTTP Server Error 503、HTTP Error 503 I…

Linux_CentOS_7.9配置时区及NTPdate同步之简易记录

前言&#xff1a;ntpdate命令来自英文词组”NTPdate“的拼写&#xff0c;其功能是用于设置日期和时间。ntpdate命令能够基于NTP协议设置Linux系统的本地日期和时间&#xff0c;利用NTP服务的时钟过滤器来选择最优方案&#xff0c;大大提高了可靠性和精度&#xff0c;让系统时间…

Django 快速整合 Swagger:实用步骤和最佳实践

Django &#xff0c;作为 Python 编写的一个优秀的开源 Web 应用框架&#xff0c;特别适用于快速开发的团队。对于很多场景来说&#xff0c;我们需要一份 API 文档&#xff0c;好处实在太多了&#xff1a; 提高开发效率&#xff1a;开发者可以基于 API 文档 快速学习和尝试 AP…

网络安全与IP地址:构建数字世界的前沿堡垒

网络安全是当今数字社会中不可忽视的挑战之一。而IP地址&#xff0c;作为互联网通信的基础协议&#xff0c;既是数字化时代的桥梁&#xff0c;也是网络安全的关键节点。本文将剖析IP地址在网络安全领域的作用&#xff0c;以及如何利用其特性建立有效的网络安全策略。 IP地址&a…

【Nodejs】基于node http模块的博客demo代码实现

目录 package.json www.js db.js app.js routes/blog.js controllers/blog.js mysql.js responseModel.js 无开发&#xff0c;不安全。 这个demo项目实现了用Promise异步处理http的GET和POST请求&#xff0c;通过mysql的api实现了博客增删改查功能&#xff0c;但因没有…