From 0a8b6335392dbc1d5e46473cccb805b1359dd0e2 Mon Sep 17 00:00:00 2001
From: eson <474420502@qq.com>
Date: Tue, 3 Jun 2025 01:29:26 +0800
Subject: [PATCH] =?UTF-8?q?=E6=8E=A5=E8=BF=91=E5=AE=8C=E7=BE=8E?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/Main.jsx | 622 +++++++++++++++++++++++++++++++--------------------
1 file changed, 377 insertions(+), 245 deletions(-)
diff --git a/src/Main.jsx b/src/Main.jsx
index 7c5ff91..a10b713 100644
--- a/src/Main.jsx
+++ b/src/Main.jsx
@@ -3,7 +3,7 @@ import axios from 'axios';
import Container from '@mui/material/Container';
import Grid from '@mui/material/Grid';
import Pagination from '@mui/material/Pagination';
-import { Link } from 'react-router-dom';
+import { Link } from 'react-router-dom'; // useLocation removed as not directly used for this fix
import CircularProgress from '@mui/material/CircularProgress';
import TextField from '@mui/material/TextField';
import InputAdornment from '@mui/material/InputAdornment';
@@ -11,10 +11,11 @@ import IconButton from '@mui/material/IconButton';
import SearchIcon from '@mui/icons-material/Search';
import ClearIcon from '@mui/icons-material/Clear';
-import ConfigContext from './Config';
-import MovieCard from './components/MovieCard';
-import CategoryNav from './components/CategoryNav';
+import ConfigContext from './Config'; // Ensure this path is correct
+import MovieCard from './components/MovieCard'; // Ensure this path is correct
+import CategoryNav from './components/CategoryNav'; // Ensure this path is correct
+// 分类配置
const categories = [
{ label: '15min', value: '15min' },
{ label: '30min', value: '30min' },
@@ -27,11 +28,15 @@ const SEARCH_CATEGORY = 'search';
const LIMIT = 20;
const DEFAULT_CATEGORY = categories[0].value;
-
const usePersistedState = (key, defaultValue) => {
const [state, setState] = useState(() => {
const stored = localStorage.getItem(key);
- return stored ? JSON.parse(stored) : defaultValue;
+ try {
+ return stored ? JSON.parse(stored) : defaultValue;
+ } catch (e) {
+ console.warn(`Error parsing persisted state for key "${key}":`, e);
+ return defaultValue;
+ }
});
useEffect(() => {
@@ -43,313 +48,440 @@ const usePersistedState = (key, defaultValue) => {
const Main = () => {
const config = useContext(ConfigContext);
- const [loading, setLoading] = useState(false);
- const isFirstLoad = useRef(true);
- const scrollListenerActive = useRef(false);
- const skipScrollRestore = useRef(false);
- const isPaginating = useRef(false); // New ref for pagination
-
- const [params, setParams] = usePersistedState('params', {
- lastCategory: DEFAULT_CATEGORY,
- history: Object.fromEntries([
- ...categories.map(cat => [cat.value, { lastPage: 1, scrollPos: 0 }]),
- [SEARCH_CATEGORY, { lastPage: 1, scrollPos: 0, searchQuery: '' }] // Ensure searchQuery is part of history for search
- ]),
- searchQuery: '' // Global search query, might be redundant if using history.searchQuery
- });
+ const [loading, setLoading] = useState(true);
const [movies, setMovies] = useState([]);
const [pagination, setPagination] = useState({ page: 1, total: 0, pages: 1 });
- const [searchInput, setSearchInput] = useState(params.searchQuery || '');
+ const [searchInput, setSearchInput] = useState('');
- const currentCategory = params.lastCategory || DEFAULT_CATEGORY;
- const currentCategoryHistory = params.history[currentCategory] || { lastPage: 1, scrollPos: 0 };
+ const [activeCategory, setActiveCategory] = useState(DEFAULT_CATEGORY);
+ const [currentPage, setCurrentPage] = useState(1);
+ const [activeSearchQuery, setActiveSearchQuery] = useState('');
+ const [persistedParams, setPersistedParams] = usePersistedState('mainViewParams', {
+ lastKnownState: {
+ category: DEFAULT_CATEGORY,
+ page: 1,
+ searchQuery: '',
+ scrollPos: 0,
+ },
+ categoryHistory: Object.fromEntries([
+ ...categories.map(cat => [cat.value, { lastPage: 1, scrollPos: 0 }]),
+ [SEARCH_CATEGORY, { lastPage: 1, scrollPos: 0, searchQuery: '' }]
+ ]),
+ });
- const handleScrollPosition = useCallback((action, category = currentCategory) => {
- if (action === 'save') {
- const currentScrollPos = window.scrollY;
- setParams(prev => ({
- ...prev,
- history: {
- ...prev.history,
- [category]: {
- ...(prev.history[category] || { lastPage: 1 }),
- scrollPos: currentScrollPos
- }
- }
- }));
- } else if (action === 'restore') {
- const scrollPos = params.history?.[category]?.scrollPos || 0;
- // console.log(`Attempting to restore scroll for ${category} to ${scrollPos}`);
- requestAnimationFrame(() => {
- // Increased timeout slightly for better rendering chance
- setTimeout(() => {
- window.scrollTo(0, scrollPos);
- // console.log(`Scrolled to ${scrollPos} for ${category}`);
- }, 150);
- });
- }
- }, [setParams, params.history, currentCategory]);
-
-
- const manageScrollListener = useCallback((action) => {
- const scrollHandler = () => {
- clearTimeout(window.scrollTimer);
- window.scrollTimer = setTimeout(() => handleScrollPosition('save'), 200); // Debounce save
- };
- window.scrollHandler = scrollHandler; // Store to remove the correct one
-
- if (action === 'setup' && !scrollListenerActive.current) {
- window.addEventListener('scroll', window.scrollHandler);
- scrollListenerActive.current = true;
- } else if (action === 'remove' && scrollListenerActive.current) {
- window.removeEventListener('scroll', window.scrollHandler);
- scrollListenerActive.current = false;
- clearTimeout(window.scrollTimer);
- }
- }, [handleScrollPosition]);
-
+ const isMounted = useRef(false);
+ const isPopStateNav = useRef(false);
+ const scrollTimeoutRef = useRef(null);
const fetchMovies = useCallback(async (category, page, search = '', options = {}) => {
- if (options.skipRestore) {
- skipScrollRestore.current = true;
- }
-
setLoading(true);
try {
const isSearch = category === SEARCH_CATEGORY;
- const isLatest = category === '最新添加';
-
let apiUrl = `/movie/list?page=${page}&limit=${LIMIT}`;
+
if (isSearch) apiUrl += `&search=${encodeURIComponent(search)}`;
- else if (isLatest) apiUrl += `&sort=created_time&order=desc`;
+ else if (category === '最新添加') apiUrl += `&sort=created_time&order=desc`;
else apiUrl += `&category=${encodeURIComponent(category)}`;
const response = await axios.get(apiUrl);
- if (response.status !== 200) {
- console.error("API Error:", response);
- setMovies([]);
- setPagination({ page, total: 0, pages: 1 });
- return;
- }
-
const { items, total } = response.data.data;
const totalPages = Math.ceil(total / LIMIT);
- if (items.length === 0 && page > 1 && !isSearch) { // Don't auto-decrement page for search
- setParams(prev => ({
- ...prev,
- history: {
- ...prev.history,
- [category]: { ...prev.history[category], lastPage: Math.max(1, page - 1) }
- }
- }));
- // Optionally, fetch page - 1 here, or let user do it.
- // For now, just update the state and show no movies.
+ if (items.length === 0 && page > 1 && !isSearch && !options.isPopState) {
+ console.log("Empty page, current page has no items:", category, page);
setMovies([]);
- setPagination({ page: Math.max(1, page - 1), total, pages: totalPages });
- return;
+ setPagination({ page, total, pages: totalPages });
+ } else {
+ setMovies(items);
+ setPagination({ page, total, pages: totalPages });
}
- setMovies(items);
- setPagination({ page, total, pages: totalPages });
-
} catch (error) {
- console.error('Error fetching movies:', error);
+ console.error('获取电影数据失败:', error);
setMovies([]);
setPagination({ page, total: 0, pages: 1 });
} finally {
- setLoading(false); // This will trigger the useEffect[loading]
+ setLoading(false);
+ if (options.restoreScrollPos !== undefined && !options.skipScrollRestore) {
+ requestAnimationFrame(() => {
+ setTimeout(() => window.scrollTo(0, options.restoreScrollPos), 50);
+ });
+ } else if (!options.skipScrollRestore && !options.isPopStatePaging) {
+ window.scrollTo(0, 0);
+ }
+ isPopStateNav.current = false;
}
- }, [setParams]); // Removed currentCategory from deps as fetchMovies doesn't directly use it for scroll
+ // IMPORTANT: Removed duplicated scroll logic block that was here
+ }, []); // No dependencies that change frequently, setPersistedParams is stable
+ const navigateAndFetch = useCallback((newCategory, newPage, newSearchQuery = '', options = {}) => {
+ const { replace = false, preserveScroll = false, isPopStatePaging = false } = options;
- const handleTransition = useCallback((actionCallback) => {
- // console.log(`Transition: Saving scroll for ${currentCategory}`);
- manageScrollListener('remove');
- handleScrollPosition('save'); // Save scroll for the category we are leaving
- actionCallback();
- // manageScrollListener will be set up again in useEffect[loading]
- }, [manageScrollListener, handleScrollPosition, currentCategory]);
-
-
- // Initial Load
- useEffect(() => {
- if (!isFirstLoad.current) return;
-
- const categoryToLoad = params.lastCategory || DEFAULT_CATEGORY;
- const historyForCategory = params.history[categoryToLoad] || { lastPage: 1, scrollPos: 0 };
- const pageToLoad = historyForCategory.lastPage;
- let searchQueryToLoad = '';
- if (categoryToLoad === SEARCH_CATEGORY) {
- searchQueryToLoad = params.searchQuery || (historyForCategory.searchQuery || '');
- setSearchInput(searchQueryToLoad); // Sync search input
- }
- // console.log(`Initial Load: Fetching ${categoryToLoad}, page ${pageToLoad}, search: "${searchQueryToLoad}"`);
- fetchMovies(categoryToLoad, pageToLoad, searchQueryToLoad);
- isFirstLoad.current = false;
- }, []); // Runs only once on mount
-
-
- // Scroll Management after data loading
- useEffect(() => {
- if (loading || isFirstLoad.current) return; // Don't run if loading or still initial phase
-
- manageScrollListener('remove'); // Ensure no listener interference
-
- if (isPaginating.current) {
- // console.log("Pagination: Scrolling to top");
- window.scrollTo(0, 0);
- isPaginating.current = false;
- } else if (skipScrollRestore.current) {
- // console.log(`Skipping scroll restore for ${currentCategory} (e.g., after rename)`);
- skipScrollRestore.current = false;
+ setActiveCategory(newCategory);
+ setCurrentPage(newPage);
+ setActiveSearchQuery(newSearchQuery);
+ if (newCategory === SEARCH_CATEGORY) {
+ setSearchInput(newSearchQuery);
} else {
- // console.log(`Restoring scroll for ${currentCategory} (normal flow)`);
- handleScrollPosition('restore', currentCategory);
+ setSearchInput('');
}
- manageScrollListener('setup'); // Setup listener after scroll decision
+ // Determine scroll position for history state
+ // If preserving scroll, use current window.scrollY or a remembered scroll for the target category if available
+ // Otherwise, new navigations (not popstate) typically scroll to 0 unless specified.
+ let scrollForHistory = 0;
+ if (preserveScroll) {
+ // For general preserveScroll (like back/fwd), use current scrollY.
+ // For specific category change restorations, this might be overridden by options.restoreScrollPos in fetchMovies.
+ scrollForHistory = window.history.state?.appState?.scrollPos || window.scrollY;
+ }
+ // If navigating to a category with a known scroll position, that should be prioritized for restoration.
+ // This is handled by passing restoreScrollPos to fetchMovies. For history state, use `scrollForHistory`.
- }, [loading, currentCategory, handleScrollPosition, manageScrollListener]);
+
+ const historyState = {
+ category: newCategory,
+ page: newPage,
+ searchQuery: newSearchQuery,
+ scrollPos: scrollForHistory, // This is the scroll position *at the moment of navigation*
+ };
+
+ const url = window.location.pathname; // Keep URL simple, no query params in URL itself for now
+ const browserHistoryState = window.history.state?.appState;
+ const needsPush = !browserHistoryState ||
+ browserHistoryState.category !== newCategory ||
+ browserHistoryState.page !== newPage ||
+ browserHistoryState.searchQuery !== newSearchQuery;
+
+ if (replace) {
+ window.history.replaceState({ appState: historyState }, '', url);
+ } else if (needsPush) {
+ window.history.pushState({ appState: historyState }, '', url);
+ }
+
+ setPersistedParams(prev => {
+ const newCategoryHistory = { ...prev.categoryHistory };
+ const oldCategoryState = prev.lastKnownState.category;
+
+ if (oldCategoryState && oldCategoryState !== newCategory) {
+ newCategoryHistory[oldCategoryState] = {
+ ...newCategoryHistory[oldCategoryState],
+ scrollPos: window.scrollY, // Save scroll of category being left
+ };
+ }
+
+ newCategoryHistory[newCategory] = {
+ ...newCategoryHistory[newCategory],
+ lastPage: newPage,
+ scrollPos: scrollForHistory, // Store the scroll we intend to be at for the new state
+ ...(newCategory === SEARCH_CATEGORY && { searchQuery: newSearchQuery }),
+ };
+
+ return {
+ lastKnownState: historyState,
+ categoryHistory: newCategoryHistory,
+ };
+ });
+
+ // Determine scroll position for fetching movies
+ // If preserving scroll, it means we want to restore to where we were or a specific point
+ let scrollPosForFetch = scrollForHistory; // Default to the scrollForHistory calculated
+ if (!preserveScroll) { // If not preserving, new main navigations usually go to top
+ scrollPosForFetch = 0;
+ }
+ // If navigating to a category and it has a specific remembered scroll, that should be preferred
+ // This part becomes complex if we want category changes to always restore *their* scroll.
+ // For now, `preserveScroll` will try to keep `window.scrollY`, otherwise `0`.
+ // PopState correctly restores its specific scroll.
+
+ fetchMovies(newCategory, newPage, newSearchQuery, {
+ restoreScrollPos: scrollPosForFetch,
+ skipScrollRestore: false, // Allow fetchMovies to handle scroll unless explicitly told otherwise later
+ isPopStatePaging: isPopStatePaging,
+ });
+ }, [fetchMovies, setPersistedParams]);
+
+ useEffect(() => {
+ const handlePopState = (event) => {
+ if (event.state && event.state.appState) {
+ const { category, page, searchQuery, scrollPos } = event.state.appState;
+ isPopStateNav.current = true;
+
+ setActiveCategory(category);
+ setCurrentPage(page);
+ setActiveSearchQuery(searchQuery);
+ setSearchInput(searchQuery || '');
+
+ setPersistedParams(prev => ({
+ lastKnownState: event.state.appState,
+ categoryHistory: {
+ ...prev.categoryHistory,
+ [category]: {
+ ...prev.categoryHistory[category],
+ lastPage: page,
+ scrollPos: scrollPos,
+ ...(category === SEARCH_CATEGORY && { searchQuery: searchQuery }),
+ }
+ }
+ }));
+ fetchMovies(category, page, searchQuery, { restoreScrollPos: scrollPos, isPopStatePaging: true });
+ } else {
+ const lastState = persistedParams.lastKnownState;
+ navigateAndFetch(lastState.category, lastState.page, lastState.searchQuery, { replace: true, preserveScroll: true });
+ }
+ };
+
+ window.addEventListener('popstate', handlePopState);
+
+ if (!isMounted.current) {
+ isMounted.current = true;
+ if ('scrollRestoration' in window.history) {
+ window.history.scrollRestoration = 'manual';
+ }
+
+ if (window.history.state && window.history.state.appState) {
+ const { category, page, searchQuery, scrollPos } = window.history.state.appState;
+ isPopStateNav.current = true;
+ setActiveCategory(category);
+ setCurrentPage(page);
+ setActiveSearchQuery(searchQuery);
+ setSearchInput(searchQuery || '');
+ setPersistedParams(prev => ({
+ lastKnownState: window.history.state.appState,
+ categoryHistory: {
+ ...prev.categoryHistory,
+ [category]: { ...prev.categoryHistory[category], lastPage: page, scrollPos: scrollPos, ...(category === SEARCH_CATEGORY && { searchQuery: searchQuery }) }
+ }
+ }));
+ fetchMovies(category, page, searchQuery, { restoreScrollPos: scrollPos, isPopStatePaging: true });
+ } else {
+ const { category, page, searchQuery } = persistedParams.lastKnownState;
+ // For initial load from localStorage, use its scrollPos. navigateAndFetch with preserveScroll=true will handle it.
+ navigateAndFetch(category, page, searchQuery, { replace: true, preserveScroll: true });
+ }
+ }
+
+ return () => {
+ window.removeEventListener('popstate', handlePopState);
+ clearTimeout(scrollTimeoutRef.current);
+ };
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [navigateAndFetch, setPersistedParams]); // Removed fetchMovies as navigateAndFetch calls it.
+
+ useEffect(() => {
+ const handleScroll = () => {
+ clearTimeout(scrollTimeoutRef.current);
+ scrollTimeoutRef.current = setTimeout(() => {
+ if (window.history.state && window.history.state.appState && !loading && movies.length > 0 && !isPopStateNav.current) {
+ const newScrollPos = window.scrollY;
+ const currentState = window.history.state.appState;
+
+ if (currentState.scrollPos !== newScrollPos) {
+ const updatedState = { ...currentState, scrollPos: newScrollPos };
+ window.history.replaceState({ appState: updatedState }, '', window.location.href);
+
+ setPersistedParams(prev => ({
+ lastKnownState: updatedState,
+ categoryHistory: {
+ ...prev.categoryHistory,
+ [currentState.category]: {
+ ...prev.categoryHistory[currentState.category],
+ lastPage: currentState.page,
+ scrollPos: newScrollPos,
+ ...(currentState.category === SEARCH_CATEGORY && { searchQuery: currentState.searchQuery }),
+ }
+ }
+ }));
+ }
+ }
+ }, 150);
+ };
+
+ if (!loading && movies.length > 0) {
+ window.addEventListener('scroll', handleScroll, { passive: true });
+ }
+ return () => {
+ window.removeEventListener('scroll', handleScroll);
+ clearTimeout(scrollTimeoutRef.current);
+ };
+ }, [loading, movies.length, setPersistedParams]);
const handleCategoryChange = useCallback((newCategory) => {
- handleTransition(() => {
- const newCategoryHistory = params.history[newCategory] || { lastPage: 1, scrollPos: 0 };
- let searchQueryForNewCategory = '';
- if (newCategory === SEARCH_CATEGORY) {
- searchQueryForNewCategory = params.searchQuery || (newCategoryHistory.searchQuery || '');
- setSearchInput(searchQueryForNewCategory); // Sync search input
- } else {
- setSearchInput(''); // Clear search input if not search category
- }
-
- setParams(prev => ({ ...prev, lastCategory: newCategory }));
- // console.log(`Category Change: Fetching ${newCategory}, page ${newCategoryHistory.lastPage}`);
- fetchMovies(newCategory, newCategoryHistory.lastPage, searchQueryForNewCategory);
+ // Save current scroll for the category we are leaving
+ setPersistedParams(prev => {
+ const oldCat = prev.lastKnownState.category;
+ return {
+ ...prev,
+ categoryHistory: {
+ ...prev.categoryHistory,
+ [oldCat]: { ...prev.categoryHistory[oldCat], scrollPos: window.scrollY }
+ }
+ };
});
- }, [handleTransition, setParams, fetchMovies, params.history, params.searchQuery]);
+
+ let targetPage = 1;
+ let targetSearchQuery = '';
+ // For category change, restore to its own last known scroll position or top
+ const categorySpecificScroll = persistedParams.categoryHistory[newCategory]?.scrollPos || 0;
- const handlePageChange = useCallback((_, page) => {
- // Save scroll position for the current page of the current category BEFORE fetching new page data
- handleScrollPosition('save', currentCategory);
- isPaginating.current = true; // Signal that this is a pagination action
+ if (newCategory === SEARCH_CATEGORY) {
+ targetSearchQuery = persistedParams.categoryHistory[SEARCH_CATEGORY]?.searchQuery || searchInput.trim() || '';
+ targetPage = persistedParams.categoryHistory[SEARCH_CATEGORY]?.lastPage || 1;
+ } else {
+ targetPage = persistedParams.categoryHistory[newCategory]?.lastPage || 1;
+ }
- setParams(prev => ({
- ...prev,
- history: {
- ...prev.history,
- [currentCategory]: { ...currentCategoryHistory, lastPage: page }
+ // Update React state first
+ setActiveCategory(newCategory);
+ setCurrentPage(targetPage);
+ setActiveSearchQuery(targetSearchQuery);
+ if (newCategory === SEARCH_CATEGORY) {
+ setSearchInput(targetSearchQuery);
+ } else {
+ setSearchInput('');
+ }
+
+ // Then fetch, telling fetchMovies to restore to the category's specific scroll
+ const historyState = { category: newCategory, page: targetPage, searchQuery: targetSearchQuery, scrollPos: categorySpecificScroll };
+ window.history.pushState({ appState: historyState }, '', window.location.pathname);
+
+ setPersistedParams(prev => ({
+ lastKnownState: historyState,
+ categoryHistory: {
+ ...prev.categoryHistory,
+ [newCategory]: { ...prev.categoryHistory[newCategory], lastPage: targetPage, scrollPos: categorySpecificScroll, ...(newCategory === SEARCH_CATEGORY && { searchQuery: targetSearchQuery }) }
}
}));
- const searchQueryForPageChange = currentCategory === SEARCH_CATEGORY ? (params.searchQuery || currentCategoryHistory.searchQuery || '') : '';
- // console.log(`Page Change: Fetching ${currentCategory}, page ${page}, search: "${searchQueryForPageChange}"`);
- fetchMovies(currentCategory, page, searchQueryForPageChange);
- // Scrolling to top will be handled by useEffect[loading] due to isPaginating.current
- }, [setParams, fetchMovies, currentCategory, params.searchQuery, currentCategoryHistory, handleScrollPosition]);
+ fetchMovies(newCategory, targetPage, targetSearchQuery, { restoreScrollPos: categorySpecificScroll });
+ }, [searchInput, persistedParams.categoryHistory, setPersistedParams, fetchMovies]);
+
+
+ const handlePageChange = useCallback((_, page) => {
+ setPersistedParams(prev => ({
+ ...prev,
+ categoryHistory: {
+ ...prev.categoryHistory,
+ [activeCategory]: { ...prev.categoryHistory[activeCategory], scrollPos: window.scrollY }
+ }
+ }));
+ // For page change, typically scroll to top
+ navigateAndFetch(activeCategory, page, activeSearchQuery, { preserveScroll: false });
+ }, [activeCategory, activeSearchQuery, navigateAndFetch, setPersistedParams]);
const handleSearchSubmit = useCallback(() => {
const trimmedSearch = searchInput.trim();
if (!trimmedSearch) return;
-
- handleTransition(() => {
- setParams(prev => ({
- ...prev,
- lastCategory: SEARCH_CATEGORY,
- searchQuery: trimmedSearch, // Update global search query
- history: {
- ...prev.history,
- [SEARCH_CATEGORY]: { // Reset search history to page 1 for new search
- lastPage: 1,
- scrollPos: 0, // New search starts at top
- searchQuery: trimmedSearch
- }
- }
- }));
- // console.log(`Search Submit: Fetching ${SEARCH_CATEGORY}, page 1, search: "${trimmedSearch}"`);
- fetchMovies(SEARCH_CATEGORY, 1, trimmedSearch);
+ setPersistedParams(prev => {
+ const oldCat = prev.lastKnownState.category;
+ if (oldCat && oldCat !== SEARCH_CATEGORY) {
+ return { ...prev, categoryHistory: { ...prev.categoryHistory, [oldCat]: { ...prev.categoryHistory[oldCat], scrollPos: window.scrollY } } };
+ }
+ return prev;
});
- }, [handleTransition, setParams, fetchMovies, searchInput]);
+ navigateAndFetch(SEARCH_CATEGORY, 1, trimmedSearch, { preserveScroll: false });
+ }, [searchInput, navigateAndFetch, setPersistedParams]);
const handleClearSearch = useCallback(() => {
- if (!searchInput && currentCategory !== SEARCH_CATEGORY) return;
-
setSearchInput('');
- // If we are currently in search results, transition to default category
- if (currentCategory === SEARCH_CATEGORY) {
- handleTransition(() => {
- const defaultCategoryHistory = params.history[DEFAULT_CATEGORY] || { lastPage: 1, scrollPos: 0 };
- setParams(prev => ({
- ...prev,
- lastCategory: DEFAULT_CATEGORY,
- searchQuery: '', // Clear global search query
- // We can choose to preserve or reset params.history[SEARCH_CATEGORY].searchQuery
- // For now, let's clear it in history as well if we are clearing and navigating away
- history: {
- ...prev.history,
- [SEARCH_CATEGORY]: { ...params.history[SEARCH_CATEGORY], searchQuery: '' }
- }
- }));
- // console.log(`Clear Search (was search category): Fetching ${DEFAULT_CATEGORY}, page ${defaultCategoryHistory.lastPage}`);
- fetchMovies(DEFAULT_CATEGORY, defaultCategoryHistory.lastPage);
- });
+ if (activeCategory === SEARCH_CATEGORY) {
+ setPersistedParams(prev => ({
+ ...prev,
+ categoryHistory: {
+ ...prev.categoryHistory,
+ [SEARCH_CATEGORY]: { ...prev.categoryHistory[SEARCH_CATEGORY], scrollPos: window.scrollY, searchQuery: '', lastPage: 1 }
+ }
+ }));
+ const targetCategory = DEFAULT_CATEGORY;
+ const targetPage = persistedParams.categoryHistory[targetCategory]?.lastPage || 1;
+ const targetScroll = persistedParams.categoryHistory[targetCategory]?.scrollPos || 0;
+
+ // Manually set active states before calling fetch to avoid race with navigateAndFetch
+ setActiveCategory(targetCategory);
+ setCurrentPage(targetPage);
+ setActiveSearchQuery('');
+
+ const historyState = { category: targetCategory, page: targetPage, searchQuery: '', scrollPos: targetScroll };
+ window.history.pushState({ appState: historyState }, '', window.location.pathname);
+ setPersistedParams(prev => ({
+ lastKnownState: historyState,
+ categoryHistory: {
+ ...prev.categoryHistory,
+ [targetCategory]: { ...prev.categoryHistory[targetCategory], lastPage: targetPage, scrollPos: targetScroll }
+ }
+ }));
+ fetchMovies(targetCategory, targetPage, '', { restoreScrollPos: targetScroll });
+
} else {
- // If not in search results, just clear the persisted search query
- setParams(prev => ({ ...prev, searchQuery: '' }));
+ setActiveSearchQuery('');
+ setPersistedParams(prev => ({ ...prev, lastKnownState: { ...prev.lastKnownState, searchQuery: '' } }));
}
- }, [currentCategory, fetchMovies, params.history, searchInput, setParams, handleTransition]);
+ }, [activeCategory, navigateAndFetch, persistedParams.categoryHistory, setPersistedParams, fetchMovies]);
+ // MODIFIED handleRename
const handleRename = useCallback(async (oldName, newName) => {
+ const scrollPosBeforeRename = window.scrollY; // 1. Capture scroll position
+
try {
const response = await axios.post('/movie/rename', { old_name: oldName, new_name: newName });
if (response.data.code === 200) {
- // Refresh current view data, but skip scroll restoration
- const pageToRefresh = currentCategoryHistory.lastPage;
- const searchQueryForRefresh = currentCategory === SEARCH_CATEGORY ? (params.searchQuery || currentCategoryHistory.searchQuery || '') : '';
- // console.log(`Rename: Refreshing ${currentCategory}, page ${pageToRefresh}, search: "${searchQueryForRefresh}" with skipRestore`);
- await fetchMovies(currentCategory, pageToRefresh, searchQueryForRefresh, { skipRestore: true });
+ // 2. await fetchMovies to ensure data is re-fetched and state is updated.
+ // Pass skipScrollRestore: true so fetchMovies doesn't scroll by itself.
+ await fetchMovies(activeCategory, currentPage, activeSearchQuery, { skipScrollRestore: true });
+
+ // 3. After movies are fetched and React has re-rendered, restore the scroll position.
+ requestAnimationFrame(() => {
+ window.scrollTo(0, scrollPosBeforeRename);
+ });
} else {
throw new Error(response.data.message || '重命名失败');
}
} catch (error) {
console.error('重命名错误:', error);
- throw error; // Re-throw to allow MovieCard to handle it if needed
+ throw error; // Re-throw for MovieCard to handle Snackbar
}
- }, [fetchMovies, currentCategory, currentCategoryHistory, params.searchQuery]);
-
+ }, [fetchMovies, activeCategory, currentPage, activeSearchQuery]); // Added fetchMovies dependency
const handleMovieCardClick = useCallback(() => {
- // This is a navigation, so save scroll state
- // console.log(`Movie Card Click: Saving scroll for ${currentCategory} before navigating away.`);
- handleScrollPosition('save', currentCategory);
- // No need for manageScrollListener('remove') here as the component might unmount
- // or if it's an in-app navigation, the next component will manage its own scroll.
- }, [handleScrollPosition, currentCategory]);
+ if (window.history.state && window.history.state.appState) {
+ const currentScrollPos = window.scrollY;
+ const currentState = window.history.state.appState;
+ if (currentState.scrollPos !== currentScrollPos) { // Only update if changed
+ const updatedState = { ...currentState, scrollPos: currentScrollPos };
+ window.history.replaceState({ appState: updatedState }, '', window.location.href);
+ setPersistedParams(prev => ({
+ ...prev,
+ lastKnownState: updatedState,
+ categoryHistory: {
+ ...prev.categoryHistory,
+ [updatedState.category]: {
+ ...prev.categoryHistory[updatedState.category],
+ lastPage: updatedState.page,
+ scrollPos: currentScrollPos,
+ ...(updatedState.category === SEARCH_CATEGORY && { searchQuery: updatedState.searchQuery }),
+ }
+ }
+ }));
+ }
+ }
+ }, [setPersistedParams]);
- const handleSearchChange = useCallback((e) => setSearchInput(e.target.value), []);
- const handleSearchKeyDown = useCallback((e) => {
+ const handleSearchChange = (e) => setSearchInput(e.target.value);
+ const handleSearchKeyDown = (e) => {
if (e.key === 'Enter') handleSearchSubmit();
- }, [handleSearchSubmit]);
+ };
- // Cleanup scroll listener on unmount
- useEffect(() => {
- return () => {
- manageScrollListener('remove');
- };
- }, [manageScrollListener]);
-
-
- const paginationComponent = movies.length > 0 && pagination.pages > 1 && (
+ const paginationComponent = !loading && movies.length > 0 && pagination.pages > 1 && (