feat: 前台动态路由与积木页面、网页路径/发布/模式、PAGE_BUILDER 文档
Made-with: Cursor
This commit is contained in:
@@ -67,11 +67,14 @@ func GetPageByID(c *gin.Context) {
|
||||
|
||||
// CreatePageInput 创建网页
|
||||
type CreatePageInput struct {
|
||||
SiteID string `json:"site_id" binding:"required"`
|
||||
Slug string `json:"slug" binding:"required"`
|
||||
Title string `json:"title" binding:"required"`
|
||||
Type string `json:"type"` // homepage, page
|
||||
Content string `json:"content"`
|
||||
SiteID string `json:"site_id" binding:"required"`
|
||||
Slug string `json:"slug" binding:"required"`
|
||||
Title string `json:"title" binding:"required"`
|
||||
Type string `json:"type"` // homepage, page
|
||||
Content string `json:"content"`
|
||||
ContentMode string `json:"content_mode"` // html | builder
|
||||
RoutePath string `json:"route_path"`
|
||||
Published *bool `json:"published"`
|
||||
}
|
||||
|
||||
// CreatePage 创建网页
|
||||
@@ -98,6 +101,15 @@ func CreatePage(c *gin.Context) {
|
||||
"content": input.Content,
|
||||
"updated_at": now,
|
||||
}
|
||||
if input.ContentMode != "" {
|
||||
doc["content_mode"] = input.ContentMode
|
||||
}
|
||||
if input.RoutePath != "" {
|
||||
doc["route_path"] = input.RoutePath
|
||||
}
|
||||
if input.Published != nil {
|
||||
doc["published"] = *input.Published
|
||||
}
|
||||
res, err := coll.InsertOne(ctx, doc)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
@@ -108,10 +120,13 @@ func CreatePage(c *gin.Context) {
|
||||
|
||||
// UpdatePageInput 更新网页
|
||||
type UpdatePageInput struct {
|
||||
Slug *string `json:"slug"`
|
||||
Title *string `json:"title"`
|
||||
Type *string `json:"type"`
|
||||
Content *string `json:"content"`
|
||||
Slug *string `json:"slug"`
|
||||
Title *string `json:"title"`
|
||||
Type *string `json:"type"`
|
||||
Content *string `json:"content"`
|
||||
ContentMode *string `json:"content_mode"`
|
||||
RoutePath *string `json:"route_path"`
|
||||
Published *bool `json:"published"`
|
||||
}
|
||||
|
||||
// UpdatePage 更新网页
|
||||
@@ -142,6 +157,15 @@ func UpdatePage(c *gin.Context) {
|
||||
if input.Content != nil {
|
||||
set["content"] = *input.Content
|
||||
}
|
||||
if input.ContentMode != nil {
|
||||
set["content_mode"] = *input.ContentMode
|
||||
}
|
||||
if input.RoutePath != nil {
|
||||
set["route_path"] = *input.RoutePath
|
||||
}
|
||||
if input.Published != nil {
|
||||
set["published"] = *input.Published
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
140
server/handlers/web_routes.go
Normal file
140
server/handlers/web_routes.go
Normal file
@@ -0,0 +1,140 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
"go.mongodb.org/mongo-driver/v2/mongo/options"
|
||||
|
||||
"yh_web/server/config"
|
||||
"yh_web/server/models"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// effectivePagePath 对外访问路径:优先 route_path,否则 /{slug};index 且无 route_path 时返回空(由首页单独处理)
|
||||
func effectivePagePath(p models.Page) string {
|
||||
if p.RoutePath != "" {
|
||||
path := strings.TrimSpace(p.RoutePath)
|
||||
if !strings.HasPrefix(path, "/") {
|
||||
path = "/" + path
|
||||
}
|
||||
return path
|
||||
}
|
||||
if p.Slug == "" || p.Slug == homepageSlug {
|
||||
return ""
|
||||
}
|
||||
return "/" + p.Slug
|
||||
}
|
||||
|
||||
// GetWebRoutes 前台:获取站点已发布页面的动态路由列表(无需鉴权)
|
||||
func GetWebRoutes(c *gin.Context) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
siteID := c.Query("site_id")
|
||||
if siteID == "" {
|
||||
siteID = getOfficialSiteID(ctx)
|
||||
}
|
||||
if siteID == "" {
|
||||
c.JSON(http.StatusOK, gin.H{"site_id": "", "routes": []any{}})
|
||||
return
|
||||
}
|
||||
|
||||
coll := config.GetDB(config.DBName).Collection("pages")
|
||||
opts := options.Find().SetSort(bson.D{{Key: "updated_at", Value: -1}})
|
||||
cursor, err := coll.Find(ctx, bson.M{"site_id": siteID}, opts)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
defer cursor.Close(ctx)
|
||||
|
||||
var pages []models.Page
|
||||
if err = cursor.All(ctx, &pages); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
routes := make([]gin.H, 0)
|
||||
for _, p := range pages {
|
||||
if p.Published != nil && !*p.Published {
|
||||
continue
|
||||
}
|
||||
path := effectivePagePath(p)
|
||||
if path == "" {
|
||||
continue
|
||||
}
|
||||
routes = append(routes, gin.H{
|
||||
"path": path,
|
||||
"title": p.Title,
|
||||
"slug": p.Slug,
|
||||
"id": p.ID.Hex(),
|
||||
"mode": p.ContentMode,
|
||||
})
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"site_id": siteID, "routes": routes})
|
||||
}
|
||||
|
||||
// GetWebPageByPath 前台:按路径取单页内容(无需鉴权)
|
||||
func GetWebPageByPath(c *gin.Context) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
siteID := c.Query("site_id")
|
||||
if siteID == "" {
|
||||
siteID = getOfficialSiteID(ctx)
|
||||
}
|
||||
path := strings.TrimSpace(c.Query("path"))
|
||||
if siteID == "" || path == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "缺少 site_id 或 path"})
|
||||
return
|
||||
}
|
||||
if !strings.HasPrefix(path, "/") {
|
||||
path = "/" + path
|
||||
}
|
||||
|
||||
coll := config.GetDB(config.DBName).Collection("pages")
|
||||
|
||||
var page models.Page
|
||||
tryDecode := func(filter bson.M) bool {
|
||||
page = models.Page{}
|
||||
err := coll.FindOne(ctx, filter).Decode(&page)
|
||||
return err == nil && !page.ID.IsZero()
|
||||
}
|
||||
if !tryDecode(bson.M{"site_id": siteID, "route_path": path}) {
|
||||
alt := strings.TrimPrefix(path, "/")
|
||||
if alt != "" {
|
||||
tryDecode(bson.M{"site_id": siteID, "route_path": alt})
|
||||
}
|
||||
}
|
||||
if page.ID.IsZero() {
|
||||
slug := strings.TrimPrefix(path, "/")
|
||||
if slug != "" {
|
||||
tryDecode(bson.M{"site_id": siteID, "slug": slug})
|
||||
}
|
||||
}
|
||||
if page.ID.IsZero() {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "页面不存在"})
|
||||
return
|
||||
}
|
||||
if page.Published != nil && !*page.Published {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "页面未发布"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"id": page.ID.Hex(),
|
||||
"site_id": page.SiteID,
|
||||
"slug": page.Slug,
|
||||
"title": page.Title,
|
||||
"type": page.Type,
|
||||
"content": page.Content,
|
||||
"content_mode": page.ContentMode,
|
||||
"route_path": page.RoutePath,
|
||||
"updated_at": page.UpdatedAt,
|
||||
})
|
||||
}
|
||||
@@ -206,6 +206,8 @@ func main() {
|
||||
|
||||
// 官网站点首页(前台,无需鉴权)
|
||||
r.GET("/api/web/homepage", handlers.GetWebHomepage)
|
||||
r.GET("/api/web/routes", handlers.GetWebRoutes)
|
||||
r.GET("/api/web/page", handlers.GetWebPageByPath)
|
||||
|
||||
// 前台 API 路由组
|
||||
web := r.Group("/api/web")
|
||||
|
||||
@@ -13,13 +13,16 @@ type Site struct {
|
||||
|
||||
// Page 网页(属于某站点)
|
||||
type Page struct {
|
||||
ID bson.ObjectID `bson:"_id,omitempty" json:"id"`
|
||||
SiteID string `bson:"site_id" json:"site_id"`
|
||||
Slug string `bson:"slug" json:"slug"` // index, about, ...
|
||||
Title string `bson:"title" json:"title"`
|
||||
Type string `bson:"type" json:"type"` // homepage, page
|
||||
Content string `bson:"content" json:"content"` // HTML 或 JSON 字符串
|
||||
UpdatedAt string `bson:"updated_at" json:"updated_at"`
|
||||
ID bson.ObjectID `bson:"_id,omitempty" json:"id"`
|
||||
SiteID string `bson:"site_id" json:"site_id"`
|
||||
Slug string `bson:"slug" json:"slug"` // index, about, ...
|
||||
Title string `bson:"title" json:"title"`
|
||||
Type string `bson:"type" json:"type"` // homepage, page
|
||||
Content string `bson:"content" json:"content"` // html 模式为 HTML;builder 模式为 JSON(见文档)
|
||||
ContentMode string `bson:"content_mode,omitempty" json:"content_mode"` // html | builder,空视为 html
|
||||
RoutePath string `bson:"route_path,omitempty" json:"route_path"` // 自定义前台路径,如 /about;空则用 /{slug}
|
||||
Published *bool `bson:"published,omitempty" json:"published"` // nil 或未设视为已发布
|
||||
UpdatedAt string `bson:"updated_at" json:"updated_at"`
|
||||
}
|
||||
|
||||
// HomepageData 首页可编辑数据(与 yuheng-download-space 对应)
|
||||
|
||||
Reference in New Issue
Block a user