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:回首页并强制刷新列表(已在首页时也会重拉) */}