feat: SSR 投票帖发帖与详情投票卡

compose 支持 poll 类型与选项,详情可投票/查看结果/结束。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-30 01:18:58 +08:00
parent 7f21ffb14e
commit 4f93397ef3
10 changed files with 403 additions and 43 deletions

View File

@@ -46,6 +46,28 @@ type PostPageData struct {
Status string
StatusLabel string
ShowModerationBanner bool
Poll *postPollView
}
type postPollOptionView struct {
ID uint
Text string
VoteCount int
Percent int
Selected bool
}
type postPollView struct {
Multi bool
MaxChoices int
Closed bool
EndsLabel string
TotalVotes int
ShowResults bool
HasVoted bool
CanVote bool
CanClose bool
Options []postPollOptionView
}
// CommentView 评论
@@ -151,6 +173,13 @@ func (d Deps) PostView(c *gin.Context) {
}
}
var pollView *postPollView
if post.PostType == models.PostTypePoll {
if pv, err := services.GetPollView(post.ID, ctx.UserID()); err == nil && pv != nil {
pollView = mapPollView(pv, ctx.IsSigned(), ctx.IsAdmin() || post.UserID == ctx.UserID())
}
}
ctx.HTML(http.StatusOK, "post", PostPageData{
PageChrome: chrome, PostID: post.ID,
PostPath: url.QueryEscape(fmt.Sprintf("/post/%d", post.ID)),
@@ -170,9 +199,37 @@ func (d Deps) PostView(c *gin.Context) {
Status: post.Status,
StatusLabel: statusLabel,
ShowModerationBanner: showBanner,
Poll: pollView,
})
}
func mapPollView(pv *services.PollView, loggedIn, canClose bool) *postPollView {
hasVoted := len(pv.MyOptionIDs) > 0
selected := make(map[uint]bool, len(pv.MyOptionIDs))
for _, id := range pv.MyOptionIDs {
selected[id] = true
}
showResults := pv.Closed || hasVoted
opts := make([]postPollOptionView, 0, len(pv.Options))
for _, o := range pv.Options {
opts = append(opts, postPollOptionView{
ID: o.ID, Text: o.Text, VoteCount: o.VoteCount, Percent: o.Percent, Selected: selected[o.ID],
})
}
ends := ""
if pv.EndsAt != nil {
ends = formatTime(*pv.EndsAt)
}
return &postPollView{
Multi: pv.Multi, MaxChoices: pv.MaxChoices, Closed: pv.Closed,
EndsLabel: ends, TotalVotes: pv.TotalVotes,
ShowResults: showResults, HasVoted: hasVoted,
CanVote: loggedIn && !pv.Closed && !hasVoted,
CanClose: canClose && !pv.Closed,
Options: opts,
}
}
func contentStatusLabel(status string) string {
switch status {
case models.ContentStatusPending:
@@ -349,6 +406,68 @@ func (d Deps) PostFavorite(c *gin.Context) {
ctx.Redirect(fmt.Sprintf("/post/%d", id))
}
// PostPollVote 投票提交
func (d Deps) PostPollVote(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
}
raw := c.PostFormArray("option_ids")
if len(raw) == 0 {
if one := strings.TrimSpace(c.PostForm("option_id")); one != "" {
raw = []string{one}
}
}
oids := make([]uint, 0, len(raw))
for _, s := range raw {
v, err := strconv.ParseUint(strings.TrimSpace(s), 10, 64)
if err == nil && v > 0 {
oids = append(oids, uint(v))
}
}
if err := services.VotePoll(id, ctx.UserID(), oids); err != nil {
ctx.SetFlash(err.Error())
ctx.Redirect(fmt.Sprintf("/post/%d", id))
return
}
ctx.SetFlash("投票成功")
ctx.Redirect(fmt.Sprintf("/post/%d", id))
}
// PostPollClose 结束投票
func (d Deps) PostPollClose(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
}
post, err := d.Post.FindByID(id)
if err != nil {
d.render404(ctx)
return
}
if err := services.ClosePoll(id, ctx.UserID(), ctx.IsAdmin(), post.UserID); err != nil {
ctx.SetFlash(err.Error())
ctx.Redirect(fmt.Sprintf("/post/%d", id))
return
}
ctx.SetFlash("投票已结束")
ctx.Redirect(fmt.Sprintf("/post/%d", id))
}
type commentEditData struct {
PageChrome
PostID uint