支持板块图标与主题色自定义,并优化列表加载与未保存离开提示。

新增后端图标校验与色槽规范化,前端展示 BoardBadge/骨架屏,发帖与板块管理页增加未保存确认。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-28 17:13:32 +08:00
parent e038d4f9e9
commit 55c256654b
63 changed files with 1966 additions and 714 deletions

View File

@@ -1,8 +1,8 @@
import { useRef, useEffect, useLayoutEffect } from 'react';
import { useVirtualizer } from '@tanstack/react-virtual';
import { Spinner } from '@/components/ui/spinner';
import { Button } from '@/components/ui/button';
import PostListItem from './PostListItem';
import PostListSkeleton from './PostListSkeleton';
import type { PostItem } from '../api/types';
import type { FeedSort } from './FeedSortBar';
@@ -50,6 +50,8 @@ export default function VirtualPostList({
const showHistoryPrompt = hasMore && !canAutoLoad && !loading;
const showEnd = !hasMore && posts.length > 0 && !loading;
const isInitialLoad = loading && posts.length === 0;
const isLoadingMore = loading && posts.length > 0;
useLayoutEffect(() => {
if (resetScrollKey <= 0) return;
@@ -88,42 +90,44 @@ export default function VirtualPostList({
return (
<div className="post-list-scroll" ref={parentRef}>
<div className="content-surface" style={{ height: virtualizer.getTotalSize(), position: 'relative' }}>
{virtualizer.getVirtualItems().map(vi => {
const post = posts[vi.index];
return (
<div
key={post.id}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
transform: `translateY(${vi.start}px)`,
}}
>
<PostListItem post={post} sort={sort} onClick={() => onSelect(post.id)} />
{isInitialLoad ? (
<PostListSkeleton />
) : (
<>
<div className="content-surface" style={{ height: virtualizer.getTotalSize(), position: 'relative' }}>
{virtualizer.getVirtualItems().map(vi => {
const post = posts[vi.index];
return (
<div
key={post.id}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
transform: `translateY(${vi.start}px)`,
}}
>
<PostListItem post={post} sort={sort} onClick={() => onSelect(post.id)} />
</div>
);
})}
</div>
{isLoadingMore && <PostListSkeleton count={2} />}
{showHistoryPrompt && (
<div className="feed-list-footer feed-list-footer--history">
<p className="feed-list-footer__hint">
{posts.length} / {postTotal}
</p>
<Button type="button" variant="outline" size="sm" onClick={onLoadMore}>
</Button>
</div>
);
})}
</div>
{loading && (
<div className="feed-list-footer">
<Spinner />
</div>
)}
{showHistoryPrompt && (
<div className="feed-list-footer feed-list-footer--history">
<p className="feed-list-footer__hint">
{posts.length} / {postTotal}
</p>
<Button type="button" variant="outline" size="sm" onClick={onLoadMore}>
</Button>
</div>
)}
{showEnd && (
<div className="feed-list-footer feed-list-footer--end"> </div>
)}
{showEnd && (
<div className="feed-list-footer feed-list-footer--end"> </div>
)}
</>
)}
</div>
);