feat: add article comment system with moderation

- Comment model with nested replies (ParentID), dual authorship
  (logged-in UserID / anonymous GuestToken cookie), email hash for
  Gravatar, private flag, and moderation status
- CommentConfig singleton (enabled / allow guest / guest-require-approval
  / use Gravatar) cached like the other platform config
- Markdown comments with built-in emoji picker, preview, and markdown
  help; rendered client-side via marked + DOMPurify, with server-side
  HTML/dangerous-scheme stripping as a first XSS defense
- Private comments visible only to admin and the author; pending
  comments visible only to admin and the author
- Admin moderation list (pending/approved/rejected/all tabs) with
  approve/reject/delete and a pending-count badge
- Comment settings page with the four toggles
- One-time session flash notice (auto-dismissed after 4s) so the
  "comment posted" banner no longer persists across refreshes

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-22 17:21:54 +08:00
co-authored by Claude Fable 5
parent 1f1123acf8
commit 6139b3ef2c
18 changed files with 1351 additions and 18 deletions
+142
View File
@@ -198,6 +198,77 @@ var translations = map[Lang]map[string]string{
"cat_archive": "Archive",
"cat_video": "Video",
"cat_other": "Other",
// Comments
"comments_title": "Comments",
"comments_count": "%d Comments",
"comments_empty": "No comments yet. Be the first to comment.",
"comments_reply": "Reply",
"comments_cancel_reply": "Cancel reply",
"comments_replying_to": "Replying to %s",
"comments_name": "Name",
"comments_email": "Email",
"comments_email_hint": "Used for your Gravatar avatar; never shown publicly.",
"comments_website": "Website (optional)",
"comments_content": "Comment",
"comments_content_ph": "Markdown is supported...",
"comments_private": "Private comment",
"comments_private_hint": "Only you and the administrator can see this.",
"comments_preview": "Preview",
"comments_edit": "Edit",
"comments_emoji": "Emoji",
"comments_markdown_help": "Markdown help",
"comments_submit": "Post Comment",
"comments_posted": "Comment posted.",
"comments_pending_notice": "Your comment is awaiting moderation.",
"comments_disabled": "Comments are disabled.",
"comments_guests_disabled": "You must sign in to comment.",
"comments_required_name": "Name is required.",
"comments_required_email": "Email is required.",
"comments_required_content":"Comment is required.",
"comments_invalid_email": "Please enter a valid email address.",
"comments_invalid_url": "Please enter a valid URL.",
"comments_too_long": "Comment is too long (max %d characters).",
"comments_pending": "Pending",
"comments_approved": "Approved",
"comments_rejected": "Rejected",
"comments_private_badge": "Private",
"comments_markdown_bold": "**bold**",
"comments_markdown_italic": "*italic*",
"comments_markdown_code": "`code`",
"comments_markdown_link": "[text](url)",
"comments_markdown_quote": "> quote",
// Admin: comments
"admin_comments": "Comments",
"admin_comments_title": "Comments",
"comment_status_pending": "Pending",
"comment_status_approved": "Approved",
"comment_status_rejected": "Rejected",
"comment_status_all": "All",
"comment_approve": "Approve",
"comment_reject": "Reject",
"comment_delete": "Delete",
"comment_delete_confirm": "Delete this comment?",
"comment_col_author": "Author",
"comment_col_article": "Article",
"comment_col_content": "Content",
"comment_col_status": "Status",
"comment_col_time": "Time",
"comment_col_actions": "Actions",
"comment_empty": "No comments.",
"comment_approved": "Comment approved.",
"comment_rejected": "Comment rejected.",
"comment_deleted": "Comment deleted.",
// Admin: comment settings
"comment_settings_title": "Comment Settings",
"comment_settings_desc": "Control whether comments are enabled and how they are moderated.",
"comment_settings_enabled": "Enable comments",
"comment_settings_allow_guest": "Allow anonymous comments",
"comment_settings_guest_approval": "Require approval for guest comments",
"comment_settings_use_gravatar": "Use Gravatar avatars",
"comment_settings_use_gravatar_hint": "When off, avatars show the author's initial on a colored background.",
},
ZH: {
// 导航
@@ -381,6 +452,77 @@ var translations = map[Lang]map[string]string{
"cat_archive": "压缩包",
"cat_video": "视频",
"cat_other": "其他",
// 评论
"comments_title": "评论",
"comments_count": "%d 条评论",
"comments_empty": "暂无评论,快来抢沙发吧。",
"comments_reply": "回复",
"comments_cancel_reply": "取消回复",
"comments_replying_to": "回复 %s",
"comments_name": "名称",
"comments_email": "邮箱",
"comments_email_hint": "用于显示 Gravatar 头像,不会公开。",
"comments_website": "网址(可选)",
"comments_content": "评论内容",
"comments_content_ph": "支持 Markdown 语法...",
"comments_private": "私密评论",
"comments_private_hint": "仅你和管理员可见。",
"comments_preview": "预览",
"comments_edit": "编辑",
"comments_emoji": "表情",
"comments_markdown_help": "Markdown 帮助",
"comments_submit": "发表评论",
"comments_posted": "评论已发表。",
"comments_pending_notice": "你的评论正在等待审核。",
"comments_disabled": "评论功能已关闭。",
"comments_guests_disabled": "请先登录后再评论。",
"comments_required_name": "请填写名称。",
"comments_required_email": "请填写邮箱。",
"comments_required_content":"请填写评论内容。",
"comments_invalid_email": "请输入有效的邮箱地址。",
"comments_invalid_url": "请输入有效的网址。",
"comments_too_long": "评论内容过长(最多 %d 字)。",
"comments_pending": "待审核",
"comments_approved": "已通过",
"comments_rejected": "已拒绝",
"comments_private_badge": "私密",
"comments_markdown_bold": "**粗体**",
"comments_markdown_italic": "*斜体*",
"comments_markdown_code": "`代码`",
"comments_markdown_link": "[文本](网址)",
"comments_markdown_quote": "> 引用",
// 后台:评论
"admin_comments": "评论管理",
"admin_comments_title": "评论管理",
"comment_status_pending": "待审核",
"comment_status_approved": "已通过",
"comment_status_rejected": "已拒绝",
"comment_status_all": "全部",
"comment_approve": "通过",
"comment_reject": "拒绝",
"comment_delete": "删除",
"comment_delete_confirm": "删除该评论?",
"comment_col_author": "评论者",
"comment_col_article": "文章",
"comment_col_content": "内容",
"comment_col_status": "状态",
"comment_col_time": "时间",
"comment_col_actions": "操作",
"comment_empty": "暂无评论。",
"comment_approved": "评论已通过。",
"comment_rejected": "评论已拒绝。",
"comment_deleted": "评论已删除。",
// 后台:评论设置
"comment_settings_title": "评论设置",
"comment_settings_desc": "控制是否启用评论以及评论的审核方式。",
"comment_settings_enabled": "启用评论",
"comment_settings_allow_guest": "允许匿名评论",
"comment_settings_guest_approval": "非登录用户评论需审核",
"comment_settings_use_gravatar": "使用 Gravatar 头像",
"comment_settings_use_gravatar_hint": "关闭后,头像将显示作者名称首字母的彩色占位。",
},
}