feat: 帖子全局/板块置顶支持权重排序,优化列表与详情渲染

This commit is contained in:
2026-09-08 23:54:03 +08:00
parent de2f5e8be8
commit 711cd8b759
16 changed files with 362 additions and 64 deletions

View File

@@ -806,7 +806,8 @@ export default function PostDetailPage() {
const handlePin = async () => {
if (!post) return;
try {
const r = await api.adminPinPost(postId, !post.pinned);
const next = post.pinned > 0 ? 0 : 1;
const r = await api.adminPinPost(postId, next);
setPost(p => p ? { ...p, pinned: r.pinned } : p);
clearAllFeedCache();
window.dispatchEvent(new Event('posts-refresh'));
@@ -819,7 +820,8 @@ export default function PostDetailPage() {
const handleBoardPin = async () => {
if (!post) return;
try {
const r = await api.adminBoardPinPost(postId, !post.board_pinned);
const next = (post.board_pinned ?? 0) > 0 ? 0 : 1;
const r = await api.adminBoardPinPost(postId, next);
setPost(p => p ? { ...p, board_pinned: r.board_pinned } : p);
clearAllFeedCache();
window.dispatchEvent(new Event('posts-refresh'));
@@ -995,10 +997,10 @@ export default function PostDetailPage() {
<div className="post-detail-head">
<h1 className="post-detail-title">
{post.pinned && (
{post.pinned > 0 && (
<span className="post-pin-badge post-pin-badge--detail" title="全局置顶">全局置顶</span>
)}
{post.board_pinned && (
{(post.board_pinned ?? 0) > 0 && (
<span className="post-pin-badge post-pin-badge--board post-pin-badge--detail" title="板块置顶">板块置顶</span>
)}
{post.featured && <FeaturedIcon className="mr-2" size={18} />}

View File

@@ -246,9 +246,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.board_pinned ? <Badge variant="green">板块置顶</Badge> : null}
{!p.featured && !p.pinned && !p.board_pinned ? '—' : null}
{p.pinned > 0 ? <Badge variant="green">全局置顶</Badge> : null}
{(p.board_pinned ?? 0) > 0 ? <Badge variant="green">板块置顶</Badge> : null}
{!p.featured && p.pinned <= 0 && (p.board_pinned ?? 0) <= 0 ? '—' : null}
</td>
<td>{new Date(p.created_at).toLocaleString('zh-CN')}</td>
</tr>

View File

@@ -141,9 +141,13 @@ export default function AdminPostsPage() {
setSearch('');
};
const togglePin = async (post: PostItem) => {
const togglePin = async (post: PostItem, nextValue?: number) => {
try {
const r = await api.adminPinPost(post.id, !post.pinned);
let value = nextValue;
if (value === undefined) {
value = post.pinned > 0 ? 0 : 1;
}
const r = await api.adminPinPost(post.id, value);
clearAllFeedCache();
window.dispatchEvent(new Event('posts-refresh'));
notify.success(r.message);
@@ -153,9 +157,13 @@ export default function AdminPostsPage() {
}
};
const toggleBoardPin = async (post: PostItem) => {
const toggleBoardPin = async (post: PostItem, nextValue?: number) => {
try {
const r = await api.adminBoardPinPost(post.id, !post.board_pinned);
let value = nextValue;
if (value === undefined) {
value = (post.board_pinned ?? 0) > 0 ? 0 : 1;
}
const r = await api.adminBoardPinPost(post.id, value);
clearAllFeedCache();
window.dispatchEvent(new Event('posts-refresh'));
notify.success(r.message);
@@ -417,8 +425,8 @@ export default function AdminPostsPage() {
<td className="max-w-[120px] truncate text-muted-foreground">{p.tags || '—'}</td>
<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.pinned > 0 ? <Badge variant="green">置顶 {p.pinned}</Badge> : '—'}</td>
<td>{(p.board_pinned ?? 0) > 0 ? <Badge variant="green">置顶 {p.board_pinned}</Badge> : '—'}</td>
<td>{p.edit_locked ? <Badge variant="destructive">是</Badge> : '—'}</td>
<td>{p.comments_locked ? <Badge variant="destructive">是</Badge> : '—'}</td>
<td>{p.like_count}</td>
@@ -444,11 +452,55 @@ export default function AdminPostsPage() {
<Button size="sm" variant="outline" onClick={() => toggleFeature(p)}>
{p.featured ? '取消推荐' : '推荐'}
</Button>
<Button size="sm" variant="outline" onClick={() => togglePin(p)}>
{p.pinned ? '取消全局置顶' : '全局置顶'}
<Button size="sm" variant="outline" onClick={() => {
const cur = p.pinned;
if (cur > 0) {
// 已置顶:弹出输入框让管理员调整顺序值
const input = window.prompt(`全局置顶顺序(当前: ${cur},输入 0 取消置顶)`, String(cur));
if (input == null) return;
const n = parseInt(input, 10);
if (Number.isNaN(n) || n < 0) {
notify.warning('请输入有效的非负整数');
return;
}
void togglePin(p, n);
} else {
// 未置顶:输入框让管理员设置初始值
const input = window.prompt('设置全局置顶顺序(输入 1 即置顶,0 表示不置顶)', '1');
if (input == null) return;
const n = parseInt(input, 10);
if (Number.isNaN(n) || n < 0) {
notify.warning('请输入有效的非负整数');
return;
}
void togglePin(p, n);
}
}}>
{p.pinned > 0 ? `全局置顶(${p.pinned})` : '全局置顶'}
</Button>
<Button size="sm" variant="outline" onClick={() => toggleBoardPin(p)}>
{p.board_pinned ? '取消板块置顶' : '板块置顶'}
<Button size="sm" variant="outline" onClick={() => {
const cur = p.board_pinned ?? 0;
if (cur > 0) {
const input = window.prompt(`板块置顶顺序(当前: ${cur},输入 0 取消置顶)`, String(cur));
if (input == null) return;
const n = parseInt(input, 10);
if (Number.isNaN(n) || n < 0) {
notify.warning('请输入有效的非负整数');
return;
}
void toggleBoardPin(p, n);
} else {
const input = window.prompt('设置板块置顶顺序(输入 1 即置顶,0 表示不置顶)', '1');
if (input == null) return;
const n = parseInt(input, 10);
if (Number.isNaN(n) || n < 0) {
notify.warning('请输入有效的非负整数');
return;
}
void toggleBoardPin(p, n);
}
}}>
{(p.board_pinned ?? 0) > 0 ? `板块置顶(${p.board_pinned})` : '板块置顶'}
</Button>
<Button size="sm" variant="outline" onClick={() => toggleLock(p)}>
{p.edit_locked ? <><LockOpen size={14} /> 解锁编辑</> : <><Lock size={14} /> 锁定编辑</>}