diff --git a/frontend/src/api/types.ts b/frontend/src/api/types.ts index d26eb04..c076eab 100644 --- a/frontend/src/api/types.ts +++ b/frontend/src/api/types.ts @@ -147,7 +147,7 @@ export interface AdminDashboard { export interface ForumLimits { post_edit_window_hours: number; - comment_edit_window_hours: number; + comment_edit_window_minutes: number; rate_limit_post: number; rate_limit_comment: number; rate_limit_register: number; @@ -176,7 +176,7 @@ export interface ForumLimitsPublic { post_tags_max: number; post_content_max: number; comment_max: number; - comment_edit_window_hours: number; + comment_edit_window_minutes: number; search_keyword_min: number; search_keyword_max: number; page_size_default: number; diff --git a/frontend/src/components/CommentThreadList.tsx b/frontend/src/components/CommentThreadList.tsx index 00d13e7..fb62dd5 100644 --- a/frontend/src/components/CommentThreadList.tsx +++ b/frontend/src/components/CommentThreadList.tsx @@ -25,20 +25,21 @@ import { } from '../utils/comment'; import { isTimeDiffSignificant } from '../utils/content'; import { useForumLimits } from '../hooks/useForumLimits'; +import { Tooltip } from './ui/Tooltip'; import UserLink from './UserLink'; function isCommentAuthor(c: Comment, user?: User | null): boolean { return !!user && c.user_id > 0 && c.user_id === user.id; } -function canEditComment(c: Comment, user: User | null | undefined, windowHours: number): boolean { +function canEditComment(c: Comment, user: User | null | undefined, windowMinutes: number): boolean { if (!user) return false; if (user.role === 'admin') return true; if (!isCommentAuthor(c, user)) return false; - if (windowHours <= 0) return true; + if (windowMinutes <= 0) return true; const created = new Date(c.created_at).getTime(); if (Number.isNaN(created)) return false; - return Date.now() - created <= windowHours * 3600_000; + return Date.now() - created <= windowMinutes * 60_000; } interface ItemProps { @@ -84,7 +85,21 @@ function CommentItem({ const isReplying = replyToId === c.id; const isEditing = editingId === c.id; const isAdmin = currentUser?.role === 'admin'; - const canEdit = canEditComment(c, currentUser, limits.comment_edit_window_hours ?? 24); + const editWindowMinutes = limits.comment_edit_window_minutes ?? 3; + // 到期后强制重渲染,使「编辑」按钮自动消失 + const [, setEditExpireTick] = useState(0); + useEffect(() => { + if (!currentUser || currentUser.role === 'admin') return; + if (!isCommentAuthor(c, currentUser)) return; + if (editWindowMinutes <= 0) return; + const created = new Date(c.created_at).getTime(); + if (Number.isNaN(created)) return; + const remaining = created + editWindowMinutes * 60_000 - Date.now(); + if (remaining <= 0) return; + const timer = window.setTimeout(() => setEditExpireTick(n => n + 1), remaining + 30); + return () => window.clearTimeout(timer); + }, [c.created_at, c.id, c.user_id, currentUser, editWindowMinutes]); + const canEdit = canEditComment(c, currentUser, editWindowMinutes); const canDelete = isAdmin; const canApprove = isAdmin && (c.status === 'pending' || c.status === 'rejected') @@ -153,6 +168,11 @@ function CommentItem({ ) : ( {nick} )} + {!c.reply_to && ( + + #{c.floor} + + )} {hidden ? ( @@ -184,9 +204,6 @@ function CommentItem({ ) : (
- {c.reply_target && ( - @{commentNick(c.reply_target)} - )}
)} @@ -195,10 +212,13 @@ function CommentItem({ {formatCommentDate(c.created_at)} - {showEdited && · 已编辑} + {isAdmin && showEdited && · 已编辑} {c.status === 'pending' && · 审核中} {c.status === 'rejected' && · 未通过} + {c.reply_target && ( + @{commentNick(c.reply_target)} + )} {!hidden && !isEditing && canApprove && ( + + + ) )} {!hidden && !isEditing && canEdit && ( diff --git a/frontend/src/hooks/useForumLimits.ts b/frontend/src/hooks/useForumLimits.ts index cb27b31..f08e62a 100644 --- a/frontend/src/hooks/useForumLimits.ts +++ b/frontend/src/hooks/useForumLimits.ts @@ -7,7 +7,7 @@ const DEFAULT_LIMITS: ForumLimitsPublic = { post_tags_max: 256, post_content_max: 50000, comment_max: 5000, - comment_edit_window_hours: 24, + comment_edit_window_minutes: 3, search_keyword_min: 1, search_keyword_max: 50, page_size_default: 30, diff --git a/frontend/src/pages/admin/AdminSettingsPage.tsx b/frontend/src/pages/admin/AdminSettingsPage.tsx index 1da2604..54d042e 100644 --- a/frontend/src/pages/admin/AdminSettingsPage.tsx +++ b/frontend/src/pages/admin/AdminSettingsPage.tsx @@ -40,7 +40,7 @@ const SETTING_SECTIONS: SettingSection[] = [ summary: '控制普通用户修改自己帖子 / 评论的时限(0 = 不限)', rows: [ { key: 'post_edit_window_hours', label: '帖子可编辑时限', unit: '小时', hint: '0 = 不限', min: 0 }, - { key: 'comment_edit_window_hours', label: '评论可编辑时限', unit: '小时', hint: '0 = 不限', min: 0 }, + { key: 'comment_edit_window_minutes', label: '评论可编辑时限', unit: '分钟', hint: '0 = 不限', min: 0 }, ], }, { diff --git a/frontend/src/styles/global.css b/frontend/src/styles/global.css index 848ca55..b7f90fe 100644 --- a/frontend/src/styles/global.css +++ b/frontend/src/styles/global.css @@ -3160,7 +3160,7 @@ a.post-title:visited { .post-detail-author-name { font-size: 14px; - font-weight: 600; + font-weight: 400; color: var(--color-text-1); } @@ -5230,7 +5230,12 @@ a.post-title:visited { border-bottom: 1px solid var(--j13-border-light); } +/* 回复:头像与昵称同行,正文从头像下方起铺满,避免逐级挤窄 */ .waline-comment.nested { + display: grid; + grid-template-columns: 32px 1fr; + gap: 6px 10px; + align-items: start; padding: 12px 0 0; border-bottom: none; } @@ -5249,11 +5254,40 @@ a.post-title:visited { } .waline-comment.nested .waline-comment-avatar { + grid-column: 1; + grid-row: 1; width: 32px; height: 32px; font-size: 13px; } +/* 让子元素直接参与 grid,正文/回复可跨列铺满 */ +.waline-comment.nested .waline-comment-main { + display: contents; +} + +.waline-comment.nested .waline-comment-head { + grid-column: 2; + grid-row: 1; + margin-bottom: 0; + align-self: center; + min-width: 0; +} + +.waline-comment.nested .waline-comment-bubble, +.waline-comment.nested .waline-comment-private-mask, +.waline-comment.nested .waline-comment-edit, +.waline-comment.nested .waline-comment-meta, +.waline-comment.nested .comment-box-wrap.inline, +.waline-comment.nested .waline-replies { + grid-column: 1 / -1; +} + +/* 更深层级与第一层回复左对齐,不再继续右移 */ +.waline-comment.nested .waline-replies { + margin-top: 4px; +} + .waline-comment-avatar { flex-shrink: 0; width: 40px; @@ -5291,12 +5325,23 @@ a.post-title:visited { .waline-comment-author { font-size: 14px; - font-weight: 600; + font-weight: 400; color: var(--color-text-1); text-decoration: none; } -a.waline-comment-author:hover { color: var(--j13-green); } +a.waline-comment-author:hover { + color: var(--j13-green); + text-decoration: none; +} + +.waline-comment-floor { + margin-left: auto; + font-size: 12px; + font-weight: 500; + color: var(--color-text-3); + flex-shrink: 0; +} .waline-comment-bubble { padding: 10px 14px; @@ -5309,10 +5354,8 @@ a.waline-comment-author:hover { color: var(--j13-green); } } .waline-reply-at { - display: block; - color: #27c; - font-weight: 600; - margin-bottom: 4px; + color: inherit; + font-weight: 400; line-height: 1.5; } @@ -5429,7 +5472,7 @@ a.waline-comment-author:hover { color: var(--j13-green); } .waline-comment { padding: 12px 14px; } } -.mention { color: var(--j13-green); font-weight: 600; } +.mention { color: var(--j13-green); font-weight: 400; } .quote-block { border-left: 3px solid var(--j13-green); @@ -5928,6 +5971,7 @@ a.waline-comment-author:hover { color: var(--j13-green); } .user-link:hover, .user-link:focus-visible { color: var(--j13-green); + text-decoration: none; } .user-link--static { @@ -5950,11 +5994,11 @@ a.user-link--avatar-only:focus-visible { .post-detail-author-name.user-link { font-size: inherit; - font-weight: 600; + font-weight: 400; } .waline-comment-author.user-link { - font-weight: 600; + font-weight: 400; } .widget-item--comment { diff --git a/service/comment.go b/service/comment.go index 01e48cc..1df4489 100644 --- a/service/comment.go +++ b/service/comment.go @@ -282,8 +282,8 @@ func (s *CommentService) Update(userID, commentID uint, isAdmin bool, content st return "", ErrPermissionDenied } if !isAdmin { - window := s.settings.CommentEditWindowHours() - if window > 0 && time.Since(comment.CreatedAt) > time.Duration(window)*time.Hour { + window := s.settings.CommentEditWindowMinutes() + if window > 0 && time.Since(comment.CreatedAt) > time.Duration(window)*time.Minute { return "", errors.New("已超过可编辑时限") } } diff --git a/service/settings.go b/service/settings.go index f38a88b..90a3b34 100644 --- a/service/settings.go +++ b/service/settings.go @@ -13,8 +13,8 @@ import ( // 论坛设置键名 const ( - SettingPostEditWindowHours = "post_edit_window_hours" - SettingCommentEditWindowHours = "comment_edit_window_hours" + SettingPostEditWindowHours = "post_edit_window_hours" + SettingCommentEditWindowMinutes = "comment_edit_window_minutes" SettingRateLimitPost = "rate_limit_post" SettingRateLimitComment = "rate_limit_comment" @@ -91,8 +91,8 @@ const ( // ForumLimits 论坛可配置限制(API 传输结构) type ForumLimits struct { - PostEditWindowHours int `json:"post_edit_window_hours"` - CommentEditWindowHours int `json:"comment_edit_window_hours"` + PostEditWindowHours int `json:"post_edit_window_hours"` + CommentEditWindowMinutes int `json:"comment_edit_window_minutes"` RateLimitPost int `json:"rate_limit_post"` RateLimitComment int `json:"rate_limit_comment"` @@ -135,7 +135,7 @@ type ForumLimitsPublic struct { AvatarMaxMB int `json:"avatar_max_mb"` SignatureMax int `json:"signature_max"` - CommentEditWindowHours int `json:"comment_edit_window_hours"` + CommentEditWindowMinutes int `json:"comment_edit_window_minutes"` OpenPostsInNewTab bool `json:"open_posts_in_new_tab"` OpenContentLinksInNewTab bool `json:"open_content_links_in_new_tab"` @@ -153,7 +153,7 @@ type settingDef struct { var forumSettingDefs = []settingDef{ {SettingPostEditWindowHours, "24", 0, 0}, - {SettingCommentEditWindowHours, "24", 0, 0}, + {SettingCommentEditWindowMinutes, "3", 0, 0}, {SettingRateLimitPost, "10", 1, 1000}, {SettingRateLimitComment, "10", 1, 1000}, @@ -427,8 +427,8 @@ func (s *ForumSettingsService) setInt(key string, value int) error { func (s *ForumSettingsService) Limits() ForumLimits { permalink := s.Permalink() return ForumLimits{ - PostEditWindowHours: s.PostEditWindowHours(), - CommentEditWindowHours: s.CommentEditWindowHours(), + PostEditWindowHours: s.PostEditWindowHours(), + CommentEditWindowMinutes: s.CommentEditWindowMinutes(), RateLimitPost: s.RateLimitFor("post"), RateLimitComment: s.RateLimitFor("comment"), @@ -473,7 +473,7 @@ func (s *ForumSettingsService) PublicLimits() ForumLimitsPublic { AvatarMaxMB: limits.AvatarMaxMB, SignatureMax: limits.SignatureMax, - CommentEditWindowHours: limits.CommentEditWindowHours, + CommentEditWindowMinutes: limits.CommentEditWindowMinutes, OpenPostsInNewTab: limits.OpenPostsInNewTab, OpenContentLinksInNewTab: limits.OpenContentLinksInNewTab, @@ -485,8 +485,8 @@ func (s *ForumSettingsService) PublicLimits() ForumLimitsPublic { func (s *ForumSettingsService) UpdateLimits(in ForumLimits) error { updates := map[string]int{ - SettingPostEditWindowHours: in.PostEditWindowHours, - SettingCommentEditWindowHours: in.CommentEditWindowHours, + SettingPostEditWindowHours: in.PostEditWindowHours, + SettingCommentEditWindowMinutes: in.CommentEditWindowMinutes, SettingRateLimitPost: in.RateLimitPost, SettingRateLimitComment: in.RateLimitComment, SettingRateLimitRegister: in.RateLimitRegister, @@ -539,8 +539,8 @@ func (s *ForumSettingsService) PostEditWindowHours() int { return s.getInt(SettingPostEditWindowHours, 24) } -func (s *ForumSettingsService) CommentEditWindowHours() int { - return s.getInt(SettingCommentEditWindowHours, 24) +func (s *ForumSettingsService) CommentEditWindowMinutes() int { + return s.getInt(SettingCommentEditWindowMinutes, 3) } func (s *ForumSettingsService) RateLimitFor(action string) int {