Files
usernode/internal/webui/webui.go
T
cao.wangrenbo ae45aba607 feat(M0): 工程骨架 — Go 后端 + Vue3 前端脚手架
- Go 工程:cmd/usernode CLI(serve/migrate/admin create/reset-password/user otp)
  + internal/{config,model,service,system,auth,cron,api,router,server,pkg,webui}
- 配置:TOML + 环境变量覆盖(USERNODE_<SEC>_<FIELD>),config.example.toml
- 数据:GORM 双驱动(SQLite/MySQL)8 表模型,migrate 子命令可跑通
- 系统层:system.Manager 接口(useradd/userdel/passwd 白名单,dev dry-run)
- HTTP:Gin 路由骨架(/api/v1 + 501 占位),healthz,slog 结构化日志,优雅启停
- 前端:Vite + Vue3 + TS + Element Plus 最小可运行(web/),go:embed 打通
- 构建:deploy/Containerfile 多阶段单二进制镜像,web/Containerfile.dev 前端 dev
  镜像,Makefile(build/test/dev/web-build/image);go.mod 锁定 go 1.22
- 验证:go build/vet/test 通过;podman 镜像构建运行 healthz+embed 通过
2026-08-29 22:43:27 +08:00

59 lines
1.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Package webui 嵌入前端构建产物(go:embed)。
//
// 构建方式:先构建 web/Vite 产物输出到 web/dist),再 go build。
// 未构建前端时(骨架阶段 / go test / 纯后端开发)由 NewHandler 提供
// SPA fallback 占位,保证服务可启动、/healthz 可用。
package webui
import (
"bytes"
"embed"
"io/fs"
"net/http"
"strings"
"time"
)
//go:embed all:dist
var dist embed.FS
// NewHandler 返回静态资源 handler。前端产物存在时提供 SPA fallback
// (非 API 路径回退 index.html,交给前端路由);否则返回占位页。
func NewHandler() http.Handler {
sub, err := fs.Sub(dist, "dist")
if err != nil {
return placeholderHandler("前端产物未嵌入(先执行 make web-build")
}
indexBytes, err := fs.ReadFile(sub, "index.html")
if err != nil {
return placeholderHandler("前端产物未嵌入(先执行 make web-build")
}
fileServer := http.FileServer(http.FS(sub))
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
p := strings.TrimPrefix(r.URL.Path, "/")
if p == "" {
p = "index.html"
}
// 存在的静态资源直接返回(含版本化 assets/ 与 favicon
if f, err := sub.Open(p); err == nil {
f.Close()
fileServer.ServeHTTP(w, r)
return
}
// SPA fallback:其余路径回退 index.html
w.Header().Set("Content-Type", "text/html; charset=utf-8")
http.ServeContent(w, r, "index.html", time.Time{}, bytes.NewReader(indexBytes))
})
}
func placeholderHandler(msg string) http.Handler {
body := "<!doctype html><html><head><meta charset=\"utf-8\"><title>ws_usernode</title></head>" +
"<body><h1>ws_usernode</h1><p>" + msg + "</p>" +
"<p>健康检查:<a href=\"/healthz\">/healthz</a></p></body></html>"
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
_, _ = w.Write([]byte(body))
})
}