diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 890a268..3b90cbb 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -51,7 +51,8 @@ "tailwindcss": "^3.4.19", "tailwindcss-animate": "^1.0.7", "turndown": "^7.2.4", - "zod": "^4.4.3" + "zod": "^4.4.3", + "zustand": "^4.5.7" }, "devDependencies": { "@types/node": "^25.9.3", @@ -4480,6 +4481,34 @@ "funding": { "url": "https://github.com/sponsors/colinhacks" } + }, + "node_modules/zustand": { + "version": "4.5.7", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", + "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.2.2" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } } } } diff --git a/frontend/package.json b/frontend/package.json index 394b148..10bf3ab 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -52,7 +52,8 @@ "tailwindcss": "^3.4.19", "tailwindcss-animate": "^1.0.7", "turndown": "^7.2.4", - "zod": "^4.4.3" + "zod": "^4.4.3", + "zustand": "^4.5.7" }, "devDependencies": { "@types/node": "^25.9.3", diff --git a/frontend/src/components/PullToRefresh.tsx b/frontend/src/components/PullToRefresh.tsx index 7e27f73..e371a99 100644 --- a/frontend/src/components/PullToRefresh.tsx +++ b/frontend/src/components/PullToRefresh.tsx @@ -1,5 +1,6 @@ import { useEffect, useRef, useState } from 'react'; import { Loader2, ArrowDown } from 'lucide-react'; +import { FEED_PULL_REFRESH_EVENT } from '../utils/feedCache'; /** 触发刷新的下拉距离(px) */ const REFRESH_THRESHOLD = 68; @@ -167,6 +168,18 @@ export default function PullToRefresh() { setSettling(true); setPull(REFRESH_THRESHOLD * 0.7); window.setTimeout(() => { + // Feed 页:应用内强制重拉(保留 SPA 其它状态);其它页仍整页 reload + const isFeed = !!( + document.querySelector('.main-content--feed-mobile-scroll') + || document.querySelector('.page-wrap--feed .post-list-scroll') + ); + if (isFeed) { + window.dispatchEvent(new Event(FEED_PULL_REFRESH_EVENT)); + setRefreshing(false); + setSettling(true); + setPull(0); + return; + } window.location.reload(); }, 180); return; diff --git a/frontend/src/components/VirtualPostList.tsx b/frontend/src/components/VirtualPostList.tsx index ddef3de..b607107 100644 --- a/frontend/src/components/VirtualPostList.tsx +++ b/frontend/src/components/VirtualPostList.tsx @@ -161,6 +161,12 @@ export default function VirtualPostList({ onScrollTopChangeRef.current?.(0); }, [resetScrollKey, virtualizer, getScrollElement]); + useLayoutEffect(() => { + // 收到新的恢复目标时允许再次 restore;null 表示已消费,勿动标记 + if (restoreScrollTop == null) return; + restoredRef.current = false; + }, [restoreScrollTop]); + useLayoutEffect(() => { if (restoreScrollTop == null || restoredRef.current || posts.length === 0) return; const el = getScrollElement(); @@ -173,10 +179,6 @@ export default function VirtualPostList({ onScrollRestoredRef.current?.(); }, [restoreScrollTop, posts.length, virtualizer, getScrollElement]); - useEffect(() => { - restoredRef.current = false; - }, [restoreScrollTop]); - useEffect(() => { const el = getScrollElement(); if (!el) return; diff --git a/frontend/src/layouts/MainLayout.tsx b/frontend/src/layouts/MainLayout.tsx index 7987ceb..76c15c7 100644 --- a/frontend/src/layouts/MainLayout.tsx +++ b/frontend/src/layouts/MainLayout.tsx @@ -386,6 +386,7 @@ export default function MainLayout() { )} + {/* 点 Logo:回首页并强制刷新列表(已在首页时也会重拉) */}