chore: 并入 iotlcm-app 历史到 app/(原提交 f40bda9)
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
package iot_excelize
|
||||
|
||||
import (
|
||||
"github.com/xuri/excelize/v2"
|
||||
)
|
||||
|
||||
type BatchResultRow struct {
|
||||
RowNo int
|
||||
ItemID string
|
||||
Status string
|
||||
ErrorReason string
|
||||
}
|
||||
|
||||
type BatchInfoItem struct {
|
||||
Field string
|
||||
Value string
|
||||
}
|
||||
|
||||
func BuildBatchResultExcel(rows []BatchResultRow, infos []BatchInfoItem) (*excelize.File, error) {
|
||||
f := excelize.NewFile()
|
||||
leftStyleID, err := f.NewStyle(&excelize.Style{
|
||||
Alignment: &excelize.Alignment{
|
||||
Horizontal: "left",
|
||||
Vertical: "center",
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resultSheet := "导入结果"
|
||||
if err := f.SetSheetName("Sheet1", resultSheet); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
headers := []string{"源文件行号", "item_id", "处理状态", "异常原因"}
|
||||
if err := f.SetSheetRow(resultSheet, "A1", &headers); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for i, item := range rows {
|
||||
values := []interface{}{item.RowNo, item.ItemID, item.Status, item.ErrorReason}
|
||||
cell, err := excelize.CoordinatesToCellName(1, i+2)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = f.SetSheetRow(resultSheet, cell, &values); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
resultEndCell, err := excelize.CoordinatesToCellName(4, len(rows)+1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := f.SetCellStyle(resultSheet, "A1", resultEndCell, leftStyleID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
infoSheet := "批次信息"
|
||||
if _, err := f.NewSheet(infoSheet); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
infoHeaders := []string{"字段", "值"}
|
||||
if err := f.SetSheetRow(infoSheet, "A1", &infoHeaders); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for i, item := range infos {
|
||||
values := []interface{}{item.Field, item.Value}
|
||||
cell, err := excelize.CoordinatesToCellName(1, i+2)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = f.SetSheetRow(infoSheet, cell, &values); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
infoEndCell, err := excelize.CoordinatesToCellName(2, len(infos)+1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := f.SetCellStyle(infoSheet, "A1", infoEndCell, leftStyleID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_ = f.SetColWidth(resultSheet, "A", "A", 14)
|
||||
_ = f.SetColWidth(resultSheet, "B", "B", 24)
|
||||
_ = f.SetColWidth(resultSheet, "C", "C", 12)
|
||||
_ = f.SetColWidth(resultSheet, "D", "D", 40)
|
||||
_ = f.SetColWidth(infoSheet, "A", "B", 24)
|
||||
f.SetActiveSheet(0)
|
||||
return f, nil
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
package iot_excelize
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
"github.com/xuri/excelize/v2"
|
||||
)
|
||||
|
||||
// BuildByStructs creates an Excel workbook from a struct slice without writing
|
||||
// it to HTTP response or disk.
|
||||
func BuildByStructs(tags []string, list interface{}, sheetName string) (f *excelize.File, err error) {
|
||||
return BuildByStructsWithOption(tags, list, sheetName, nil)
|
||||
}
|
||||
|
||||
// BuildByStructsWithOption creates an Excel workbook and applies optional
|
||||
// styles or advanced customizations.
|
||||
func BuildByStructsWithOption(tags []string, list interface{}, sheetName string, option *BuildOption) (f *excelize.File, err error) {
|
||||
if sheetName == "" {
|
||||
sheetName = "Sheet1"
|
||||
}
|
||||
|
||||
f = excelize.NewFile()
|
||||
if err = f.SetSheetName("Sheet1", sheetName); err != nil {
|
||||
_ = f.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = f.SetSheetRow(sheetName, "A1", &tags); err != nil {
|
||||
_ = f.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
length := len(tags)
|
||||
rowNum := 1
|
||||
for _, item := range gconv.Interfaces(list) {
|
||||
row := structValues(item)
|
||||
rowNum++
|
||||
if err = f.SetSheetRow(sheetName, "A"+gconv.String(rowNum), &row); err != nil {
|
||||
_ = f.Close()
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if err = applyBuildOption(f, sheetName, length, rowNum, option); err != nil {
|
||||
_ = f.Close()
|
||||
return nil, err
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
|
||||
func applyBuildOption(f *excelize.File, sheetName string, colCount int, rowCount int, option *BuildOption) error {
|
||||
if option == nil {
|
||||
return nil
|
||||
}
|
||||
if option.HeaderHeight > 0 {
|
||||
if err := f.SetRowHeight(sheetName, 1, option.HeaderHeight); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if colCount > 0 && option.ColWidth > 0 {
|
||||
lastCol := letter(colCount)[colCount-1]
|
||||
if err := f.SetColWidth(sheetName, "A", lastCol, option.ColWidth); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if colCount > 0 && option.HeaderStyle != nil {
|
||||
styleID, err := f.NewStyle(option.HeaderStyle)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
lastCell, err := excelize.CoordinatesToCellName(colCount, 1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = f.SetCellStyle(sheetName, "A1", lastCell, styleID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if colCount > 0 && rowCount > 1 && option.RowStyle != nil {
|
||||
styleID, err := f.NewStyle(option.RowStyle)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
startCell := "A2"
|
||||
endCell, err := excelize.CoordinatesToCellName(colCount, rowCount)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = f.SetCellStyle(sheetName, startCell, endCell, styleID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if option.AfterBuild != nil {
|
||||
return option.AfterBuild(f, sheetName)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func structValues(item interface{}) []interface{} {
|
||||
value := reflect.ValueOf(item)
|
||||
if !value.IsValid() {
|
||||
return nil
|
||||
}
|
||||
if value.Kind() == reflect.Ptr {
|
||||
if value.IsNil() {
|
||||
return nil
|
||||
}
|
||||
value = value.Elem()
|
||||
}
|
||||
if value.Kind() != reflect.Struct {
|
||||
return []interface{}{item}
|
||||
}
|
||||
return collectStructValues(value)
|
||||
}
|
||||
|
||||
func collectStructValues(value reflect.Value) []interface{} {
|
||||
var row []interface{}
|
||||
for i := 0; i < value.NumField(); i++ {
|
||||
fieldValue := value.Field(i)
|
||||
fieldInfo := value.Type().Field(i)
|
||||
if !fieldInfo.IsExported() {
|
||||
continue
|
||||
}
|
||||
if fieldInfo.Anonymous && derefType(fieldInfo.Type).Kind() == reflect.Struct {
|
||||
if fieldValue.Kind() == reflect.Ptr {
|
||||
if fieldValue.IsNil() {
|
||||
continue
|
||||
}
|
||||
fieldValue = fieldValue.Elem()
|
||||
}
|
||||
row = append(row, collectStructValues(fieldValue)...)
|
||||
continue
|
||||
}
|
||||
row = append(row, fieldValue.Interface())
|
||||
}
|
||||
return row
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
package iot_excelize
|
||||
|
||||
import (
|
||||
"io"
|
||||
"reflect"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
"github.com/xuri/excelize/v2"
|
||||
)
|
||||
|
||||
// ImportByReader parses an uploaded Excel file into typed rows. The first row is
|
||||
// treated as the header row and matched by excel/dc/description/json tags.
|
||||
func ImportByReader[T any](r io.Reader, sheetName string) (*ImportResult[T], error) {
|
||||
f, err := excelize.OpenReader(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
if sheetName == "" {
|
||||
sheetName = firstSheetName(f)
|
||||
}
|
||||
rows, err := f.Rows(sheetName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
schemas := parseSchema(reflect.TypeOf((*T)(nil)).Elem())
|
||||
schemaByHeader := make(map[string]fieldSchema, len(schemas))
|
||||
for _, schema := range schemas {
|
||||
schemaByHeader[normalizeHeader(schema.Header)] = schema
|
||||
}
|
||||
if len(schemaByHeader) == 0 {
|
||||
return nil, gerror.New("导入模型没有可读取字段")
|
||||
}
|
||||
|
||||
result := &ImportResult[T]{Rows: make([]T, 0)}
|
||||
headerIndex := make(map[int]fieldSchema)
|
||||
rowIndex := 0
|
||||
hasHeader := false
|
||||
|
||||
for rows.Next() {
|
||||
rowIndex++
|
||||
cols, err := rows.Columns()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if rowIndex == 1 {
|
||||
hasHeader = true
|
||||
for i, header := range cols {
|
||||
if schema, ok := schemaByHeader[normalizeHeader(header)]; ok {
|
||||
headerIndex[i] = schema
|
||||
}
|
||||
}
|
||||
if len(headerIndex) == 0 {
|
||||
return nil, gerror.New("Excel表头与导入模型不匹配")
|
||||
}
|
||||
continue
|
||||
}
|
||||
if isEmptyRow(cols) {
|
||||
continue
|
||||
}
|
||||
|
||||
result.TotalRows++
|
||||
item, rowErrors := parseImportRow[T](rowIndex, cols, headerIndex, schemas)
|
||||
if len(rowErrors) > 0 {
|
||||
result.Errors = append(result.Errors, rowErrors...)
|
||||
result.FailedRows++
|
||||
continue
|
||||
}
|
||||
result.Rows = append(result.Rows, item)
|
||||
result.SuccessRows++
|
||||
}
|
||||
if err = rows.Error(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !hasHeader {
|
||||
return nil, gerror.New("Excel文件缺少表头")
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func parseImportRow[T any](rowIndex int, cols []string, headerIndex map[int]fieldSchema, schemas []fieldSchema) (T, []RowError) {
|
||||
var item T
|
||||
var errors []RowError
|
||||
value := reflect.ValueOf(&item).Elem()
|
||||
if value.Kind() == reflect.Ptr {
|
||||
value.Set(reflect.New(value.Type().Elem()))
|
||||
value = value.Elem()
|
||||
}
|
||||
|
||||
seen := make(map[string]bool)
|
||||
for colIndex, schema := range headerIndex {
|
||||
raw := ""
|
||||
if colIndex < len(cols) {
|
||||
raw = normalizeHeader(cols[colIndex])
|
||||
}
|
||||
seen[schema.Name] = true
|
||||
if raw == "" {
|
||||
if schema.Required {
|
||||
errors = append(errors, RowError{
|
||||
Row: rowIndex,
|
||||
Column: schema.Header,
|
||||
Field: schema.Name,
|
||||
Message: requiredMessage(schema.Field, schema.Header),
|
||||
})
|
||||
}
|
||||
continue
|
||||
}
|
||||
if err := setFieldValue(value, schema, raw); err != nil {
|
||||
errors = append(errors, RowError{
|
||||
Row: rowIndex,
|
||||
Column: schema.Header,
|
||||
Field: schema.Name,
|
||||
Message: err.Error(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
for _, schema := range schemas {
|
||||
if schema.Required && !seen[schema.Name] {
|
||||
errors = append(errors, RowError{
|
||||
Row: rowIndex,
|
||||
Column: schema.Header,
|
||||
Field: schema.Name,
|
||||
Message: "缺少必要列:" + schema.Header,
|
||||
})
|
||||
}
|
||||
}
|
||||
return item, errors
|
||||
}
|
||||
|
||||
func setFieldValue(root reflect.Value, schema fieldSchema, raw string) error {
|
||||
field := fieldByIndexAlloc(root, schema.Index)
|
||||
if field.Kind() == reflect.Ptr {
|
||||
elemType := field.Type().Elem()
|
||||
elemValue := reflect.New(elemType).Elem()
|
||||
if err := setValue(elemValue, raw); err != nil {
|
||||
return err
|
||||
}
|
||||
field.Set(elemValue.Addr())
|
||||
return nil
|
||||
}
|
||||
return setValue(field, raw)
|
||||
}
|
||||
|
||||
func fieldByIndexAlloc(root reflect.Value, index []int) reflect.Value {
|
||||
field := root
|
||||
for _, i := range index {
|
||||
if field.Kind() == reflect.Ptr {
|
||||
if field.IsNil() {
|
||||
field.Set(reflect.New(field.Type().Elem()))
|
||||
}
|
||||
field = field.Elem()
|
||||
}
|
||||
field = field.Field(i)
|
||||
}
|
||||
return field
|
||||
}
|
||||
|
||||
func setValue(field reflect.Value, raw string) error {
|
||||
if !field.CanSet() {
|
||||
return nil
|
||||
}
|
||||
if field.Type() == reflect.TypeOf(gtime.Time{}) {
|
||||
t := gtime.NewFromStr(raw)
|
||||
if t == nil {
|
||||
return gconv.Scan(raw, field.Addr().Interface())
|
||||
}
|
||||
field.Set(reflect.ValueOf(*t))
|
||||
return nil
|
||||
}
|
||||
if field.Type() == reflect.TypeOf(>ime.Time{}) {
|
||||
t := gtime.NewFromStr(raw)
|
||||
if t == nil {
|
||||
return gconv.Scan(raw, field.Addr().Interface())
|
||||
}
|
||||
field.Set(reflect.ValueOf(t))
|
||||
return nil
|
||||
}
|
||||
return gconv.Scan(raw, field.Addr().Interface())
|
||||
}
|
||||
|
||||
func firstSheetName(f *excelize.File) string {
|
||||
sheets := f.GetSheetList()
|
||||
if len(sheets) == 0 {
|
||||
return "Sheet1"
|
||||
}
|
||||
return sheets[0]
|
||||
}
|
||||
|
||||
func isEmptyRow(cols []string) bool {
|
||||
for _, col := range cols {
|
||||
if normalizeHeader(col) != "" {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package iot_excelize
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/entity"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/service"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/utility/uuid32"
|
||||
|
||||
"github.com/xuri/excelize/v2"
|
||||
)
|
||||
|
||||
// SaveLocal saves an Excel workbook through SysOss local storage and records it
|
||||
// in the sys_oss table.
|
||||
func SaveLocal(ctx context.Context, f *excelize.File, fileName string, category string) (*entity.SysOss, error) {
|
||||
if category == "" {
|
||||
category = "export"
|
||||
}
|
||||
category = safePathName(category)
|
||||
if fileName == "" {
|
||||
fileName = "excel"
|
||||
}
|
||||
|
||||
fileName = uniqueExcelFileName(fileName)
|
||||
reader, err := ToReadSeeker(f)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err = reader.Seek(0, io.SeekStart); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
content, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result, err := service.SysOss().SaveContent(ctx, &model.SysOssSaveContentParam{
|
||||
Content: content,
|
||||
FileName: fileName,
|
||||
NewFileType: consts.SysUploadNewFileTypeUserFile,
|
||||
SubDirName: "excel/" + category,
|
||||
NotAddDate: 0,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if result == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return result.SysOss, nil
|
||||
}
|
||||
|
||||
func safeExcelFileName(fileName string) string {
|
||||
fileName = strings.TrimSpace(fileName)
|
||||
if fileName == "" {
|
||||
fileName = "excel"
|
||||
}
|
||||
fileName = strings.TrimSuffix(fileName, ".xlsx")
|
||||
for _, old := range []string{"\\", "/", ":", "*", "?", "\"", "<", ">", "|"} {
|
||||
fileName = strings.ReplaceAll(fileName, old, "_")
|
||||
}
|
||||
return fileName + ".xlsx"
|
||||
}
|
||||
|
||||
func uniqueExcelFileName(fileName string) string {
|
||||
fileName = strings.TrimSuffix(safeExcelFileName(fileName), ".xlsx")
|
||||
uid := uuid32.S()
|
||||
if len(uid) > 8 {
|
||||
uid = uid[:8]
|
||||
}
|
||||
return fmt.Sprintf("%s_%s_%s.xlsx", time.Now().Format("20060102150405000"), uid, fileName)
|
||||
}
|
||||
|
||||
func safePathName(name string) string {
|
||||
name = strings.TrimSpace(name)
|
||||
if name == "" {
|
||||
return "export"
|
||||
}
|
||||
for _, old := range []string{"\\", "/", ":", "*", "?", "\"", "<", ">", "|", "."} {
|
||||
name = strings.ReplaceAll(name, old, "_")
|
||||
}
|
||||
return name
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package iot_excelize
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type fieldSchema struct {
|
||||
Index []int
|
||||
Field reflect.StructField
|
||||
Name string
|
||||
Header string
|
||||
Required bool
|
||||
}
|
||||
|
||||
func parseSchema(t reflect.Type) []fieldSchema {
|
||||
if t == nil {
|
||||
return nil
|
||||
}
|
||||
if t.Kind() == reflect.Ptr {
|
||||
t = t.Elem()
|
||||
}
|
||||
if t.Kind() != reflect.Struct {
|
||||
return nil
|
||||
}
|
||||
return parseStructSchema(t, nil)
|
||||
}
|
||||
|
||||
func parseStructSchema(t reflect.Type, parent []int) []fieldSchema {
|
||||
var fields []fieldSchema
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
field := t.Field(i)
|
||||
if !field.IsExported() {
|
||||
continue
|
||||
}
|
||||
if field.Anonymous && derefType(field.Type).Kind() == reflect.Struct {
|
||||
fields = append(fields, parseStructSchema(derefType(field.Type), appendIndex(parent, i))...)
|
||||
continue
|
||||
}
|
||||
|
||||
header := fieldHeader(field)
|
||||
if header == "-" || header == "" {
|
||||
continue
|
||||
}
|
||||
fields = append(fields, fieldSchema{
|
||||
Index: appendIndex(parent, i),
|
||||
Field: field,
|
||||
Name: field.Name,
|
||||
Header: header,
|
||||
Required: isRequired(field),
|
||||
})
|
||||
}
|
||||
return fields
|
||||
}
|
||||
|
||||
func fieldHeader(field reflect.StructField) string {
|
||||
for _, tag := range []string{"excel", "dc", "description", "json"} {
|
||||
if value, ok := field.Tag.Lookup(tag); ok {
|
||||
value = strings.TrimSpace(strings.Split(value, ",")[0])
|
||||
if value != "" {
|
||||
return value
|
||||
}
|
||||
}
|
||||
}
|
||||
return field.Name
|
||||
}
|
||||
|
||||
func isRequired(field reflect.StructField) bool {
|
||||
rule := field.Tag.Get("v")
|
||||
if rule == "" {
|
||||
return false
|
||||
}
|
||||
for _, item := range strings.Split(rule, "|") {
|
||||
if strings.HasPrefix(strings.TrimSpace(item), "required") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func requiredMessage(field reflect.StructField, header string) string {
|
||||
rule := field.Tag.Get("v")
|
||||
for _, item := range strings.Split(rule, "|") {
|
||||
item = strings.TrimSpace(item)
|
||||
if !strings.HasPrefix(item, "required") {
|
||||
continue
|
||||
}
|
||||
if parts := strings.SplitN(item, "#", 2); len(parts) == 2 && strings.TrimSpace(parts[1]) != "" {
|
||||
return strings.TrimSpace(parts[1])
|
||||
}
|
||||
}
|
||||
return header + " is required"
|
||||
}
|
||||
|
||||
func appendIndex(parent []int, i int) []int {
|
||||
index := make([]int, 0, len(parent)+1)
|
||||
index = append(index, parent...)
|
||||
index = append(index, i)
|
||||
return index
|
||||
}
|
||||
|
||||
func derefType(t reflect.Type) reflect.Type {
|
||||
for t.Kind() == reflect.Ptr {
|
||||
t = t.Elem()
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
func normalizeHeader(s string) string {
|
||||
return strings.TrimSpace(strings.ReplaceAll(s, "\uFEFF", ""))
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package iot_excelize
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/xuri/excelize/v2"
|
||||
)
|
||||
|
||||
// ApplyHeaderColumnsTextStyle sets all columns with non-empty first-row headers
|
||||
// in the active sheet to Excel text format.
|
||||
func ApplyHeaderColumnsTextStyle(f *excelize.File) error {
|
||||
if f == nil {
|
||||
return gerror.New("Excel文件不能为空")
|
||||
}
|
||||
|
||||
sheetName := f.GetSheetName(f.GetActiveSheetIndex())
|
||||
if sheetName == "" {
|
||||
sheets := f.GetSheetList()
|
||||
if len(sheets) == 0 {
|
||||
return gerror.New("Excel文件没有工作表")
|
||||
}
|
||||
sheetName = sheets[0]
|
||||
}
|
||||
|
||||
rows, err := f.GetRows(sheetName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(rows) == 0 || len(rows[0]) == 0 {
|
||||
return gerror.New("Excel文件表头为空")
|
||||
}
|
||||
|
||||
styleID, err := f.NewStyle(&excelize.Style{NumFmt: 49})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for i, header := range rows[0] {
|
||||
if strings.TrimSpace(header) == "" {
|
||||
continue
|
||||
}
|
||||
colName, err := excelize.ColumnNumberToName(i + 1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = f.SetColStyle(sheetName, colName, styleID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package iot_excelize
|
||||
|
||||
import "github.com/xuri/excelize/v2"
|
||||
|
||||
// BuildOption customizes workbook styling and post-processing.
|
||||
type BuildOption struct {
|
||||
HeaderStyle *excelize.Style
|
||||
RowStyle *excelize.Style
|
||||
ColWidth float64
|
||||
HeaderHeight float64
|
||||
AfterBuild func(f *excelize.File, sheetName string) error
|
||||
}
|
||||
|
||||
func letter(n int) []string {
|
||||
if n <= 0 {
|
||||
return nil
|
||||
}
|
||||
var result []string
|
||||
for i := 0; i < n; i++ {
|
||||
result = append(result, colToLetter(i))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func colToLetter(n int) string {
|
||||
result := ""
|
||||
for n >= 0 {
|
||||
result = string(rune('A'+n%26)) + result
|
||||
n = n/26 - 1
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// RowError records one import error at row/column level.
|
||||
type RowError struct {
|
||||
Row int `json:"row"`
|
||||
Column string `json:"column"`
|
||||
Field string `json:"field"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// ImportResult contains parsed rows and row-level validation/conversion errors.
|
||||
type ImportResult[T any] struct {
|
||||
Rows []T `json:"rows"`
|
||||
Errors []RowError `json:"errors"`
|
||||
TotalRows int `json:"totalRows"`
|
||||
SuccessRows int `json:"successRows"`
|
||||
FailedRows int `json:"failedRows"`
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package iot_excelize
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/net/ghttp"
|
||||
"github.com/xuri/excelize/v2"
|
||||
)
|
||||
|
||||
// WriteHTTP writes an Excel workbook as an attachment to the current HTTP response.
|
||||
func WriteHTTP(ctx context.Context, f *excelize.File, fileName string) (err error) {
|
||||
r := ghttp.RequestFromCtx(ctx)
|
||||
if r == nil {
|
||||
return gerror.New("当前上下文不是HTTP请求")
|
||||
}
|
||||
if fileName == "" {
|
||||
fileName = "export"
|
||||
}
|
||||
|
||||
writer := r.Response.Writer
|
||||
writer.Header().Set("Content-Type", "application/octet-stream")
|
||||
writer.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s.xlsx", url.QueryEscape(fileName)))
|
||||
writer.Header().Set("Content-Transfer-Encoding", "binary")
|
||||
writer.Header().Set("Access-Control-Expose-Headers", "Content-Disposition")
|
||||
|
||||
if err = f.Write(writer); err != nil {
|
||||
return err
|
||||
}
|
||||
r.Response.Status = http.StatusOK
|
||||
writer.Flush()
|
||||
return nil
|
||||
}
|
||||
|
||||
// ToReadSeeker serializes an Excel workbook into memory for secondary handling.
|
||||
func ToReadSeeker(f *excelize.File) (io.ReadSeeker, error) {
|
||||
var buffer bytes.Buffer
|
||||
if err := f.Write(&buffer); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return bytes.NewReader(buffer.Bytes()), nil
|
||||
}
|
||||
Reference in New Issue
Block a user