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 && ( @@ -377,13 +509,13 @@ const Main = () => { - {currentCategory === SEARCH_CATEGORY && ( + {activeCategory === SEARCH_CATEGORY && activeSearchQuery && (
- 搜索: "{params.searchQuery || currentCategoryHistory.searchQuery}" ({pagination.total} 个结果) + 搜索: "{activeSearchQuery}" ({pagination.total} 个结果)
)} @@ -393,7 +525,7 @@ const Main = () => { {!loading && movies.length === 0 && (
- 没有找到结果。 + {activeCategory === SEARCH_CATEGORY && (searchInput || activeSearchQuery) ? `没有找到关于 "${searchInput || activeSearchQuery}" 的结果。` : '没有找到结果。'}
)} @@ -402,8 +534,8 @@ const Main = () => { {movies.map(item => (