增加板块内置顶,与全局置顶分离:首页仅抬升全局置顶,板块列表优先全局再板块。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-07 05:14:00 +08:00
parent bb8d415eb7
commit 0f83183620
11 changed files with 103 additions and 13 deletions

View File

@@ -81,6 +81,10 @@ export const api = {
request<{ message: string; pinned: boolean }>(`/api/admin/posts/${id}/pin`, {
method: 'POST', body: JSON.stringify({ pinned }),
}),
adminBoardPinPost: (id: number, boardPinned: boolean) =>
request<{ message: string; board_pinned: boolean }>(`/api/admin/posts/${id}/board-pin`, {
method: 'POST', body: JSON.stringify({ board_pinned: boardPinned }),
}),
adminFeaturePost: (id: number, featured: boolean) =>
request<{ message: string; featured: boolean }>(`/api/admin/posts/${id}/feature`, {
method: 'POST', body: JSON.stringify({ featured }),

View File

@@ -89,6 +89,8 @@ export interface PostItem {
/** 仅问答帖有意义 */
question_resolved?: boolean;
pinned: boolean;
/** 板块内置顶(仅板块列表抬升,首页不抬升) */
board_pinned?: boolean;
featured?: boolean;
edit_locked?: boolean;
status?: 'pending' | 'published' | 'rejected' | string;

View File

@@ -78,7 +78,10 @@ function PostListItem({ post, sort = 'latest', onSelect }: Props) {
<div className="post-title-row">
{post.pinned && (
<span className="post-pin-badge" title="置顶"></span>
<span className="post-pin-badge" title="全局置顶"></span>
)}
{post.board_pinned && (
<span className="post-pin-badge post-pin-badge--board" title="板块置顶"></span>
)}
{post.featured && (
<span className="post-feature-badge" title="精华">

View File

@@ -475,6 +475,19 @@ export default function PostDetailPage() {
}
};
const handleBoardPin = async () => {
if (!post) return;
try {
const r = await api.adminBoardPinPost(postId, !post.board_pinned);
setPost(p => p ? { ...p, board_pinned: r.board_pinned } : p);
clearAllFeedCache();
window.dispatchEvent(new Event('posts-refresh'));
notify.success(r.message);
} catch (e: unknown) {
notify.error(e instanceof Error ? e.message : '操作失败');
}
};
const handleFeature = async () => {
if (!post) return;
try {
@@ -601,7 +614,10 @@ export default function PostDetailPage() {
<div className="post-detail-head">
<h1 className="post-detail-title">
{post.pinned && (
<span className="post-pin-badge post-pin-badge--detail" title="置顶"></span>
<span className="post-pin-badge post-pin-badge--detail" title="全局置顶"></span>
)}
{post.board_pinned && (
<span className="post-pin-badge post-pin-badge--board post-pin-badge--detail" title="板块置顶"></span>
)}
{post.featured && <FeaturedIcon className="mr-2" size={18} />}
{post.status === 'pending' && <Badge variant="orange" className="mr-2 align-middle"></Badge>}
@@ -792,7 +808,11 @@ export default function PostDetailPage() {
</Button>
<Button variant="outline" size="sm" onClick={handlePin}>
<Pin />
{post.pinned ? '取消置顶' : '置顶'}
{post.pinned ? '取消全局置顶' : '全局置顶'}
</Button>
<Button variant="outline" size="sm" onClick={handleBoardPin}>
<Pin />
{post.board_pinned ? '取消板块置顶' : '板块置顶'}
</Button>
<Button variant="outline" size="sm" onClick={handleLock}>
<Lock />

View File

@@ -153,8 +153,9 @@ export default function AdminDashboardPage() {
</td>
<td className="space-x-1">
{p.featured ? <Badge variant="orange"></Badge> : null}
{p.pinned ? <Badge variant="green"></Badge> : null}
{!p.featured && !p.pinned ? '—' : null}
{p.pinned ? <Badge variant="green"></Badge> : null}
{p.board_pinned ? <Badge variant="green"></Badge> : null}
{!p.featured && !p.pinned && !p.board_pinned ? '—' : null}
</td>
<td>{new Date(p.created_at).toLocaleString('zh-CN')}</td>
</tr>

View File

@@ -111,6 +111,18 @@ export default function AdminPostsPage() {
}
};
const toggleBoardPin = async (post: PostItem) => {
try {
const r = await api.adminBoardPinPost(post.id, !post.board_pinned);
clearAllFeedCache();
window.dispatchEvent(new Event('posts-refresh'));
notify.success(r.message);
load(page);
} catch (e: unknown) {
notify.error(e instanceof Error ? e.message : '操作失败');
}
};
const toggleFeature = async (post: PostItem) => {
try {
const r = await api.adminFeaturePost(post.id, !post.featured);
@@ -196,7 +208,7 @@ export default function AdminPostsPage() {
? '回收站中的帖子可恢复或永久删除;永久删除后不可撤销'
: tab === 'pending'
? '审核普通用户提交的帖子;通过后公开,拒绝后仅作者可见并私信通知'
: '精华、置顶、锁定编辑、删除(移入回收站);支持按标题、标签或正文搜索'}
: '精华、全局置顶、板块置顶、锁定编辑、删除(移入回收站);支持按标题、标签或正文搜索'}
</p>
</div>
@@ -316,7 +328,8 @@ export default function AdminPostsPage() {
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
@@ -348,6 +361,7 @@ export default function AdminPostsPage() {
<td>{p.comment_count ?? 0}</td>
<td>{p.featured ? <Badge variant="orange"></Badge> : '—'}</td>
<td>{p.pinned ? <Badge variant="green"></Badge> : '—'}</td>
<td>{p.board_pinned ? <Badge variant="green"></Badge> : '—'}</td>
<td>{p.edit_locked ? <Badge variant="destructive"></Badge> : '—'}</td>
<td>{p.like_count}</td>
<td>{p.view_count}</td>
@@ -373,7 +387,10 @@ export default function AdminPostsPage() {
{p.featured ? '取消精华' : '精华'}
</Button>
<Button size="sm" variant="outline" onClick={() => togglePin(p)}>
{p.pinned ? '取消置顶' : '置顶'}
{p.pinned ? '取消全局置顶' : '全局置顶'}
</Button>
<Button size="sm" variant="outline" onClick={() => toggleBoardPin(p)}>
{p.board_pinned ? '取消板块置顶' : '板块置顶'}
</Button>
<Button size="sm" variant="outline" onClick={() => toggleLock(p)}>
{p.edit_locked ? <><LockOpen size={14} /> </> : <><Lock size={14} /> </>}

View File

@@ -2321,6 +2321,13 @@ body:has(.admin-topbar) .ptr-indicator {
border: 1px solid rgba(220, 38, 38, 0.22);
}
/* 板块置顶:与全局置顶同结构,用偏暖橙区分 */
.post-pin-badge--board {
color: #c2410c;
background: rgba(234, 88, 12, 0.1);
border-color: rgba(234, 88, 12, 0.22);
}
.post-pin-badge--detail {
margin-right: 8px;
height: 22px;
@@ -2335,6 +2342,12 @@ body:has(.admin-topbar) .ptr-indicator {
border-color: rgba(248, 113, 113, 0.28);
}
.dark .post-pin-badge--board {
color: #fb923c;
background: rgba(251, 146, 60, 0.14);
border-color: rgba(251, 146, 60, 0.28);
}
.post-status-badge {
display: inline-flex;
align-items: center;