package template import ( "fmt" "html/template" "io/fs" "os" "path/filepath" "strings" "sync" "time" "github.com/gofiber/fiber/v3" "log/slog" ) type TemplateLoader struct { templates map[string]*template.Template cache map[string]*template.Template mu sync.RWMutex config TemplateConfig versionManager *VersionManager hotReload *HotReloadWatcher configLoader *ConfigLoader logger *slog.Logger } type TemplateConfig struct { BuiltinPath string CustomPath string CacheEnabled bool HotReload bool Templates map[string]struct { Name string Type string Path string Description string Fields map[string]string Config map[string]interface{} } TemplateTypes map[string]struct { Layout string Fields map[string]string Options map[string]interface{} } } func NewTemplateLoader(config TemplateConfig) *TemplateLoader { loader := &TemplateLoader{ templates: make(map[string]*template.Template), cache: make(map[string]*template.Template), config: config, logger: slog.With("component", "template_loader"), versionManager: NewVersionManager(config.CustomPath), configLoader: NewConfigLoader("./config"), } // Initialize hot reload if enabled if config.HotReload { var err error loader.hotReload, err = NewHotReloadWatcher(loader, config.CustomPath) if err != nil { loader.logger.Error("failed to initialize hot reload", "error", err) } else { if err := loader.hotReload.Start(); err != nil { loader.logger.Error("failed to start hot reload", "error", err) } } } // Load configuration if _, err := loader.configLoader.LoadTemplateConfig(); err != nil { loader.logger.Warn("failed to load template config", "error", err) } return loader } func (tl *TemplateLoader) LoadTemplate(templateType, tableName string) (*template.Template, error) { cacheKey := fmt.Sprintf("%s_%s", templateType, tableName) tl.mu.RLock() if cached, exists := tl.cache[cacheKey]; exists && tl.config.CacheEnabled { tl.mu.RUnlock() return cached, nil } tl.mu.RUnlock() // 尝试加载自定义模板 customPath := filepath.Join(tl.config.CustomPath, tableName, fmt.Sprintf("%s.html", templateType)) if _, err := os.Stat(customPath); err == nil { tmpl, err := tl.parseTemplate(customPath, templateType) if err == nil { tl.cacheTemplate(cacheKey, tmpl) return tmpl, nil } } // 回退到默认自定义模板 defaultPath := filepath.Join(tl.config.CustomPath, "_default", fmt.Sprintf("%s.html", templateType)) if _, err := os.Stat(defaultPath); err == nil { tmpl, err := tl.parseTemplate(defaultPath, templateType) if err == nil { tl.cacheTemplate(cacheKey, tmpl) return tmpl, nil } } // 回退到内置模板 builtinPath := filepath.Join(tl.config.BuiltinPath, fmt.Sprintf("%s.html", templateType)) tmpl, err := tl.parseTemplate(builtinPath, templateType) if err != nil { return nil, fmt.Errorf("failed to load template %s: %w", templateType, err) } tl.cacheTemplate(cacheKey, tmpl) return tmpl, nil } func (tl *TemplateLoader) parseTemplate(path, name string) (*template.Template, error) { // 基础模板函数 funcs := template.FuncMap{ "dict": func(values ...interface{}) map[string]interface{} { dict := make(map[string]interface{}) for i := 0; i < len(values); i += 2 { if i+1 < len(values) { key := fmt.Sprintf("%v", values[i]) dict[key] = values[i+1] } } return dict }, "add": func(a, b int) int { return a + b }, "sub": func(a, b int) int { return a - b }, "mul": func(a, b int) int { return a * b }, "min": func(a, b int) int { if a < b { return a } return b }, "max": func(a, b int) int { if a > b { return a } return b }, "json": func(v interface{}) string { return fmt.Sprintf("%v", v) }, "split": func(s string, sep string) []string { return strings.Split(s, sep) }, "until": func(n int) []int { result := make([]int, n) for i := 0; i < n; i++ { result[i] = i + 1 } return result }, "eq": func(a, b interface{}) bool { return fmt.Sprintf("%v", a) == fmt.Sprintf("%v", b) }, "ge": func(a, b int) bool { return a >= b }, "gt": func(a, b int) bool { return a > b }, "lt": func(a, b int) bool { return a < b }, "le": func(a, b int) bool { return a <= b }, "index": func(m interface{}, key string) interface{} { if m == nil { return nil } switch v := m.(type) { case map[string]interface{}: return v[key] default: return nil } }, "printf": func(format string, a ...interface{}) string { return fmt.Sprintf(format, a...) }, } // 解析布局模板 layoutPath := filepath.Join(tl.config.BuiltinPath, "layout.html") if _, err := os.Stat(layoutPath); err != nil { return nil, fmt.Errorf("layout template not found: %w", err) } tmpl := template.New(name).Funcs(funcs) // 加载布局模板 layoutContent, err := os.ReadFile(layoutPath) if err != nil { return nil, fmt.Errorf("failed to read layout template: %w", err) } tmpl, err = tmpl.Parse(string(layoutContent)) if err != nil { return nil, fmt.Errorf("failed to parse layout template: %w", err) } // 加载主模板 content, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("failed to read template %s: %w", path, err) } tmpl, err = tmpl.Parse(string(content)) if err != nil { return nil, fmt.Errorf("failed to parse template %s: %w", path, err) } // 加载字段模板 fieldTemplates, err := tl.loadFieldTemplates() if err != nil { return nil, fmt.Errorf("failed to load field templates: %w", err) } for _, fieldTpl := range fieldTemplates { tmpl, err = tmpl.Parse(fieldTpl) if err != nil { return nil, fmt.Errorf("failed to parse field template: %w", err) } } return tmpl, nil } func (tl *TemplateLoader) loadFieldTemplates() ([]string, error) { var templates []string // 加载内置字段模板 builtinFieldPath := filepath.Join(tl.config.BuiltinPath, "field") if err := filepath.WalkDir(builtinFieldPath, func(path string, d fs.DirEntry, err error) error { if err != nil { return err } if !d.IsDir() && strings.HasSuffix(path, ".html") { content, err := os.ReadFile(path) if err != nil { return err } templates = append(templates, string(content)) } return nil }); err != nil { return nil, err } return templates, nil } func (tl *TemplateLoader) cacheTemplate(key string, tmpl *template.Template) { if !tl.config.CacheEnabled { return } tl.mu.Lock() tl.cache[key] = tmpl tl.mu.Unlock() } func (tl *TemplateLoader) ClearCache() { tl.mu.Lock() defer tl.mu.Unlock() for k := range tl.cache { delete(tl.cache, k) } } func (tl *TemplateLoader) watchTemplates() { if tl.config.CustomPath == "" { return } ticker := time.NewTicker(2 * time.Second) defer ticker.Stop() var lastModTime time.Time for range ticker.C { info, err := os.Stat(tl.config.CustomPath) if err != nil { continue } if info.ModTime().After(lastModTime) { lastModTime = info.ModTime() tl.ClearCache() } } } // 验证模板完整性 func (tl *TemplateLoader) ValidateTemplate(templateType, tableName string) error { _, err := tl.LoadTemplate(templateType, tableName) return err } // 获取可用模板列表 func (tl *TemplateLoader) GetAvailableTemplates(tableName string) ([]string, error) { var templates []string // 检查自定义模板 customPath := filepath.Join(tl.config.CustomPath, tableName) if entries, err := os.ReadDir(customPath); err == nil { for _, entry := range entries { if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".html") { templates = append(templates, strings.TrimSuffix(entry.Name(), ".html")) } } } // 检查默认模板 defaultPath := filepath.Join(tl.config.CustomPath, "_default") if entries, err := os.ReadDir(defaultPath); err == nil { for _, entry := range entries { if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".html") { templateName := strings.TrimSuffix(entry.Name(), ".html") if !contains(templates, templateName) { templates = append(templates, templateName) } } } } // 添加内置模板 builtinTemplates := []string{"list", "card", "timeline"} for _, bt := range builtinTemplates { if !contains(templates, bt) { templates = append(templates, bt) } } return templates, nil } func contains(slice []string, item string) bool { for _, s := range slice { if s == item { return true } } return false } // Fiber模板引擎适配器 func (tl *TemplateLoader) Render(data interface{}, templateName string, c fiber.Ctx) error { parts := strings.Split(templateName, ":") if len(parts) != 2 { return fmt.Errorf("invalid template format, expected 'type:table'") } templateType, tableName := parts[0], parts[1] tmpl, err := tl.LoadTemplate(templateType, tableName) if err != nil { return err } c.Set("Content-Type", "text/html; charset=utf-8") return tmpl.Execute(c.Response().BodyWriter(), data) }