优化评论区展示:弱化用户名样式、调整回复布局与编辑时限。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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({
|
||||
) : (
|
||||
<span className="waline-comment-author">{nick}</span>
|
||||
)}
|
||||
{!c.reply_to && (
|
||||
<span className="waline-comment-floor" aria-label={`第 ${c.floor} 楼`}>
|
||||
#{c.floor}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{hidden ? (
|
||||
@@ -184,9 +204,6 @@ function CommentItem({
|
||||
</div>
|
||||
) : (
|
||||
<div className="waline-comment-bubble">
|
||||
{c.reply_target && (
|
||||
<span className="waline-reply-at">@{commentNick(c.reply_target)}</span>
|
||||
)}
|
||||
<CommentContent content={c.content} />
|
||||
</div>
|
||||
)}
|
||||
@@ -195,10 +212,13 @@ function CommentItem({
|
||||
<span className="waline-comment-date">
|
||||
<Clock size={14} />
|
||||
{formatCommentDate(c.created_at)}
|
||||
{showEdited && <span className="waline-comment-edited"> · 已编辑</span>}
|
||||
{isAdmin && showEdited && <span className="waline-comment-edited"> · 已编辑</span>}
|
||||
{c.status === 'pending' && <span className="waline-comment-status waline-comment-status--pending"> · 审核中</span>}
|
||||
{c.status === 'rejected' && <span className="waline-comment-status waline-comment-status--rejected"> · 未通过</span>}
|
||||
</span>
|
||||
{c.reply_target && (
|
||||
<span className="waline-reply-at">@{commentNick(c.reply_target)}</span>
|
||||
)}
|
||||
{!hidden && !isEditing && canApprove && (
|
||||
<button
|
||||
type="button"
|
||||
@@ -224,10 +244,12 @@ function CommentItem({
|
||||
取消
|
||||
</button>
|
||||
) : (
|
||||
<Tooltip content={`回复给 ${nick}`} side="top">
|
||||
<button type="button" className="waline-comment-reply-btn" onClick={() => onReply(c)}>
|
||||
<MessageSquare size={14} />
|
||||
回复
|
||||
</button>
|
||||
</Tooltip>
|
||||
)
|
||||
)}
|
||||
{!hidden && !isEditing && canEdit && (
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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("已超过可编辑时限")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
// 论坛设置键名
|
||||
const (
|
||||
SettingPostEditWindowHours = "post_edit_window_hours"
|
||||
SettingCommentEditWindowHours = "comment_edit_window_hours"
|
||||
SettingCommentEditWindowMinutes = "comment_edit_window_minutes"
|
||||
|
||||
SettingRateLimitPost = "rate_limit_post"
|
||||
SettingRateLimitComment = "rate_limit_comment"
|
||||
@@ -92,7 +92,7 @@ const (
|
||||
// ForumLimits 论坛可配置限制(API 传输结构)
|
||||
type ForumLimits struct {
|
||||
PostEditWindowHours int `json:"post_edit_window_hours"`
|
||||
CommentEditWindowHours int `json:"comment_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},
|
||||
@@ -428,7 +428,7 @@ func (s *ForumSettingsService) Limits() ForumLimits {
|
||||
permalink := s.Permalink()
|
||||
return ForumLimits{
|
||||
PostEditWindowHours: s.PostEditWindowHours(),
|
||||
CommentEditWindowHours: s.CommentEditWindowHours(),
|
||||
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,
|
||||
@@ -486,7 +486,7 @@ func (s *ForumSettingsService) PublicLimits() ForumLimitsPublic {
|
||||
func (s *ForumSettingsService) UpdateLimits(in ForumLimits) error {
|
||||
updates := map[string]int{
|
||||
SettingPostEditWindowHours: in.PostEditWindowHours,
|
||||
SettingCommentEditWindowHours: in.CommentEditWindowHours,
|
||||
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 {
|
||||
|
||||
Reference in New Issue
Block a user