优化评论区展示:弱化用户名样式、调整回复布局与编辑时限。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-06 00:33:06 +08:00
parent e8b28a37e4
commit 322ac14055
7 changed files with 107 additions and 41 deletions

View File

@@ -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;

View File

@@ -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>
) : (
<button type="button" className="waline-comment-reply-btn" onClick={() => onReply(c)}>
<MessageSquare size={14} />
</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 && (

View File

@@ -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,

View File

@@ -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 },
],
},
{

View File

@@ -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 {