- 修复 MaxPreview=0 仍被覆盖为默认值的 bug - 修复 API Endpoint 自动补全逻辑(避免 /v1/v1/chat/completions) - 为 AI 配置与匹配状态字段增加并发锁 - AI 增强未匹配行改为按索引跟踪,避免重复行误判 - 无时间列时 AI 匹配 B 表行数可配置并增加截断警告 - 导出时防御参差不齐行导致的数组越界 panic - Excel 读取时对单元格统一 TrimSpace - 删除未使用的 minInt 函数 - 修复 wails.json 开发服务器地址为 http://localhost:5173 - 重新生成 Wails 前端绑定 - 新增 ai_test.go / export_test.go 单元测试
217 lines
5.5 KiB
Go
217 lines
5.5 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/wailsapp/wails/v2/pkg/runtime"
|
|
"github.com/xuri/excelize/v2"
|
|
)
|
|
|
|
// ---------- 导出结果 ----------
|
|
|
|
// ExportResults 将匹配结果导出为 Excel 或 CSV 文件
|
|
func (a *App) ExportResults(results []MatchResult) (string, error) {
|
|
if len(results) == 0 {
|
|
return "", fmt.Errorf("没有匹配结果可以导出")
|
|
}
|
|
|
|
a.dataMu.RLock()
|
|
useCSV := a.lastConfig.ExportFormat == "csv"
|
|
includeHdr := a.lastConfig.IncludeHeader
|
|
a.dataMu.RUnlock()
|
|
|
|
isCSV := useCSV
|
|
ext := ".xlsx"
|
|
filterDisplay := "Excel 文件 (*.xlsx)"
|
|
filterPattern := "*.xlsx"
|
|
if isCSV {
|
|
ext = ".csv"
|
|
filterDisplay = "CSV 文件 (*.csv)"
|
|
filterPattern = "*.csv"
|
|
}
|
|
|
|
savePath, err := runtime.SaveFileDialog(a.ctx, runtime.SaveDialogOptions{
|
|
Title: "导出匹配结果",
|
|
DefaultFilename: fmt.Sprintf("匹配结果_%s%s", time.Now().Format("20060102_150405"), ext),
|
|
Filters: []runtime.FileFilter{
|
|
{DisplayName: filterDisplay, Pattern: filterPattern},
|
|
},
|
|
})
|
|
if err != nil {
|
|
return "", fmt.Errorf("打开保存对话框失败: %v", err)
|
|
}
|
|
if savePath == "" {
|
|
return "", nil
|
|
}
|
|
if !strings.HasSuffix(strings.ToLower(savePath), ext) {
|
|
savePath += ext
|
|
}
|
|
|
|
if isCSV {
|
|
return a.exportResultsCSV(results, savePath, includeHdr)
|
|
}
|
|
return a.exportResultsXLSX(results, savePath, includeHdr)
|
|
}
|
|
|
|
// maxACols 计算结果中 A 表行的最大列数(处理参差不齐的行)
|
|
func maxACols(results []MatchResult) int {
|
|
max := 0
|
|
for _, r := range results {
|
|
if len(r.RowAData) > max {
|
|
max = len(r.RowAData)
|
|
}
|
|
}
|
|
return max
|
|
}
|
|
|
|
// exportHeaders 构建导出表头行(使用真实表头或回退默认)
|
|
func (a *App) exportHeaders(numACols int) []string {
|
|
a.dataMu.RLock()
|
|
hdrA := make([]string, len(a.headersA))
|
|
copy(hdrA, a.headersA)
|
|
a.dataMu.RUnlock()
|
|
|
|
headers := make([]string, 0, numACols+1)
|
|
if len(hdrA) >= numACols {
|
|
for _, h := range hdrA[:numACols] {
|
|
n := h
|
|
if n == "" {
|
|
n = fmt.Sprintf("Col%d", len(headers)+1)
|
|
}
|
|
headers = append(headers, n)
|
|
}
|
|
} else {
|
|
for i := 0; i < numACols; i++ {
|
|
headers = append(headers, fmt.Sprintf("A-Col%d", i+1))
|
|
}
|
|
}
|
|
headers = append(headers, "匹配结果(由B表提取)")
|
|
return headers
|
|
}
|
|
|
|
func (a *App) exportResultsXLSX(results []MatchResult, savePath string, includeHeader bool) (string, error) {
|
|
f := excelize.NewFile()
|
|
defer f.Close()
|
|
sheetName := "匹配结果"
|
|
f.SetSheetName("Sheet1", sheetName)
|
|
|
|
numACols := maxACols(results)
|
|
colLetter := func(n int) string { c, _ := excelize.ColumnNumberToName(n + 1); return c }
|
|
|
|
headers := a.exportHeaders(numACols)
|
|
extractCol := numACols
|
|
|
|
// 表头
|
|
if includeHeader {
|
|
for i, h := range headers {
|
|
f.SetCellValue(sheetName, fmt.Sprintf("%s1", colLetter(i)), h)
|
|
}
|
|
headerStyle, _ := f.NewStyle(&excelize.Style{
|
|
Font: &excelize.Font{Bold: true, Size: 12, Color: "FFFFFF"},
|
|
Fill: excelize.Fill{Type: "pattern", Pattern: 1, Color: []string{"4472C4"}},
|
|
})
|
|
f.SetCellStyle(sheetName, "A1", fmt.Sprintf("%s1", colLetter(extractCol)), headerStyle)
|
|
// 数据行样式(带边框和行号字体)
|
|
dataStyle, _ := f.NewStyle(&excelize.Style{
|
|
Font: &excelize.Font{Size: 11},
|
|
Border: []excelize.Border{
|
|
{Type: "bottom", Color: "D9D9D9", Style: 1},
|
|
},
|
|
})
|
|
firstDataRow := 2
|
|
lastDataRow := len(results) + 1
|
|
for ci := 0; ci <= numACols; ci++ {
|
|
f.SetCellStyle(sheetName,
|
|
fmt.Sprintf("%s%d", colLetter(ci), firstDataRow),
|
|
fmt.Sprintf("%s%d", colLetter(ci), lastDataRow),
|
|
dataStyle)
|
|
}
|
|
} else {
|
|
dataStyle, _ := f.NewStyle(&excelize.Style{
|
|
Font: &excelize.Font{Size: 11},
|
|
Border: []excelize.Border{
|
|
{Type: "bottom", Color: "D9D9D9", Style: 1},
|
|
},
|
|
})
|
|
lastDataRow := len(results)
|
|
for ci := 0; ci <= numACols; ci++ {
|
|
f.SetCellStyle(sheetName,
|
|
fmt.Sprintf("%s%d", colLetter(ci), 1),
|
|
fmt.Sprintf("%s%d", colLetter(ci), lastDataRow),
|
|
dataStyle)
|
|
}
|
|
}
|
|
|
|
// 数据行
|
|
for i, r := range results {
|
|
rowNum := i + 2
|
|
if !includeHeader {
|
|
rowNum = i + 1
|
|
}
|
|
for ci := 0; ci < numACols; ci++ {
|
|
f.SetCellValue(sheetName, fmt.Sprintf("%s%d", colLetter(ci), rowNum), getCell(r.RowAData, ci))
|
|
}
|
|
f.SetCellValue(sheetName, fmt.Sprintf("%s%d", colLetter(extractCol), rowNum), r.ExtractValue)
|
|
}
|
|
|
|
// 列宽
|
|
for ci := 0; ci <= numACols; ci++ {
|
|
f.SetColWidth(sheetName, colLetter(ci), colLetter(ci), 22)
|
|
}
|
|
|
|
if err := f.SaveAs(savePath); err != nil {
|
|
return "", fmt.Errorf("保存文件失败: %v", err)
|
|
}
|
|
return savePath, nil
|
|
}
|
|
|
|
func (a *App) exportResultsCSV(results []MatchResult, savePath string, includeHeader bool) (string, error) {
|
|
var buf bytes.Buffer
|
|
// 使用 UTF-8 BOM 帮助 Excel 正确识别编码
|
|
buf.Write([]byte{0xEF, 0xBB, 0xBF})
|
|
|
|
numACols := maxACols(results)
|
|
headers := a.exportHeaders(numACols)
|
|
|
|
// 表头行
|
|
if includeHeader {
|
|
for i, h := range headers {
|
|
if i > 0 {
|
|
buf.WriteByte(',')
|
|
}
|
|
buf.WriteString(csvEscape(h))
|
|
}
|
|
buf.WriteByte('\n')
|
|
}
|
|
|
|
// 数据行
|
|
for _, r := range results {
|
|
for ci := 0; ci < numACols; ci++ {
|
|
if ci > 0 {
|
|
buf.WriteByte(',')
|
|
}
|
|
buf.WriteString(csvEscape(getCell(r.RowAData, ci)))
|
|
}
|
|
buf.WriteByte(',')
|
|
buf.WriteString(csvEscape(r.ExtractValue))
|
|
buf.WriteByte('\n')
|
|
}
|
|
|
|
if err := os.WriteFile(savePath, buf.Bytes(), 0600); err != nil {
|
|
return "", fmt.Errorf("保存 CSV 文件失败: %v", err)
|
|
}
|
|
return savePath, nil
|
|
}
|
|
|
|
// csvEscape 对 CSV 字段进行转义(含逗号或引号时包裹双引号)
|
|
func csvEscape(s string) string {
|
|
if strings.ContainsAny(s, "\",\n\r") {
|
|
return `"` + strings.ReplaceAll(s, `"`, `""`) + `"`
|
|
}
|
|
return s
|
|
}
|