fix(local-tool): SPA fallback 改用 Next.js dynamic route shell HTML

根因:點模型卡片 → /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) <noreply@anthropic.com>
This commit is contained in:
jim800121chen 2026-04-12 19:44:25 +08:00
parent ebe86663b1
commit 819885c85d

View File

@ -144,7 +144,17 @@ func broadcasterLogger(b *logger.Broadcaster) gin.HandlerFunc {
} }
// spaFallback tries to serve the exact file from the embedded FS. // 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 { func spaFallback(staticFS http.FileSystem) gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
path := c.Request.URL.Path path := c.Request.URL.Path
@ -155,15 +165,31 @@ func spaFallback(staticFS http.FileSystem) gin.HandlerFunc {
return return
} }
// Try to serve the exact file (e.g., /models/index.html) // Try to serve the exact file
f, err := staticFS.Open(path) if f, err := staticFS.Open(path); err == nil {
if err == nil {
f.Close() f.Close()
http.FileServer(staticFS).ServeHTTP(c.Writer, c.Request) http.FileServer(staticFS).ServeHTTP(c.Writer, c.Request)
return 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") index, err := staticFS.Open("/index.html")
if err != nil { if err != nil {
c.Status(http.StatusInternalServerError) c.Status(http.StatusInternalServerError)