feat: SSR 帖/评举报与 Admin 处理
EOF Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -62,7 +62,7 @@
|
||||
- [ ] 修订历史列表与单条详情(可做 diff)
|
||||
- [x] 点赞切换;收藏切换;收藏列表 — SSR `/favorites`
|
||||
- [x] 浏览量 — 详情页计数
|
||||
- [ ] 举报帖子
|
||||
- [x] 举报帖子 — SSR 表单 + `/admin/reports`
|
||||
- [ ] 内容审核状态展示(作者可见待审/被拒)
|
||||
|
||||
### D.1 帖子类型
|
||||
@@ -104,7 +104,7 @@
|
||||
- [ ] 游客评论(公开接口可写,字段 guest_*)
|
||||
- [ ] 编辑评论(时限);删除评论
|
||||
- [x] 评论点赞 — `POST /post/:id/comments/:cid/like`
|
||||
- [ ] 评论举报
|
||||
- [x] 评论举报 — SSR
|
||||
- [ ] @ 提及 → 通知
|
||||
- [ ] 回复提醒(站内信 + 可选邮件)
|
||||
- [ ] 审核中 / 被拒评论可见性规则
|
||||
@@ -185,7 +185,7 @@
|
||||
|
||||
## N. 管理后台其它
|
||||
|
||||
- [x] 仪表盘:用户/帖/板块计数 + 待审帖/评 — SSR `/admin/dashboard`(举报/友链待迁)
|
||||
- [x] 仪表盘:用户/帖/板块计数 + 待审帖/评 + 待处理举报 — SSR `/admin/dashboard`
|
||||
- [x] 敏感词:`forum_settings.filter_words` 读写 + 热更 — SSR `/admin/settings`
|
||||
- [x] 基础限流(post/comment/register/login/window)— SSR;完整 Limits 字数等未迁
|
||||
- [x] SMTP 配置与测试信 — SSR `/admin/settings` 邮件区
|
||||
|
||||
@@ -51,9 +51,10 @@
|
||||
| `/admin/settings` | 品牌 + 基础限流 + 敏感词 + SMTP | 已迁 |
|
||||
| `/admin/friend-links` | 品牌友链、申请审核、入口开关 | 已迁 |
|
||||
| `/admin/pages` | 站点单页 CRUD / 发布 | 已迁 |
|
||||
| `/admin/reports` | 举报处理 | 已迁 |
|
||||
| `/admin/login` | 重定向前台登录 | 已迁 |
|
||||
|
||||
未迁(原 SPA):reports / users / badges / media / 完整 Limits 等。
|
||||
未迁(原 SPA):users / badges / media / 完整 Limits 等。
|
||||
|
||||
---
|
||||
|
||||
@@ -145,7 +146,7 @@
|
||||
| 投票卡 | 选选项提交;显示百分比;作者可结束 |
|
||||
| 悬赏条 | 显示积分与状态;采纳按钮在他人评论上;退款按钮按规则禁用并提示 |
|
||||
| 抽奖卡 | 显示参与人数;开奖;中奖名单 |
|
||||
| 评论 | 楼层列表、`reply_to` 引用、私密开关、点赞 — SSR;@ / 编辑 / 举报未迁 |
|
||||
| 评论 | 楼层列表、`reply_to` 引用、私密开关、点赞、举报 — SSR;@ / 编辑未迁 |
|
||||
| 修订 | 面板列出历史,可选对比 |
|
||||
| 图片 | Lightbox 查看 |
|
||||
|
||||
@@ -189,7 +190,7 @@
|
||||
| Links | 品牌友链增删;申请通过/拒绝;回链检测开关;nav/footer/aside 开关 — SSR `/admin/friend-links`(完整侧栏组件编辑 / 复检按钮未迁) |
|
||||
| Posts | 按状态筛;通过/拒绝;置顶/版顶/精华/锁编/锁评;进回收站恢复/清除 |
|
||||
| Comments | 审核;修订查看;回收站 |
|
||||
| Reports | 处理动作四选一 |
|
||||
| Reports | 处理动作:dismiss / resolve / reject_post / reject_comment — SSR `/admin/reports` |
|
||||
| Users | 搜索;禁言;认证;设等级;调积分;授徽章 |
|
||||
| Badges | 定义自动/限定徽章 |
|
||||
| Media | 分类浏览;批量删 |
|
||||
|
||||
@@ -98,4 +98,6 @@ Feed 搜索:`/` / `/board/:id` 面板(`keyword` / `tag` / `author` / `title_
|
||||
|
||||
站点单页:公开 `/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`。
|
||||
举报:帖/评 SSR 表单;Admin `/admin/reports`(dismiss / resolve / reject_post / reject_comment)。
|
||||
|
||||
Admin:`/admin/dashboard`、`/admin/boards`、`/admin/moderation`、`/admin/settings`(品牌/限流/敏感词/SMTP)、`/admin/friend-links`、`/admin/pages`、`/admin/reports`。
|
||||
@@ -839,3 +839,60 @@ points-only[data-locked="false"] {
|
||||
line-height: 1.65;
|
||||
}
|
||||
|
||||
.j13-report {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
}
|
||||
.j13-report > summary {
|
||||
cursor: pointer;
|
||||
list-style: none;
|
||||
}
|
||||
.j13-report > summary::-webkit-details-marker { display: none; }
|
||||
.j13-report__form {
|
||||
position: absolute;
|
||||
z-index: 5;
|
||||
right: 0;
|
||||
top: 100%;
|
||||
margin-top: 0.35rem;
|
||||
min-width: 14rem;
|
||||
padding: 0.65rem 0.75rem;
|
||||
border: 1px solid var(--j13-border);
|
||||
border-radius: var(--j13-radius);
|
||||
background: var(--j13-bg);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.45rem;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.08);
|
||||
}
|
||||
.j13-report__form label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.2rem;
|
||||
font-size: 0.8rem;
|
||||
color: var(--j13-muted);
|
||||
}
|
||||
.j13-report__form select,
|
||||
.j13-report__form input {
|
||||
font: inherit;
|
||||
color: var(--j13-text);
|
||||
padding: 0.3rem 0.4rem;
|
||||
border: 1px solid var(--j13-border);
|
||||
border-radius: var(--j13-radius);
|
||||
background: var(--j13-bg);
|
||||
}
|
||||
.j13-admin-filters {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.75rem;
|
||||
margin: 0 0 1rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.j13-admin-filters a { color: var(--j13-muted); text-decoration: none; }
|
||||
.j13-admin-filters a.is-active,
|
||||
.j13-admin-filters a:hover { color: var(--j13-accent); font-weight: 600; }
|
||||
.j13-report-handle .j13-form__row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
|
||||
@@ -106,6 +106,7 @@ func Setup(cfg *config.Config) (*gin.Engine, error) {
|
||||
FriendLink: friendLinkApplySvc,
|
||||
Mail: mailSvc,
|
||||
SitePage: sitePageSvc,
|
||||
Report: reportSvc,
|
||||
}, authMW)
|
||||
|
||||
r.GET("/media/thumb/*filepath", h.ServeImageThumb)
|
||||
|
||||
@@ -32,11 +32,12 @@ func (d Deps) adminChrome(ctx *webctx.Context, title, nav string) AdminChrome {
|
||||
|
||||
type adminDashData struct {
|
||||
AdminChrome
|
||||
UserCount int64
|
||||
PostCount int64
|
||||
PendingPosts int64
|
||||
PendingComments int64
|
||||
BoardCount int64
|
||||
UserCount int64
|
||||
PostCount int64
|
||||
PendingPosts int64
|
||||
PendingComments int64
|
||||
PendingReports int64
|
||||
BoardCount int64
|
||||
}
|
||||
|
||||
// AdminDashboard 概览
|
||||
@@ -48,12 +49,17 @@ func (d Deps) AdminDashboard(c *gin.Context) {
|
||||
_ = models.DB.Model(&models.Board{}).Count(&boards).Error
|
||||
pendingPosts, _ := d.Post.PendingPostCount()
|
||||
pendingComments, _ := d.Comment.PendingCommentCount()
|
||||
var pendingReports int64
|
||||
if d.Report != nil {
|
||||
pendingReports, _ = d.Report.PendingCount()
|
||||
}
|
||||
ctx.HTML(http.StatusOK, "admin/dashboard", adminDashData{
|
||||
AdminChrome: d.adminChrome(ctx, "仪表盘", "dashboard"),
|
||||
UserCount: users,
|
||||
PostCount: posts,
|
||||
PendingPosts: pendingPosts,
|
||||
PendingComments: pendingComments,
|
||||
PendingReports: pendingReports,
|
||||
BoardCount: boards,
|
||||
})
|
||||
}
|
||||
|
||||
143
routers/web/admin_reports.go
Normal file
143
routers/web/admin_reports.go
Normal file
@@ -0,0 +1,143 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"git.iioio.com/freefire/jiang13-forum/models"
|
||||
"git.iioio.com/freefire/jiang13-forum/modules/webctx"
|
||||
"git.iioio.com/freefire/jiang13-forum/services"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type adminReportRow struct {
|
||||
ID uint
|
||||
CreatedAt string
|
||||
Status string
|
||||
StatusLabel string
|
||||
ReasonLabel string
|
||||
Detail string
|
||||
ReporterName string
|
||||
TargetLabel string
|
||||
PostID uint
|
||||
IsComment bool
|
||||
Pending bool
|
||||
}
|
||||
|
||||
type adminReportsData struct {
|
||||
AdminChrome
|
||||
Reports []adminReportRow
|
||||
PendingCount int64
|
||||
StatusFilter string
|
||||
Page int
|
||||
HasPrev bool
|
||||
HasMore bool
|
||||
PrevPage int
|
||||
NextPage int
|
||||
}
|
||||
|
||||
// AdminReportsGet 举报列表
|
||||
func (d Deps) AdminReportsGet(c *gin.Context) {
|
||||
ctx := d.ctx(c)
|
||||
d.renderAdminReports(ctx, "")
|
||||
}
|
||||
|
||||
func (d Deps) renderAdminReports(ctx *webctx.Context, errMsg string) {
|
||||
chrome := d.adminChrome(ctx, "举报", "reports")
|
||||
chrome.Error = errMsg
|
||||
status := strings.TrimSpace(ctx.C.DefaultQuery("status", "pending"))
|
||||
if status == "" {
|
||||
status = "pending"
|
||||
}
|
||||
page, _ := strconv.Atoi(ctx.C.DefaultQuery("page", "1"))
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
data := adminReportsData{
|
||||
AdminChrome: chrome,
|
||||
StatusFilter: status,
|
||||
Page: page,
|
||||
PrevPage: page - 1,
|
||||
NextPage: page + 1,
|
||||
HasPrev: page > 1,
|
||||
}
|
||||
if d.Report != nil {
|
||||
data.PendingCount, _ = d.Report.PendingCount()
|
||||
list, total, _ := d.Report.ListAdmin(services.ReportListQuery{
|
||||
Status: status, Page: page, Size: d.Settings.PageSizeDefault(),
|
||||
})
|
||||
data.HasMore = int64(page*d.Settings.PageSizeDefault()) < total
|
||||
data.Reports = make([]adminReportRow, 0, len(list))
|
||||
for _, r := range list {
|
||||
row := adminReportRow{
|
||||
ID: r.ID, CreatedAt: r.CreatedAt.Format("2006-01-02 15:04"),
|
||||
Status: r.Status, StatusLabel: reportStatusLabel(r.Status),
|
||||
ReasonLabel: services.ReportReasonLabel(r.Reason), Detail: r.Detail,
|
||||
PostID: r.PostID, Pending: r.Status == models.ReportStatusPending,
|
||||
}
|
||||
if r.Reporter.ID > 0 {
|
||||
row.ReporterName = r.Reporter.Username
|
||||
}
|
||||
isComment := r.CommentID != nil && *r.CommentID > 0
|
||||
row.IsComment = isComment
|
||||
title := ""
|
||||
if r.Post.ID > 0 {
|
||||
title = r.Post.Title
|
||||
}
|
||||
if isComment {
|
||||
floor := 0
|
||||
if r.Comment != nil {
|
||||
floor = r.Comment.Floor
|
||||
}
|
||||
row.TargetLabel = "评论 #" + strconv.Itoa(floor) + " · 《" + title + "》"
|
||||
} else {
|
||||
row.TargetLabel = "帖子 · 《" + title + "》"
|
||||
}
|
||||
data.Reports = append(data.Reports, row)
|
||||
}
|
||||
}
|
||||
ctx.HTML(http.StatusOK, "admin/reports", data)
|
||||
}
|
||||
|
||||
// AdminReportHandlePost 处理举报
|
||||
func (d Deps) AdminReportHandlePost(c *gin.Context) {
|
||||
ctx := d.ctx(c)
|
||||
if !ctx.CheckCSRF() {
|
||||
ctx.SetFlash("无效请求,请重试")
|
||||
ctx.Redirect("/admin/reports")
|
||||
return
|
||||
}
|
||||
if d.Report == nil {
|
||||
ctx.SetFlash("举报服务未就绪")
|
||||
ctx.Redirect("/admin/reports")
|
||||
return
|
||||
}
|
||||
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
action := strings.TrimSpace(c.PostForm("action"))
|
||||
if _, err := d.Report.Handle(services.HandleReportInput{
|
||||
ReportID: uint(id),
|
||||
HandlerID: ctx.UserID(),
|
||||
Action: action,
|
||||
HandleNote: strings.TrimSpace(c.PostForm("handle_note")),
|
||||
RejectReason: strings.TrimSpace(c.PostForm("reject_reason")),
|
||||
}); err != nil {
|
||||
d.renderAdminReports(ctx, err.Error())
|
||||
return
|
||||
}
|
||||
ctx.SetFlash("举报已处理")
|
||||
ctx.Redirect("/admin/reports?status=pending")
|
||||
}
|
||||
|
||||
func reportStatusLabel(s string) string {
|
||||
switch s {
|
||||
case models.ReportStatusPending:
|
||||
return "待处理"
|
||||
case models.ReportStatusResolved:
|
||||
return "已处理"
|
||||
case models.ReportStatusDismissed:
|
||||
return "已驳回"
|
||||
default:
|
||||
return s
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,7 @@ type Deps struct {
|
||||
FriendLink *services.FriendLinkApplyService
|
||||
Mail *services.MailService
|
||||
SitePage *services.SitePageService
|
||||
Report *services.ReportService
|
||||
}
|
||||
|
||||
// SitePageLink 导航/页脚站点单页链接
|
||||
|
||||
@@ -27,6 +27,8 @@ func Register(r *gin.Engine, deps Deps, authMW *auth.AuthMiddleware) {
|
||||
g.POST("/post/:id/like", authMW.RequireAuth(), deps.PostLike)
|
||||
g.POST("/post/:id/favorite", authMW.RequireAuth(), deps.PostFavorite)
|
||||
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)
|
||||
g.GET("/login", deps.LoginGet)
|
||||
g.POST("/login", deps.LoginPost)
|
||||
g.POST("/logout", deps.LogoutPost)
|
||||
@@ -72,6 +74,8 @@ func Register(r *gin.Engine, deps Deps, authMW *auth.AuthMiddleware) {
|
||||
admin.POST("/pages/:id", deps.AdminPageUpdate)
|
||||
admin.POST("/pages/:id/delete", deps.AdminPageDelete)
|
||||
admin.POST("/pages/:id/publish", deps.AdminPagePublishPost)
|
||||
admin.GET("/reports", deps.AdminReportsGet)
|
||||
admin.POST("/reports/:id/handle", deps.AdminReportHandlePost)
|
||||
}
|
||||
|
||||
g.GET("/user/:id", deps.UserPublic)
|
||||
|
||||
@@ -36,6 +36,7 @@ type PostPageData struct {
|
||||
Comments []CommentView
|
||||
CommentsLocked bool
|
||||
CanEdit bool
|
||||
CanReportPost bool
|
||||
}
|
||||
|
||||
// CommentView 评论
|
||||
@@ -53,6 +54,7 @@ type CommentView struct {
|
||||
LikeCount int
|
||||
Liked bool
|
||||
IsPrivate bool
|
||||
CanReport bool
|
||||
}
|
||||
|
||||
// PostView GET /post/:id
|
||||
@@ -88,6 +90,7 @@ func (d Deps) PostView(c *gin.Context) {
|
||||
CreatedLabel: formatTime(cm.CreatedAt),
|
||||
Content: cm.Content, ContentHidden: cm.ContentHidden,
|
||||
LikeCount: cm.LikeCount, Liked: cm.Liked, IsPrivate: cm.IsPrivate,
|
||||
CanReport: ctx.IsSigned() && (cm.UserID == 0 || cm.UserID != ctx.UserID()),
|
||||
}
|
||||
if cm.ReplyTarget != nil {
|
||||
view.ReplyToID = cm.ReplyTarget.ID
|
||||
@@ -130,6 +133,7 @@ func (d Deps) PostView(c *gin.Context) {
|
||||
BodyHTML: body, CommentCount: len(cv), Comments: cv,
|
||||
CommentsLocked: post.CommentsLocked,
|
||||
CanEdit: d.Post.CanUserEdit(post, ctx.UserID(), ctx.IsAdmin()),
|
||||
CanReportPost: ctx.IsSigned() && post.UserID != ctx.UserID(),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
72
routers/web/report.go
Normal file
72
routers/web/report.go
Normal file
@@ -0,0 +1,72 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// PostReportPost 举报帖子
|
||||
func (d Deps) PostReportPost(c *gin.Context) {
|
||||
ctx := d.ctx(c)
|
||||
postID, _ := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
redir := fmt.Sprintf("/post/%d", postID)
|
||||
if !ctx.CheckCSRF() {
|
||||
ctx.SetFlash("无效请求,请重试")
|
||||
ctx.Redirect(redir)
|
||||
return
|
||||
}
|
||||
if d.Report == nil {
|
||||
ctx.SetFlash("举报服务未就绪")
|
||||
ctx.Redirect(redir)
|
||||
return
|
||||
}
|
||||
if d.Limiter != nil && !d.Limiter.Allow("report", fmt.Sprintf("%d", ctx.UserID())) {
|
||||
ctx.SetFlash("举报过于频繁,请稍后再试")
|
||||
ctx.Redirect(redir)
|
||||
return
|
||||
}
|
||||
reason := strings.TrimSpace(c.PostForm("reason"))
|
||||
detail := strings.TrimSpace(c.PostForm("detail"))
|
||||
if _, err := d.Report.Create(ctx.UserID(), uint(postID), reason, detail); err != nil {
|
||||
ctx.SetFlash(err.Error())
|
||||
ctx.Redirect(redir)
|
||||
return
|
||||
}
|
||||
ctx.SetFlash("举报已提交,感谢反馈")
|
||||
ctx.Redirect(redir)
|
||||
}
|
||||
|
||||
// CommentReportPost 举报评论
|
||||
func (d Deps) CommentReportPost(c *gin.Context) {
|
||||
ctx := d.ctx(c)
|
||||
postID, _ := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
cid, _ := strconv.ParseUint(c.Param("cid"), 10, 64)
|
||||
redir := fmt.Sprintf("/post/%d", postID)
|
||||
if !ctx.CheckCSRF() {
|
||||
ctx.SetFlash("无效请求,请重试")
|
||||
ctx.Redirect(redir)
|
||||
return
|
||||
}
|
||||
if d.Report == nil {
|
||||
ctx.SetFlash("举报服务未就绪")
|
||||
ctx.Redirect(redir)
|
||||
return
|
||||
}
|
||||
if d.Limiter != nil && !d.Limiter.Allow("report", fmt.Sprintf("%d", ctx.UserID())) {
|
||||
ctx.SetFlash("举报过于频繁,请稍后再试")
|
||||
ctx.Redirect(redir)
|
||||
return
|
||||
}
|
||||
reason := strings.TrimSpace(c.PostForm("reason"))
|
||||
detail := strings.TrimSpace(c.PostForm("detail"))
|
||||
if _, err := d.Report.CreateCommentReport(ctx.UserID(), uint(cid), reason, detail); err != nil {
|
||||
ctx.SetFlash(err.Error())
|
||||
ctx.Redirect(redir)
|
||||
return
|
||||
}
|
||||
ctx.SetFlash("举报已提交,感谢反馈")
|
||||
ctx.Redirect(redir)
|
||||
}
|
||||
@@ -11,8 +11,12 @@
|
||||
<li><strong>{{.BoardCount}}</strong><span>板块</span></li>
|
||||
<li><strong>{{.PendingPosts}}</strong><span>待审帖</span></li>
|
||||
<li><strong>{{.PendingComments}}</strong><span>待审评</span></li>
|
||||
<li><strong>{{.PendingReports}}</strong><span>待处理举报</span></li>
|
||||
</ul>
|
||||
<p class="j13-muted">有待审内容时请前往 <a href="/admin/moderation">内容审核</a>。</p>
|
||||
<p class="j13-muted">
|
||||
有待审内容时请前往 <a href="/admin/moderation">内容审核</a>;
|
||||
有举报时请前往 <a href="/admin/reports">举报处理</a>。
|
||||
</p>
|
||||
</main>
|
||||
{{template "base/footer" .}}
|
||||
{{end}}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
<a href="/admin/dashboard"{{if eq .NavActive "dashboard"}} 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/reports"{{if eq .NavActive "reports"}} 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>
|
||||
|
||||
55
templates/admin/reports.tmpl
Normal file
55
templates/admin/reports.tmpl
Normal file
@@ -0,0 +1,55 @@
|
||||
{{define "admin/reports"}}
|
||||
{{template "base/head" .}}
|
||||
{{template "base/navbar" .}}
|
||||
<main class="j13-main j13-admin">
|
||||
<h1>举报处理{{if gt .PendingCount 0}}(待处理 {{.PendingCount}}){{end}}</h1>
|
||||
{{template "admin/nav" .}}
|
||||
{{template "base/alert" .}}
|
||||
|
||||
<p class="j13-admin-filters">
|
||||
<a href="/admin/reports?status=pending"{{if eq .StatusFilter "pending"}} class="is-active"{{end}}>待处理</a>
|
||||
<a href="/admin/reports?status=resolved"{{if eq .StatusFilter "resolved"}} class="is-active"{{end}}>已处理</a>
|
||||
<a href="/admin/reports?status=dismissed"{{if eq .StatusFilter "dismissed"}} class="is-active"{{end}}>已驳回</a>
|
||||
<a href="/admin/reports?status=all"{{if eq .StatusFilter "all"}} class="is-active"{{end}}>全部</a>
|
||||
</p>
|
||||
|
||||
{{if .Reports}}
|
||||
<ul class="j13-admin-list">
|
||||
{{range .Reports}}
|
||||
<li class="j13-admin-card">
|
||||
<p><strong>{{.TargetLabel}}</strong> · {{.ReasonLabel}} · {{.StatusLabel}}</p>
|
||||
<p class="j13-muted">{{.CreatedAt}} · 举报人 {{if .ReporterName}}{{.ReporterName}}{{else}}—{{end}}
|
||||
· <a href="/post/{{.PostID}}">查看帖子</a></p>
|
||||
{{if .Detail}}<p>说明:{{.Detail}}</p>{{end}}
|
||||
{{if .Pending}}
|
||||
<form method="post" action="/admin/reports/{{.ID}}/handle" class="j13-form j13-admin-form j13-report-handle">
|
||||
<input type="hidden" name="_csrf" value="{{$.CSRF}}"/>
|
||||
<label>处理备注(可选) <input name="handle_note" maxlength="500"/></label>
|
||||
<label>拒绝原因(拒绝内容时必填) <input name="reject_reason" maxlength="1000"/></label>
|
||||
<div class="j13-form__row">
|
||||
<button type="submit" name="action" value="dismiss">驳回举报</button>
|
||||
<button type="submit" name="action" value="resolve" class="j13-btn-secondary">标记已处理</button>
|
||||
{{if .IsComment}}
|
||||
<button type="submit" name="action" value="reject_comment">拒绝该评论</button>
|
||||
{{else}}
|
||||
<button type="submit" name="action" value="reject_post">拒绝该帖</button>
|
||||
{{end}}
|
||||
</div>
|
||||
</form>
|
||||
{{end}}
|
||||
</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
{{if or .HasPrev .HasMore}}
|
||||
<nav class="j13-pager">
|
||||
{{if .HasPrev}}<a href="/admin/reports?status={{.StatusFilter}}&page={{.PrevPage}}">上一页</a>{{end}}
|
||||
<span>第 {{.Page}} 页</span>
|
||||
{{if .HasMore}}<a href="/admin/reports?status={{.StatusFilter}}&page={{.NextPage}}">下一页</a>{{end}}
|
||||
</nav>
|
||||
{{end}}
|
||||
{{else}}
|
||||
<p class="j13-empty">暂无举报。</p>
|
||||
{{end}}
|
||||
</main>
|
||||
{{template "base/footer" .}}
|
||||
{{end}}
|
||||
@@ -22,6 +22,28 @@
|
||||
<button type="submit">{{if .Favorited}}取消收藏{{else}}收藏{{end}}</button>
|
||||
</form>
|
||||
{{if .CanEdit}}<a class="j13-linkbtn" href="/post/{{.PostID}}/edit">编辑</a>{{end}}
|
||||
{{if .CanReportPost}}
|
||||
<details class="j13-report">
|
||||
<summary class="j13-linkbtn">举报</summary>
|
||||
<form method="post" action="/post/{{.PostID}}/report" class="j13-report__form">
|
||||
<input type="hidden" name="_csrf" value="{{.CSRF}}"/>
|
||||
<label>原因
|
||||
<select name="reason" required>
|
||||
<option value="">请选择</option>
|
||||
<option value="spam">垃圾广告</option>
|
||||
<option value="abuse">人身攻击 / 辱骂</option>
|
||||
<option value="illegal">违法违规</option>
|
||||
<option value="irrelevant">内容无关 / 灌水</option>
|
||||
<option value="other">其他</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>补充说明(可选)
|
||||
<input name="detail" maxlength="500" placeholder="可选"/>
|
||||
</label>
|
||||
<button type="submit" class="j13-btn j13-btn--sm">提交举报</button>
|
||||
</form>
|
||||
</details>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
</header>
|
||||
@@ -59,6 +81,28 @@
|
||||
{{if not $.CommentsLocked}}
|
||||
<button type="button" class="j13-linkbtn j13-comment__reply-btn" data-reply-to="{{.ID}}" data-reply-floor="{{.Floor}}" data-reply-author="{{.AuthorName}}">回复</button>
|
||||
{{end}}
|
||||
{{if .CanReport}}
|
||||
<details class="j13-report">
|
||||
<summary class="j13-linkbtn">举报</summary>
|
||||
<form method="post" action="/post/{{$.PostID}}/comments/{{.ID}}/report" class="j13-report__form">
|
||||
<input type="hidden" name="_csrf" value="{{$.CSRF}}"/>
|
||||
<label>原因
|
||||
<select name="reason" required>
|
||||
<option value="">请选择</option>
|
||||
<option value="spam">垃圾广告</option>
|
||||
<option value="abuse">人身攻击 / 辱骂</option>
|
||||
<option value="illegal">违法违规</option>
|
||||
<option value="irrelevant">内容无关 / 灌水</option>
|
||||
<option value="other">其他</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>补充说明(可选)
|
||||
<input name="detail" maxlength="500" placeholder="可选"/>
|
||||
</label>
|
||||
<button type="submit" class="j13-btn j13-btn--sm">提交举报</button>
|
||||
</form>
|
||||
</details>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
</li>
|
||||
|
||||
@@ -839,3 +839,60 @@ points-only[data-locked="false"] {
|
||||
line-height: 1.65;
|
||||
}
|
||||
|
||||
.j13-report {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
}
|
||||
.j13-report > summary {
|
||||
cursor: pointer;
|
||||
list-style: none;
|
||||
}
|
||||
.j13-report > summary::-webkit-details-marker { display: none; }
|
||||
.j13-report__form {
|
||||
position: absolute;
|
||||
z-index: 5;
|
||||
right: 0;
|
||||
top: 100%;
|
||||
margin-top: 0.35rem;
|
||||
min-width: 14rem;
|
||||
padding: 0.65rem 0.75rem;
|
||||
border: 1px solid var(--j13-border);
|
||||
border-radius: var(--j13-radius);
|
||||
background: var(--j13-bg);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.45rem;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.08);
|
||||
}
|
||||
.j13-report__form label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.2rem;
|
||||
font-size: 0.8rem;
|
||||
color: var(--j13-muted);
|
||||
}
|
||||
.j13-report__form select,
|
||||
.j13-report__form input {
|
||||
font: inherit;
|
||||
color: var(--j13-text);
|
||||
padding: 0.3rem 0.4rem;
|
||||
border: 1px solid var(--j13-border);
|
||||
border-radius: var(--j13-radius);
|
||||
background: var(--j13-bg);
|
||||
}
|
||||
.j13-admin-filters {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.75rem;
|
||||
margin: 0 0 1rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.j13-admin-filters a { color: var(--j13-muted); text-decoration: none; }
|
||||
.j13-admin-filters a.is-active,
|
||||
.j13-admin-filters a:hover { color: var(--j13-accent); font-weight: 600; }
|
||||
.j13-report-handle .j13-form__row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user