feat: SSR 站点单页 /page/:slug 与 Admin CRUD
EOF Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -150,8 +150,8 @@
|
|||||||
|
|
||||||
## J. 站点单页
|
## J. 站点单页
|
||||||
|
|
||||||
- [ ] 公开:`/page/:slug` 列表入口(nav/footer)
|
- [x] 公开:`/page/:slug` 列表入口(nav/footer)— SSR
|
||||||
- [ ] 管理员 CRUD;发布开关;排序;nav/footer 展示开关
|
- [x] 管理员 CRUD;发布开关;排序;nav/footer 展示开关 — SSR `/admin/pages`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -37,7 +37,7 @@
|
|||||||
| `/links` | 友链 | 已迁 |
|
| `/links` | 友链 | 已迁 |
|
||||||
| `/messages` | 私信/通知会话列表 | 已迁 |
|
| `/messages` | 私信/通知会话列表 | 已迁 |
|
||||||
| `/messages/with/:peerId` | 会话详情(peer=0 系统通知) | 已迁 |
|
| `/messages/with/:peerId` | 会话详情(peer=0 系统通知) | 已迁 |
|
||||||
| `/page/:slug` | 站点单页 | 未注册 |
|
| `/page/:slug` | 站点单页 | 已迁 |
|
||||||
| `*` | 404 / pending | 已迁 |
|
| `*` | 404 / pending | 已迁 |
|
||||||
|
|
||||||
### 1.3 后台(Admin SSR,表单 + CSRF,不挂管理 JSON `/api`)
|
### 1.3 后台(Admin SSR,表单 + CSRF,不挂管理 JSON `/api`)
|
||||||
@@ -50,9 +50,10 @@
|
|||||||
| `/admin/moderation` | 待审帖/评 通过/拒绝 | 已迁 |
|
| `/admin/moderation` | 待审帖/评 通过/拒绝 | 已迁 |
|
||||||
| `/admin/settings` | 品牌 + 基础限流 + 敏感词 + SMTP | 已迁 |
|
| `/admin/settings` | 品牌 + 基础限流 + 敏感词 + SMTP | 已迁 |
|
||||||
| `/admin/friend-links` | 品牌友链、申请审核、入口开关 | 已迁 |
|
| `/admin/friend-links` | 品牌友链、申请审核、入口开关 | 已迁 |
|
||||||
|
| `/admin/pages` | 站点单页 CRUD / 发布 | 已迁 |
|
||||||
| `/admin/login` | 重定向前台登录 | 已迁 |
|
| `/admin/login` | 重定向前台登录 | 已迁 |
|
||||||
|
|
||||||
未迁(原 SPA):reports / users / badges / media / pages / 完整 Limits 等。
|
未迁(原 SPA):reports / users / badges / media / 完整 Limits 等。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -184,7 +185,7 @@
|
|||||||
|----|----------|
|
|----|----------|
|
||||||
| Dashboard | 看计数与待办;点进对应列表 |
|
| Dashboard | 看计数与待办;点进对应列表 |
|
||||||
| Boards | 拖拽或数字排序;图标/色板选择;增删改 |
|
| Boards | 拖拽或数字排序;图标/色板选择;增删改 |
|
||||||
| Pages | 列表发布开关;进编辑器写正文;nav/footer 勾选 |
|
| Pages | 列表发布开关;编辑正文(HTML textarea);nav/footer 勾选 — SSR `/admin/pages` |
|
||||||
| Links | 品牌友链增删;申请通过/拒绝;回链检测开关;nav/footer/aside 开关 — SSR `/admin/friend-links`(完整侧栏组件编辑 / 复检按钮未迁) |
|
| Links | 品牌友链增删;申请通过/拒绝;回链检测开关;nav/footer/aside 开关 — SSR `/admin/friend-links`(完整侧栏组件编辑 / 复检按钮未迁) |
|
||||||
| Posts | 按状态筛;通过/拒绝;置顶/版顶/精华/锁编/锁评;进回收站恢复/清除 |
|
| Posts | 按状态筛;通过/拒绝;置顶/版顶/精华/锁编/锁评;进回收站恢复/清除 |
|
||||||
| Comments | 审核;修订查看;回收站 |
|
| Comments | 审核;修订查看;回收站 |
|
||||||
|
|||||||
@@ -96,4 +96,6 @@ Feed 搜索:`/` / `/board/:id` 面板(`keyword` / `tag` / `author` / `title_
|
|||||||
|
|
||||||
友链:`/links`(列表、登录申请/取消、Logo 上传);Admin `/admin/friend-links`(品牌增删、审核、nav/footer/aside 入口开关)。
|
友链:`/links`(列表、登录申请/取消、Logo 上传);Admin `/admin/friend-links`(品牌增删、审核、nav/footer/aside 入口开关)。
|
||||||
|
|
||||||
Admin:`/admin/dashboard`、`/admin/boards`、`/admin/moderation`、`/admin/settings`(品牌/限流/敏感词/SMTP)、`/admin/friend-links`。
|
站点单页:公开 `/page/:slug`;nav/footer 按 `show_in_nav` / `show_in_footer` 注入;Admin `/admin/pages`(CRUD、发布、排序、展示开关)。
|
||||||
|
|
||||||
|
Admin:`/admin/dashboard`、`/admin/boards`、`/admin/moderation`、`/admin/settings`(品牌/限流/敏感词/SMTP)、`/admin/friend-links`、`/admin/pages`。
|
||||||
@@ -73,6 +73,7 @@ var parseGlobs = []string{
|
|||||||
"messages/*.tmpl",
|
"messages/*.tmpl",
|
||||||
"links/*.tmpl",
|
"links/*.tmpl",
|
||||||
"boards/*.tmpl",
|
"boards/*.tmpl",
|
||||||
|
"page/*.tmpl",
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load 解析全部模板(进程内一次)
|
// Load 解析全部模板(进程内一次)
|
||||||
|
|||||||
@@ -826,3 +826,16 @@ points-only[data-locked="false"] {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.j13-page {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 1rem 1rem 2.5rem;
|
||||||
|
}
|
||||||
|
.j13-page__article h1 {
|
||||||
|
margin: 0 0 1rem;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
.j13-page__body {
|
||||||
|
line-height: 1.65;
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -76,6 +76,8 @@ func Setup(cfg *config.Config) (*gin.Engine, error) {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
sitePageSvc := services.NewSitePageService(filter)
|
||||||
|
|
||||||
h := &api.Handlers{
|
h := &api.Handlers{
|
||||||
Cfg: cfg, Store: uploadStore, Auth: authSvc, User: userSvc, Board: boardSvc,
|
Cfg: cfg, Store: uploadStore, Auth: authSvc, User: userSvc, Board: boardSvc,
|
||||||
Post: postSvc, Comment: commentSvc, Message: messageSvc, Notify: notifySvc, Report: reportSvc,
|
Post: postSvc, Comment: commentSvc, Message: messageSvc, Notify: notifySvc, Report: reportSvc,
|
||||||
@@ -84,7 +86,7 @@ func Setup(cfg *config.Config) (*gin.Engine, error) {
|
|||||||
Captcha: captchaSvc, Mail: mailSvc, EmailCode: emailCodeSvc,
|
Captcha: captchaSvc, Mail: mailSvc, EmailCode: emailCodeSvc,
|
||||||
OIDC: oidcSvc, Gitea: giteaSvc,
|
OIDC: oidcSvc, Gitea: giteaSvc,
|
||||||
Points: services.NewPointsService(), Badge: services.NewBadgeService(),
|
Points: services.NewPointsService(), Badge: services.NewBadgeService(),
|
||||||
SitePage: services.NewSitePageService(filter),
|
SitePage: sitePageSvc,
|
||||||
FriendLinkApply: friendLinkApplySvc,
|
FriendLinkApply: friendLinkApplySvc,
|
||||||
}
|
}
|
||||||
authMW := auth.NewAuthMiddleware(authSvc)
|
authMW := auth.NewAuthMiddleware(authSvc)
|
||||||
@@ -103,6 +105,7 @@ func Setup(cfg *config.Config) (*gin.Engine, error) {
|
|||||||
Points: services.NewPointsService(),
|
Points: services.NewPointsService(),
|
||||||
FriendLink: friendLinkApplySvc,
|
FriendLink: friendLinkApplySvc,
|
||||||
Mail: mailSvc,
|
Mail: mailSvc,
|
||||||
|
SitePage: sitePageSvc,
|
||||||
}, authMW)
|
}, authMW)
|
||||||
|
|
||||||
r.GET("/media/thumb/*filepath", h.ServeImageThumb)
|
r.GET("/media/thumb/*filepath", h.ServeImageThumb)
|
||||||
|
|||||||
198
routers/web/admin_pages.go
Normal file
198
routers/web/admin_pages.go
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
package web
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.iioio.com/freefire/jiang13-forum/modules/webctx"
|
||||||
|
"git.iioio.com/freefire/jiang13-forum/services"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type adminPageRow struct {
|
||||||
|
ID uint
|
||||||
|
Title string
|
||||||
|
Slug string
|
||||||
|
Published bool
|
||||||
|
SortOrder int
|
||||||
|
ShowInNav bool
|
||||||
|
ShowInFooter bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type adminPageForm struct {
|
||||||
|
ID uint
|
||||||
|
Title string
|
||||||
|
Slug string
|
||||||
|
Content string
|
||||||
|
Published bool
|
||||||
|
SortOrder int
|
||||||
|
ShowInNav bool
|
||||||
|
ShowInFooter bool
|
||||||
|
IsEdit bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type adminPagesData struct {
|
||||||
|
AdminChrome
|
||||||
|
Pages []adminPageRow
|
||||||
|
Form adminPageForm
|
||||||
|
}
|
||||||
|
|
||||||
|
// AdminPagesGet 站点单页列表
|
||||||
|
func (d Deps) AdminPagesGet(c *gin.Context) {
|
||||||
|
ctx := d.ctx(c)
|
||||||
|
d.renderAdminPages(ctx, "", adminPageForm{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d Deps) renderAdminPages(ctx *webctx.Context, errMsg string, form adminPageForm) {
|
||||||
|
chrome := d.adminChrome(ctx, "站点单页", "pages")
|
||||||
|
chrome.Error = errMsg
|
||||||
|
data := adminPagesData{AdminChrome: chrome, Form: form}
|
||||||
|
if d.SitePage != nil {
|
||||||
|
list, _ := d.SitePage.ListAll()
|
||||||
|
data.Pages = make([]adminPageRow, 0, len(list))
|
||||||
|
for _, p := range list {
|
||||||
|
data.Pages = append(data.Pages, adminPageRow{
|
||||||
|
ID: p.ID, Title: p.Title, Slug: p.Slug, Published: p.Published,
|
||||||
|
SortOrder: p.SortOrder, ShowInNav: p.ShowInNav, ShowInFooter: p.ShowInFooter,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctx.HTML(http.StatusOK, "admin/pages", data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AdminPageCreate 新建单页
|
||||||
|
func (d Deps) AdminPageCreate(c *gin.Context) {
|
||||||
|
ctx := d.ctx(c)
|
||||||
|
form := adminPageFormFrom(c)
|
||||||
|
if !ctx.CheckCSRF() {
|
||||||
|
d.renderAdminPages(ctx, "无效请求,请重试", form)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if d.SitePage == nil {
|
||||||
|
d.renderAdminPages(ctx, "单页服务未就绪", form)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, err := d.SitePage.Create(adminPageInputFrom(form)); err != nil {
|
||||||
|
d.renderAdminPages(ctx, err.Error(), form)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.SetFlash("单页已创建")
|
||||||
|
ctx.Redirect("/admin/pages")
|
||||||
|
}
|
||||||
|
|
||||||
|
// AdminPageEditGet 编辑表单
|
||||||
|
func (d Deps) AdminPageEditGet(c *gin.Context) {
|
||||||
|
ctx := d.ctx(c)
|
||||||
|
if d.SitePage == nil {
|
||||||
|
d.renderAdminPages(ctx, "单页服务未就绪", adminPageForm{})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||||
|
page, err := d.SitePage.GetByID(uint(id))
|
||||||
|
if err != nil {
|
||||||
|
ctx.SetFlash(err.Error())
|
||||||
|
ctx.Redirect("/admin/pages")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
d.renderAdminPages(ctx, "", adminPageForm{
|
||||||
|
ID: page.ID, Title: page.Title, Slug: page.Slug, Content: page.Content,
|
||||||
|
Published: page.Published, SortOrder: page.SortOrder,
|
||||||
|
ShowInNav: page.ShowInNav, ShowInFooter: page.ShowInFooter, IsEdit: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// AdminPageUpdate 更新单页
|
||||||
|
func (d Deps) AdminPageUpdate(c *gin.Context) {
|
||||||
|
ctx := d.ctx(c)
|
||||||
|
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||||
|
form := adminPageFormFrom(c)
|
||||||
|
form.ID = uint(id)
|
||||||
|
form.IsEdit = true
|
||||||
|
if !ctx.CheckCSRF() {
|
||||||
|
d.renderAdminPages(ctx, "无效请求,请重试", form)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if d.SitePage == nil {
|
||||||
|
d.renderAdminPages(ctx, "单页服务未就绪", form)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := d.SitePage.Update(uint(id), adminPageInputFrom(form)); err != nil {
|
||||||
|
d.renderAdminPages(ctx, err.Error(), form)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.SetFlash("单页已保存")
|
||||||
|
ctx.Redirect("/admin/pages")
|
||||||
|
}
|
||||||
|
|
||||||
|
// AdminPageDelete 删除
|
||||||
|
func (d Deps) AdminPageDelete(c *gin.Context) {
|
||||||
|
ctx := d.ctx(c)
|
||||||
|
if !ctx.CheckCSRF() {
|
||||||
|
ctx.SetFlash("无效请求,请重试")
|
||||||
|
ctx.Redirect("/admin/pages")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if d.SitePage == nil {
|
||||||
|
ctx.SetFlash("单页服务未就绪")
|
||||||
|
ctx.Redirect("/admin/pages")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||||
|
if err := d.SitePage.Delete(uint(id)); err != nil {
|
||||||
|
ctx.SetFlash(err.Error())
|
||||||
|
ctx.Redirect("/admin/pages")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.SetFlash("单页已删除")
|
||||||
|
ctx.Redirect("/admin/pages")
|
||||||
|
}
|
||||||
|
|
||||||
|
// AdminPagePublishPost 快捷发布/下架
|
||||||
|
func (d Deps) AdminPagePublishPost(c *gin.Context) {
|
||||||
|
ctx := d.ctx(c)
|
||||||
|
if !ctx.CheckCSRF() {
|
||||||
|
ctx.SetFlash("无效请求,请重试")
|
||||||
|
ctx.Redirect("/admin/pages")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if d.SitePage == nil {
|
||||||
|
ctx.SetFlash("单页服务未就绪")
|
||||||
|
ctx.Redirect("/admin/pages")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||||
|
published := c.PostForm("published") == "1" || c.PostForm("published") == "on"
|
||||||
|
if err := d.SitePage.SetPublished(uint(id), published); err != nil {
|
||||||
|
ctx.SetFlash(err.Error())
|
||||||
|
ctx.Redirect("/admin/pages")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if published {
|
||||||
|
ctx.SetFlash("已发布")
|
||||||
|
} else {
|
||||||
|
ctx.SetFlash("已下架")
|
||||||
|
}
|
||||||
|
ctx.Redirect("/admin/pages")
|
||||||
|
}
|
||||||
|
|
||||||
|
func adminPageFormFrom(c *gin.Context) adminPageForm {
|
||||||
|
sort, _ := strconv.Atoi(c.PostForm("sort_order"))
|
||||||
|
return adminPageForm{
|
||||||
|
Title: strings.TrimSpace(c.PostForm("title")),
|
||||||
|
Slug: strings.TrimSpace(c.PostForm("slug")),
|
||||||
|
Content: c.PostForm("content"),
|
||||||
|
Published: c.PostForm("published") == "1" || c.PostForm("published") == "on",
|
||||||
|
SortOrder: sort,
|
||||||
|
ShowInNav: c.PostForm("show_in_nav") == "1" || c.PostForm("show_in_nav") == "on",
|
||||||
|
ShowInFooter: c.PostForm("show_in_footer") == "1" || c.PostForm("show_in_footer") == "on",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func adminPageInputFrom(form adminPageForm) services.SitePageInput {
|
||||||
|
return services.SitePageInput{
|
||||||
|
Title: form.Title, Slug: form.Slug, Content: form.Content,
|
||||||
|
Published: form.Published, SortOrder: form.SortOrder,
|
||||||
|
ShowInNav: form.ShowInNav, ShowInFooter: form.ShowInFooter,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,6 +26,13 @@ type Deps struct {
|
|||||||
Points *services.PointsService
|
Points *services.PointsService
|
||||||
FriendLink *services.FriendLinkApplyService
|
FriendLink *services.FriendLinkApplyService
|
||||||
Mail *services.MailService
|
Mail *services.MailService
|
||||||
|
SitePage *services.SitePageService
|
||||||
|
}
|
||||||
|
|
||||||
|
// SitePageLink 导航/页脚站点单页链接
|
||||||
|
type SitePageLink struct {
|
||||||
|
Title string
|
||||||
|
Slug string
|
||||||
}
|
}
|
||||||
|
|
||||||
// BoardView 侧栏
|
// BoardView 侧栏
|
||||||
@@ -110,6 +117,8 @@ type PageChrome struct {
|
|||||||
ViewerPoints int // 登录用户当前积分;未登录为 0
|
ViewerPoints int // 登录用户当前积分;未登录为 0
|
||||||
ShowFriendLinksNav bool
|
ShowFriendLinksNav bool
|
||||||
ShowFriendLinksFooter bool
|
ShowFriendLinksFooter bool
|
||||||
|
NavPages []SitePageLink
|
||||||
|
FooterPages []SitePageLink
|
||||||
RightAside RightAsideData
|
RightAside RightAsideData
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,6 +158,7 @@ func (d Deps) chrome(ctx *webctx.Context, title, desc, inner string) PageChrome
|
|||||||
if ctx.C != nil && ctx.C.Request != nil && ctx.C.Request.URL != nil {
|
if ctx.C != nil && ctx.C.Request != nil && ctx.C.Request.URL != nil {
|
||||||
path = ctx.C.Request.URL.Path
|
path = ctx.C.Request.URL.Path
|
||||||
}
|
}
|
||||||
|
navPages, footerPages := d.sitePageLinks()
|
||||||
return PageChrome{
|
return PageChrome{
|
||||||
Title: title,
|
Title: title,
|
||||||
Description: desc,
|
Description: desc,
|
||||||
@@ -167,10 +177,33 @@ func (d Deps) chrome(ctx *webctx.Context, title, desc, inner string) PageChrome
|
|||||||
ViewerPoints: viewerPoints,
|
ViewerPoints: viewerPoints,
|
||||||
ShowFriendLinksNav: d.Settings.NavShowFriendLinks(),
|
ShowFriendLinksNav: d.Settings.NavShowFriendLinks(),
|
||||||
ShowFriendLinksFooter: d.Settings.FooterShowFriendLinks(),
|
ShowFriendLinksFooter: d.Settings.FooterShowFriendLinks(),
|
||||||
|
NavPages: navPages,
|
||||||
|
FooterPages: footerPages,
|
||||||
RightAside: d.loadRightAside(ctx, brand),
|
RightAside: d.loadRightAside(ctx, brand),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 (
|
const (
|
||||||
rightAsideHotLimit = 5
|
rightAsideHotLimit = 5
|
||||||
rightAsideTagLimit = 24
|
rightAsideTagLimit = 24
|
||||||
|
|||||||
@@ -66,6 +66,12 @@ func Register(r *gin.Engine, deps Deps, authMW *auth.AuthMiddleware) {
|
|||||||
admin.POST("/friend-links/brand/delete", deps.AdminFriendLinksBrandDeletePost)
|
admin.POST("/friend-links/brand/delete", deps.AdminFriendLinksBrandDeletePost)
|
||||||
admin.POST("/friend-links/applies/:id/approve", deps.AdminFriendLinkApprovePost)
|
admin.POST("/friend-links/applies/:id/approve", deps.AdminFriendLinkApprovePost)
|
||||||
admin.POST("/friend-links/applies/:id/reject", deps.AdminFriendLinkRejectPost)
|
admin.POST("/friend-links/applies/:id/reject", deps.AdminFriendLinkRejectPost)
|
||||||
|
admin.GET("/pages", deps.AdminPagesGet)
|
||||||
|
admin.POST("/pages", deps.AdminPageCreate)
|
||||||
|
admin.GET("/pages/:id/edit", deps.AdminPageEditGet)
|
||||||
|
admin.POST("/pages/:id", deps.AdminPageUpdate)
|
||||||
|
admin.POST("/pages/:id/delete", deps.AdminPageDelete)
|
||||||
|
admin.POST("/pages/:id/publish", deps.AdminPagePublishPost)
|
||||||
}
|
}
|
||||||
|
|
||||||
g.GET("/user/:id", deps.UserPublic)
|
g.GET("/user/:id", deps.UserPublic)
|
||||||
@@ -87,6 +93,7 @@ func Register(r *gin.Engine, deps Deps, authMW *auth.AuthMiddleware) {
|
|||||||
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.BoardsGet)
|
g.GET("/boards", deps.BoardsGet)
|
||||||
|
g.GET("/page/:slug", deps.SitePageGet)
|
||||||
}
|
}
|
||||||
|
|
||||||
// HomePageData Feed
|
// HomePageData Feed
|
||||||
|
|||||||
44
routers/web/page.go
Normal file
44
routers/web/page.go
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
package web
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"html/template"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"git.iioio.com/freefire/jiang13-forum/services"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type sitePageViewData struct {
|
||||||
|
PageChrome
|
||||||
|
PageTitle string
|
||||||
|
Slug string
|
||||||
|
BodyHTML template.HTML
|
||||||
|
}
|
||||||
|
|
||||||
|
// SitePageGet 公开站点单页
|
||||||
|
func (d Deps) SitePageGet(c *gin.Context) {
|
||||||
|
ctx := d.ctx(c)
|
||||||
|
if d.SitePage == nil {
|
||||||
|
d.render404(ctx)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
slug := c.Param("slug")
|
||||||
|
page, err := d.SitePage.GetBySlug(slug, false)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, services.ErrSitePageNotFound) {
|
||||||
|
d.render404(ctx)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.String(http.StatusInternalServerError, "加载失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
html := services.SanitizePostHTML(page.Content)
|
||||||
|
chrome := d.chrome(ctx, page.Title+" · "+d.Settings.SiteBranding().Name, "", "")
|
||||||
|
ctx.HTML(http.StatusOK, "page/view", sitePageViewData{
|
||||||
|
PageChrome: chrome,
|
||||||
|
PageTitle: page.Title,
|
||||||
|
Slug: page.Slug,
|
||||||
|
BodyHTML: template.HTML(html),
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
<a href="/admin/boards"{{if eq .NavActive "boards"}} class="is-active"{{end}}>板块</a>
|
<a href="/admin/boards"{{if eq .NavActive "boards"}} class="is-active"{{end}}>板块</a>
|
||||||
<a href="/admin/moderation"{{if eq .NavActive "moderation"}} class="is-active"{{end}}>审核</a>
|
<a href="/admin/moderation"{{if eq .NavActive "moderation"}} class="is-active"{{end}}>审核</a>
|
||||||
<a href="/admin/friend-links"{{if eq .NavActive "friend-links"}} class="is-active"{{end}}>友链</a>
|
<a href="/admin/friend-links"{{if eq .NavActive "friend-links"}} class="is-active"{{end}}>友链</a>
|
||||||
|
<a href="/admin/pages"{{if eq .NavActive "pages"}} class="is-active"{{end}}>单页</a>
|
||||||
<a href="/admin/settings"{{if eq .NavActive "settings"}} class="is-active"{{end}}>设置</a>
|
<a href="/admin/settings"{{if eq .NavActive "settings"}} class="is-active"{{end}}>设置</a>
|
||||||
<a href="/">返回前台</a>
|
<a href="/">返回前台</a>
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
72
templates/admin/pages.tmpl
Normal file
72
templates/admin/pages.tmpl
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
{{define "admin/pages"}}
|
||||||
|
{{template "base/head" .}}
|
||||||
|
{{template "base/navbar" .}}
|
||||||
|
<main class="j13-main j13-admin">
|
||||||
|
<h1>站点单页</h1>
|
||||||
|
{{template "admin/nav" .}}
|
||||||
|
{{template "base/alert" .}}
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>{{if .Form.IsEdit}}编辑:{{.Form.Title}}{{else}}新建单页{{end}}</h2>
|
||||||
|
<form method="post" action="{{if .Form.IsEdit}}/admin/pages/{{.Form.ID}}{{else}}/admin/pages{{end}}" class="j13-form j13-admin-form">
|
||||||
|
<input type="hidden" name="_csrf" value="{{.CSRF}}"/>
|
||||||
|
<label>标题 <input name="title" required value="{{.Form.Title}}"/></label>
|
||||||
|
<label>Slug <input name="slug" required pattern="[a-z0-9-]{2,64}" value="{{.Form.Slug}}" placeholder="about"/></label>
|
||||||
|
<label>排序 <input name="sort_order" type="number" value="{{.Form.SortOrder}}"/></label>
|
||||||
|
<label class="j13-check"><input type="checkbox" name="published" value="1"{{if .Form.Published}} checked{{end}}/> 已发布</label>
|
||||||
|
<label class="j13-check"><input type="checkbox" name="show_in_nav" value="1"{{if .Form.ShowInNav}} checked{{end}}/> 导航显示</label>
|
||||||
|
<label class="j13-check"><input type="checkbox" name="show_in_footer" value="1"{{if .Form.ShowInFooter}} checked{{end}}/> 页脚显示</label>
|
||||||
|
<label>正文(HTML)
|
||||||
|
<textarea name="content" rows="12" required>{{.Form.Content}}</textarea>
|
||||||
|
</label>
|
||||||
|
<button type="submit">{{if .Form.IsEdit}}保存{{else}}创建{{end}}</button>
|
||||||
|
{{if .Form.IsEdit}}<a href="/admin/pages" class="j13-linkbtn">取消编辑</a>{{end}}
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>已有单页</h2>
|
||||||
|
{{if .Pages}}
|
||||||
|
<table class="j13-admin-table">
|
||||||
|
<thead>
|
||||||
|
<tr><th>标题</th><th>Slug</th><th>排序</th><th>状态</th><th>入口</th><th>操作</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{{range .Pages}}
|
||||||
|
<tr>
|
||||||
|
<td>{{.Title}}</td>
|
||||||
|
<td><code>{{.Slug}}</code>{{if .Published}} · <a href="/page/{{.Slug}}" target="_blank" rel="noopener">预览</a>{{end}}</td>
|
||||||
|
<td>{{.SortOrder}}</td>
|
||||||
|
<td>{{if .Published}}已发布{{else}}草稿{{end}}</td>
|
||||||
|
<td>
|
||||||
|
{{if .ShowInNav}}导航 {{end}}{{if .ShowInFooter}}页脚{{end}}
|
||||||
|
{{if and (not .ShowInNav) (not .ShowInFooter)}}—{{end}}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href="/admin/pages/{{.ID}}/edit">编辑</a>
|
||||||
|
<form method="post" action="/admin/pages/{{.ID}}/publish" class="j13-inline-form">
|
||||||
|
<input type="hidden" name="_csrf" value="{{$.CSRF}}"/>
|
||||||
|
{{if .Published}}
|
||||||
|
<input type="hidden" name="published" value="0"/>
|
||||||
|
<button type="submit" class="j13-linkbtn">下架</button>
|
||||||
|
{{else}}
|
||||||
|
<input type="hidden" name="published" value="1"/>
|
||||||
|
<button type="submit" class="j13-linkbtn">发布</button>
|
||||||
|
{{end}}
|
||||||
|
</form>
|
||||||
|
<form method="post" action="/admin/pages/{{.ID}}/delete" class="j13-inline-form" onsubmit="return confirm('确认删除该单页?');">
|
||||||
|
<input type="hidden" name="_csrf" value="{{$.CSRF}}"/>
|
||||||
|
<button type="submit" class="j13-linkbtn">删除</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{{end}}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{{else}}
|
||||||
|
<p class="j13-empty">暂无单页</p>
|
||||||
|
{{end}}
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
{{template "base/footer" .}}
|
||||||
|
{{end}}
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
<p>
|
<p>
|
||||||
{{.SiteName}}
|
{{.SiteName}}
|
||||||
{{if .ShowFriendLinksFooter}} · <a href="/links">友情链接</a>{{end}}
|
{{if .ShowFriendLinksFooter}} · <a href="/links">友情链接</a>{{end}}
|
||||||
|
{{range .FooterPages}} · <a href="/page/{{.Slug}}">{{.Title}}</a>{{end}}
|
||||||
</p>
|
</p>
|
||||||
</footer>
|
</footer>
|
||||||
<script src="/ssr-assets/site.js" defer></script>
|
<script src="/ssr-assets/site.js" defer></script>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
<a href="/#search">搜索</a>
|
<a href="/#search">搜索</a>
|
||||||
<a href="/boards">板块</a>
|
<a href="/boards">板块</a>
|
||||||
{{if .ShowFriendLinksNav}}<a href="/links">友链</a>{{end}}
|
{{if .ShowFriendLinksNav}}<a href="/links">友链</a>{{end}}
|
||||||
|
{{range .NavPages}}<a href="/page/{{.Slug}}">{{.Title}}</a>{{end}}
|
||||||
{{if .LoggedIn}}
|
{{if .LoggedIn}}
|
||||||
<a href="/compose">发帖</a>
|
<a href="/compose">发帖</a>
|
||||||
<a href="/messages">私信{{if gt .UnreadCount 0}} <span class="j13-badge">{{.UnreadCount}}</span>{{end}}</a>
|
<a href="/messages">私信{{if gt .UnreadCount 0}} <span class="j13-badge">{{.UnreadCount}}</span>{{end}}</a>
|
||||||
|
|||||||
@@ -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 boards/*.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 page/*.tmpl
|
||||||
var FS embed.FS
|
var FS embed.FS
|
||||||
|
|||||||
12
templates/page/view.tmpl
Normal file
12
templates/page/view.tmpl
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{{define "page/view"}}
|
||||||
|
{{template "base/head" .}}
|
||||||
|
{{template "base/navbar" .}}
|
||||||
|
{{if .Flash}}<div class="j13-flash" role="status">{{.Flash}}</div>{{end}}
|
||||||
|
<main class="j13-main j13-page">
|
||||||
|
<article class="j13-page__article">
|
||||||
|
<h1>{{.PageTitle}}</h1>
|
||||||
|
<div class="j13-page__body post-detail-content">{{.BodyHTML}}</div>
|
||||||
|
</article>
|
||||||
|
</main>
|
||||||
|
{{template "base/footer" .}}
|
||||||
|
{{end}}
|
||||||
@@ -826,3 +826,16 @@ points-only[data-locked="false"] {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.j13-page {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 1rem 1rem 2.5rem;
|
||||||
|
}
|
||||||
|
.j13-page__article h1 {
|
||||||
|
margin: 0 0 1rem;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
.j13-page__body {
|
||||||
|
line-height: 1.65;
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user