Merge branch 'xjjdev' into dev

This commit is contained in:
徐俊杰 2023-03-18 10:22:09 +08:00
commit d8d16033f7
10 changed files with 0 additions and 590 deletions

Binary file not shown.

View File

@ -1,70 +0,0 @@
// Package excel -----------------------------
// @file : templateInter_test.go
// @author : JJXu
// @contact : wavingbear@163.com
// @time : 2022/7/23 15:59
// -------------------------------------------
package example
import (
"fmt"
"gingogo2/utils/excel"
"path/filepath"
"strings"
"testing"
"time"
)
//定义sheet表结构
type sheet1Define struct {
Xid int `json:"xid" form:"xid" db:"column:xid;comment: "`
Name string `json:"name" form:"name" db:"column:name;comment: "`
Age int `json:"age" form:"age" db:"column:age;comment: "`
}
func TestExcelTpl_WriteToExcel(t *testing.T) {
var sheet1Data = []sheet1Define{
{1, "张三", 16},
{2, "黑猫警长", 18},
}
var sheet1 = excel.NewSheet("Sheet1", sheet1Data)
//var suffixFunc = func() string { return fmt.Sprintf("%v", time.Now().Unix()) }
exCreator, err := excel.NewExcelTemplate("demo.xlsx", "./", "./demo.xlsx", sheet1)
if err != nil {
t.Log(err)
}
exCreator.UseOption(func(excel *excel.Excel) {
ext := filepath.Ext(excel.SaveName)
name := strings.Split(excel.SaveName, ext)[0]
excel.SaveName = fmt.Sprintf("%s_%v%s", name, time.Now().Unix(), ext)
})
path, name, err := exCreator.WriteToFile()
if err != nil {
t.Log(err)
} else {
t.Log(path, name)
}
}
func TestReadExcel(t *testing.T) {
var file = "demo_1671435052.xlsx"
ex := excel.Excel{OriginFilePath: file}
var datas []sheet1Define
err := ex.ReadSheetData("Sheet1", func(rowIndex int, rows []string) {
if rowIndex == 0 {
//跳过首行
return
}
datas = append(datas, sheet1Define{
Xid: excel.Int[int](rows[0]),
Name: rows[1],
Age: excel.Int[int](rows[2]),
})
})
if err != nil {
t.Error(err.Error())
}
fmt.Println(datas)
}

View File

@ -1,253 +0,0 @@
// Package excel -----------------------------
// @file : templateInter.go
// @author : JJXu
// @contact : wavingbear@163.com
// @time : 2022/7/23 15:34
// -------------------------------------------
package excel
import (
"errors"
"fmt"
"github.com/xuri/excelize/v2"
"io"
"log"
"path/filepath"
"reflect"
"sync"
)
var (
ErrSheetNotExist = errors.New("sheet does not exist")
ErrSheetDataFrormat = errors.New("sheet data format must be slice or struct")
//ErrSheetNameNotInTemplate = errors.New("sheet name not in emailTemplate")
)
//======================================================================================================================
// Sheet define
// 抽象工作簿
type Sheet interface {
GetData() any
SheetName() string
}
// 实例化工作簿
func NewSheet(sheetName string, datas any) Sheet {
return &newSheetDefine{
Datas: datas,
Name: sheetName,
}
}
// 定义一个能够通用的工作簿结构此结构必须遵循Sheet接口规范
type newSheetDefine struct {
Datas any
Name string
}
func (s *newSheetDefine) GetData() any {
return s.Datas
}
func (s *newSheetDefine) SheetName() string {
return s.Name
}
//======================================================================================================================
// Sheet define
// WriteToExcel 通过模板文件写入数据并另存为
// param fileName : 文件名
// param filesSuffix : 文件后缀名生成函数
// param fileRoot : 导出目录
// param templatePath : 模板文件路径
// param sheets : Sheet类型的数据类型为[]Sheet
// return path : 文件路径
// return exfileName : 导出后的文件名
// return err
func WriteToExcel(fileName string, fileRoot string, templatePath string, sheets ...Sheet) (path string, exFileName string, err error) {
var exc *Excel
exc, err = NewExcelTemplate(fileName, fileRoot, templatePath, sheets...)
if err != nil {
return
}
return exc.WriteToFile()
}
// ReadDataFromExcel 从excel文件读取数据
func ReadDataFromExcel(filepath string, sheetName string, handler func(rowIndex int, rows []string)) error {
ex := Excel{OriginFilePath: filepath}
return ex.ReadSheetData(sheetName, handler)
}
// ReadDataFromBytes 从io口读取数据用户http上传的附件
func ReadDataFromBytes(file io.Reader, sheetName string, handler func(rowIndex int, rows []string)) error {
exce, err := excelize.OpenReader(file)
if err != nil {
return err
}
var ex = Excel{ex: exce}
return ex.ReadSheetData(sheetName, handler)
}
// 新建工作表实例
func NewExcelTemplate(fileName string, fileRoot string, templatePath string, sheets ...Sheet) (exc *Excel, err error) {
exc = &Excel{
SaveRoot: fileRoot,
SaveName: fileName,
OriginFilePath: templatePath,
rwLock: sync.RWMutex{},
}
if sheets != nil {
err = exc.AddSheets(sheets...)
if err != nil {
return
}
}
return exc, nil
}
type Excel struct {
ex *excelize.File
SaveRoot string
SaveName string
OriginFilePath string
Sheets map[string]Sheet
rwLock sync.RWMutex
Opts []Option
}
// UseOption 使用可选项
func (s *Excel) UseOption(opts ...Option) {
if opts != nil {
s.Opts = append(s.Opts, opts...)
}
}
// 添加工作簿
// 注意如果添加相同的工作簿,之前的会被覆盖
func (s *Excel) AddSheets(sheets ...Sheet) (err error) {
if s.Sheets == nil {
s.Sheets = make(map[string]Sheet, 0)
}
for _, sheet := range sheets {
var sheetName = sheet.SheetName()
s.Sheets[sheetName] = sheet
}
return
}
// 删除工作簿
func (s *Excel) DeleteSheets(sheetName string) error {
if s.Sheets == nil {
return nil
} else if s.Sheets[sheetName] != nil {
delete(s.Sheets, sheetName)
} else {
return ErrSheetNotExist
}
return nil
}
// 读取工作簿
func (s *Excel) ReadSheetData(sheetName string, handler func(rowIndex int, rows []string)) (err error) {
if s.ex == nil {
s.ex, err = excelize.OpenFile(s.OriginFilePath)
if err != nil {
return
}
}
datas, err := s.ex.GetRows(sheetName)
for i, rows := range datas {
handler(i, rows)
}
return nil
}
// 写入到文件
func (s *Excel) WriteToFile() (path string, fileName string, err error) {
if s.ex == nil {
s.ex, err = excelize.OpenFile(s.OriginFilePath)
if err != nil {
return
}
}
if s.Opts != nil {
for _, opt := range s.Opts {
opt(s)
}
}
//插入数据
var cellNameList []string
for sheetName, st := range s.Sheets {
var firstRow = s.getFirstEmptyRowIndex(s.ex, sheetName)
var SheetData = reflect.ValueOf(st.GetData())
var SheetType = reflect.TypeOf(st.GetData())
switch SheetData.Kind() {
case reflect.Slice:
cellNameList = s.getJsonFieldList(SheetType)
var rowLen = SheetData.Len()
for i := 0; i < rowLen; i++ {
var dataMap = s.dataToMap(SheetData.Index(i), SheetType)
for column, v := range cellNameList {
var axis = GetCellIndex(i+firstRow+1, column+1)
err = s.ex.SetCellValue(sheetName, axis, dataMap[v])
if err != nil {
fmt.Println(err.Error())
}
}
}
case reflect.Struct:
cellNameList = s.getJsonFieldList(SheetType)
var dataMap = s.dataToMap(SheetData, SheetType)
for column, v := range cellNameList {
var axis = GetCellIndex(firstRow+1, column+1)
err = s.ex.SetCellValue(sheetName, axis, dataMap[v])
if err != nil {
fmt.Println(err.Error())
}
}
default:
return "", "", ErrSheetDataFrormat
}
}
//保存
path = filepath.ToSlash(filepath.Join(s.SaveRoot, s.SaveName))
s.rwLock.Lock()
if err = s.ex.SaveAs(path); err != nil {
log.Println(fmt.Sprintf("save file error :%v", err))
s.rwLock.Unlock()
return
}
s.rwLock.Unlock()
return
}
// getJsonFieldList 获取json字段列表
func (s *Excel) getJsonFieldList(sheetType reflect.Type) (tagList []string) {
t := sheetType.Elem()
for i := 0; i < t.NumField(); i++ {
tagList = append(tagList, t.Field(i).Tag.Get("json"))
}
return
}
// dataToMap 数据转字典
func (s *Excel) dataToMap(sheet reflect.Value, sheetType reflect.Type) (dataMap map[string]any) {
dataMap = make(map[string]any)
t := sheetType.Elem()
for i := 0; i < t.NumField(); i++ {
dataMap[t.Field(i).Tag.Get("json")] = sheet.Field(i).Interface()
}
return dataMap
}
// getFirstEmptyRowIndex 获取首个空行的索引位置
func (s *Excel) getFirstEmptyRowIndex(ex *excelize.File, sheetName string) (index int) {
rows, err := ex.GetRows(sheetName)
if err != nil {
return 1
}
return len(rows)
}

View File

@ -1,38 +0,0 @@
// Package excel -----------------------------
// @file : excel_test.go
// @author : JJXu
// @contact : wavingBear@163.com
// @time : 2022/12/19 16:52:11
// -------------------------------------------
package excel
import (
"reflect"
"testing"
)
//定义sheet表结构
type sheet1Define struct {
Xid int `json:"xid" form:"xid" db:"column:xid;comment: "`
Name string `json:"name" form:"name" db:"column:name;comment: "`
Age int `json:"age" form:"age" db:"column:age;comment: "`
}
func TestSlice(t *testing.T) {
var sheet1Datas = []sheet1Define{
{1, "张三", 16},
{2, "黑猫警长", 18},
}
var sheet1 = NewSheet("Sheet1", &sheet1Datas)
d := sheet1.GetData()
v := reflect.ValueOf(d)
switch v.Kind() {
case reflect.Slice:
t.Log(v.Index(0))
case reflect.Struct:
t.Log(v)
default:
t.Log("错误格式")
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

View File

@ -1,27 +0,0 @@
// Package excel -----------------------------
// @file : options.go
// @author : JJXu
// @contact : wavingBear@163.com
// @time : 2022/12/19 12:41:40
// -------------------------------------------
package excel
import (
"fmt"
"path/filepath"
"strings"
"time"
)
type Option func(excel *Excel)
//func AddSaveFileSuffixWithUnixTime(excel *Excel) {
// excel.SaveName
// fmt.Sprintf("%v", time.Now().Unix())
//}
// 时间戳作为文件后缀
func OptionFileNameSuffixWithUnixTime(excel *Excel) {
ext := filepath.Ext(excel.SaveName)
name := strings.Split(excel.SaveName, ext)[0]
excel.SaveName = fmt.Sprintf("%s_%v%s", name, time.Now().Unix(), ext)
}

View File

@ -1,161 +0,0 @@
# excel文件生成器
## excel文件生成器简介
此模块提供了通过`.xlsx`格式的excel模板文件创建文件并自动添加数据的方法。
此模块通过设计模式,在"github.com/xuri/excelize/v2"的基础上封装了`ExcelCreator`这一泛型的方法。适用于快速开发报表导出功能。
### 基本使用示例
```go
package main
import (
"fmt"
"github.com/flipped-aurora/gin-vue-admin/server/utils/excel"
"github.com/flipped-aurora/gin-vue-admin/server/utils/simpletime"
"sync"
"time"
)
//读写锁
var exportEpidemicPreventStaticStaticLock sync.RWMutex
//定义sheet1数据结构
type EpidemicPreventStaticReport struct {
Xid int `json:"xid" form:"xid" db:"column:xid;comment:序号"`
Time string `json:"time" form:"time" db:"column:time;comment:日期"`
Name string `json:"name" form:"name" db:"column:name;comment:项目名称"`
Code string `json:"code" form:"code" db:"column:code;comment:监督备案号"`
Street string `json:"street" form:"street" db:"column:street;comment:街道"`
State string `json:"state" form:"state" db:"column:state;comment:状态:未完成、已完成"`
}
//导出数据
func (m EpidemicPreventStaticReport) WriteToExcel(datas []EpidemicPreventStaticReport) (path string, filename string, err error) {
defer exportEpidemicPreventStaticStaticLock.Unlock()
//实例化sheet1,载入数据
st1 := excel.NewSheet("Sheet1", datas)
var suffixName = func() string {
return fmt.Sprintf("%v", simpletime.TimeToString(time.Now(), simpletime.TimeFormat.NoSpacer_YMDhms))
}
//实例化excelCreator
exc, err := excel.NewExcelCreator("防疫日报日完成项目数统计.xlsx", &suffixName, "uploads/template2/file", "uploads/template2/防疫日报日完成项目数统计.xlsx", st1)
if err != nil {
fmt.Println(err.error())
}
exportEpidemicPreventStaticStaticLock.Lock()
// 导出数据
return exc.WriteToExcel()
}
func main(){
var (
reportData []EpidemicPreventStaticReport
excel EpidemicPreventStaticReport
)
//do something
//..
//..
path,filename,err:=excel.WriteToExcel(reportData)
if err!=nil{
fmt.Println(err.Error())
}else{
fmt.Printf("path:%v\nfilename:%v\n",path,filename)
}
}
```
## 使用说明
此模块的使用流程如下
### 1. 创建excel模板定义好工作簿名称和表头
请注意,程序默认数据都是一行行连续的。如果两条数据之间间隔了一个或多个空行,导出的数据可能会出现错误。
![img.png](img.png)
### 2. 定义工作簿数据结构
注意事项:
1. 结构体字段顺序要与工作簿的表头顺序一一对应、命名规则随意;
2. json标签用于数据的转换同时也便于将结构体直接作为接口请求参数来使用;没有json标签时数据将会被忽略;
```go
type Sheet1 struct {
Xid int `json:"xid"`
Name string `json:"name"`
Age int `json:"age"`
}
```
### 3. 准备数据,实例化sheet对象
数据类型至此结构体和切片<br/>
`NewSheet`方法的第一个参数是工作簿名称,需要与模板文件中的对应,不然导出数据时会报错
```go
//准备数据
var sheet1Data = []Sheet1Define{
{1, "张三", 16},
{2, "黑猫警长", 18},
}
//实例化sheet对象
var sheet1 = excel.NewSheet("Sheet1", &sheet1Data)
```
### 4 生成excel文件
### 4.1 直接生成文件
如果你不需要什么额外操作只想直接生成excel文件那么可以直接调用这个方法
```go
//定义后缀名生成器如果不需要可以传nil
var suffixFunc = func() string { return fmt.Sprintf("%v", time.Now().Unix()) }
//导出文件
path,err:=excel.WriteToExcel("demo.xlsx", &suffixFunc, "./", "./demo.xlsx", sheet_1)
```
#### 生成效果:
![img_1.png](img_1.png)
### 4.2 `ExcelCreator`
ExcelCreator可以用来对工作簿和工作簿中的数据进行增删改查以及生成文件。
#### 4.2.1 实例化`ExcelCreator`
```go
var suffixFunc = func() string { return fmt.Sprintf("%v", time.Now().Unix()) } //文件后缀名生成方法
exCreator,err := excel.NewExcel("demo.xlsx", &suffixFunc, "./", "./demo.xlsx", sheet_1)
if err!=nil{
fmt.Println(err.Error())
}
//可连续添加多个工作簿或者不传,当然也支持一张工作簿数据的多次传入
//exCreator,err := excel.NewExcel("demo.xlsx", &suffixFunc, "./", "./demo.xlsx", sheet_1,sheet_2,sheet_3)
//exCreator,err := excel.NewExcel("demo.xlsx", &suffixFunc, "./", "./demo.xlsx")
//exCreator,err := excel.NewExcel("demo.xlsx", &suffixFunc, "./", "./demo.xlsx", sheet_1_1,sheet_1_2,sheet_1_3)
//不使用文件后缀名
//exCreator,err := excel.NewExcelCreator("demo.xlsx", nil, "./", "./demo.xlsx", sheet_1)
//exCreator,err := excel.NewExcelCreatorWithoutSuffix("demo.xlsx","./", "./demo.xlsx", sheet_1)
```
#### `FileName``filesSuffix`参数的说明
当设置了`filesSuffix`参数后创建excel文件时会自动生成文件后缀名。
如`FileName`设置为"demo.xlsx"、`filesSuffix`返回值为"20200723",那么最终文件名为"demo20200723.xlsx"
### 4.2.2 新增工作簿
如果工作簿已存在,数据会组合而不是覆盖
```go
var err = exCreator.SheetsAdd(sheet_2)
```
### 4.2.3 删除工作簿
```go
var err = exCreator.SheetsDelete(sheet_2.SheetName())
```
### 4.2.5 取出工作簿map和缓存数据
工作簿存储在`ExcelCreator.Sheets`中,存储结构为`map[string][]sheet`。string即工作簿名称。取出后可以进行任意的操作
```go
var sheets = exCreator.Sheets
datas:= sheets["Sheet1"].GetDatas
//do somthing...
```
### 4.2.6 将数据生成到文件
```go
path,fileName, err := exCreator.WriteToExcel()
if err != nil {
fmt.Println(err.Error())
} else {
fmt.Println(path)
fmt.Println(fileName)
}
```
## 相关问题
### 1. 为什么不支持通过代码直接生成表头?
鉴于在部分场景下,表头格式比较复杂,且通过代码生成灵活较度差,单元格格式通过代码设置比较繁琐。相比较下没有直接创建excel文件模板来调整更为直观方便所以没做此方面的设计。

View File

@ -1,41 +0,0 @@
// Package utils -----------------------------
// @file : excelHelper.go
// @author : JJXu
// @contact : wavingbear@163.com
// @time : 2022/6/9 13:41
// -------------------------------------------
package excel
import (
"fmt"
"strconv"
)
//行列坐标值转换为excel的坐标。注意row和columnCount的初始值都是1
func GetCellIndex(row int, columnCount int) string {
var column = getColumnIndex(columnCount)
return fmt.Sprintf("%s%d", column, row)
}
//获取excel的列索引
var columnIndices = []string{"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"}
func getColumnIndex(num int) string {
num--
var column = columnIndices[num%26]
for num = num / 26; num > 0; num = num / 26 {
column = columnIndices[(num-1)%26] + column
num--
}
return column
}
func Int[T int | int | uint8 | uint32 | uint64 | int32 | int64](value string) T {
v, _ := strconv.Atoi(value)
return T(v)
}
func Float[T float64 | float32](value string) T {
v, _ := strconv.ParseFloat(value, 64)
return T(v)
}