feat: SSR 公开板块索引 /boards
EOF Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -44,7 +44,7 @@
|
|||||||
|
|
||||||
## C. 板块
|
## C. 板块
|
||||||
|
|
||||||
- [x] 列出板块(含帖数等展示字段)— 前台侧栏 + Admin
|
- [x] 列出板块(含帖数等展示字段)— SSR `/boards` + 侧栏 + Admin
|
||||||
- [x] 管理员:创建 / 改 / 删板块 — SSR `/admin/boards`
|
- [x] 管理员:创建 / 改 / 删板块 — SSR `/admin/boards`
|
||||||
- [x] 板块名称、描述、图标、色板索引、排序
|
- [x] 板块名称、描述、图标、色板索引、排序
|
||||||
- [x] 默认板块保障(空站可引导创建)
|
- [x] 默认板块保障(空站可引导创建)
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
|------|------|-----|
|
|------|------|-----|
|
||||||
| `/` | Feed | 已迁 |
|
| `/` | Feed | 已迁 |
|
||||||
| `/board/:id` | 板块 Feed | 已迁 |
|
| `/board/:id` | 板块 Feed | 已迁 |
|
||||||
|
| `/boards` | 板块索引 | 已迁 |
|
||||||
| `/post/:id` | 帖详情 + 评论(回复/赞/私密)/赞/藏 | 已迁 |
|
| `/post/:id` | 帖详情 + 评论(回复/赞/私密)/赞/藏 | 已迁 |
|
||||||
| `/compose` | 发帖(normal;textarea + 图片 + 门控插入) | 已迁 |
|
| `/compose` | 发帖(normal;textarea + 图片 + 门控插入) | 已迁 |
|
||||||
| `/post/:id/edit` | 编辑帖 | 已迁 |
|
| `/post/:id/edit` | 编辑帖 | 已迁 |
|
||||||
|
|||||||
@@ -88,6 +88,8 @@ web_src/ → public/assets/
|
|||||||
|
|
||||||
私信:`/messages`、`/messages/with/:peerId`(系统 peer=0 只读;打开会话标已读)。
|
私信:`/messages`、`/messages/with/:peerId`(系统 peer=0 只读;打开会话标已读)。
|
||||||
|
|
||||||
|
公开浏览:`/boards` 板块索引(链到 `/board/:id`)。
|
||||||
|
|
||||||
友链:`/links`(列表、登录申请/取消、Logo 上传);Admin `/admin/friend-links`(品牌增删、审核、入口开关)。
|
友链:`/links`(列表、登录申请/取消、Logo 上传);Admin `/admin/friend-links`(品牌增删、审核、入口开关)。
|
||||||
|
|
||||||
Admin:`/admin/dashboard`、`/admin/boards`、`/admin/moderation`、`/admin/settings`(品牌/限流/敏感词/SMTP)、`/admin/friend-links`。
|
Admin:`/admin/dashboard`、`/admin/boards`、`/admin/moderation`、`/admin/settings`(品牌/限流/敏感词/SMTP)、`/admin/friend-links`。
|
||||||
@@ -71,6 +71,7 @@ var parseGlobs = []string{
|
|||||||
"favorites/*.tmpl",
|
"favorites/*.tmpl",
|
||||||
"messages/*.tmpl",
|
"messages/*.tmpl",
|
||||||
"links/*.tmpl",
|
"links/*.tmpl",
|
||||||
|
"boards/*.tmpl",
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load 解析全部模板(进程内一次)
|
// Load 解析全部模板(进程内一次)
|
||||||
|
|||||||
@@ -610,3 +610,59 @@ points-only[data-locked="false"] {
|
|||||||
margin-right: 0.25rem;
|
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
53
routers/web/boards.go
Normal 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,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -46,6 +46,7 @@ type PageChrome struct {
|
|||||||
ViewerName string
|
ViewerName string
|
||||||
Boards []BoardView
|
Boards []BoardView
|
||||||
ActiveBoard uint
|
ActiveBoard uint
|
||||||
|
Path string // 当前请求路径,供侧栏高亮
|
||||||
Inner string // 保留字段;入口模板已固定组合,不再动态 template
|
Inner string // 保留字段;入口模板已固定组合,不再动态 template
|
||||||
CSRF string
|
CSRF string
|
||||||
Flash 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 {
|
if ctx.IsSigned() && ctx.Doer != nil {
|
||||||
viewerPoints = ctx.Doer.Points
|
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{
|
return PageChrome{
|
||||||
Title: title,
|
Title: title,
|
||||||
Description: desc,
|
Description: desc,
|
||||||
@@ -98,6 +103,7 @@ func (d Deps) chrome(ctx *webctx.Context, title, desc, inner string) PageChrome
|
|||||||
IsAdmin: ctx.IsAdmin(),
|
IsAdmin: ctx.IsAdmin(),
|
||||||
ViewerName: name,
|
ViewerName: name,
|
||||||
Boards: bv,
|
Boards: bv,
|
||||||
|
Path: path,
|
||||||
Inner: inner,
|
Inner: inner,
|
||||||
CSRF: ctx.EnsureCSRF(),
|
CSRF: ctx.EnsureCSRF(),
|
||||||
Flash: ctx.TakeFlash(),
|
Flash: ctx.TakeFlash(),
|
||||||
|
|||||||
@@ -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/apply/:id/cancel", authMW.RequireAuth(), deps.LinksApplyCancelPost)
|
||||||
g.POST("/links/logo", authMW.RequireAuth(), deps.LinksLogoUpload)
|
g.POST("/links/logo", authMW.RequireAuth(), deps.LinksLogoUpload)
|
||||||
g.GET("/projects", deps.PendingPage)
|
g.GET("/projects", deps.PendingPage)
|
||||||
g.GET("/boards", deps.PendingPage)
|
g.GET("/boards", deps.BoardsGet)
|
||||||
}
|
}
|
||||||
|
|
||||||
// HomePageData Feed
|
// HomePageData Feed
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
<nav class="j13-nav">
|
<nav class="j13-nav">
|
||||||
|
<a href="/boards">板块</a>
|
||||||
{{if .ShowFriendLinksNav}}<a href="/links">友链</a>{{end}}
|
{{if .ShowFriendLinksNav}}<a href="/links">友链</a>{{end}}
|
||||||
{{if .LoggedIn}}
|
{{if .LoggedIn}}
|
||||||
<a href="/compose">发帖</a>
|
<a href="/compose">发帖</a>
|
||||||
|
|||||||
28
templates/boards/list.tmpl
Normal file
28
templates/boards/list.tmpl
Normal 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}}
|
||||||
@@ -2,5 +2,5 @@ package templates
|
|||||||
|
|
||||||
import "embed"
|
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
|
var FS embed.FS
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{{define "shared/board_aside"}}
|
{{define "shared/board_aside"}}
|
||||||
<aside class="j13-aside j13-aside--left">
|
<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}}
|
{{range .Boards}}
|
||||||
<a class="j13-aside__link{{if eq $.ActiveBoard .ID}} is-active{{end}}" href="/board/{{.ID}}">{{.Name}}</a>
|
<a class="j13-aside__link{{if eq $.ActiveBoard .ID}} is-active{{end}}" href="/board/{{.ID}}">{{.Name}}</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|||||||
@@ -610,3 +610,59 @@ points-only[data-locked="false"] {
|
|||||||
margin-right: 0.25rem;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user