Merge pull request 'Serve admin console at root path' (#1) from feat/dev into main
Reviewed-on: #1
This commit was merged in pull request #1.
This commit is contained in:
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
# 肉垫计划
|
# 肉垫计划
|
||||||
|
|
||||||
核心功能:多宠物档案、日常照顾任务、多宠物遛狗与主动位置分享、约遛。后端用 Gin + GORM + PostgreSQL,管理端用 React + Vite + shadcn/ui + Tailwind,构建后通过 `go:embed` 随服务提供 `/admin/`。
|
核心功能:多宠物档案、日常照顾任务、多宠物遛狗与主动位置分享、约遛。后端用 Gin + GORM + PostgreSQL,管理端用 React + Vite + shadcn/ui + Tailwind,构建后通过 `go:embed` 随服务提供 `/`。
|
||||||
|
|
||||||
## 目录
|
## 目录
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
3. 若数据库还不存在,在 `pets-be` 目录执行一次 `go run ./cmd/dbinit`。
|
3. 若数据库还不存在,在 `pets-be` 目录执行一次 `go run ./cmd/dbinit`。
|
||||||
4. 执行 `go run ./cmd/server`。
|
4. 执行 `go run ./cmd/server`。
|
||||||
|
|
||||||
默认业务接口 `http://localhost:9090/api`,管理端 `http://localhost:9090/admin/`。小程序生产 API 地址是 `https://pet.sundynix.cn/api`。
|
默认业务接口 `http://localhost:9090/api`,管理端 `http://localhost:9090/`。小程序生产 API 地址是 `https://pet.sundynix.cn/api`。
|
||||||
|
|
||||||
配置可用 `PETS_` 前缀的环境变量覆盖,例如 `PETS_POSTGRES_PASSWORD`。生产模式必须设置 `PETS_ADMIN_PASSWORD`,微信登录需要 App ID 和 App Secret。`auth.dev_login` 只能在 debug 模式开启。
|
配置可用 `PETS_` 前缀的环境变量覆盖,例如 `PETS_POSTGRES_PASSWORD`。生产模式必须设置 `PETS_ADMIN_PASSWORD`,微信登录需要 App ID 和 App Secret。`auth.dev_login` 只能在 debug 模式开启。
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+5
-5
@@ -1,12 +1,12 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="zh-CN">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<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" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>admin</title>
|
<title>肉垫计划 · 管理端</title>
|
||||||
<script type="module" crossorigin src="/admin/assets/index-DmUsbLpt.js"></script>
|
<script type="module" crossorigin src="/assets/index-COwxdvZQ.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/admin/assets/index-BQA5NViF.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-BQA5NViF.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import (
|
|||||||
//go:embed all:dist
|
//go:embed all:dist
|
||||||
var distFS embed.FS
|
var distFS embed.FS
|
||||||
|
|
||||||
// Register 把内嵌的后台 SPA 挂到 /admin 下,index.html 做 SPA fallback
|
// Register 把内嵌的后台 SPA 挂到根路径,保留 /api 给业务接口。
|
||||||
func Register(r *gin.Engine) {
|
func Register(r *gin.Engine) {
|
||||||
sub, err := fs.Sub(distFS, "dist")
|
sub, err := fs.Sub(distFS, "dist")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -21,9 +21,7 @@ func Register(r *gin.Engine) {
|
|||||||
fileServer := http.FileServer(http.FS(sub))
|
fileServer := http.FileServer(http.FS(sub))
|
||||||
|
|
||||||
handler := func(c *gin.Context) {
|
handler := func(c *gin.Context) {
|
||||||
// 去掉前缀 /admin,交给内嵌文件服务器
|
reqPath := strings.TrimPrefix(c.Request.URL.Path, "/")
|
||||||
reqPath := strings.TrimPrefix(c.Request.URL.Path, "/admin")
|
|
||||||
reqPath = strings.TrimPrefix(reqPath, "/")
|
|
||||||
|
|
||||||
// 若请求的是真实存在的静态资源,直出;否则回退 index.html(前端路由)
|
// 若请求的是真实存在的静态资源,直出;否则回退 index.html(前端路由)
|
||||||
if reqPath != "" {
|
if reqPath != "" {
|
||||||
@@ -43,6 +41,12 @@ func Register(r *gin.Engine) {
|
|||||||
c.Data(http.StatusOK, "text/html; charset=utf-8", data)
|
c.Data(http.StatusOK, "text/html; charset=utf-8", data)
|
||||||
}
|
}
|
||||||
|
|
||||||
r.GET("/admin", func(c *gin.Context) { c.Redirect(http.StatusFound, "/admin/") })
|
r.GET("/", handler)
|
||||||
r.GET("/admin/*filepath", 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)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
# 管理端
|
# 管理端
|
||||||
|
|
||||||
React + Vite + Tailwind + shadcn/ui。`npm ci && npm run build` 会把产物输出到 `../../internal/admin/dist`,由 Go 服务嵌入并提供 `/admin/`。
|
React + Vite + Tailwind + shadcn/ui。`npm ci && npm run build` 会把产物输出到 `../../internal/admin/dist`,由 Go 服务嵌入并提供 `/`。
|
||||||
|
|
||||||
开发时执行 `npm run dev`,Vite 会把 `/api` 代理到本地 9090 端口。
|
开发时执行 `npm run dev`,Vite 会把 `/api` 代理到本地 9090 端口。
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="zh-CN">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>admin</title>
|
<title>肉垫计划 · 管理端</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
Generated
-58
@@ -17,7 +17,6 @@
|
|||||||
"lucide-react": "^1.23.0",
|
"lucide-react": "^1.23.0",
|
||||||
"react": "^19.2.7",
|
"react": "^19.2.7",
|
||||||
"react-dom": "^19.2.7",
|
"react-dom": "^19.2.7",
|
||||||
"react-router-dom": "^7.18.1",
|
|
||||||
"recharts": "^3.10.1",
|
"recharts": "^3.10.1",
|
||||||
"tailwind-merge": "^3.6.0"
|
"tailwind-merge": "^3.6.0"
|
||||||
},
|
},
|
||||||
@@ -1601,19 +1600,6 @@
|
|||||||
"node": ">= 6"
|
"node": ">= 6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/cookie": {
|
|
||||||
"version": "1.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
|
|
||||||
"integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/express"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/cssesc": {
|
"node_modules/cssesc": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
|
||||||
@@ -3090,44 +3076,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-router": {
|
|
||||||
"version": "7.18.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.1.tgz",
|
|
||||||
"integrity": "sha512-GDLgg3i3uM0aeJO3Fm+TCS+sDQ7gu12T6x0qdTEzcwqEfleci7JwugVNIF3U//0FWKnJT7ptG+20B2jfDqnZAg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"cookie": "^1.0.1",
|
|
||||||
"set-cookie-parser": "^2.6.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=20.0.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"react": ">=18",
|
|
||||||
"react-dom": ">=18"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"react-dom": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/react-router-dom": {
|
|
||||||
"version": "7.18.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.1.tgz",
|
|
||||||
"integrity": "sha512-KaZh+X/6UtEp28x51AUYZDMg9NGoz2ja3dNHa+ta/tk40vCzKhQ/RypCWBMLbmDr6//E24Vv5uPsrqXFozdkAg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"react-router": "7.18.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=20.0.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"react": ">=18",
|
|
||||||
"react-dom": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/react-style-singleton": {
|
"node_modules/react-style-singleton": {
|
||||||
"version": "2.2.3",
|
"version": "2.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz",
|
||||||
@@ -3334,12 +3282,6 @@
|
|||||||
"integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
|
"integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/set-cookie-parser": {
|
|
||||||
"version": "2.7.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
|
|
||||||
"integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/source-map-js": {
|
"node_modules/source-map-js": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||||
|
|||||||
@@ -19,7 +19,6 @@
|
|||||||
"lucide-react": "^1.23.0",
|
"lucide-react": "^1.23.0",
|
||||||
"react": "^19.2.7",
|
"react": "^19.2.7",
|
||||||
"react-dom": "^19.2.7",
|
"react-dom": "^19.2.7",
|
||||||
"react-router-dom": "^7.18.1",
|
|
||||||
"recharts": "^3.10.1",
|
"recharts": "^3.10.1",
|
||||||
"tailwind-merge": "^3.6.0"
|
"tailwind-merge": "^3.6.0"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
import { StrictMode } from 'react'
|
import { StrictMode } from 'react'
|
||||||
import { createRoot } from 'react-dom/client'
|
import { createRoot } from 'react-dom/client'
|
||||||
import { BrowserRouter } from 'react-router-dom'
|
|
||||||
import './index.css'
|
import './index.css'
|
||||||
import App from './App.tsx'
|
import App from './App.tsx'
|
||||||
|
|
||||||
createRoot(document.getElementById('root')!).render(
|
createRoot(document.getElementById('root')!).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<BrowserRouter basename="/admin">
|
<App />
|
||||||
<App />
|
|
||||||
</BrowserRouter>
|
|
||||||
</StrictMode>,
|
</StrictMode>,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import path from 'node:path'
|
|||||||
|
|
||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
base: '/admin/',
|
base: '/',
|
||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
|
|||||||
Reference in New Issue
Block a user