From 819885c85d57e4f456e24a068acd74bf7e73a33a Mon Sep 17 00:00:00 2001 From: jim800121chen Date: Sun, 12 Apr 2026 19:44:25 +0800 Subject: [PATCH] =?UTF-8?q?fix(local-tool):=20SPA=20fallback=20=E6=94=B9?= =?UTF-8?q?=E7=94=A8=20Next.js=20dynamic=20route=20shell=20HTML?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 根因:點模型卡片 → /models/yolov5-face-detection → server SPA fallback 固定回傳根路徑 /index.html(儀表板) → Next.js CSR router 初始化時 pathname 對不上 → 使用者被跳回儀表板。 修法:spaFallback handler 改成三層 fallback: 1. 精確檔案(/models/index.html 等) 2. Next.js dynamic route shell(把最後一段替換為 _ → /models/_/index.html) 這是 generateStaticParams 產的 placeholder 頁面,Next.js CSR 會從 URL 讀真正的 param 值 3. 根目錄 /index.html(最終 fallback) 這修好了: - 模型詳情頁 /models/:id 不再跳回儀表板 - 裝置詳情頁 /devices/:id 同理 - 工作區裝置頁 /workspace/:deviceId 同理 - Sidebar active 狀態也會正確(因為 pathname 匹配了) Co-Authored-By: Claude Opus 4.6 (1M context) --- local-tool/server/internal/api/router.go | 36 ++++++++++++++++++++---- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/local-tool/server/internal/api/router.go b/local-tool/server/internal/api/router.go index 15fbf45..124b251 100644 --- a/local-tool/server/internal/api/router.go +++ b/local-tool/server/internal/api/router.go @@ -144,7 +144,17 @@ func broadcasterLogger(b *logger.Broadcaster) gin.HandlerFunc { } // spaFallback tries to serve the exact file from the embedded FS. -// If the file doesn't exist, it serves index.html for client-side routing. +// If the file doesn't exist, it finds the best matching route shell HTML +// for Next.js static export client-side routing. +// +// Next.js static export with generateStaticParams creates: +// /models/index.html — static page +// /models/_/index.html — dynamic route shell (placeholder param '_') +// +// For a request like /models/yolov5-face-detection: +// 1. Try exact file → not found +// 2. Try /models/_/index.html → found → serve it (Next.js CSR picks up real param from URL) +// 3. Fall back to /index.html (root) func spaFallback(staticFS http.FileSystem) gin.HandlerFunc { return func(c *gin.Context) { path := c.Request.URL.Path @@ -155,15 +165,31 @@ func spaFallback(staticFS http.FileSystem) gin.HandlerFunc { return } - // Try to serve the exact file (e.g., /models/index.html) - f, err := staticFS.Open(path) - if err == nil { + // Try to serve the exact file + if f, err := staticFS.Open(path); err == nil { f.Close() http.FileServer(staticFS).ServeHTTP(c.Writer, c.Request) return } - // Fall back to root index.html for SPA routing + // Try Next.js dynamic route shell: replace last path segment with '_' + // e.g. /models/yolov5 → /models/_/index.html + // /devices/kl520-0 → /devices/_/index.html + // /workspace/kl520-0 → /workspace/_/index.html + segments := strings.Split(strings.TrimRight(path, "/"), "/") + if len(segments) >= 2 { + segments[len(segments)-1] = "_" + shellPath := strings.Join(segments, "/") + "/index.html" + if f, err := staticFS.Open(shellPath); err == nil { + defer f.Close() + c.Header("Content-Type", "text/html; charset=utf-8") + c.Status(http.StatusOK) + _, _ = io.Copy(c.Writer, f) + return + } + } + + // Final fallback: root index.html index, err := staticFS.Open("/index.html") if err != nil { c.Status(http.StatusInternalServerError)