diff --git a/docs/rebuild-spec/02-features.md b/docs/rebuild-spec/02-features.md index ae70e5e..0fa4b84 100644 --- a/docs/rebuild-spec/02-features.md +++ b/docs/rebuild-spec/02-features.md @@ -105,8 +105,8 @@ - [x] 编辑评论(时限);删除评论 — SSR 帖详情入口 + `/comments/:cid/edit|delete`;作者软删进回收站 - [x] 评论点赞 — `POST /post/:id/comments/:cid/like` - [x] 评论举报 — SSR -- [ ] @ 提及 → 通知 -- [ ] 回复提醒(站内信 + 可选邮件) +- [x] @ 提及 → 通知 — SSR 发评/审通调用 `NotifyCommentMentions` +- [x] 回复提醒(站内信 + 可选邮件)— SSR 发评/审通调用 `NotifyCommentPublished` - [ ] 审核中 / 被拒评论可见性规则 - [x] 管理员:通过 / 拒绝待审评论 — SSR `/admin/moderation`(回收站/修订未迁) @@ -119,7 +119,7 @@ - [x] 发送私信 — 表单 PRG;用户主页「发私信」入口 - [x] 未读数(可分私信 / 通知)— 列表分项 + 导航角标 - [x] 标记会话已读 / 通知已读 / 全部已读 — 打开会话自动已读;全部标已读 -- [ ] 系统通知种类:`system` / `reject` / `report_result` / `reply` / `mention` / `moderation` 等(写入已有;筛选 UI 未迁) +- [x] 系统通知种类:`system` / `reject` / `report_result` / `reply` / `mention` / `moderation` 等(写入已有;`/messages/with/0?kind=` 筛选) --- diff --git a/docs/rebuild-spec/06-pages-ux.md b/docs/rebuild-spec/06-pages-ux.md index 88ceecf..2e3e4ab 100644 --- a/docs/rebuild-spec/06-pages-ux.md +++ b/docs/rebuild-spec/06-pages-ux.md @@ -147,7 +147,7 @@ | 投票卡 | 选选项提交;显示百分比;作者可结束 | | 悬赏条 | 显示积分与状态;采纳按钮在他人评论上;退款按钮按规则禁用并提示 | | 抽奖卡 | 显示参与人数;开奖;中奖名单 | -| 评论 | 楼层列表、`reply_to` 引用、私密开关、点赞、举报、编辑(时限)、删除 — SSR;@ 未迁 | +| 评论 | 楼层列表、`reply_to` 引用、私密开关、点赞、举报、编辑(时限)、删除、回复/@ 通知 — SSR;嵌套树 UI 未做 | | 修订 | 面板列出历史,可选对比 | | 图片 | Lightbox 查看 | @@ -155,10 +155,10 @@ ## 5. 消息页 -- 左:会话列表(系统会话单独) -- 右:消息时间线;发送框 +- 左:会话列表(系统会话单独)— SSR `/messages`;快捷链到回复/提及筛选 +- 右:消息时间线;发送框 — SSR `/messages/with/:peerId` - 顶:未读角标(全局导航也可显示) -- 通知筛选(kind) +- 通知筛选(kind)— 系统会话 `?kind=reply|mention|…` --- diff --git a/public/assets/site.css b/public/assets/site.css index 4de600d..79e247f 100644 --- a/public/assets/site.css +++ b/public/assets/site.css @@ -698,6 +698,18 @@ body.j13-body { .j13-msg-bubble__subj { font-weight: 600; margin-bottom: 0.25rem; } .j13-msg-bubble__body { white-space: pre-wrap; overflow-wrap: anywhere; } .j13-msg-compose { margin-top: 1.25rem; } +.j13-msg-kinds { + display: flex; + flex-wrap: wrap; + gap: 0.5rem 0.85rem; + margin: 0.5rem 0 0; + font-size: 0.9rem; +} +.j13-msg-kinds a { color: var(--j13-muted); } +.j13-msg-kinds a.is-active { + color: var(--j13-accent); + font-weight: 600; +} /* 内容门控锁定壳 */ members-only, diff --git a/routers/setup.go b/routers/setup.go index b2897a1..a0dda64 100644 --- a/routers/setup.go +++ b/routers/setup.go @@ -108,6 +108,7 @@ func Setup(cfg *config.Config) (*gin.Engine, error) { SitePage: sitePageSvc, Report: reportSvc, Captcha: captchaSvc, + Notify: notifySvc, }, authMW) r.GET("/media/thumb/*filepath", h.ServeImageThumb) diff --git a/routers/web/admin.go b/routers/web/admin.go index 1749b08..0ef353d 100644 --- a/routers/web/admin.go +++ b/routers/web/admin.go @@ -357,6 +357,13 @@ func (d Deps) AdminCommentApprove(c *gin.Context) { if err := d.Comment.SetStatus(uint(id), models.ContentStatusPublished); err != nil { ctx.SetFlash(err.Error()) } else { + if d.Notify != nil { + if comment, err := d.Comment.GetByID(uint(id)); err == nil { + comment.Status = models.ContentStatusPublished + d.Notify.AsyncNotifyCommentPublished(comment) + d.Notify.AsyncNotifyCommentMentions(comment) + } + } ctx.SetFlash("评论已通过") } ctx.Redirect("/admin/moderation") diff --git a/routers/web/chrome.go b/routers/web/chrome.go index a0b1cdb..373597e 100644 --- a/routers/web/chrome.go +++ b/routers/web/chrome.go @@ -29,6 +29,7 @@ type Deps struct { SitePage *services.SitePageService Report *services.ReportService Captcha *services.CaptchaService + Notify *services.NotifyService } // SitePageLink 导航/页脚站点单页链接 diff --git a/routers/web/messages.go b/routers/web/messages.go index fefa57d..46c827e 100644 --- a/routers/web/messages.go +++ b/routers/web/messages.go @@ -6,6 +6,7 @@ import ( "strconv" "strings" + "git.iioio.com/freefire/jiang13-forum/models" "git.iioio.com/freefire/jiang13-forum/services" "github.com/gin-gonic/gin" ) @@ -52,6 +53,15 @@ type messagesThreadData struct { OlderBefore uint HasOlder bool Draft string + KindFilter string + KindLinks []msgKindLink +} + +type msgKindLink struct { + Kind string + Label string + URL string + Active bool } // MessagesList GET /messages @@ -121,9 +131,22 @@ func (d Deps) renderMessagesThread(c *gin.Context, peerID uint, errMsg, beforeRa uid := ctx.UserID() before, _ := strconv.ParseUint(beforeRaw, 10, 64) size := 40 - list, total, err := d.Message.ListConversationMessages(services.ConversationMessagesQuery{ - UserID: uid, PeerID: peerID, Page: 1, Size: size, Before: uint(before), - }) + kindFilter := "" + if peerID == 0 { + kindFilter = normalizeNotifyKind(c.Query("kind")) + } + + var list []models.PrivateMessage + var total int64 + var err error + if peerID == 0 && kindFilter != "" && kindFilter != "all" { + list, total, err = d.Message.ListNotifications(uid, 1, size, kindFilter) + _ = before // kind 筛选暂不分页 before + } else { + list, total, err = d.Message.ListConversationMessages(services.ConversationMessagesQuery{ + UserID: uid, PeerID: peerID, Page: 1, Size: size, Before: uint(before), + }) + } if err != nil { ctx.SetFlash(err.Error()) ctx.Redirect("/messages") @@ -156,21 +179,69 @@ func (d Deps) renderMessagesThread(c *gin.Context, peerID uint, errMsg, beforeRa }) } hasOlder := false - if oldest > 0 { - if before == 0 { - hasOlder = total > int64(len(list)) - } else { - hasOlder = len(list) >= size + if peerID != 0 || kindFilter == "" || kindFilter == "all" { + if oldest > 0 { + if before == 0 { + hasOlder = total > int64(len(list)) + } else { + hasOlder = len(list) >= size + } } + } else { + hasOlder = false } chrome := d.chrome(ctx, peerName+" · 私信 · "+d.Settings.SiteBranding().Name, "", "") chrome.Error = errMsg - ctx.HTML(http.StatusOK, "messages/thread", messagesThreadData{ + data := messagesThreadData{ PageChrome: chrome, PeerID: peerID, PeerName: peerName, IsSystem: peerID == 0, CanReply: canReply, Messages: bubbles, OlderBefore: oldest, HasOlder: hasOlder, Draft: draft, - }) + } + if peerID == 0 { + data.KindFilter = kindFilter + data.KindLinks = notifyKindLinks(kindFilter) + } + ctx.HTML(http.StatusOK, "messages/thread", data) +} + +func normalizeNotifyKind(raw string) string { + k := strings.TrimSpace(strings.ToLower(raw)) + switch k { + case "", "all": + return "all" + case models.MessageKindReply, models.MessageKindMention, models.MessageKindSystem, + models.MessageKindReject, models.MessageKindReportResult, models.MessageKindModeration: + return k + default: + return "all" + } +} + +func notifyKindLinks(active string) []msgKindLink { + if active == "" { + active = "all" + } + items := []struct{ kind, label string }{ + {"all", "全部"}, + {models.MessageKindReply, "回复"}, + {models.MessageKindMention, "提及"}, + {models.MessageKindSystem, "系统"}, + {models.MessageKindReject, "拒绝"}, + {models.MessageKindReportResult, "举报结果"}, + {models.MessageKindModeration, "审核"}, + } + out := make([]msgKindLink, 0, len(items)) + for _, it := range items { + url := "/messages/with/0" + if it.kind != "all" { + url += "?kind=" + it.kind + } + out = append(out, msgKindLink{ + Kind: it.kind, Label: it.label, URL: url, Active: active == it.kind, + }) + } + return out } // MessagesSend POST /messages/with/:peerId diff --git a/routers/web/post.go b/routers/web/post.go index afb7690..db02e29 100644 --- a/routers/web/post.go +++ b/routers/web/post.go @@ -201,6 +201,16 @@ func (d Deps) PostComment(c *gin.Context) { ctx.Redirect(fmt.Sprintf("/post/%d#comments", id)) return } + if d.Notify != nil && cm != nil { + switch cm.Status { + case models.ContentStatusPublished: + d.Notify.AsyncNotifyCommentPublished(cm) + d.Notify.AsyncNotifyCommentMentions(cm) + case models.ContentStatusPending: + ctx.SetFlash("评论已提交,审核通过后公开显示") + d.Notify.AsyncNotifyPendingComment(cm) + } + } anchor := "#comments" if cm != nil && cm.Floor > 0 { anchor = fmt.Sprintf("#floor-%d", cm.Floor) diff --git a/templates/messages/list.tmpl b/templates/messages/list.tmpl index cd660aa..db499ae 100644 --- a/templates/messages/list.tmpl +++ b/templates/messages/list.tmpl @@ -14,6 +14,7 @@ {{end}}

+

系统通知 · 回复 · 提及

{{if .Conversations}}