package api import ( "io/fs" "net/http" "path" "strings" "github.com/gin-gonic/gin" "oci-portal/internal/webui" ) // registerWebUI 挂载嵌入的前端产物。dev 模式走 vite devServer,不经此路径。 func registerWebUI(r *gin.Engine) error { sub, err := webui.Dist() if err != nil { return err } mountSPA(r, sub) return nil } // mountSPA 注册 NoRoute 兜底:命中真实文件直出,API 前缀保持 JSON 404, // 其余路径回 index.html 交给 SPA 路由。 func mountSPA(r *gin.Engine, sub fs.FS) { fileServer := http.FileServer(http.FS(sub)) r.NoRoute(func(c *gin.Context) { p := c.Request.URL.Path if strings.HasPrefix(p, "/api/") || strings.HasPrefix(p, "/ai/") { c.JSON(http.StatusNotFound, gin.H{"error": "not found"}) return } if name := strings.TrimPrefix(path.Clean(p), "/"); name != "" { if _, err := fs.Stat(sub, name); err == nil { setStaticCacheHeader(c, p) fileServer.ServeHTTP(c.Writer, c.Request) return } } // SPA 路由统一回 index.html,禁缓存保证发版后入口即时更新 c.Header("Cache-Control", "no-cache") c.Request.URL.Path = "/" fileServer.ServeHTTP(c.Writer, c.Request) }) } // setStaticCacheHeader 按路径设缓存策略:vite 产物带内容 hash,/assets/* 可永久缓存; // 其余真实文件(favicon 等)不带 hash,禁强缓存。 func setStaticCacheHeader(c *gin.Context, p string) { if strings.HasPrefix(p, "/assets/") { c.Header("Cache-Control", "public, max-age=31536000, immutable") return } c.Header("Cache-Control", "no-cache") }