`)
- } else {
- b.WriteString(`
`)
- }
-
- b.WriteString(`
`)
- if post.Pinned > 0 {
- b.WriteString(`全局置顶 `)
- }
- if post.BoardPinned > 0 {
- b.WriteString(`板块置顶 `)
- }
- if post.Featured {
- b.WriteString(`推荐 `)
- }
- if post.Status == model.ContentStatusPending {
- b.WriteString(`审核中 `)
- }
- if post.Status == model.ContentStatusRejected {
- b.WriteString(`未通过 `)
- }
- b.WriteString(`` + html.EscapeString(post.Title) + ` `)
- hasTypeBadge := post.PostType == model.PostTypeQuestion ||
- post.PostType == model.PostTypePoll ||
- post.PostType == model.PostTypeLottery ||
- (post.PostType == model.PostTypeBounty && ((post.BountyStatus == model.BountyStatusOpen && post.BountyPoints > 0) ||
- post.BountyStatus == model.BountyStatusAwarded))
- if hasTypeBadge {
- b.WriteString(``)
- switch post.PostType {
- case model.PostTypeQuestion:
- if post.QuestionResolved {
- b.WriteString(`已解决 `)
- } else {
- b.WriteString(`未解决 `)
- }
- case model.PostTypePoll:
- b.WriteString(`投票 `)
- case model.PostTypeBounty:
- if post.BountyStatus == model.BountyStatusOpen && post.BountyPoints > 0 {
- b.WriteString(`悬赏 ` + strconv.Itoa(post.BountyPoints) + ` `)
- } else if post.BountyStatus == model.BountyStatusAwarded {
- b.WriteString(`已采纳 `)
- }
- case model.PostTypeLottery:
- if post.LotteryStatus == model.PostLotteryStatusDrawn {
- b.WriteString(`已开奖 `)
- } else {
- b.WriteString(`抽奖 `)
- }
- }
- b.WriteString(` `)
- }
- b.WriteString(`
`)
- if excerpt != "" {
- b.WriteString(`
` + html.EscapeString(excerpt) + `
`)
- }
-
- if thumb != "" {
- b.WriteString(`
`)
- } else {
- b.WriteString(`
`)
- }
-
- b.WriteString(`
`)
-
- zero := ""
- if post.CommentCount == 0 {
- zero = " post-stat--zero"
- }
- b.WriteString(`
` + ssrIconMessageCircle() + strconv.Itoa(post.CommentCount) + `
`)
-
- if thumb != "" {
- b.WriteString(`
`)
- } else {
- b.WriteString(`
`)
- }
- b.WriteString(`
`)
-}
-
-func writeSSRAside(b *strings.Builder, boot homeBootPayload) {
- b.WriteString(`
`)
- brandName := strings.TrimSpace(boot.Branding.Name)
- if brandName == "" {
- brandName = "姜十三论坛"
- }
- intro := strings.TrimSpace(boot.Branding.Description)
- if intro == "" {
- intro = strings.TrimSpace(boot.Branding.Slogan)
- }
- isSiteHome := boot.BoardID == 0 && boot.Keyword == "" && boot.Tag == "" && boot.Author == ""
-
- b.WriteString(`
`)
-
- for _, w := range boot.Limits.AsideWidgets {
- if !w.Enabled {
- continue
- }
- switch w.ID {
- case service.AsideWidgetFriendLinks:
- writeSSRFriendLinks(b, boot)
- case service.AsideWidgetTagCloud:
- writeSSRTagCloud(b, boot)
- case service.AsideWidgetRecentComments:
- writeSSRRecentComments(b, boot)
- case service.AsideWidgetRecentUsers:
- writeSSRRecentUsers(b, boot)
- case service.AsideWidgetShowcase:
- writeSSRShowcase(b, boot)
- }
- }
- b.WriteString(`
`)
-}
-
-func writeSSRCheckIn(b *strings.Builder, boot homeBootPayload) {
- loggedIn := boot.User != nil && boot.User.ID > 0
- if !loggedIn {
- b.WriteString(`
`)
- return
- }
- if boot.CheckIn == nil {
- return
- }
- st := boot.CheckIn
- checkedIn := st.CheckedIn
- panelClass := "widget-checkin-panel"
- if checkedIn {
- panelClass += " widget-checkin-panel--done"
- }
- b.WriteString(`
`)
-}
-
-func writeSSRFriendLinks(b *strings.Builder, boot homeBootPayload) {
- b.WriteString(`
`)
-}
-
-func writeSSRTagCloud(b *strings.Builder, boot homeBootPayload) {
- b.WriteString(`
`)
-}
-
-func writeSSRRecentComments(b *strings.Builder, boot homeBootPayload) {
- b.WriteString(`
`)
-}
-
-func writeSSRRecentUsers(b *strings.Builder, boot homeBootPayload) {
- b.WriteString(`
`)
-}
-
-func writeSSRShowcase(b *strings.Builder, boot homeBootPayload) {
- b.WriteString(`
`)
-}
-
-func writeSSRFooter(b *strings.Builder, boot homeBootPayload, meta homeSSRMeta) {
- brandName := strings.TrimSpace(boot.Branding.Name)
- if brandName == "" {
- brandName = "姜十三论坛"
- }
- year := time.Now().Year()
- b.WriteString(``)
-}
-
-func boardThemeIndex(colorIndex int, id uint) int {
- const n = 8
- if colorIndex >= 0 {
- return colorIndex % n
- }
- return int(id % uint(n))
-}
-
-func firstImageSrc(htmlContent string) string {
- m := firstImgSrcRe.FindStringSubmatch(htmlContent)
- if len(m) < 2 {
- return ""
- }
- src := strings.TrimSpace(m[1])
- if src == "" || strings.HasPrefix(src, "data:") {
- return ""
- }
- return src
-}
-
-func formatSSRTime(t time.Time) string {
- if t.IsZero() {
- return ""
- }
- return t.Local().Format("01-02 15:04")
-}
diff --git a/handler/ssr_icons.go b/handler/ssr_icons.go
deleted file mode 100644
index 62ca7b6..0000000
--- a/handler/ssr_icons.go
+++ /dev/null
@@ -1,361 +0,0 @@
-package handler
-
-import (
- "strconv"
- "strings"
- "time"
-)
-
-// 内联 SVG:path 对齐 lucide-react@1.18(由 scripts/extract-lucide-paths.mjs 抽取)
-
-func ssrSVG(size int, paths ...string) string {
- return ssrSVGWithClass(size, "", paths...)
-}
-
-func ssrSVGWithClass(size int, className string, paths ...string) string {
- var b strings.Builder
- b.WriteString(`
`)
- for _, p := range paths {
- b.WriteString(p)
- }
- b.WriteString(` `)
- return b.String()
-}
-
-func ssrIconSearch() string {
- return ssrSVGWithClass(16, "header-search-icon",
- `
`,
- `
`,
- )
-}
-
-func ssrIconSliders() string {
- return ssrSVG(15,
- `
`,
- `
`,
- `
`,
- `
`,
- `
`,
- `
`,
- `
`,
- `
`,
- `
`,
- )
-}
-
-func ssrIconPlus() string {
- return ssrSVG(16,
- `
`,
- `
`,
- )
-}
-
-func ssrIconMenu() string {
- return ssrSVG(18,
- `
`,
- `
`,
- `
`,
- )
-}
-
-func ssrIconMoon() string {
- return ssrSVG(18,
- `
`,
- )
-}
-
-func ssrIconSun() string {
- return ssrSVG(18,
- `
`,
- `
`,
- `
`,
- `
`,
- `
`,
- `
`,
- `
`,
- `
`,
- `
`,
- )
-}
-
-func ssrIconMail() string {
- return ssrSVG(18,
- `
`,
- `
`,
- )
-}
-
-func ssrIconHome() string {
- return ssrSVG(18,
- `
`,
- `
`,
- )
-}
-
-func ssrIconStar() string {
- return ssrSVG(18,
- `
`,
- )
-}
-
-func ssrIconFolderGit() string {
- return ssrSVG(18,
- `
`,
- `
`,
- `
`,
- `
`,
- )
-}
-
-func ssrIconLink2() string {
- return ssrSVG(18,
- `
`,
- `
`,
- `
`,
- )
-}
-
-func ssrWidgetIconLink2() string {
- return ssrSVGWithClass(15, "widget-card-icon widget-card-icon--links",
- `
`,
- `
`,
- `
`,
- )
-}
-
-func ssrIconEarth() string {
- return ssrSVG(18,
- `
`,
- `
`,
- `
`,
- `
`,
- )
-}
-
-func ssrWidgetIconEarth() string {
- return ssrSVGWithClass(15, "widget-card-icon widget-card-icon--showcase",
- `
`,
- `
`,
- `
`,
- `
`,
- )
-}
-
-func ssrIconFileText() string {
- return ssrSVG(18,
- `
`,
- `
`,
- `
`,
- `
`,
- `
`,
- )
-}
-
-func ssrIconLayoutDashboard() string {
- return ssrSVG(18,
- `
`,
- `
`,
- `
`,
- `
`,
- )
-}
-
-func ssrIconMessageCircle() string {
- return ssrSVG(16,
- `
`,
- )
-}
-
-func ssrWidgetIconMessageCircle() string {
- return ssrSVGWithClass(15, "widget-card-icon widget-card-icon--notice",
- `
`,
- )
-}
-
-func ssrIconClock() string {
- return ssrSVG(16,
- `
`,
- `
`,
- )
-}
-
-func ssrIconBadgeCheck() string {
- return ssrSVG(16,
- `
`,
- `
`,
- )
-}
-
-func ssrIconTags() string {
- return ssrSVG(16,
- `
`,
- `
`,
- `
`,
- )
-}
-
-func ssrWidgetIconTags() string {
- return ssrSVGWithClass(15, "widget-card-icon widget-card-icon--tags",
- `
`,
- `
`,
- `
`,
- )
-}
-
-func ssrIconUserPlus() string {
- return ssrSVG(16,
- `
`,
- `
`,
- `
`,
- `
`,
- )
-}
-
-func ssrWidgetIconUserPlus() string {
- return ssrSVGWithClass(15, "widget-card-icon widget-card-icon--users",
- `
`,
- `
`,
- `
`,
- `
`,
- )
-}
-
-func ssrIconCalendarCheck() string {
- return ssrSVG(18,
- `
`,
- `
`,
- `
`,
- `
`,
- `
`,
- )
-}
-
-func ssrIconCheck() string {
- return ssrSVG(18,
- `
`,
- )
-}
-
-func ssrIconGift() string {
- return ssrSVG(15,
- `
`,
- `
`,
- `
`,
- `
`,
- )
-}
-
-func ssrIconPanelRight() string {
- return ssrSVG(18,
- `
`,
- `
`,
- )
-}
-
-// 板块图标 path(key 对齐 BOARD_ICON_OPTIONS / AllowedBoardIcons)
-var ssrBoardIconInner = map[string]string{
- "code-2": `
`,
- "coffee": `
`,
- "help-circle": `
`,
- "message-square": `
`,
- "lightbulb": `
`,
- "book-open": `
`,
- "gamepad-2": `
`,
- "palette": `
`,
- "music": `
`,
- "camera": `
`,
- "heart": `
`,
- "zap": `
`,
- "globe": `
`,
- "users": `
`,
- "briefcase": `
`,
- "graduation-cap": `
`,
- "shopping-bag": `
`,
- "map-pin": `
`,
- "megaphone": `
`,
- "flame": `
`,
- "star": `
`,
- "folder": `
`,
- "wrench": `
`,
- "cpu": `
`,
-}
-
-// 与前端 DEFAULT_ICONS 顺序一致(按 themeIndex 回退)
-var ssrBoardDefaultIcons = []string{
- "code-2",
- "coffee",
- "help-circle",
- "message-square",
- "lightbulb",
- "book-open",
- "gamepad-2",
- "palette",
-}
-
-// ssrBoardIconSVG 输出板块 Lucide 图标;class 打在 svg 上(与 React BoardIconDisplay 一致)
-func ssrBoardIconSVG(icon string, themeIndex int, className string) string {
- key := strings.TrimSpace(strings.ToLower(icon))
- inner, ok := ssrBoardIconInner[key]
- if !ok || inner == "" {
- if themeIndex < 0 {
- themeIndex = 0
- }
- key = ssrBoardDefaultIcons[themeIndex%len(ssrBoardDefaultIcons)]
- inner = ssrBoardIconInner[key]
- }
- return ssrSVGWithClass(18, className, inner)
-}
-
-// formatSSRRelativeTime 与前端 formatTime 同规则
-func formatSSRRelativeTime(t time.Time) string {
- if t.IsZero() {
- return ""
- }
- now := time.Now()
- diffSec := now.Sub(t).Seconds()
- if diffSec < 0 {
- diffSec = 0
- }
- if diffSec < 60 {
- return "刚刚"
- }
- if diffSec < 3600 {
- return strconv.Itoa(int(diffSec/60)) + "分钟前"
- }
- if diffSec < 86400 {
- return strconv.Itoa(int(diffSec/3600)) + "小时前"
- }
- diffDay := int(diffSec / 86400)
- if diffDay < 30 {
- return strconv.Itoa(diffDay) + "天前"
- }
- if t.Year() == now.Year() {
- return strconv.Itoa(int(t.Month())) + "月" + strconv.Itoa(t.Day()) + "日"
- }
- return strconv.Itoa(t.Year()) + "年" + strconv.Itoa(int(t.Month())) + "月" + strconv.Itoa(t.Day()) + "日"
-}
-
-// formatSSRShortDateTime 与前端 formatShortDateTime 同规则(MM-DD HH:mm)
-func formatSSRShortDateTime(t time.Time) string {
- if t.IsZero() {
- return ""
- }
- local := t.Local()
- pad := func(n int) string {
- if n < 10 {
- return "0" + strconv.Itoa(n)
- }
- return strconv.Itoa(n)
- }
- return pad(int(local.Month())) + "-" + pad(local.Day()) + " " + pad(local.Hour()) + ":" + pad(local.Minute())
-}
diff --git a/router/router.go b/router/router.go
index 093c8fc..34a6ed3 100644
--- a/router/router.go
+++ b/router/router.go
@@ -1,14 +1,12 @@
package router
import (
- "encoding/json"
"fmt"
"net/http"
"os"
"path/filepath"
"git.iioio.com/freefire/jiang13-forum/config"
- "git.iioio.com/freefire/jiang13-forum/embed_static"
"git.iioio.com/freefire/jiang13-forum/handler"
"git.iioio.com/freefire/jiang13-forum/middleware"
"git.iioio.com/freefire/jiang13-forum/service"
@@ -31,13 +29,9 @@ func Setup(cfg *config.Config) (*gin.Engine, error) {
monitorSvc.StartBackground()
r.Use(middleware.AccessLogMiddleware(monitorSvc))
- // dev 模式:跳过内嵌静态资源,前端由 Vite 开发服务器(:5173)提供
- // 用户应访问 5173 端口,Vite 通过 proxy 将 /api 等请求转发到本服务(:3000)
- if !cfg.DevMode {
- if err := embed_static.SetupEmbed(r); err != nil {
- return nil, err
- }
- } else {
+ // 前端由 Node 渲染服务器提供(对外 :3000),本服务仅提供 API;
+ // dev 模式前端由 Vite 开发服务器(:5173)提供,/api 经 Vite proxy 转发到本服务
+ if cfg.DevMode {
fmt.Fprintf(os.Stderr, "[dev] 后端仅提供 API,前端请访问 http://localhost:5173\n")
}
@@ -46,20 +40,6 @@ func Setup(cfg *config.Config) (*gin.Engine, error) {
if cfg.CommunityHub {
fmt.Fprintf(os.Stderr, "[community] 社区枢纽已开启(运维配置),可接收自愿上报\n")
}
- // SPA 入口 HTML 注入标题与品牌 JSON,避免刷新时先闪默认文案
- embed_static.SetSPADocumentTitle(func() string {
- return settingsSvc.SiteBranding().DocumentTitle()
- })
- embed_static.SetSPABrandingJSON(func() []byte {
- b, err := json.Marshal(settingsSvc.SiteBranding())
- if err != nil {
- return nil
- }
- return b
- })
- embed_static.SetSPAFaviconURL(func() string {
- return settingsSvc.SiteBranding().Favicon
- })
authSvc := service.NewAuthService(cfg.JWTSecret, filter, settingsSvc)
userSvc := service.NewUserService(filter, settingsSvc)
boardSvc := service.NewBoardService()
@@ -138,6 +118,8 @@ func Setup(cfg *config.Config) (*gin.Engine, error) {
pubAPI.GET("/me", h.APIMe)
pubAPI.GET("/boards", h.APIBoards)
pubAPI.GET("/stats", h.APIStats)
+ // SSR 聚合数据(Vike +data 使用,Node 内部调用)
+ pubAPI.GET("/home-boot", h.APIHomeBoot)
pubAPI.POST("/community/heartbeat", middleware.RateLimitMiddleware(limiter, "community_heartbeat"), h.APICommunityHeartbeat)
pubAPI.GET("/community/showcase", h.APICommunityShowcase)
pubAPI.POST("/monitor/pageview", middleware.RateLimitMiddleware(limiter, "monitor_pageview"), h.APIMonitorPageview)
@@ -300,48 +282,17 @@ func Setup(cfg *config.Config) (*gin.Engine, error) {
adminAPI.GET("/backup/download/:name", h.APIAdminDownloadBackup)
}
- // 后台管理页面由 React SPA 渲染(JSON API 见上方 /api/admin)
- // dev 模式下前端由 Vite 提供,后台页面路由不在此注册
- if !cfg.DevMode {
- admin := r.Group("/admin")
- {
- admin.GET("/login", func(c *gin.Context) {
- c.Redirect(http.StatusFound, "/login")
- })
-
- adminAuth := admin.Group("/", authMW.RequireAuth(), authMW.RequireAdmin())
- {
- adminAuth.GET("/", func(c *gin.Context) { c.Redirect(http.StatusFound, "/admin/dashboard") })
- for _, page := range []string{
- "dashboard", "boards", "pages", "links", "community", "posts",
- "comments", "reports", "users", "badges", "media", "monitor", "settings",
- } {
- adminAuth.GET("/"+page, embed_static.ServeSPANoIndex)
- }
- }
- }
- }
-
- // React SPA 入口
- // dev 模式:前端由 Vite(:5173) 提供,非 API 请求返回开发提示
- // 生产模式:注入 SEO meta / JSON-LD / 预渲染摘要
- if cfg.DevMode {
- r.NoRoute(func(c *gin.Context) {
+ // 非 API 路径由 Node 渲染服务器(生产)或 Vite 开发服务器(dev)处理,
+ // 本服务不再服务前端页面,未知路径一律 404 JSON
+ r.NoRoute(func(c *gin.Context) {
+ if cfg.DevMode {
c.JSON(http.StatusNotFound, gin.H{
"error": "dev 模式下前端由 Vite 提供,请访问 http://localhost:5173",
})
- })
- } else {
- // 文档请求也解析登录 cookie,首页 SSR 才能输出头像/签到/收藏等完整壳
- r.GET("/", authMW.OptionalAuth(), h.ServePublicSPA)
- r.NoRoute(authMW.OptionalAuth(), func(c *gin.Context) {
- if embed_static.IsSPARoute(c.Request.URL.Path) {
- h.ServePublicSPA(c)
- return
- }
- c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
- })
- }
+ return
+ }
+ c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
+ })
return r, nil
}