接近完美
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 Container from '@mui/material/Container';
|
||||||
import Grid from '@mui/material/Grid';
|
import Grid from '@mui/material/Grid';
|
||||||
import Pagination from '@mui/material/Pagination';
|
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 CircularProgress from '@mui/material/CircularProgress';
|
||||||
import TextField from '@mui/material/TextField';
|
import TextField from '@mui/material/TextField';
|
||||||
import InputAdornment from '@mui/material/InputAdornment';
|
import InputAdornment from '@mui/material/InputAdornment';
|
||||||
@ -11,10 +11,11 @@ import IconButton from '@mui/material/IconButton';
|
|||||||
import SearchIcon from '@mui/icons-material/Search';
|
import SearchIcon from '@mui/icons-material/Search';
|
||||||
import ClearIcon from '@mui/icons-material/Clear';
|
import ClearIcon from '@mui/icons-material/Clear';
|
||||||
|
|
||||||
import ConfigContext from './Config';
|
import ConfigContext from './Config'; // Ensure this path is correct
|
||||||
import MovieCard from './components/MovieCard';
|
import MovieCard from './components/MovieCard'; // Ensure this path is correct
|
||||||
import CategoryNav from './components/CategoryNav';
|
import CategoryNav from './components/CategoryNav'; // Ensure this path is correct
|
||||||
|
|
||||||
|
// 分类配置
|
||||||
const categories = [
|
const categories = [
|
||||||
{ label: '15min', value: '15min' },
|
{ label: '15min', value: '15min' },
|
||||||
{ label: '30min', value: '30min' },
|
{ label: '30min', value: '30min' },
|
||||||
@ -27,11 +28,15 @@ const SEARCH_CATEGORY = 'search';
|
|||||||
const LIMIT = 20;
|
const LIMIT = 20;
|
||||||
const DEFAULT_CATEGORY = categories[0].value;
|
const DEFAULT_CATEGORY = categories[0].value;
|
||||||
|
|
||||||
|
|
||||||
const usePersistedState = (key, defaultValue) => {
|
const usePersistedState = (key, defaultValue) => {
|
||||||
const [state, setState] = useState(() => {
|
const [state, setState] = useState(() => {
|
||||||
const stored = localStorage.getItem(key);
|
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(() => {
|
useEffect(() => {
|
||||||
@ -43,313 +48,440 @@ const usePersistedState = (key, defaultValue) => {
|
|||||||
|
|
||||||
const Main = () => {
|
const Main = () => {
|
||||||
const config = useContext(ConfigContext);
|
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 [movies, setMovies] = useState([]);
|
||||||
const [pagination, setPagination] = useState({ page: 1, total: 0, pages: 1 });
|
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 [activeCategory, setActiveCategory] = useState(DEFAULT_CATEGORY);
|
||||||
const currentCategoryHistory = params.history[currentCategory] || { lastPage: 1, scrollPos: 0 };
|
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) => {
|
const isMounted = useRef(false);
|
||||||
if (action === 'save') {
|
const isPopStateNav = useRef(false);
|
||||||
const currentScrollPos = window.scrollY;
|
const scrollTimeoutRef = useRef(null);
|
||||||
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 fetchMovies = useCallback(async (category, page, search = '', options = {}) => {
|
const fetchMovies = useCallback(async (category, page, search = '', options = {}) => {
|
||||||
if (options.skipRestore) {
|
|
||||||
skipScrollRestore.current = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const isSearch = category === SEARCH_CATEGORY;
|
const isSearch = category === SEARCH_CATEGORY;
|
||||||
const isLatest = category === '最新添加';
|
|
||||||
|
|
||||||
let apiUrl = `/movie/list?page=${page}&limit=${LIMIT}`;
|
let apiUrl = `/movie/list?page=${page}&limit=${LIMIT}`;
|
||||||
|
|
||||||
if (isSearch) apiUrl += `&search=${encodeURIComponent(search)}`;
|
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)}`;
|
else apiUrl += `&category=${encodeURIComponent(category)}`;
|
||||||
|
|
||||||
const response = await axios.get(apiUrl);
|
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 { items, total } = response.data.data;
|
||||||
const totalPages = Math.ceil(total / LIMIT);
|
const totalPages = Math.ceil(total / LIMIT);
|
||||||
|
|
||||||
if (items.length === 0 && page > 1 && !isSearch) { // Don't auto-decrement page for search
|
if (items.length === 0 && page > 1 && !isSearch && !options.isPopState) {
|
||||||
setParams(prev => ({
|
console.log("Empty page, current page has no items:", category, page);
|
||||||
...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.
|
|
||||||
setMovies([]);
|
setMovies([]);
|
||||||
setPagination({ page: Math.max(1, page - 1), total, pages: totalPages });
|
setPagination({ page, total, pages: totalPages });
|
||||||
return;
|
} else {
|
||||||
|
setMovies(items);
|
||||||
|
setPagination({ page, total, pages: totalPages });
|
||||||
}
|
}
|
||||||
|
|
||||||
setMovies(items);
|
|
||||||
setPagination({ page, total, pages: totalPages });
|
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching movies:', error);
|
console.error('获取电影数据失败:', error);
|
||||||
setMovies([]);
|
setMovies([]);
|
||||||
setPagination({ page, total: 0, pages: 1 });
|
setPagination({ page, total: 0, pages: 1 });
|
||||||
} finally {
|
} 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) => {
|
setActiveCategory(newCategory);
|
||||||
// console.log(`Transition: Saving scroll for ${currentCategory}`);
|
setCurrentPage(newPage);
|
||||||
manageScrollListener('remove');
|
setActiveSearchQuery(newSearchQuery);
|
||||||
handleScrollPosition('save'); // Save scroll for the category we are leaving
|
if (newCategory === SEARCH_CATEGORY) {
|
||||||
actionCallback();
|
setSearchInput(newSearchQuery);
|
||||||
// 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;
|
|
||||||
} else {
|
} else {
|
||||||
// console.log(`Restoring scroll for ${currentCategory} (normal flow)`);
|
setSearchInput('');
|
||||||
handleScrollPosition('restore', currentCategory);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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) => {
|
const handleCategoryChange = useCallback((newCategory) => {
|
||||||
handleTransition(() => {
|
// Save current scroll for the category we are leaving
|
||||||
const newCategoryHistory = params.history[newCategory] || { lastPage: 1, scrollPos: 0 };
|
setPersistedParams(prev => {
|
||||||
let searchQueryForNewCategory = '';
|
const oldCat = prev.lastKnownState.category;
|
||||||
if (newCategory === SEARCH_CATEGORY) {
|
return {
|
||||||
searchQueryForNewCategory = params.searchQuery || (newCategoryHistory.searchQuery || '');
|
...prev,
|
||||||
setSearchInput(searchQueryForNewCategory); // Sync search input
|
categoryHistory: {
|
||||||
} else {
|
...prev.categoryHistory,
|
||||||
setSearchInput(''); // Clear search input if not search category
|
[oldCat]: { ...prev.categoryHistory[oldCat], scrollPos: window.scrollY }
|
||||||
}
|
}
|
||||||
|
};
|
||||||
setParams(prev => ({ ...prev, lastCategory: newCategory }));
|
|
||||||
// console.log(`Category Change: Fetching ${newCategory}, page ${newCategoryHistory.lastPage}`);
|
|
||||||
fetchMovies(newCategory, newCategoryHistory.lastPage, searchQueryForNewCategory);
|
|
||||||
});
|
});
|
||||||
}, [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) => {
|
if (newCategory === SEARCH_CATEGORY) {
|
||||||
// Save scroll position for the current page of the current category BEFORE fetching new page data
|
targetSearchQuery = persistedParams.categoryHistory[SEARCH_CATEGORY]?.searchQuery || searchInput.trim() || '';
|
||||||
handleScrollPosition('save', currentCategory);
|
targetPage = persistedParams.categoryHistory[SEARCH_CATEGORY]?.lastPage || 1;
|
||||||
isPaginating.current = true; // Signal that this is a pagination action
|
} else {
|
||||||
|
targetPage = persistedParams.categoryHistory[newCategory]?.lastPage || 1;
|
||||||
|
}
|
||||||
|
|
||||||
setParams(prev => ({
|
// Update React state first
|
||||||
...prev,
|
setActiveCategory(newCategory);
|
||||||
history: {
|
setCurrentPage(targetPage);
|
||||||
...prev.history,
|
setActiveSearchQuery(targetSearchQuery);
|
||||||
[currentCategory]: { ...currentCategoryHistory, lastPage: page }
|
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 || '') : '';
|
fetchMovies(newCategory, targetPage, targetSearchQuery, { restoreScrollPos: categorySpecificScroll });
|
||||||
// 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]);
|
|
||||||
|
|
||||||
|
}, [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 handleSearchSubmit = useCallback(() => {
|
||||||
const trimmedSearch = searchInput.trim();
|
const trimmedSearch = searchInput.trim();
|
||||||
if (!trimmedSearch) return;
|
if (!trimmedSearch) return;
|
||||||
|
setPersistedParams(prev => {
|
||||||
handleTransition(() => {
|
const oldCat = prev.lastKnownState.category;
|
||||||
setParams(prev => ({
|
if (oldCat && oldCat !== SEARCH_CATEGORY) {
|
||||||
...prev,
|
return { ...prev, categoryHistory: { ...prev.categoryHistory, [oldCat]: { ...prev.categoryHistory[oldCat], scrollPos: window.scrollY } } };
|
||||||
lastCategory: SEARCH_CATEGORY,
|
}
|
||||||
searchQuery: trimmedSearch, // Update global search query
|
return prev;
|
||||||
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);
|
|
||||||
});
|
});
|
||||||
}, [handleTransition, setParams, fetchMovies, searchInput]);
|
navigateAndFetch(SEARCH_CATEGORY, 1, trimmedSearch, { preserveScroll: false });
|
||||||
|
}, [searchInput, navigateAndFetch, setPersistedParams]);
|
||||||
|
|
||||||
|
|
||||||
const handleClearSearch = useCallback(() => {
|
const handleClearSearch = useCallback(() => {
|
||||||
if (!searchInput && currentCategory !== SEARCH_CATEGORY) return;
|
|
||||||
|
|
||||||
setSearchInput('');
|
setSearchInput('');
|
||||||
// If we are currently in search results, transition to default category
|
if (activeCategory === SEARCH_CATEGORY) {
|
||||||
if (currentCategory === SEARCH_CATEGORY) {
|
setPersistedParams(prev => ({
|
||||||
handleTransition(() => {
|
...prev,
|
||||||
const defaultCategoryHistory = params.history[DEFAULT_CATEGORY] || { lastPage: 1, scrollPos: 0 };
|
categoryHistory: {
|
||||||
setParams(prev => ({
|
...prev.categoryHistory,
|
||||||
...prev,
|
[SEARCH_CATEGORY]: { ...prev.categoryHistory[SEARCH_CATEGORY], scrollPos: window.scrollY, searchQuery: '', lastPage: 1 }
|
||||||
lastCategory: DEFAULT_CATEGORY,
|
}
|
||||||
searchQuery: '', // Clear global search query
|
}));
|
||||||
// We can choose to preserve or reset params.history[SEARCH_CATEGORY].searchQuery
|
const targetCategory = DEFAULT_CATEGORY;
|
||||||
// For now, let's clear it in history as well if we are clearing and navigating away
|
const targetPage = persistedParams.categoryHistory[targetCategory]?.lastPage || 1;
|
||||||
history: {
|
const targetScroll = persistedParams.categoryHistory[targetCategory]?.scrollPos || 0;
|
||||||
...prev.history,
|
|
||||||
[SEARCH_CATEGORY]: { ...params.history[SEARCH_CATEGORY], searchQuery: '' }
|
// Manually set active states before calling fetch to avoid race with navigateAndFetch
|
||||||
}
|
setActiveCategory(targetCategory);
|
||||||
}));
|
setCurrentPage(targetPage);
|
||||||
// console.log(`Clear Search (was search category): Fetching ${DEFAULT_CATEGORY}, page ${defaultCategoryHistory.lastPage}`);
|
setActiveSearchQuery('');
|
||||||
fetchMovies(DEFAULT_CATEGORY, defaultCategoryHistory.lastPage);
|
|
||||||
});
|
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 {
|
} else {
|
||||||
// If not in search results, just clear the persisted search query
|
setActiveSearchQuery('');
|
||||||
setParams(prev => ({ ...prev, searchQuery: '' }));
|
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 handleRename = useCallback(async (oldName, newName) => {
|
||||||
|
const scrollPosBeforeRename = window.scrollY; // 1. Capture scroll position
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await axios.post('/movie/rename', { old_name: oldName, new_name: newName });
|
const response = await axios.post('/movie/rename', { old_name: oldName, new_name: newName });
|
||||||
if (response.data.code === 200) {
|
if (response.data.code === 200) {
|
||||||
// Refresh current view data, but skip scroll restoration
|
// 2. await fetchMovies to ensure data is re-fetched and state is updated.
|
||||||
const pageToRefresh = currentCategoryHistory.lastPage;
|
// Pass skipScrollRestore: true so fetchMovies doesn't scroll by itself.
|
||||||
const searchQueryForRefresh = currentCategory === SEARCH_CATEGORY ? (params.searchQuery || currentCategoryHistory.searchQuery || '') : '';
|
await fetchMovies(activeCategory, currentPage, activeSearchQuery, { skipScrollRestore: true });
|
||||||
// console.log(`Rename: Refreshing ${currentCategory}, page ${pageToRefresh}, search: "${searchQueryForRefresh}" with skipRestore`);
|
|
||||||
await fetchMovies(currentCategory, pageToRefresh, searchQueryForRefresh, { skipRestore: true });
|
// 3. After movies are fetched and React has re-rendered, restore the scroll position.
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
window.scrollTo(0, scrollPosBeforeRename);
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
throw new Error(response.data.message || '重命名失败');
|
throw new Error(response.data.message || '重命名失败');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('重命名错误:', 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(() => {
|
const handleMovieCardClick = useCallback(() => {
|
||||||
// This is a navigation, so save scroll state
|
if (window.history.state && window.history.state.appState) {
|
||||||
// console.log(`Movie Card Click: Saving scroll for ${currentCategory} before navigating away.`);
|
const currentScrollPos = window.scrollY;
|
||||||
handleScrollPosition('save', currentCategory);
|
const currentState = window.history.state.appState;
|
||||||
// No need for manageScrollListener('remove') here as the component might unmount
|
if (currentState.scrollPos !== currentScrollPos) { // Only update if changed
|
||||||
// or if it's an in-app navigation, the next component will manage its own scroll.
|
const updatedState = { ...currentState, scrollPos: currentScrollPos };
|
||||||
}, [handleScrollPosition, currentCategory]);
|
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 handleSearchChange = (e) => setSearchInput(e.target.value);
|
||||||
const handleSearchKeyDown = useCallback((e) => {
|
const handleSearchKeyDown = (e) => {
|
||||||
if (e.key === 'Enter') handleSearchSubmit();
|
if (e.key === 'Enter') handleSearchSubmit();
|
||||||
}, [handleSearchSubmit]);
|
};
|
||||||
|
|
||||||
// Cleanup scroll listener on unmount
|
const paginationComponent = !loading && movies.length > 0 && pagination.pages > 1 && (
|
||||||
useEffect(() => {
|
|
||||||
return () => {
|
|
||||||
manageScrollListener('remove');
|
|
||||||
};
|
|
||||||
}, [manageScrollListener]);
|
|
||||||
|
|
||||||
|
|
||||||
const paginationComponent = movies.length > 0 && pagination.pages > 1 && (
|
|
||||||
<Pagination
|
<Pagination
|
||||||
count={pagination.pages}
|
count={pagination.pages}
|
||||||
page={pagination.page}
|
page={currentPage}
|
||||||
onChange={handlePageChange}
|
onChange={handlePageChange}
|
||||||
sx={{ my: 2, display: 'flex', justifyContent: 'center' }}
|
sx={{ my: 2, display: 'flex', justifyContent: 'center' }}
|
||||||
/>
|
/>
|
||||||
@ -377,13 +509,13 @@ const Main = () => {
|
|||||||
|
|
||||||
<CategoryNav
|
<CategoryNav
|
||||||
categories={categories}
|
categories={categories}
|
||||||
currentCategory={currentCategory === SEARCH_CATEGORY ? SEARCH_CATEGORY : currentCategory} // Pass SEARCH_CATEGORY special value
|
currentCategory={activeCategory}
|
||||||
onCategoryChange={handleCategoryChange}
|
onCategoryChange={handleCategoryChange}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{currentCategory === SEARCH_CATEGORY && (
|
{activeCategory === SEARCH_CATEGORY && activeSearchQuery && (
|
||||||
<div style={{ textAlign: 'center', margin: '10px 0' }}>
|
<div style={{ textAlign: 'center', margin: '10px 0' }}>
|
||||||
搜索: "{params.searchQuery || currentCategoryHistory.searchQuery}" ({pagination.total} 个结果)
|
搜索: "{activeSearchQuery}" ({pagination.total} 个结果)
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@ -393,7 +525,7 @@ const Main = () => {
|
|||||||
|
|
||||||
{!loading && movies.length === 0 && (
|
{!loading && movies.length === 0 && (
|
||||||
<div style={{ textAlign: 'center', margin: '20px 0', color: 'gray' }}>
|
<div style={{ textAlign: 'center', margin: '20px 0', color: 'gray' }}>
|
||||||
没有找到结果。
|
{activeCategory === SEARCH_CATEGORY && (searchInput || activeSearchQuery) ? `没有找到关于 "${searchInput || activeSearchQuery}" 的结果。` : '没有找到结果。'}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@ -402,8 +534,8 @@ const Main = () => {
|
|||||||
{movies.map(item => (
|
{movies.map(item => (
|
||||||
<Grid item xs={6} sm={4} md={3} lg={2} key={item.filename}>
|
<Grid item xs={6} sm={4} md={3} lg={2} key={item.filename}>
|
||||||
<Link
|
<Link
|
||||||
to={`/res/${item.filename}`}
|
to={`/res/${item.filename}`} // Ensure your routing handles this path
|
||||||
style={{ textDecoration: 'none' }} // Removed paddingBottom, handle in Card
|
style={{ textDecoration: 'none' }}
|
||||||
onClick={handleMovieCardClick}
|
onClick={handleMovieCardClick}
|
||||||
>
|
>
|
||||||
<MovieCard movie={item} config={config} onRename={handleRename} />
|
<MovieCard movie={item} config={config} onRename={handleRename} />
|
||||||
|
Loading…
x
Reference in New Issue
Block a user