feat: SSR 问答帖发帖与解决状态切换
compose 支持 question;详情徽章并由作者/管理员切换。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -68,7 +68,7 @@
|
||||
### D.1 帖子类型
|
||||
|
||||
- [x] `normal` 普通讨论 — SSR compose 默认
|
||||
- [ ] `question` 问答:可标记已解决 / 未解决
|
||||
- [x] `question` 问答:可标记已解决 / 未解决 — SSR compose + 详情徽章与切换
|
||||
- [x] `poll` 投票:2–10 选项;单选/多选;可选截止时间;投票;作者可结束 — SSR compose + 详情卡
|
||||
- [ ] `bounty` 悬赏:发帖托管积分;采纳评论发奖;可退款(规则见 05)
|
||||
- [ ] `lottery` 抽奖帖:设定中奖人数;从评论参与者开奖
|
||||
|
||||
@@ -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` |
|
||||
| 悬赏条 | 显示积分与状态;采纳按钮在他人评论上;退款按钮按规则禁用并提示 |
|
||||
| 抽奖卡 | 显示参与人数;开奖;中奖名单 |
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -46,6 +46,9 @@ type PostPageData struct {
|
||||
Status string
|
||||
StatusLabel string
|
||||
ShowModerationBanner bool
|
||||
IsQuestion bool
|
||||
QuestionResolved bool
|
||||
CanToggleQuestion bool
|
||||
Poll *postPollView
|
||||
}
|
||||
|
||||
@@ -199,6 +202,9 @@ func (d Deps) PostView(c *gin.Context) {
|
||||
Status: post.Status,
|
||||
StatusLabel: statusLabel,
|
||||
ShowModerationBanner: showBanner,
|
||||
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
|
||||
|
||||
@@ -16,15 +16,17 @@
|
||||
</select>
|
||||
</label>
|
||||
{{if .IsEdit}}
|
||||
<p class="j13-muted">类型:{{if eq .PostType "poll"}}投票{{else}}普通讨论{{end}}(不可更改)</p>
|
||||
<p class="j13-muted">类型:{{if eq .PostType "poll"}}投票{{else if eq .PostType "question"}}问答{{else}}普通讨论{{end}}(不可更改)</p>
|
||||
<input type="hidden" name="post_type" value="{{.PostType}}"/>
|
||||
{{else}}
|
||||
<label>类型
|
||||
<select name="post_type" id="compose-post-type">
|
||||
<option value="normal"{{if ne .PostType "poll"}} selected{{end}}>普通讨论</option>
|
||||
<option value="normal"{{if or (eq .PostType "normal") (eq .PostType "")}} selected{{end}}>普通讨论</option>
|
||||
<option value="question"{{if eq .PostType "question"}} selected{{end}}>问答</option>
|
||||
<option value="poll"{{if eq .PostType "poll"}} selected{{end}}>投票</option>
|
||||
</select>
|
||||
</label>
|
||||
<p class="j13-muted" id="compose-question-hint"{{if ne .PostType "question"}} hidden{{end}}>问答帖无额外字段;解决状态可在帖详情由作者或管理员切换。</p>
|
||||
<fieldset class="j13-poll-fields" id="compose-poll-fields"{{if ne .PostType "poll"}} hidden{{end}}>
|
||||
<legend>投票设置</legend>
|
||||
<label class="j13-check"><input type="checkbox" name="poll_multi" value="1"{{if .PollMulti}} checked{{end}}/> 允许多选</label>
|
||||
@@ -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();
|
||||
})();
|
||||
|
||||
@@ -14,6 +14,9 @@
|
||||
{{if .EditLocked}}<span class="j13-tag">禁编</span>{{end}}
|
||||
{{if .CommentsLocked}}<span class="j13-tag">已结贴</span>{{end}}
|
||||
{{if .PostTypeLabel}}<span class="j13-tag">{{.PostTypeLabel}}</span>{{end}}
|
||||
{{if .IsQuestion}}
|
||||
{{if .QuestionResolved}}<span class="j13-tag j13-tag--resolved">已解决</span>{{else}}<span class="j13-tag j13-tag--unresolved">未解决</span>{{end}}
|
||||
{{end}}
|
||||
{{if .BoardName}}<a class="j13-tag" href="/board/{{.BoardID}}">{{.BoardName}}</a>{{end}}
|
||||
{{if .AuthorID}}<a href="/user/{{.AuthorID}}">{{.AuthorName}}</a>{{else}}<span>{{.AuthorName}}</span>{{end}}
|
||||
<span>{{.CreatedLabel}}</span>
|
||||
@@ -21,6 +24,18 @@
|
||||
</div>
|
||||
{{if .LoggedIn}}
|
||||
<div class="j13-post__actions">
|
||||
{{if .CanToggleQuestion}}
|
||||
<form method="post" action="/post/{{.PostID}}/question/resolve" class="j13-inline-form">
|
||||
<input type="hidden" name="_csrf" value="{{.CSRF}}"/>
|
||||
{{if .QuestionResolved}}
|
||||
<input type="hidden" name="resolved" value="0"/>
|
||||
<button type="submit" class="j13-btn-secondary">标为未解决</button>
|
||||
{{else}}
|
||||
<input type="hidden" name="resolved" value="1"/>
|
||||
<button type="submit">标为已解决</button>
|
||||
{{end}}
|
||||
</form>
|
||||
{{end}}
|
||||
<form method="post" action="/post/{{.PostID}}/like" class="j13-inline-form">
|
||||
<input type="hidden" name="_csrf" value="{{.CSRF}}"/>
|
||||
<button type="submit">{{if .Liked}}取消赞{{else}}赞{{end}} ({{.LikeCount}})</button>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user