顶栏搜索胶囊/发帖绿钮/Ctrl+K,侧栏板块色槽,列表 post-row--v2 与 SPA 同密度。 Co-authored-by: Cursor <cursoragent@cursor.com>
387 lines
10 KiB
Go
387 lines
10 KiB
Go
package web
|
||
|
||
import (
|
||
"net/http"
|
||
"strings"
|
||
|
||
"git.iioio.com/freefire/jiang13-forum/modules/webctx"
|
||
"git.iioio.com/freefire/jiang13-forum/services"
|
||
"github.com/gin-gonic/gin"
|
||
)
|
||
|
||
// Deps 页面依赖
|
||
type Deps struct {
|
||
DataDir string
|
||
JWTSecret string
|
||
AssetVersion string // 静态资源 ?v=;来自构建 ldflags
|
||
Settings *services.ForumSettingsService
|
||
Auth *services.AuthService
|
||
User *services.UserService
|
||
Board *services.BoardService
|
||
Post *services.PostService
|
||
Comment *services.CommentService
|
||
Message *services.MessageService
|
||
Filter *services.SensitiveFilter
|
||
Limiter *services.RateLimiter
|
||
EmailCode *services.EmailCodeService
|
||
Store *services.UploadStore
|
||
Points *services.PointsService
|
||
Badge *services.BadgeService
|
||
FriendLink *services.FriendLinkApplyService
|
||
Mail *services.MailService
|
||
SitePage *services.SitePageService
|
||
Report *services.ReportService
|
||
Captcha *services.CaptchaService
|
||
Notify *services.NotifyService
|
||
Backup *services.BackupService
|
||
}
|
||
|
||
// SitePageLink 导航/页脚站点单页链接
|
||
type SitePageLink struct {
|
||
Title string
|
||
Slug string
|
||
}
|
||
|
||
// BoardView 侧栏
|
||
type BoardView struct {
|
||
ID uint
|
||
Name string
|
||
Href string
|
||
PostCount int
|
||
ColorSlot int // 0–7 板块色槽
|
||
}
|
||
|
||
// RightAsideHotPost 右栏热门帖
|
||
type RightAsideHotPost struct {
|
||
ID uint
|
||
Title string
|
||
Href string
|
||
}
|
||
|
||
// RightAsideTag 右栏标签
|
||
type RightAsideTag struct {
|
||
Name string
|
||
Count int
|
||
}
|
||
|
||
// RightAsideComment 右栏最新评论
|
||
type RightAsideComment struct {
|
||
PostID uint
|
||
PostHref string
|
||
Author string
|
||
Excerpt string
|
||
PostTitle string
|
||
}
|
||
|
||
// RightAsideUser 右栏最新用户
|
||
type RightAsideUser struct {
|
||
ID uint
|
||
Nickname string
|
||
Href string
|
||
}
|
||
|
||
// RightAsideFriendLink 右栏友链
|
||
type RightAsideFriendLink struct {
|
||
Name string
|
||
URL string
|
||
Logo string
|
||
}
|
||
|
||
// RightAsideWidget 按 aside_widgets 顺序的一块(仅已开启)
|
||
type RightAsideWidget struct {
|
||
Kind string // tag_cloud | recent_comments | recent_users | friend_links
|
||
Tags []RightAsideTag
|
||
Comments []RightAsideComment
|
||
Users []RightAsideUser
|
||
FriendLinks []RightAsideFriendLink
|
||
}
|
||
|
||
// RightAsideData 右栏:签到条 + 固定热门 + 可配置 widgets
|
||
type RightAsideData struct {
|
||
ShowCheckIn bool
|
||
CheckedIn bool
|
||
CheckInStreak int
|
||
CheckInPoints int // 已签实得或预计可得
|
||
LotteryDrawn bool
|
||
LotteryPoints int
|
||
LotteryCost int
|
||
HotPosts []RightAsideHotPost
|
||
Widgets []RightAsideWidget
|
||
}
|
||
|
||
// PageChrome 布局公共字段
|
||
type PageChrome struct {
|
||
Title string
|
||
Description string
|
||
SiteName string
|
||
Slogan string
|
||
LogoMark string
|
||
LogoURL string
|
||
FaviconURL string
|
||
OGImageURL string
|
||
PermalinkSuffix string // 如 ".html";未启用为空
|
||
OpenPostsInNewTab bool
|
||
LoggedIn bool
|
||
IsAdmin bool
|
||
ViewerName string
|
||
ViewerAvatar string
|
||
ViewerMark string
|
||
Boards []BoardView
|
||
ActiveBoard uint
|
||
Path string // 当前请求路径,供侧栏高亮
|
||
Inner string // 保留字段;入口模板已固定组合,不再动态 template
|
||
CSRF string
|
||
Flash string
|
||
Error string
|
||
UnreadCount int64 // 登录用户未读私信/通知总数;未登录为 0
|
||
ViewerPoints int // 登录用户当前积分;未登录为 0
|
||
ShowFriendLinksNav bool
|
||
ShowFriendLinksFooter bool
|
||
NavPages []SitePageLink
|
||
FooterPages []SitePageLink
|
||
RightAside RightAsideData
|
||
AssetVersion string
|
||
}
|
||
|
||
func (d Deps) ctx(c *gin.Context) *webctx.Context {
|
||
return webctx.New(c, d.JWTSecret)
|
||
}
|
||
|
||
func (d Deps) chrome(ctx *webctx.Context, title, desc, inner string) PageChrome {
|
||
brand := d.Settings.SiteBranding()
|
||
if title == "" {
|
||
title = brand.DocumentTitle()
|
||
}
|
||
if desc == "" {
|
||
desc = brand.MetaDescription()
|
||
}
|
||
name := "我的"
|
||
if ctx.IsSigned() {
|
||
name = strings.TrimSpace(ctx.Doer.Nickname)
|
||
if name == "" {
|
||
name = ctx.Doer.Username
|
||
}
|
||
}
|
||
boards, _ := d.Board.ListWithStats()
|
||
bv := make([]BoardView, 0, len(boards))
|
||
pl := d.permalink()
|
||
for _, b := range boards {
|
||
slot := b.ColorIndex
|
||
if slot < 0 {
|
||
slot = int(b.ID % 8)
|
||
}
|
||
bv = append(bv, BoardView{
|
||
ID: b.ID, Name: b.Name, Href: pl.BoardPath(b.ID),
|
||
PostCount: b.PostCount, ColorSlot: slot % 8,
|
||
})
|
||
}
|
||
var unread int64
|
||
if ctx.IsSigned() && d.Message != nil {
|
||
unread, _ = d.Message.UnreadCount(ctx.UserID())
|
||
}
|
||
viewerPoints := 0
|
||
viewerAvatar := ""
|
||
viewerMark := "我"
|
||
if ctx.IsSigned() && ctx.Doer != nil {
|
||
viewerPoints = ctx.Doer.Points
|
||
viewerAvatar = strings.TrimSpace(ctx.Doer.Avatar)
|
||
viewerMark = firstRuneOr(name, "我")
|
||
}
|
||
path := ""
|
||
if ctx.C != nil && ctx.C.Request != nil && ctx.C.Request.URL != nil {
|
||
path = ctx.C.Request.URL.Path
|
||
}
|
||
navPages, footerPages := d.sitePageLinks()
|
||
assetVer := strings.TrimSpace(d.AssetVersion)
|
||
if assetVer == "" {
|
||
assetVer = "dev"
|
||
}
|
||
return PageChrome{
|
||
Title: title,
|
||
Description: desc,
|
||
SiteName: brand.Name,
|
||
Slogan: brand.Slogan,
|
||
LogoMark: firstRuneOr(brand.LogoMark, "姜"),
|
||
LogoURL: strings.TrimSpace(brand.Logo),
|
||
FaviconURL: strings.TrimSpace(brand.Favicon),
|
||
OGImageURL: strings.TrimSpace(brand.DefaultShareImage()),
|
||
PermalinkSuffix: d.Settings.Permalink().Suffix(),
|
||
OpenPostsInNewTab: d.Settings.OpenPostsInNewTab(),
|
||
LoggedIn: ctx.IsSigned(),
|
||
IsAdmin: ctx.IsAdmin(),
|
||
ViewerName: name,
|
||
ViewerAvatar: viewerAvatar,
|
||
ViewerMark: viewerMark,
|
||
Boards: bv,
|
||
Path: path,
|
||
Inner: inner,
|
||
CSRF: ctx.EnsureCSRF(),
|
||
Flash: ctx.TakeFlash(),
|
||
UnreadCount: unread,
|
||
ViewerPoints: viewerPoints,
|
||
ShowFriendLinksNav: d.Settings.NavShowFriendLinks(),
|
||
ShowFriendLinksFooter: d.Settings.FooterShowFriendLinks(),
|
||
NavPages: navPages,
|
||
FooterPages: footerPages,
|
||
RightAside: d.loadRightAside(ctx, brand),
|
||
AssetVersion: assetVer,
|
||
}
|
||
}
|
||
|
||
func (d Deps) sitePageLinks() (nav, footer []SitePageLink) {
|
||
nav, footer = []SitePageLink{}, []SitePageLink{}
|
||
if d.SitePage == nil {
|
||
return
|
||
}
|
||
list, err := d.SitePage.ListPublished()
|
||
if err != nil {
|
||
return
|
||
}
|
||
for _, p := range list {
|
||
link := SitePageLink{Title: p.Title, Slug: p.Slug}
|
||
if p.ShowInNav {
|
||
nav = append(nav, link)
|
||
}
|
||
if p.ShowInFooter {
|
||
footer = append(footer, link)
|
||
}
|
||
}
|
||
return
|
||
}
|
||
|
||
const (
|
||
rightAsideHotLimit = 5
|
||
rightAsideTagLimit = 24
|
||
rightAsideCommentLimit = 8
|
||
rightAsideUserLimit = 8
|
||
)
|
||
|
||
func (d Deps) loadRightAside(ctx *webctx.Context, brand services.SiteBranding) RightAsideData {
|
||
out := RightAsideData{
|
||
HotPosts: []RightAsideHotPost{},
|
||
Widgets: []RightAsideWidget{},
|
||
}
|
||
if ctx.IsSigned() && d.Points != nil {
|
||
out.ShowCheckIn = true
|
||
if st, err := d.Points.GetCheckInStatus(ctx.UserID()); err == nil {
|
||
out.CheckedIn = st.CheckedIn
|
||
out.CheckInStreak = st.Streak
|
||
out.CheckInPoints = st.TodayPoints
|
||
}
|
||
if st, err := d.Points.GetLotteryStatus(ctx.UserID()); err == nil {
|
||
out.LotteryDrawn = st.Drawn
|
||
out.LotteryPoints = st.Points
|
||
out.LotteryCost = st.Cost
|
||
}
|
||
}
|
||
if d.Post != nil {
|
||
if items, err := d.Post.HotPosts(rightAsideHotLimit); err == nil {
|
||
pl := d.permalink()
|
||
for _, it := range items {
|
||
out.HotPosts = append(out.HotPosts, RightAsideHotPost{ID: it.ID, Title: it.Title, Href: pl.PostPath(it.ID)})
|
||
}
|
||
}
|
||
}
|
||
for _, w := range d.Settings.AsideWidgets() {
|
||
if !w.Enabled {
|
||
continue
|
||
}
|
||
block := RightAsideWidget{Kind: w.ID}
|
||
switch w.ID {
|
||
case services.AsideWidgetTagCloud:
|
||
if d.Post != nil {
|
||
if tags, err := d.Post.PopularTags(rightAsideTagLimit); err == nil {
|
||
for _, t := range tags {
|
||
block.Tags = append(block.Tags, RightAsideTag{Name: t.Name, Count: t.Count})
|
||
}
|
||
}
|
||
}
|
||
case services.AsideWidgetRecentComments:
|
||
if d.Comment != nil {
|
||
if list, err := d.Comment.ListRecentPublic(rightAsideCommentLimit); err == nil {
|
||
pl := d.permalink()
|
||
for _, c := range list {
|
||
block.Comments = append(block.Comments, RightAsideComment{
|
||
PostID: c.PostID, PostHref: pl.PostPath(c.PostID), Author: c.Author, Excerpt: c.Excerpt, PostTitle: c.PostTitle,
|
||
})
|
||
}
|
||
}
|
||
}
|
||
case services.AsideWidgetRecentUsers:
|
||
if d.User != nil {
|
||
if list, err := d.User.ListRecentRegistered(rightAsideUserLimit); err == nil {
|
||
pl := d.permalink()
|
||
for _, u := range list {
|
||
block.Users = append(block.Users, RightAsideUser{ID: u.ID, Nickname: u.Nickname, Href: pl.UserPath(u.ID)})
|
||
}
|
||
}
|
||
}
|
||
case services.AsideWidgetFriendLinks:
|
||
for _, l := range brand.FriendLinks {
|
||
block.FriendLinks = append(block.FriendLinks, RightAsideFriendLink{Name: l.Name, URL: l.URL, Logo: l.Logo})
|
||
}
|
||
default:
|
||
continue
|
||
}
|
||
out.Widgets = append(out.Widgets, block)
|
||
}
|
||
return out
|
||
}
|
||
|
||
func firstRuneOr(s, fallback string) string {
|
||
s = strings.TrimSpace(s)
|
||
if s == "" {
|
||
return fallback
|
||
}
|
||
for _, r := range s {
|
||
return string(r)
|
||
}
|
||
return fallback
|
||
}
|
||
|
||
func stripIDParam(raw, permalinkExt string) string {
|
||
raw = strings.TrimSpace(raw)
|
||
if permalinkExt != "" {
|
||
raw = strings.TrimSuffix(raw, "."+permalinkExt)
|
||
}
|
||
raw = strings.TrimSuffix(raw, ".html")
|
||
raw = strings.TrimSuffix(raw, ".htm")
|
||
return raw
|
||
}
|
||
|
||
func (d Deps) permalink() services.PermalinkConfig {
|
||
if d.Settings == nil {
|
||
return services.PermalinkConfig{}
|
||
}
|
||
return d.Settings.Permalink()
|
||
}
|
||
|
||
func (d Deps) postHref(id uint) string {
|
||
return d.permalink().PostPath(id)
|
||
}
|
||
|
||
func (d Deps) boardHref(id uint) string {
|
||
return d.permalink().BoardPath(id)
|
||
}
|
||
|
||
func (d Deps) userHref(id uint) string {
|
||
return d.permalink().UserPath(id)
|
||
}
|
||
|
||
// redirectIfNotCanonical 伪静态开启时把无后缀/错误后缀 301 到规范路径
|
||
func (d Deps) redirectIfNotCanonical(c *gin.Context, match services.PermalinkMatch) bool {
|
||
if !match.OK || !d.permalink().Enabled {
|
||
return false
|
||
}
|
||
path := c.Request.URL.Path
|
||
if !match.NeedsCanonicalRedirect(path) {
|
||
return false
|
||
}
|
||
loc := match.Canonical
|
||
if q := c.Request.URL.RawQuery; q != "" {
|
||
loc = loc + "?" + q
|
||
}
|
||
c.Redirect(http.StatusMovedPermanently, loc)
|
||
return true
|
||
}
|