接近完美
This commit is contained in:
parent
90c8df4ceb
commit
0a8b633539
622
src/Main.jsx
622
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 && (
|
||||
<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} />
|
||||
|
Loading…
x
Reference in New Issue
Block a user