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:
parent
ebe86663b1
commit
819885c85d
@ -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)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user