新增 `/movie/delete` 接口用于删除指定视频文件及其缩略图,并从全局电影列表中移除记录。 同时完善了前端删除交互,包括确认对话框、成功提示及错误处理。 - 后端: - 新增 `PostDelete` 处理函数,支持通过文件名删除视频 - 实现 `Movie.DeleteVideo()` 方法,用于删除视频文件和关联缩略图 - 更新路由注册,增加 `/delete` 接口 - 前端: - 新增删除确认弹窗与交互逻辑 - 添加删除成功的 Snackbar 提示 - 调整 MovieCard 按钮布局以容纳删除按钮 - 传递并使用 onDelete 回调函数处理删除操作 该功能增强了系统的文件管理能力,使用户能够安全地移除不需要的视频内容。
577 lines
21 KiB
JavaScript
577 lines
21 KiB
JavaScript
// ...existing code...
|
|
import React, { useState, useEffect, useContext, useCallback, useRef } from 'react';
|
|
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 CircularProgress from '@mui/material/CircularProgress';
|
|
import TextField from '@mui/material/TextField';
|
|
import InputAdornment from '@mui/material/InputAdornment';
|
|
import IconButton from '@mui/material/IconButton';
|
|
import SearchIcon from '@mui/icons-material/Search';
|
|
import ClearIcon from '@mui/icons-material/Clear';
|
|
import Snackbar from '@mui/material/Snackbar';
|
|
import Alert from '@mui/material/Alert';
|
|
|
|
import ConfigContext from './Config';
|
|
import MovieCard from './components/MovieCard';
|
|
import CategoryNav from './components/CategoryNav';
|
|
// ...existing code...
|
|
|
|
// 分类配置
|
|
const categories = [
|
|
{ label: '15min', value: '15min' },
|
|
{ label: '30min', value: '30min' },
|
|
{ label: '60min', value: '60min' },
|
|
{ label: '大于60min', value: '大于60min' },
|
|
{ label: '最新添加', value: '最新添加' },
|
|
];
|
|
|
|
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);
|
|
try {
|
|
return stored ? JSON.parse(stored) : defaultValue;
|
|
} catch (e) {
|
|
console.warn(`Error parsing persisted state for key "${key}":`, e);
|
|
return defaultValue;
|
|
}
|
|
});
|
|
|
|
useEffect(() => {
|
|
localStorage.setItem(key, JSON.stringify(state));
|
|
}, [key, state]);
|
|
|
|
return [state, setState];
|
|
};
|
|
|
|
const Main = () => {
|
|
const config = useContext(ConfigContext);
|
|
|
|
const [loading, setLoading] = useState(true);
|
|
const [movies, setMovies] = useState([]);
|
|
const [pagination, setPagination] = useState({ page: 1, total: 0, pages: 1 });
|
|
const [searchInput, setSearchInput] = useState('');
|
|
const [successMessage, setSuccessMessage] = useState('');
|
|
|
|
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 scrollTimeoutRef = useRef(null);
|
|
const isPopStateNav = useRef(false);
|
|
const isMounted = useRef(false);
|
|
|
|
const fetchMovies = useCallback(async (category, page, search = '', options = {}) => {
|
|
setLoading(true);
|
|
try {
|
|
const isSearch = category === SEARCH_CATEGORY;
|
|
let apiUrl = `/movie/list?page=${page}&limit=${LIMIT}`;
|
|
|
|
if (isSearch) apiUrl += `&search=${encodeURIComponent(search)}`;
|
|
else if (category === '最新添加') apiUrl += `&sort=created_time&order=desc`;
|
|
else apiUrl += `&category=${encodeURIComponent(category)}`;
|
|
|
|
const response = await axios.get(apiUrl);
|
|
const { items, total } = response.data.data;
|
|
const totalPages = Math.ceil(total / LIMIT);
|
|
|
|
if (items.length === 0 && page > 1 && !isSearch && !options.isPopState) {
|
|
console.log("Empty page, current page has no items:", category, page);
|
|
setMovies([]);
|
|
setPagination({ page, total, pages: totalPages });
|
|
} else {
|
|
setMovies(items);
|
|
setPagination({ page, total, pages: totalPages });
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('获取电影数据失败:', error);
|
|
setMovies([]);
|
|
setPagination({ page, total: 0, pages: 1 });
|
|
} finally {
|
|
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;
|
|
}
|
|
// IMPORTANT: Removed duplicated scroll logic block that was here
|
|
}, [isPopStateNav]);
|
|
|
|
const navigateAndFetch = useCallback((newCategory, newPage, newSearchQuery = '', options = {}) => {
|
|
const { replace = false, preserveScroll = false, isPopStatePaging = false } = options;
|
|
|
|
setActiveCategory(newCategory);
|
|
setCurrentPage(newPage);
|
|
setActiveSearchQuery(newSearchQuery);
|
|
|
|
if (newCategory === SEARCH_CATEGORY) {
|
|
setSearchInput(newSearchQuery);
|
|
} else {
|
|
setSearchInput('');
|
|
}
|
|
|
|
const scrollForHistory = preserveScroll
|
|
? (window.history.state?.appState?.scrollPos || window.scrollY)
|
|
: 0;
|
|
|
|
const historyState = {
|
|
category: newCategory,
|
|
page: newPage,
|
|
searchQuery: newSearchQuery,
|
|
scrollPos: scrollForHistory,
|
|
};
|
|
|
|
const url = window.location.pathname;
|
|
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,
|
|
};
|
|
}
|
|
|
|
newCategoryHistory[newCategory] = {
|
|
...newCategoryHistory[newCategory],
|
|
lastPage: newPage,
|
|
scrollPos: scrollForHistory,
|
|
...(newCategory === SEARCH_CATEGORY && { searchQuery: newSearchQuery }),
|
|
};
|
|
|
|
return {
|
|
lastKnownState: historyState,
|
|
categoryHistory: newCategoryHistory,
|
|
};
|
|
});
|
|
|
|
const scrollPosForFetch = preserveScroll ? scrollForHistory : 0;
|
|
|
|
fetchMovies(newCategory, newPage, newSearchQuery, {
|
|
restoreScrollPos: scrollPosForFetch,
|
|
skipScrollRestore: false,
|
|
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]);
|
|
|
|
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, isPopStateNav, scrollTimeoutRef]);
|
|
|
|
|
|
const handleCategoryChange = useCallback((newCategory) => {
|
|
// 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 }
|
|
}
|
|
};
|
|
});
|
|
|
|
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;
|
|
|
|
|
|
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;
|
|
}
|
|
|
|
// 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 }) }
|
|
}
|
|
}));
|
|
|
|
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;
|
|
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;
|
|
});
|
|
navigateAndFetch(SEARCH_CATEGORY, 1, trimmedSearch, { preserveScroll: false });
|
|
}, [searchInput, navigateAndFetch, setPersistedParams]);
|
|
|
|
|
|
const handleClearSearch = useCallback(() => {
|
|
setSearchInput('');
|
|
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 {
|
|
setActiveSearchQuery('');
|
|
setPersistedParams(prev => ({ ...prev, lastKnownState: { ...prev.lastKnownState, searchQuery: '' } }));
|
|
}
|
|
}, [activeCategory, persistedParams.categoryHistory, setPersistedParams, fetchMovies]);
|
|
|
|
|
|
// 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) {
|
|
// 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 for MovieCard to handle Snackbar
|
|
}
|
|
}, [fetchMovies, activeCategory, currentPage, activeSearchQuery]);
|
|
|
|
// handleDelete
|
|
const handleDelete = useCallback(async (filename) => {
|
|
try {
|
|
const response = await axios.post('/movie/delete', { file_name: filename });
|
|
if (response.data.code === 200) {
|
|
// 显示成功消息
|
|
setSuccessMessage(`已成功删除文件:${filename}`);
|
|
|
|
// 重新获取电影列表
|
|
await fetchMovies(activeCategory, currentPage, activeSearchQuery, { skipScrollRestore: true });
|
|
} else {
|
|
throw new Error(response.data.message || '删除失败');
|
|
}
|
|
} catch (error) {
|
|
console.error('删除错误:', error);
|
|
throw error; // Re-throw for MovieCard to handle Snackbar
|
|
}
|
|
}, [fetchMovies, activeCategory, currentPage, activeSearchQuery]);
|
|
|
|
const handleMovieCardClick = useCallback(() => {
|
|
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 = (e) => setSearchInput(e.target.value);
|
|
const handleSearchKeyDown = (e) => {
|
|
if (e.key === 'Enter') handleSearchSubmit();
|
|
};
|
|
|
|
const handleSuccessSnackbarClose = () => {
|
|
setSuccessMessage('');
|
|
};
|
|
|
|
const paginationComponent = !loading && movies.length > 0 && pagination.pages > 1 && (
|
|
<Pagination
|
|
count={pagination.pages}
|
|
page={currentPage}
|
|
onChange={handlePageChange}
|
|
sx={{ my: 2, display: 'flex', justifyContent: 'center' }}
|
|
/>
|
|
);
|
|
|
|
return (
|
|
<Container style={{ marginTop: 20 }}>
|
|
<TextField
|
|
fullWidth
|
|
variant="outlined"
|
|
placeholder="搜索文件名..."
|
|
value={searchInput}
|
|
onChange={handleSearchChange}
|
|
onKeyDown={handleSearchKeyDown}
|
|
InputProps={{
|
|
startAdornment: <InputAdornment position="start"><SearchIcon /></InputAdornment>,
|
|
endAdornment: searchInput && (
|
|
<InputAdornment position="end">
|
|
<IconButton onClick={handleClearSearch} edge="end"><ClearIcon /></IconButton>
|
|
</InputAdornment>
|
|
)
|
|
}}
|
|
sx={{ mb: 2 }}
|
|
/>
|
|
|
|
<CategoryNav
|
|
categories={categories}
|
|
currentCategory={activeCategory}
|
|
onCategoryChange={handleCategoryChange}
|
|
/>
|
|
|
|
{activeCategory === SEARCH_CATEGORY && activeSearchQuery && (
|
|
<div style={{ textAlign: 'center', margin: '10px 0' }}>
|
|
搜索: "{activeSearchQuery}" ({pagination.total} 个结果)
|
|
</div>
|
|
)}
|
|
|
|
{paginationComponent}
|
|
|
|
{loading && <CircularProgress sx={{ display: 'block', margin: '20px auto' }} />}
|
|
|
|
{!loading && movies.length === 0 && (
|
|
<div style={{ textAlign: 'center', margin: '20px 0', color: 'gray' }}>
|
|
{activeCategory === SEARCH_CATEGORY && (searchInput || activeSearchQuery) ? `没有找到关于 "${searchInput || activeSearchQuery}" 的结果。` : '没有找到结果。'}
|
|
</div>
|
|
)}
|
|
|
|
{!loading && movies.length > 0 && (
|
|
<Grid container spacing={2} sx={{ mt: 0.5 }}>
|
|
{movies.map(item => (
|
|
<Grid item xs={6} sm={4} md={3} lg={2} key={item.filename}>
|
|
<Link
|
|
to={`/res/${item.filename}`} // Ensure your routing handles this path
|
|
style={{ textDecoration: 'none' }}
|
|
onClick={handleMovieCardClick}
|
|
>
|
|
<MovieCard movie={item} config={config} onRename={handleRename} onDelete={handleDelete} />
|
|
</Link>
|
|
</Grid>
|
|
))}
|
|
</Grid>
|
|
)}
|
|
|
|
{paginationComponent}
|
|
|
|
{/* 删除成功提示 */}
|
|
<Snackbar
|
|
open={!!successMessage}
|
|
autoHideDuration={4000}
|
|
onClose={handleSuccessSnackbarClose}
|
|
anchorOrigin={{ vertical: 'top', horizontal: 'center' }}
|
|
>
|
|
<Alert onClose={handleSuccessSnackbarClose} severity="success" sx={{ width: '100%' }}>
|
|
{successMessage}
|
|
</Alert>
|
|
</Snackbar>
|
|
</Container>
|
|
);
|
|
};
|
|
|
|
export default Main;
|