From 303755b47a3642abc2e9f3b38630cafe7c739d69 Mon Sep 17 00:00:00 2001 From: freefire Date: Sun, 30 Aug 2026 02:04:56 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20SSR=20=E9=97=AE=E7=AD=94=E5=B8=96?= =?UTF-8?q?=E5=8F=91=E5=B8=96=E4=B8=8E=E8=A7=A3=E5=86=B3=E7=8A=B6=E6=80=81?= =?UTF-8?q?=E5=88=87=E6=8D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit compose 支持 question;详情徽章并由作者/管理员切换。 Co-authored-by: Cursor --- docs/rebuild-spec/02-features.md | 2 +- docs/rebuild-spec/06-pages-ux.md | 5 +++-- public/assets/site.css | 2 ++ routers/web/compose.go | 4 ++-- routers/web/home.go | 1 + routers/web/post.go | 37 ++++++++++++++++++++++++++++++-- templates/compose.tmpl | 14 ++++++++---- templates/post/body.tmpl | 15 +++++++++++++ web_src/css/site.css | 2 ++ 9 files changed, 71 insertions(+), 11 deletions(-) diff --git a/docs/rebuild-spec/02-features.md b/docs/rebuild-spec/02-features.md index c97d563..af9d8e9 100644 --- a/docs/rebuild-spec/02-features.md +++ b/docs/rebuild-spec/02-features.md @@ -68,7 +68,7 @@ ### D.1 帖子类型 - [x] `normal` 普通讨论 — SSR compose 默认 -- [ ] `question` 问答:可标记已解决 / 未解决 +- [x] `question` 问答:可标记已解决 / 未解决 — SSR compose + 详情徽章与切换 - [x] `poll` 投票:2–10 选项;单选/多选;可选截止时间;投票;作者可结束 — SSR compose + 详情卡 - [ ] `bounty` 悬赏:发帖托管积分;采纳评论发奖;可退款(规则见 05) - [ ] `lottery` 抽奖帖:设定中奖人数;从评论参与者开奖 diff --git a/docs/rebuild-spec/06-pages-ux.md b/docs/rebuild-spec/06-pages-ux.md index b25bb9a..152387e 100644 --- a/docs/rebuild-spec/06-pages-ux.md +++ b/docs/rebuild-spec/06-pages-ux.md @@ -28,7 +28,7 @@ | `/board/:id` | 板块 Feed | 已迁 | | `/boards` | 板块索引 | 已迁 | | `/post/:id` | 帖详情 + 评论(回复/赞/私密)/赞/藏 | 已迁 | -| `/compose` | 发帖(normal / poll;textarea + 图片 + 门控插入) | 已迁 | +| `/compose` | 发帖(normal / question / poll;textarea + 图片 + 门控插入) | 已迁 | | `/post/:id/edit` | 编辑帖 | 已迁 | | `/profile` | 个人中心(资料/密码/头像/积分钱包) | 已迁 | | `/user/:id` | 公开用户页 | 已迁 | @@ -115,7 +115,7 @@ | 类型 | 附加 UI | |------|---------| | 讨论 | 无 | -| 问答 | 无额外字段(解决状态在详情) | +| 问答 | 无额外字段(解决状态在详情)— SSR compose 可选;详情切换已迁 | | 投票 | 选项列表、多选开关、最多可选、截止时间或无截止 | | 悬赏 | 积分输入(显示余额) | | 抽奖 | 中奖人数 1–20 | @@ -144,6 +144,7 @@ | 模块 | 行为 | |------|------| | 门控块 | 锁定壳 UI(长度/价格/引导);`POST /post/:id/unlock` 返回 inner HTML 后替换 | +| 问答状态 | 已解决/未解决徽章;作者或管理员 `POST /post/:id/question/resolve` 切换 — SSR | | 投票卡 | 选选项提交;显示百分比;作者可结束 — SSR `/post/:id/poll/vote|close` | | 悬赏条 | 显示积分与状态;采纳按钮在他人评论上;退款按钮按规则禁用并提示 | | 抽奖卡 | 显示参与人数;开奖;中奖名单 | diff --git a/public/assets/site.css b/public/assets/site.css index 1d36f91..1b2e9bf 100644 --- a/public/assets/site.css +++ b/public/assets/site.css @@ -318,6 +318,8 @@ body.j13-body { border: 1px solid #fecaca; } .j13-tag--feat { background: #ede9fe; color: #5b21b6; } +.j13-tag--resolved { background: #ecfdf5; color: #065f46; } +.j13-tag--unresolved { background: #f8fafc; color: #475569; } .j13-empty { color: var(--j13-muted); } .j13-pager { diff --git a/routers/web/compose.go b/routers/web/compose.go index 25dfab5..cb16cda 100644 --- a/routers/web/compose.go +++ b/routers/web/compose.go @@ -67,7 +67,7 @@ func (d Deps) ComposePost(c *gin.Context) { form := composeFormFrom(c) htmlBody := services.ComposeBodyToHTML(form.Content) postType := form.PostType - if postType != models.PostTypePoll { + if postType != models.PostTypePoll && postType != models.PostTypeQuestion { postType = models.PostTypeNormal } post, err := d.Post.Create(ctx.UserID(), form.BoardID, form.Title, htmlBody, form.Tags, postType, ctx.SkipsModeration()) @@ -197,7 +197,7 @@ func composeFormFrom(c *gin.Context) composeForm { maxChoices = 1 } postType := strings.TrimSpace(c.PostForm("post_type")) - if postType != models.PostTypePoll { + if postType != models.PostTypePoll && postType != models.PostTypeQuestion { postType = models.PostTypeNormal } return composeForm{ diff --git a/routers/web/home.go b/routers/web/home.go index 2d295a9..54a38be 100644 --- a/routers/web/home.go +++ b/routers/web/home.go @@ -33,6 +33,7 @@ func Register(r *gin.Engine, deps Deps, authMW *auth.AuthMiddleware) { g.POST("/post/:id/favorite", authMW.RequireAuth(), deps.PostFavorite) g.POST("/post/:id/poll/vote", authMW.RequireAuth(), deps.PostPollVote) g.POST("/post/:id/poll/close", authMW.RequireAuth(), deps.PostPollClose) + g.POST("/post/:id/question/resolve", authMW.RequireAuth(), deps.PostQuestionResolvePost) g.POST("/post/:id/unlock", authMW.RequireAuth(), deps.PostUnlock) g.POST("/post/:id/report", authMW.RequireAuth(), deps.PostReportPost) g.POST("/post/:id/comments/:cid/report", authMW.RequireAuth(), deps.CommentReportPost) diff --git a/routers/web/post.go b/routers/web/post.go index 0cd9052..5510f6f 100644 --- a/routers/web/post.go +++ b/routers/web/post.go @@ -46,7 +46,10 @@ type PostPageData struct { Status string StatusLabel string ShowModerationBanner bool - Poll *postPollView + IsQuestion bool + QuestionResolved bool + CanToggleQuestion bool + Poll *postPollView } type postPollOptionView struct { @@ -199,7 +202,10 @@ func (d Deps) PostView(c *gin.Context) { Status: post.Status, StatusLabel: statusLabel, ShowModerationBanner: showBanner, - Poll: pollView, + IsQuestion: post.PostType == models.PostTypeQuestion, + QuestionResolved: post.QuestionResolved, + CanToggleQuestion: post.PostType == models.PostTypeQuestion && (ctx.IsAdmin() || post.UserID == ctx.UserID()), + Poll: pollView, }) } @@ -468,6 +474,33 @@ func (d Deps) PostPollClose(c *gin.Context) { ctx.Redirect(fmt.Sprintf("/post/%d", id)) } +// PostQuestionResolvePost 标记问答帖已解决 / 未解决 +func (d Deps) PostQuestionResolvePost(c *gin.Context) { + ctx := d.ctx(c) + id, err := parsePostID(c, d) + if err != nil || id == 0 { + d.render404(ctx) + return + } + if !ctx.CheckCSRF() { + ctx.SetFlash("无效请求,请重试") + ctx.Redirect(fmt.Sprintf("/post/%d", id)) + return + } + resolved := c.PostForm("resolved") == "1" || c.PostForm("resolved") == "on" + if err := d.Post.SetQuestionResolved(ctx.UserID(), id, ctx.IsAdmin(), resolved); err != nil { + ctx.SetFlash(err.Error()) + ctx.Redirect(fmt.Sprintf("/post/%d", id)) + return + } + if resolved { + ctx.SetFlash("已标为已解决") + } else { + ctx.SetFlash("已标为未解决") + } + ctx.Redirect(fmt.Sprintf("/post/%d", id)) +} + type commentEditData struct { PageChrome PostID uint diff --git a/templates/compose.tmpl b/templates/compose.tmpl index 3f2b5b3..e5e22be 100644 --- a/templates/compose.tmpl +++ b/templates/compose.tmpl @@ -16,15 +16,17 @@ {{if .IsEdit}} -

类型:{{if eq .PostType "poll"}}投票{{else}}普通讨论{{end}}(不可更改)

+

类型:{{if eq .PostType "poll"}}投票{{else if eq .PostType "question"}}问答{{else}}普通讨论{{end}}(不可更改)

{{else}} +

问答帖无额外字段;解决状态可在帖详情由作者或管理员切换。

投票设置 @@ -68,8 +70,12 @@ (function () { var sel = document.getElementById('compose-post-type'); var box = document.getElementById('compose-poll-fields'); - if (!sel || !box) return; - function sync() { box.hidden = sel.value !== 'poll'; } + var qHint = document.getElementById('compose-question-hint'); + if (!sel) return; + function sync() { + if (box) box.hidden = sel.value !== 'poll'; + if (qHint) qHint.hidden = sel.value !== 'question'; + } sel.addEventListener('change', sync); sync(); })(); diff --git a/templates/post/body.tmpl b/templates/post/body.tmpl index 437001b..5a2bf0e 100644 --- a/templates/post/body.tmpl +++ b/templates/post/body.tmpl @@ -14,6 +14,9 @@ {{if .EditLocked}}禁编{{end}} {{if .CommentsLocked}}已结贴{{end}} {{if .PostTypeLabel}}{{.PostTypeLabel}}{{end}} + {{if .IsQuestion}} + {{if .QuestionResolved}}已解决{{else}}未解决{{end}} + {{end}} {{if .BoardName}}{{.BoardName}}{{end}} {{if .AuthorID}}{{.AuthorName}}{{else}}{{.AuthorName}}{{end}} {{.CreatedLabel}} @@ -21,6 +24,18 @@ {{if .LoggedIn}}
+ {{if .CanToggleQuestion}} +
+ + {{if .QuestionResolved}} + + + {{else}} + + + {{end}} +
+ {{end}}
diff --git a/web_src/css/site.css b/web_src/css/site.css index 1d36f91..1b2e9bf 100644 --- a/web_src/css/site.css +++ b/web_src/css/site.css @@ -318,6 +318,8 @@ body.j13-body { border: 1px solid #fecaca; } .j13-tag--feat { background: #ede9fe; color: #5b21b6; } +.j13-tag--resolved { background: #ecfdf5; color: #065f46; } +.j13-tag--unresolved { background: #f8fafc; color: #475569; } .j13-empty { color: var(--j13-muted); } .j13-pager {