接近完美

This commit is contained in:
eson 2025-06-03 01:29:26 +08:00
parent 90c8df4ceb
commit 0a8b633539

View File

@ -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 && (
<Pagination
count={pagination.pages}
page={pagination.page}
page={currentPage}
onChange={handlePageChange}
sx={{ my: 2, display: 'flex', justifyContent: 'center' }}
/>
@ -377,13 +509,13 @@ const Main = () => {
<CategoryNav
categories={categories}
currentCategory={currentCategory === SEARCH_CATEGORY ? SEARCH_CATEGORY : currentCategory} // Pass SEARCH_CATEGORY special value
currentCategory={activeCategory}
onCategoryChange={handleCategoryChange}
/>
{currentCategory === SEARCH_CATEGORY && (
{activeCategory === SEARCH_CATEGORY && activeSearchQuery && (
<div style={{ textAlign: 'center', margin: '10px 0' }}>
搜索: "{params.searchQuery || currentCategoryHistory.searchQuery}" ({pagination.total} 个结果)
搜索: "{activeSearchQuery}" ({pagination.total} 个结果)
</div>
)}
@ -393,7 +525,7 @@ const Main = () => {
{!loading && movies.length === 0 && (
<div style={{ textAlign: 'center', margin: '20px 0', color: 'gray' }}>
没有找到结果
{activeCategory === SEARCH_CATEGORY && (searchInput || activeSearchQuery) ? `没有找到关于 "${searchInput || activeSearchQuery}" 的结果。` : '没有找到结果。'}
</div>
)}
@ -402,8 +534,8 @@ const Main = () => {
{movies.map(item => (
<Grid item xs={6} sm={4} md={3} lg={2} key={item.filename}>
<Link
to={`/res/${item.filename}`}
style={{ textDecoration: 'none' }} // Removed paddingBottom, handle in Card
to={`/res/${item.filename}`} // Ensure your routing handles this path
style={{ textDecoration: 'none' }}
onClick={handleMovieCardClick}
>
<MovieCard movie={item} config={config} onRename={handleRename} />