docs+feat: 重构规格与 Gitea 式 SSR 骨架(首页)
在 rebuild/gitea-ssr 落地产品规格、Cursor 规则,以及 Go 模板 SSR 首页/板块列表;未迁移路径仍回落 SPA,便于对照 main。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
219
routers/web/home.go
Normal file
219
routers/web/home.go
Normal file
@@ -0,0 +1,219 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.iioio.com/freefire/jiang13-forum/middleware"
|
||||
"git.iioio.com/freefire/jiang13-forum/model"
|
||||
"git.iioio.com/freefire/jiang13-forum/modules/webrender"
|
||||
"git.iioio.com/freefire/jiang13-forum/service"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// Deps 页面路由依赖(复用现有 service,避免 Phase 1 大搬家)
|
||||
type Deps struct {
|
||||
Settings *service.ForumSettingsService
|
||||
Board *service.BoardService
|
||||
Post *service.PostService
|
||||
}
|
||||
|
||||
// BoardView 侧栏板块
|
||||
type BoardView struct {
|
||||
ID uint
|
||||
Name string
|
||||
}
|
||||
|
||||
// PostView 列表项
|
||||
type PostView struct {
|
||||
ID uint
|
||||
Title string
|
||||
AuthorName string
|
||||
BoardName string
|
||||
Pinned bool
|
||||
Featured bool
|
||||
CommentCount int
|
||||
CreatedLabel string
|
||||
}
|
||||
|
||||
// HomePageData 首页 / 板块 Feed
|
||||
type HomePageData struct {
|
||||
Title string
|
||||
Description string
|
||||
SiteName string
|
||||
Slogan string
|
||||
LogoMark string
|
||||
LoggedIn bool
|
||||
IsAdmin bool
|
||||
ViewerName string
|
||||
Boards []BoardView
|
||||
ActiveBoard uint
|
||||
BoardName string
|
||||
Sort string
|
||||
Posts []PostView
|
||||
Page int
|
||||
PrevPage int
|
||||
NextPage int
|
||||
HasPrev bool
|
||||
HasMore bool
|
||||
}
|
||||
|
||||
// Register 注册已迁移的 SSR 页面(优先于 SPA)
|
||||
func Register(r *gin.Engine, deps Deps, authMW *middleware.AuthMiddleware) {
|
||||
g := r.Group("/", authMW.OptionalAuth())
|
||||
g.GET("/", deps.Home)
|
||||
g.GET("/board/:id", deps.Home)
|
||||
}
|
||||
|
||||
// Home SSR 首页与板块列表
|
||||
func (d Deps) Home(c *gin.Context) {
|
||||
brand := d.Settings.SiteBranding()
|
||||
sort := c.DefaultQuery("sort", "latest")
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
size := d.Settings.PageSizeDefault()
|
||||
|
||||
var boardID uint
|
||||
var boardName string
|
||||
if idStr := c.Param("id"); idStr != "" {
|
||||
// 兼容伪静态后缀 123.html
|
||||
idStr = strings.TrimSuffix(idStr, "."+d.Settings.Permalink().Ext)
|
||||
idStr = strings.TrimSuffix(idStr, ".html")
|
||||
idStr = strings.TrimSuffix(idStr, ".htm")
|
||||
if n, err := strconv.ParseUint(idStr, 10, 64); err == nil {
|
||||
boardID = uint(n)
|
||||
}
|
||||
}
|
||||
|
||||
boards, _ := d.Board.List()
|
||||
boardViews := make([]BoardView, 0, len(boards))
|
||||
for _, b := range boards {
|
||||
boardViews = append(boardViews, BoardView{ID: b.ID, Name: b.Name})
|
||||
if b.ID == boardID {
|
||||
boardName = b.Name
|
||||
}
|
||||
}
|
||||
|
||||
var uid uint
|
||||
if v, ok := c.Get(middleware.CtxUserID); ok {
|
||||
uid, _ = v.(uint)
|
||||
}
|
||||
isAdmin := false
|
||||
if v, ok := c.Get(middleware.CtxRole); ok {
|
||||
switch r := v.(type) {
|
||||
case model.Role:
|
||||
isAdmin = r == model.RoleAdmin
|
||||
case string:
|
||||
isAdmin = r == string(model.RoleAdmin)
|
||||
}
|
||||
}
|
||||
username, _ := c.Get(middleware.CtxUsername)
|
||||
|
||||
q := service.PostListQuery{
|
||||
BoardID: boardID,
|
||||
Page: page,
|
||||
Size: size,
|
||||
Sort: sort,
|
||||
ViewerID: uid,
|
||||
ViewerIsAdmin: isAdmin,
|
||||
}
|
||||
items, total, err := d.Post.ListItems(q)
|
||||
if err != nil {
|
||||
c.String(http.StatusInternalServerError, "加载帖子失败")
|
||||
return
|
||||
}
|
||||
|
||||
posts := make([]PostView, 0, len(items))
|
||||
for _, it := range items {
|
||||
author := strings.TrimSpace(it.User.Nickname)
|
||||
if author == "" {
|
||||
author = it.User.Username
|
||||
}
|
||||
bname := ""
|
||||
if it.Board.ID > 0 {
|
||||
bname = it.Board.Name
|
||||
}
|
||||
posts = append(posts, PostView{
|
||||
ID: it.ID,
|
||||
Title: it.Title,
|
||||
AuthorName: author,
|
||||
BoardName: bname,
|
||||
Pinned: it.Pinned,
|
||||
Featured: it.Featured,
|
||||
CommentCount: it.CommentCount,
|
||||
CreatedLabel: formatTime(it.CreatedAt),
|
||||
})
|
||||
}
|
||||
|
||||
title := brand.DocumentTitle()
|
||||
if boardName != "" {
|
||||
title = boardName + " · " + brand.Name
|
||||
}
|
||||
|
||||
data := HomePageData{
|
||||
Title: title,
|
||||
Description: brand.MetaDescription(),
|
||||
SiteName: brand.Name,
|
||||
Slogan: brand.Slogan,
|
||||
LogoMark: firstRuneOr(brand.LogoMark, "姜"),
|
||||
LoggedIn: uid > 0,
|
||||
IsAdmin: isAdmin,
|
||||
ViewerName: fmtViewer(username),
|
||||
Boards: boardViews,
|
||||
ActiveBoard: boardID,
|
||||
BoardName: boardName,
|
||||
Sort: normalizeSort(sort),
|
||||
Posts: posts,
|
||||
Page: page,
|
||||
PrevPage: page - 1,
|
||||
NextPage: page + 1,
|
||||
HasPrev: page > 1,
|
||||
HasMore: int64(page*size) < total,
|
||||
}
|
||||
|
||||
c.Header("Content-Type", "text/html; charset=utf-8")
|
||||
c.Status(http.StatusOK)
|
||||
if err := webrender.Execute(c.Writer, "home", data); err != nil {
|
||||
c.String(http.StatusInternalServerError, "模板渲染失败: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeSort(s string) string {
|
||||
switch s {
|
||||
case "reply", "hot":
|
||||
return s
|
||||
default:
|
||||
return "latest"
|
||||
}
|
||||
}
|
||||
|
||||
func formatTime(t time.Time) string {
|
||||
return t.Local().Format("2006-01-02 15:04")
|
||||
}
|
||||
|
||||
func firstRuneOr(s, fallback string) string {
|
||||
s = strings.TrimSpace(s)
|
||||
if s == "" {
|
||||
return fallback
|
||||
}
|
||||
for _, r := range s {
|
||||
return string(r)
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
func fmtViewer(v any) string {
|
||||
if v == nil {
|
||||
return "我的"
|
||||
}
|
||||
s, _ := v.(string)
|
||||
s = strings.TrimSpace(s)
|
||||
if s == "" {
|
||||
return "我的"
|
||||
}
|
||||
return s
|
||||
}
|
||||
Reference in New Issue
Block a user