优化帖子详情与列表展示,并新增问答帖类型与已解决状态。

统一标题锚点定位、编辑底栏固定与操作栏分层,列表徽章前置并区分置顶/问答配色。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-04 00:19:32 +08:00
parent 3aca5c42c5
commit b24f6c23ea
16 changed files with 696 additions and 239 deletions

View File

@@ -8,7 +8,7 @@ interface Props {
boards: Board[];
stats: ForumStats | null;
postTotal: number;
/** 首页「全部帖子」用 h2板块/搜索页用 h1 */
/** 搜索页用 h1;首页/板块页中间栏不再展示标题 */
titleAs?: 'h1' | 'h2';
}
@@ -16,18 +16,15 @@ export default function FeedHeader({ boardId, keyword, boards, stats, postTotal,
const nav = useNavigate();
const board = boards.find(b => b.id === boardId);
const title = keyword
? `搜索:${keyword}`
: (boardId && board ? board.name : '全部帖子');
const boardHint = boardId && board ? (board.description || '') : '';
const TitleTag = titleAs;
const inBoard = !keyword && boardId > 0 && !!board;
/** 侧栏已有「全部帖子 / 板块名」,中间栏不再重复;仅搜索保留标题 */
const title = keyword ? `搜索:${keyword}` : '';
const TitleTag = titleAs;
return (
<div className={`feed-head${keyword ? ' feed-head--solo' : ''}`}>
<div className={`feed-head${keyword ? ' feed-head--solo' : ' feed-head--stats-only'}`}>
<div className="feed-head__title">
<TitleTag title={boardHint || undefined}>{title}</TitleTag>
{title ? <TitleTag>{title}</TitleTag> : null}
{!keyword && inBoard && (
<div className="feed-head__stats">
<span className="feed-stat-chip">

View File

@@ -8,9 +8,8 @@ export default function FeedPageSkeleton() {
<div className="feed-panel">
<div className="feed-top">
<div className="feed-top__bar">
<div className="feed-head">
<div className="feed-head feed-head--stats-only">
<div className="feed-head__title">
<Skeleton className="skeleton--feed-title" />
<div className="feed-head__stats">
<Skeleton className="skeleton--stat-chip" />
<Skeleton className="skeleton--stat-chip" />

View File

@@ -66,6 +66,20 @@ export default function PostContent({
openLightbox(zoomImg);
return;
}
const headingCopy = target.closest<HTMLElement>('[data-heading-copy]');
if (headingCopy) {
e.preventDefault();
const id = headingCopy.getAttribute('data-heading-copy') || '';
if (!id) return;
const url = `${window.location.origin}${window.location.pathname}${window.location.search}#${id}`;
try {
await navigator.clipboard.writeText(url);
notify.success('已复制本节链接');
} catch {
notify.error('复制失败');
}
return;
}
const copyBtn = target.closest<HTMLElement>('[data-code-copy]');
if (copyBtn) {
e.preventDefault();

View File

@@ -75,33 +75,38 @@ function PostListItem({ post, sort = 'latest', onSelect }: Props) {
<span className="post-head-dot" aria-hidden>·</span>
<span className="post-time">{timeLabel}</span>
</div>
{(post.featured || post.pinned || post.status === 'pending' || post.status === 'rejected') && (
<div className="post-head-badges">
{post.status === 'pending' && (
<span className="post-status-badge post-status-badge--pending" title="审核中"></span>
)}
{post.status === 'rejected' && (
<span className="post-status-badge post-status-badge--rejected" title="未通过"></span>
)}
{post.featured && (
<span className="post-feature-badge" title="精华">
<FeaturedIcon size={12} />
</span>
)}
{post.pinned && (
<span className="post-pin-badge" title="置顶">
<PinnedIcon size={12} />
</span>
)}
</div>
)}
</div>
<a href={href} className="post-title" onClick={onTitleClick}>
{post.title}
</a>
<div className="post-title-row">
{post.pinned && (
<span className="post-pin-badge post-pin-badge--icon" title="置顶">
<PinnedIcon size={13} />
</span>
)}
{post.featured && (
<span className="post-feature-badge" title="精华">
<FeaturedIcon size={12} />
</span>
)}
{post.status === 'pending' && (
<span className="post-status-badge post-status-badge--pending" title="审核中"></span>
)}
{post.status === 'rejected' && (
<span className="post-status-badge post-status-badge--rejected" title="未通过"></span>
)}
{post.post_type === 'question' && (
<span
className={`post-qa-badge${post.question_resolved ? ' post-qa-badge--resolved' : ' post-qa-badge--open'}`}
title={post.question_resolved ? '已解决' : '未解决'}
>
{post.question_resolved ? '已解决' : '未解决'}
</span>
)}
<a href={href} className="post-title" onClick={onTitleClick}>
{post.title}
</a>
</div>
{excerpt && <p className="post-excerpt">{excerpt}</p>}