Serve admin console at root path
ci-cd / verify (push) Successful in 1m48s
ci-cd / deploy (push) Skipped

This commit is contained in:
2026-10-01 14:13:04 +08:00
parent e9828d6149
commit 8de48cb44d
12 changed files with 75 additions and 99 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+5 -5
View File
@@ -1,12 +1,12 @@
<!doctype html>
<html lang="en">
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/admin/favicon.svg" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>admin</title>
<script type="module" crossorigin src="/admin/assets/index-DmUsbLpt.js"></script>
<link rel="stylesheet" crossorigin href="/admin/assets/index-BQA5NViF.css">
<title>肉垫计划 · 管理端</title>
<script type="module" crossorigin src="/assets/index-COwxdvZQ.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BQA5NViF.css">
</head>
<body>
<div id="root"></div>
+10 -6
View File
@@ -12,7 +12,7 @@ import (
//go:embed all:dist
var distFS embed.FS
// Register 把内嵌的后台 SPA 挂到 /admin 下,index.html 做 SPA fallback
// Register 把内嵌的后台 SPA 挂到根路径,保留 /api 给业务接口。
func Register(r *gin.Engine) {
sub, err := fs.Sub(distFS, "dist")
if err != nil {
@@ -21,9 +21,7 @@ func Register(r *gin.Engine) {
fileServer := http.FileServer(http.FS(sub))
handler := func(c *gin.Context) {
// 去掉前缀 /admin,交给内嵌文件服务器
reqPath := strings.TrimPrefix(c.Request.URL.Path, "/admin")
reqPath = strings.TrimPrefix(reqPath, "/")
reqPath := strings.TrimPrefix(c.Request.URL.Path, "/")
// 若请求的是真实存在的静态资源,直出;否则回退 index.html(前端路由)
if reqPath != "" {
@@ -43,6 +41,12 @@ func Register(r *gin.Engine) {
c.Data(http.StatusOK, "text/html; charset=utf-8", data)
}
r.GET("/admin", func(c *gin.Context) { c.Redirect(http.StatusFound, "/admin/") })
r.GET("/admin/*filepath", handler)
r.GET("/", handler)
r.NoRoute(func(c *gin.Context) {
if c.Request.Method != http.MethodGet || strings.HasPrefix(c.Request.URL.Path, "/api/") || c.Request.URL.Path == "/api" || strings.HasPrefix(c.Request.URL.Path, "/assets/") {
c.Status(http.StatusNotFound)
return
}
handler(c)
})
}
+36
View File
@@ -0,0 +1,36 @@
package admin
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/gin-gonic/gin"
"github.com/sundynix/pets-be/internal/config"
"github.com/sundynix/pets-be/internal/httpapi"
)
func TestAdminAtRootAndAPIRoutes(t *testing.T) {
gin.SetMode(gin.TestMode)
r := gin.New()
httpapi.New(nil, nil, &config.Config{}).Register(r)
Register(r)
for _, tc := range []struct {
path string
status int
body string
}{
{path: "/", status: http.StatusOK, body: "<div id=\"root\"></div>"},
{path: "/api/ping", status: http.StatusOK, body: "pong"},
{path: "/api/missing", status: http.StatusNotFound},
{path: "/assets/missing.js", status: http.StatusNotFound},
} {
recorder := httptest.NewRecorder()
r.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, tc.path, nil))
if recorder.Code != tc.status || (tc.body != "" && !strings.Contains(recorder.Body.String(), tc.body)) {
t.Errorf("GET %s: got %d %q, want %d containing %q", tc.path, recorder.Code, recorder.Body.String(), tc.status, tc.body)
}
}
}