feat: update auto render
This commit is contained in:
373
internal/template/template_loader.go
Normal file
373
internal/template/template_loader.go
Normal file
@@ -0,0 +1,373 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user