支持客户、对话记录与统计报告 CSV 导出
- 新增 /customers/export、/sessions/export、/statistics/export - 沿用列表筛选与角色可见范围,UTF-8 BOM 便于 Excel 打开 - 前端三处导出按钮接入真实下载
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/csv"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
const exportMaxRows = 5000
|
||||
|
||||
func csvEscape(s string) string {
|
||||
return strings.ReplaceAll(s, "\r\n", " ")
|
||||
}
|
||||
|
||||
func writeCSVResponse(c *gin.Context, filename string, header []string, rows [][]string) {
|
||||
if !strings.HasSuffix(strings.ToLower(filename), ".csv") {
|
||||
filename += ".csv"
|
||||
}
|
||||
// 兼容 Excel 中文:UTF-8 BOM
|
||||
c.Header("Content-Type", "text/csv; charset=utf-8")
|
||||
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename*=UTF-8''%s", url.PathEscape(filename)))
|
||||
c.Status(http.StatusOK)
|
||||
|
||||
if _, err := c.Writer.Write([]byte{0xEF, 0xBB, 0xBF}); err != nil {
|
||||
return
|
||||
}
|
||||
w := csv.NewWriter(c.Writer)
|
||||
_ = w.Write(header)
|
||||
for _, row := range rows {
|
||||
for i := range row {
|
||||
row[i] = csvEscape(row[i])
|
||||
}
|
||||
_ = w.Write(row)
|
||||
}
|
||||
w.Flush()
|
||||
}
|
||||
|
||||
func formatCSVTime(t time.Time) string {
|
||||
if t.IsZero() {
|
||||
return ""
|
||||
}
|
||||
return t.In(time.Local).Format("2006-01-02 15:04:05")
|
||||
}
|
||||
|
||||
func formatCSVTimePtr(t *time.Time) string {
|
||||
if t == nil {
|
||||
return ""
|
||||
}
|
||||
return formatCSVTime(*t)
|
||||
}
|
||||
|
||||
func formatUintPtr(v *uint) string {
|
||||
if v == nil {
|
||||
return ""
|
||||
}
|
||||
return strconv.FormatUint(uint64(*v), 10)
|
||||
}
|
||||
|
||||
func formatIntPtr(v *int) string {
|
||||
if v == nil {
|
||||
return ""
|
||||
}
|
||||
return strconv.Itoa(*v)
|
||||
}
|
||||
Reference in New Issue
Block a user