package report import ( "fmt" "math" "strings" "text/template" "time" ) // ============================ // 模板渲染(80mm 热敏纸适配) // ============================ // TemplateConfig 模板渲染配置 type TemplateConfig struct { Width int // 行宽(字符数),80mm 热敏纸建议 42 } // DefaultTemplateConfig 默认配置(80mm 热敏纸) func DefaultTemplateConfig() TemplateConfig { return TemplateConfig{Width: 42} } // RenderTemplate 使用 Go template 渲染报告 // tpl 为模板内容,data 为报告数据 func RenderTemplate(tpl string, data interface{}, cfg TemplateConfig) (string, error) { if cfg.Width <= 0 { cfg.Width = 42 } funcMap := template.FuncMap{ // 格式化辅助 "timeFmt": func(t time.Time) string { return t.Format("2006-01-02 15:04:05") }, // 字符串截断/填充到固定宽度 "trunc": func(s string, n int) string { r := []rune(s) if len(r) <= n { return string(r) } return string(r[:n-1]) + "…" }, // 左侧填充(右对齐) "padRight": func(s string, n int) string { r := []rune(s) if len(r) >= n { return string(r[:n]) } return s + strings.Repeat(" ", n-len(r)) }, // 分隔线 "hr": func(sharp rune) string { return strings.Repeat(string(sharp), cfg.Width) }, // 曲线图(时间序列 → sparkline) "sparkline": func(data []float64, width int) string { return Sparkline(data, width) }, // 曲线摘要: 最高/最低/平均 "curveSummary": func(data []float64) string { if len(data) == 0 { return "无数据" } min, max := math.MaxFloat64, -math.MaxFloat64 var sum float64 for _, v := range data { if v < min { min = v } if v > max { max = v } sum += v } avg := sum / float64(len(data)) return fmt.Sprintf("max=%.1f min=%.1f avg=%.1f", max, min, avg) }, } t, err := template.New("report").Funcs(funcMap).Parse(tpl) if err != nil { return "", fmt.Errorf("解析模板失败: %w", err) } var sb strings.Builder if err := t.Execute(&sb, data); err != nil { return "", fmt.Errorf("渲染模板失败: %w", err) } return sb.String(), nil } // ============================ // Sparkline — 80mm 热敏纸曲线图 // ============================ // sparkChars 8 级块状字符(低→高) var sparkChars = []rune("▁▂▃▄▅▆▇█") // Sparkline 将时间序列渲染为一行块状曲线 // width 为字符宽度(80mm 纸 ≤42) func Sparkline(data []float64, width int) string { if len(data) == 0 { return "(无数据)" } if width <= 0 { width = 42 } if width > 42 { width = 42 } // 单点 if len(data) == 1 { return fmt.Sprintf("▊ %.1f", data[0]) } // 按宽度采样(超出宽度时等距抽取) step := float64(len(data)) / float64(width) sampled := make([]float64, 0, width) for i := 0; i < width && int(float64(i)*step) < len(data); i++ { idx := int(float64(i) * step) if idx >= len(data) { break } sampled = append(sampled, data[idx]) } // 归一化到 0-7 min, max := math.MaxFloat64, -math.MaxFloat64 for _, v := range sampled { if v < min { min = v } if v > max { max = v } } var sb strings.Builder if max-min < 1e-9 { // 全相同值 idx := 0 if max > 0 { idx = 3 } for range sampled { sb.WriteRune(sparkChars[idx]) } return sb.String() } for _, v := range sampled { idx := int((v - min) / (max - min) * 7) if idx < 0 { idx = 0 } if idx > 7 { idx = 7 } sb.WriteRune(sparkChars[idx]) } return sb.String() }