feat: SSR 公开板块索引 /boards

EOF

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-29 06:53:20 +08:00
parent d4b29f5b7c
commit 2a46c01c85
13 changed files with 209 additions and 4 deletions

View File

@@ -44,7 +44,7 @@
## C. 板块
- [x] 列出板块(含帖数等展示字段)— 前台侧栏 + Admin
- [x] 列出板块(含帖数等展示字段)— SSR `/boards` + 侧栏 + Admin
- [x] 管理员:创建 / 改 / 删板块 — SSR `/admin/boards`
- [x] 板块名称、描述、图标、色板索引、排序
- [x] 默认板块保障(空站可引导创建)

View File

@@ -26,6 +26,7 @@
|------|------|-----|
| `/` | Feed | 已迁 |
| `/board/:id` | 板块 Feed | 已迁 |
| `/boards` | 板块索引 | 已迁 |
| `/post/:id` | 帖详情 + 评论(回复/赞/私密)/赞/藏 | 已迁 |
| `/compose` | 发帖normaltextarea + 图片 + 门控插入) | 已迁 |
| `/post/:id/edit` | 编辑帖 | 已迁 |

View File

@@ -88,6 +88,8 @@ web_src/ → public/assets/
私信:`/messages``/messages/with/:peerId`(系统 peer=0 只读;打开会话标已读)。
公开浏览:`/boards` 板块索引(链到 `/board/:id`)。
友链:`/links`(列表、登录申请/取消、Logo 上传Admin `/admin/friend-links`(品牌增删、审核、入口开关)。
Admin`/admin/dashboard``/admin/boards``/admin/moderation``/admin/settings`(品牌/限流/敏感词/SMTP`/admin/friend-links`

View File

@@ -71,6 +71,7 @@ var parseGlobs = []string{
"favorites/*.tmpl",
"messages/*.tmpl",
"links/*.tmpl",
"boards/*.tmpl",
}
// Load 解析全部模板(进程内一次)

View File

@@ -610,3 +610,59 @@ points-only[data-locked="false"] {
margin-right: 0.25rem;
}
.j13-boards { max-width: 800px; margin: 0 auto; padding: 1rem 1rem 2.5rem; }
.j13-boards h1 { margin: 0 0 1rem; font-size: 1.4rem; }
.j13-boards__grid {
list-style: none;
margin: 0;
padding: 0;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 0.85rem;
}
.j13-boards__card {
display: flex;
gap: 0.75rem;
align-items: flex-start;
padding: 0.85rem 0.9rem;
border: 1px solid var(--j13-border);
border-radius: var(--j13-radius);
background: var(--j13-bg);
color: inherit;
text-decoration: none;
min-height: 5rem;
}
.j13-boards__card:hover { border-color: var(--j13-accent); }
.j13-boards__dot {
width: 0.65rem;
height: 0.65rem;
border-radius: 50%;
margin-top: 0.35rem;
flex-shrink: 0;
background: var(--j13-accent);
}
.j13-boards__card[data-color="0"] .j13-boards__dot { background: #0f766e; }
.j13-boards__card[data-color="1"] .j13-boards__dot { background: #b45309; }
.j13-boards__card[data-color="2"] .j13-boards__dot { background: #1d4ed8; }
.j13-boards__card[data-color="3"] .j13-boards__dot { background: #be123c; }
.j13-boards__card[data-color="4"] .j13-boards__dot { background: #7c3aed; }
.j13-boards__card[data-color="5"] .j13-boards__dot { background: #047857; }
.j13-boards__card[data-color="6"] .j13-boards__dot { background: #c2410c; }
.j13-boards__card[data-color="7"] .j13-boards__dot { background: #0369a1; }
.j13-boards__body {
display: flex;
flex-direction: column;
gap: 0.2rem;
min-width: 0;
}
.j13-boards__body strong { font-size: 1rem; }
.j13-boards__icon { font-size: 0.75rem; }
.j13-boards__desc {
font-size: 0.85rem;
color: var(--j13-muted);
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}

53
routers/web/boards.go Normal file
View File

@@ -0,0 +1,53 @@
package web
import (
"net/http"
"git.iioio.com/freefire/jiang13-forum/modules/webctx"
"github.com/gin-gonic/gin"
)
type boardsItemView struct {
ID uint
Name string
Description string
Icon string
ColorIndex int
PostCount int
}
type boardsPageData struct {
PageChrome
Items []boardsItemView
}
// BoardsGet 公开板块索引
func (d Deps) BoardsGet(c *gin.Context) {
ctx := d.ctx(c)
d.renderBoards(ctx)
}
func (d Deps) renderBoards(ctx *webctx.Context) {
brand := d.Settings.SiteBranding()
chrome := d.chrome(ctx, "板块 · "+brand.Name, "", "")
list, _ := d.Board.ListWithStats()
items := make([]boardsItemView, 0, len(list))
for _, b := range list {
color := b.ColorIndex
if color < 0 {
color = int(b.ID % 8)
}
items = append(items, boardsItemView{
ID: b.ID,
Name: b.Name,
Description: b.Description,
Icon: b.Icon,
ColorIndex: color,
PostCount: b.PostCount,
})
}
ctx.HTML(http.StatusOK, "boards/list", boardsPageData{
PageChrome: chrome,
Items: items,
})
}

View File

@@ -46,6 +46,7 @@ type PageChrome struct {
ViewerName string
Boards []BoardView
ActiveBoard uint
Path string // 当前请求路径,供侧栏高亮
Inner string // 保留字段;入口模板已固定组合,不再动态 template
CSRF string
Flash string
@@ -88,6 +89,10 @@ func (d Deps) chrome(ctx *webctx.Context, title, desc, inner string) PageChrome
if ctx.IsSigned() && ctx.Doer != nil {
viewerPoints = ctx.Doer.Points
}
path := ""
if ctx.C != nil && ctx.C.Request != nil && ctx.C.Request.URL != nil {
path = ctx.C.Request.URL.Path
}
return PageChrome{
Title: title,
Description: desc,
@@ -98,6 +103,7 @@ func (d Deps) chrome(ctx *webctx.Context, title, desc, inner string) PageChrome
IsAdmin: ctx.IsAdmin(),
ViewerName: name,
Boards: bv,
Path: path,
Inner: inner,
CSRF: ctx.EnsureCSRF(),
Flash: ctx.TakeFlash(),

View File

@@ -83,7 +83,7 @@ func Register(r *gin.Engine, deps Deps, authMW *auth.AuthMiddleware) {
g.POST("/links/apply/:id/cancel", authMW.RequireAuth(), deps.LinksApplyCancelPost)
g.POST("/links/logo", authMW.RequireAuth(), deps.LinksLogoUpload)
g.GET("/projects", deps.PendingPage)
g.GET("/boards", deps.PendingPage)
g.GET("/boards", deps.BoardsGet)
}
// HomePageData Feed

View File

@@ -9,6 +9,7 @@
</span>
</a>
<nav class="j13-nav">
<a href="/boards">板块</a>
{{if .ShowFriendLinksNav}}<a href="/links">友链</a>{{end}}
{{if .LoggedIn}}
<a href="/compose">发帖</a>

View File

@@ -0,0 +1,28 @@
{{define "boards/list"}}
{{template "base/head" .}}
{{template "base/navbar" .}}
<main class="j13-main j13-boards">
<h1>板块</h1>
{{template "base/alert" .}}
{{if .Items}}
<ul class="j13-boards__grid">
{{range .Items}}
<li>
<a class="j13-boards__card" href="/board/{{.ID}}" data-color="{{.ColorIndex}}">
<span class="j13-boards__dot" aria-hidden="true"></span>
<span class="j13-boards__body">
<strong>{{.Name}}</strong>
{{if .Icon}}<span class="j13-muted j13-boards__icon">{{.Icon}}</span>{{end}}
{{if .Description}}<span class="j13-boards__desc">{{.Description}}</span>{{end}}
<span class="j13-muted">{{.PostCount}} 帖</span>
</span>
</a>
</li>
{{end}}
</ul>
{{else}}
<p class="j13-empty">暂无板块。{{if .IsAdmin}}请前往 <a href="/admin/boards">后台</a> 创建。{{end}}</p>
{{end}}
</main>
{{template "base/footer" .}}
{{end}}

View File

@@ -2,5 +2,5 @@ package templates
import "embed"
//go:embed *.tmpl base/*.tmpl home/*.tmpl post/*.tmpl shared/*.tmpl status/*.tmpl auth/*.tmpl admin/*.tmpl profile/*.tmpl user/*.tmpl favorites/*.tmpl messages/*.tmpl links/*.tmpl
//go:embed *.tmpl base/*.tmpl home/*.tmpl post/*.tmpl shared/*.tmpl status/*.tmpl auth/*.tmpl admin/*.tmpl profile/*.tmpl user/*.tmpl favorites/*.tmpl messages/*.tmpl links/*.tmpl boards/*.tmpl
var FS embed.FS

View File

@@ -1,6 +1,7 @@
{{define "shared/board_aside"}}
<aside class="j13-aside j13-aside--left">
<a class="j13-aside__link{{if eq .ActiveBoard 0}} is-active{{end}}" href="/">全部帖子</a>
<a class="j13-aside__link{{if eq .Path "/boards"}} is-active{{end}}" href="/boards">全部板块</a>
<a class="j13-aside__link{{if and (eq .ActiveBoard 0) (ne .Path "/boards")}} is-active{{end}}" href="/">全部帖子</a>
{{range .Boards}}
<a class="j13-aside__link{{if eq $.ActiveBoard .ID}} is-active{{end}}" href="/board/{{.ID}}">{{.Name}}</a>
{{end}}

View File

@@ -610,3 +610,59 @@ points-only[data-locked="false"] {
margin-right: 0.25rem;
}
.j13-boards { max-width: 800px; margin: 0 auto; padding: 1rem 1rem 2.5rem; }
.j13-boards h1 { margin: 0 0 1rem; font-size: 1.4rem; }
.j13-boards__grid {
list-style: none;
margin: 0;
padding: 0;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 0.85rem;
}
.j13-boards__card {
display: flex;
gap: 0.75rem;
align-items: flex-start;
padding: 0.85rem 0.9rem;
border: 1px solid var(--j13-border);
border-radius: var(--j13-radius);
background: var(--j13-bg);
color: inherit;
text-decoration: none;
min-height: 5rem;
}
.j13-boards__card:hover { border-color: var(--j13-accent); }
.j13-boards__dot {
width: 0.65rem;
height: 0.65rem;
border-radius: 50%;
margin-top: 0.35rem;
flex-shrink: 0;
background: var(--j13-accent);
}
.j13-boards__card[data-color="0"] .j13-boards__dot { background: #0f766e; }
.j13-boards__card[data-color="1"] .j13-boards__dot { background: #b45309; }
.j13-boards__card[data-color="2"] .j13-boards__dot { background: #1d4ed8; }
.j13-boards__card[data-color="3"] .j13-boards__dot { background: #be123c; }
.j13-boards__card[data-color="4"] .j13-boards__dot { background: #7c3aed; }
.j13-boards__card[data-color="5"] .j13-boards__dot { background: #047857; }
.j13-boards__card[data-color="6"] .j13-boards__dot { background: #c2410c; }
.j13-boards__card[data-color="7"] .j13-boards__dot { background: #0369a1; }
.j13-boards__body {
display: flex;
flex-direction: column;
gap: 0.2rem;
min-width: 0;
}
.j13-boards__body strong { font-size: 1rem; }
.j13-boards__icon { font-size: 0.75rem; }
.j13-boards__desc {
font-size: 0.85rem;
color: var(--j13-muted);
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}