完善 TipTap 编辑器与个人资料页:头像裁剪、Tab 缩进、文章内链、Markdown 工具与 Tooltip,并更新样式与 Feed 缓存。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
freefire
2026-06-16 05:57:15 +08:00
parent e4d1dd139e
commit 57172eb053
56 changed files with 3128 additions and 531 deletions

View File

@@ -1,5 +1,5 @@
import { useState, useEffect, useCallback, useRef } from 'react';
import { useNavigate, useOutletContext, useSearchParams } from 'react-router-dom';
import { useNavigate, useOutletContext, useSearchParams, useLocation } from 'react-router-dom';
import { notify } from '@/lib/notify';
import { api } from '../api/client';
import type { PostItem } from '../api/types';
@@ -7,10 +7,18 @@ import type { LayoutCtx } from '../layouts/MainLayout';
import VirtualPostList from '../components/VirtualPostList';
import FeedHeader from '../components/FeedHeader';
import FeedSortBar, { parseFeedSort, buildHomeUrl, type FeedSort } from '../components/FeedSortBar';
import { getFeedCache, setFeedCache, clearAllFeedCache } from '../utils/feedCache';
import {
getFeedCache,
setFeedCache,
clearAllFeedCache,
navigateFeed,
FEED_RESET_EVENT,
type FeedNavState,
} from '../utils/feedCache';
export default function HomePage() {
const nav = useNavigate();
const location = useLocation();
const [params] = useSearchParams();
const ctx = useOutletContext<LayoutCtx>();
const boardId = Number(params.get('board')) || ctx?.boardId || 0;
@@ -24,7 +32,24 @@ export default function HomePage() {
const [hasMore, setHasMore] = useState(() => initialCache?.hasMore ?? true);
const [loading, setLoading] = useState(() => !initialCache);
const [restoreScrollTop, setRestoreScrollTop] = useState<number | null>(() => initialCache?.scrollTop ?? null);
const [listResetKey, setListResetKey] = useState(0);
const scrollTopRef = useRef(initialCache?.scrollTop ?? 0);
const pageWrapRef = useRef<HTMLDivElement>(null);
/** 主动刷新时不把旧列表/滚动位置写回 cache */
const skipCacheSaveRef = useRef(false);
const resetFeedView = useCallback(() => {
setRestoreScrollTop(null);
scrollTopRef.current = 0;
setListResetKey(k => k + 1);
pageWrapRef.current?.scrollTo(0);
}, []);
const beginFeedRefresh = useCallback(() => {
skipCacheSaveRef.current = true;
clearAllFeedCache();
resetFeedView();
}, [resetFeedView]);
const load = useCallback(async (p: number, reset = false) => {
setLoading(true);
@@ -37,7 +62,6 @@ export default function HomePage() {
sort: sort === 'latest' ? '' : sort,
});
const batch = Array.isArray(data.posts) ? data.posts : [];
// 切换筛选时保留旧列表,避免中间区域瞬间空白
setPosts(prev => (reset ? batch : [...prev, ...batch]));
setPostTotal(data.total ?? 0);
setHasMore(!!data.has_more);
@@ -80,6 +104,12 @@ export default function HomePage() {
}, [boardId, keyword, sort]);
useEffect(() => {
const forceRefresh = (location.state as FeedNavState | null)?.refreshFeed;
if (forceRefresh) {
beginFeedRefresh();
load(1, true);
return;
}
const cached = getFeedCache(boardId, keyword, sort);
if (cached) {
setPosts(cached.posts);
@@ -95,11 +125,11 @@ export default function HomePage() {
setRestoreScrollTop(null);
scrollTopRef.current = 0;
load(1, true);
}, [boardId, keyword, sort, load, revalidate]);
}, [boardId, keyword, sort, location.key, location.state, load, revalidate, beginFeedRefresh]);
useEffect(() => {
return () => {
if (posts.length === 0) return;
if (skipCacheSaveRef.current || posts.length === 0) return;
setFeedCache(boardId, keyword, sort, {
posts,
postTotal,
@@ -110,29 +140,42 @@ export default function HomePage() {
};
}, [boardId, keyword, sort, posts, postTotal, page, hasMore]);
useEffect(() => {
if (!loading && posts.length > 0) {
skipCacheSaveRef.current = false;
}
}, [loading, posts.length]);
useEffect(() => {
const onFeedReset = () => {
beginFeedRefresh();
};
window.addEventListener(FEED_RESET_EVENT, onFeedReset);
return () => window.removeEventListener(FEED_RESET_EVENT, onFeedReset);
}, [beginFeedRefresh]);
useEffect(() => {
const fn = () => {
clearAllFeedCache();
setRestoreScrollTop(null);
scrollTopRef.current = 0;
beginFeedRefresh();
load(1, true);
};
window.addEventListener('posts-refresh', fn);
return () => window.removeEventListener('posts-refresh', fn);
}, [boardId, keyword, sort, load]);
}, [beginFeedRefresh, load]);
const handleSortChange = (next: FeedSort) => {
if (next === sort) return;
clearAllFeedCache();
setRestoreScrollTop(null);
scrollTopRef.current = 0;
nav(buildHomeUrl(boardId, next));
if (next === sort) {
beginFeedRefresh();
load(1, true);
return;
}
navigateFeed(nav, buildHomeUrl(boardId, next));
};
const showSortBar = !keyword;
return (
<div className="page-wrap">
<div className="page-wrap" ref={pageWrapRef}>
<div className="feed-top">
<FeedHeader
boardId={boardId}
@@ -153,6 +196,7 @@ export default function HomePage() {
onLoadMore={() => !loading && hasMore && load(page + 1)}
onSelect={(id) => nav(`/post/${id}`)}
restoreScrollTop={restoreScrollTop}
resetScrollKey={listResetKey}
onScrollTopChange={(top) => { scrollTopRef.current = top; }}
onScrollRestored={() => setRestoreScrollTop(null)}
/>