feat: 帖子全局/板块置顶支持权重排序,优化列表与详情渲染
This commit is contained in:
@@ -154,12 +154,12 @@ export const api = {
|
||||
},
|
||||
adminApprovePost: (id: number) =>
|
||||
request<{ message: string; status: string }>(`/api/admin/posts/${id}/approve`, { method: 'POST' }),
|
||||
adminPinPost: (id: number, pinned: boolean) =>
|
||||
request<{ message: string; pinned: boolean }>(`/api/admin/posts/${id}/pin`, {
|
||||
adminPinPost: (id: number, pinned: number) =>
|
||||
request<{ message: string; pinned: number }>(`/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`, {
|
||||
adminBoardPinPost: (id: number, boardPinned: number) =>
|
||||
request<{ message: string; board_pinned: number }>(`/api/admin/posts/${id}/board-pin`, {
|
||||
method: 'POST', body: JSON.stringify({ board_pinned: boardPinned }),
|
||||
}),
|
||||
adminFeaturePost: (id: number, featured: boolean) =>
|
||||
|
||||
@@ -94,9 +94,9 @@ export interface PostItem {
|
||||
bounty_comment_id?: number;
|
||||
lottery_winner_count?: number;
|
||||
lottery_status?: 'open' | 'drawn' | string;
|
||||
pinned: boolean;
|
||||
pinned: number;
|
||||
/** 板块内置顶(仅板块列表抬升,首页不抬升) */
|
||||
board_pinned?: boolean;
|
||||
board_pinned?: number;
|
||||
featured?: boolean;
|
||||
edit_locked?: boolean;
|
||||
/** 禁止新评论(结贴) */
|
||||
|
||||
@@ -71,10 +71,10 @@ function PostListItem({ post, sort = 'latest', boardId = 0, onSelect }: Props) {
|
||||
|
||||
const titleRow = (
|
||||
<div className="post-title-row">
|
||||
{post.pinned && (
|
||||
{post.pinned > 0 && (
|
||||
<span className="post-pin-badge" title="全局置顶">全局置顶</span>
|
||||
)}
|
||||
{post.board_pinned && (
|
||||
{(post.board_pinned ?? 0) > 0 && (
|
||||
<span className="post-pin-badge post-pin-badge--board" title="板块置顶">板块置顶</span>
|
||||
)}
|
||||
{post.featured && (
|
||||
|
||||
@@ -151,11 +151,11 @@ export default function PostManageMenu({
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onSelect={onPin}>
|
||||
<Pin size={14} aria-hidden />
|
||||
{post.pinned ? '取消全局置顶' : '全局置顶'}
|
||||
{post.pinned > 0 ? '取消全局置顶' : '全局置顶'}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onSelect={onBoardPin}>
|
||||
<Pin size={14} aria-hidden />
|
||||
{post.board_pinned ? '取消板块置顶' : '板块置顶'}
|
||||
{(post.board_pinned ?? 0) > 0 ? '取消板块置顶' : '板块置顶'}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
|
||||
@@ -98,10 +98,10 @@ export default function StaticFeedList({ posts, sort, boardId }: Props) {
|
||||
|
||||
const titleRow = (
|
||||
<div className="post-title-row">
|
||||
{post.pinned ? (
|
||||
{post.pinned > 0 ? (
|
||||
<span className="post-pin-badge" title="全局置顶">全局置顶</span>
|
||||
) : null}
|
||||
{post.board_pinned ? (
|
||||
{(post.board_pinned ?? 0) > 0 ? (
|
||||
<span className="post-pin-badge post-pin-badge--board" title="板块置顶">板块置顶</span>
|
||||
) : null}
|
||||
{post.featured ? <span className="post-feature-badge" title="推荐">推荐</span> : null}
|
||||
|
||||
@@ -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} />}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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} /> 锁定编辑</>}
|
||||
|
||||
@@ -5905,15 +5905,6 @@ a.post-title:visited {
|
||||
background: color-mix(in srgb, var(--j13-green) 6%, var(--j13-bg-block-muted));
|
||||
color: var(--color-text-2);
|
||||
border-radius: 0 10px 10px 0;
|
||||
transition:
|
||||
border-left-width 0.28s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
background 0.28s ease,
|
||||
padding-left 0.28s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
.post-detail-content blockquote:hover {
|
||||
border-left-width: 5px;
|
||||
padding-left: calc(1em - 2px);
|
||||
background: color-mix(in srgb, var(--j13-green) 11%, var(--j13-bg-block-muted));
|
||||
}
|
||||
.post-detail-content blockquote p:last-child { margin-bottom: 0; }
|
||||
|
||||
@@ -6194,7 +6185,7 @@ a.post-title:visited {
|
||||
|
||||
/* highlight.js token(亮色) */
|
||||
.post-detail-content .hljs-comment,
|
||||
.post-detail-content .hljs-quote { color: #6e7781; font-style: italic; }
|
||||
.post-detail-content .hljs-quote { color: #6e7781; }
|
||||
.post-detail-content .hljs-keyword,
|
||||
.post-detail-content .hljs-selector-tag,
|
||||
.post-detail-content .hljs-addition { color: #cf222e; }
|
||||
|
||||
@@ -101,28 +101,22 @@ export function enhanceCodeBlocks(root: ParentNode): void {
|
||||
const raw = code.textContent || '';
|
||||
const display = readDisplayOptions(pre);
|
||||
|
||||
// 作者写了语言标签则以标注为准,绝不被自动识别覆盖
|
||||
// 作者写了语言标签则以标注为准,绝不被自动识别覆盖;纯文本不做自动检测
|
||||
const declaredLang = detectLang(code, pre);
|
||||
let label = formatLangLabel(declaredLang || 'code');
|
||||
let label = declaredLang ? formatLangLabel(declaredLang) : '';
|
||||
|
||||
try {
|
||||
if (declaredLang && hljs.getLanguage(declaredLang)) {
|
||||
// 作者标注且 hljs 收录 → 语法高亮
|
||||
const result = hljs.highlight(raw, { language: declaredLang, ignoreIllegals: true });
|
||||
code.innerHTML = wrapCodeLines(result.value, display.lineNumbers);
|
||||
code.classList.add('hljs', `language-${declaredLang}`);
|
||||
} else if (declaredLang) {
|
||||
// 未收录语言:保留原文与标签,不做自动猜测
|
||||
// 作者标注但 hljs 未收录 → 保留原文与标签,不做自动猜测
|
||||
code.innerHTML = wrapCodeLines(escapeHtml(raw), display.lineNumbers);
|
||||
code.classList.add('hljs', `language-${declaredLang}`);
|
||||
} else if (raw.length >= 24) {
|
||||
const result = hljs.highlightAuto(raw);
|
||||
code.innerHTML = wrapCodeLines(result.value, display.lineNumbers);
|
||||
code.classList.add('hljs');
|
||||
if (result.language) {
|
||||
label = formatLangLabel(result.language);
|
||||
code.classList.add(`language-${result.language}`);
|
||||
}
|
||||
} else {
|
||||
// 纯文本(无语言标注)→ 不做自动检测,保持纯文本显示
|
||||
code.innerHTML = wrapCodeLines(escapeHtml(raw), display.lineNumbers);
|
||||
code.classList.add('hljs');
|
||||
}
|
||||
@@ -146,7 +140,7 @@ export function enhanceCodeBlocks(root: ParentNode): void {
|
||||
initiallyCollapsed ? 'md-codeblock--collapsed' : '',
|
||||
initiallyCollapsed && lineCount <= CODE_FOLD_MIN_LINES ? 'md-codeblock--short' : '',
|
||||
].filter(Boolean).join(' ');
|
||||
wrap.setAttribute('data-lang', label);
|
||||
if (label) wrap.setAttribute('data-lang', label);
|
||||
if (display.lineNumbers) wrap.setAttribute('data-line-numbers', 'true');
|
||||
if (display.collapsed) wrap.setAttribute('data-collapsed', 'true');
|
||||
wrap.setAttribute('data-line-count', String(lineCount));
|
||||
@@ -165,8 +159,9 @@ export function enhanceCodeBlocks(root: ParentNode): void {
|
||||
}
|
||||
actions.push('<button type="button" class="md-codeblock__copy" data-code-copy>复制</button>');
|
||||
|
||||
const langHtml = label ? `<span class="md-codeblock__lang">${escapeHtml(label)}</span>` : '';
|
||||
head.innerHTML = `
|
||||
<span class="md-codeblock__lang">${escapeHtml(label)}</span>
|
||||
${langHtml}
|
||||
<span class="md-codeblock__actions">${actions.join('')}</span>
|
||||
`;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user