From 1a058a31c80778bafeb5e3e8cc020e9a01257b6e Mon Sep 17 00:00:00 2001 From: eson Date: Wed, 12 Nov 2025 01:21:20 +0800 Subject: [PATCH] =?UTF-8?q?feat(server):=20=E6=B7=BB=E5=8A=A0=E5=88=A0?= =?UTF-8?q?=E9=99=A4=E8=A7=86=E9=A2=91=E6=96=87=E4=BB=B6=E7=9A=84=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 `/movie/delete` 接口用于删除指定视频文件及其缩略图,并从全局电影列表中移除记录。 同时完善了前端删除交互,包括确认对话框、成功提示及错误处理。 - 后端: - 新增 `PostDelete` 处理函数,支持通过文件名删除视频 - 实现 `Movie.DeleteVideo()` 方法,用于删除视频文件和关联缩略图 - 更新路由注册,增加 `/delete` 接口 - 前端: - 新增删除确认弹窗与交互逻辑 - 添加删除成功的 Snackbar 提示 - 调整 MovieCard 按钮布局以容纳删除按钮 - 传递并使用 onDelete 回调函数处理删除操作 该功能增强了系统的文件管理能力,使用户能够安全地移除不需要的视频内容。 --- server/handler.go | 67 ++++++++++++++++++- server/main.go | 5 +- server/movie.go | 26 +++++++- src/Main.jsx | 46 +++++++++++-- src/components/MovieCard.jsx | 121 +++++++++++++++++++++++++++++++++-- 5 files changed, 251 insertions(+), 14 deletions(-) diff --git a/server/handler.go b/server/handler.go index 6eea1c1..668124d 100644 --- a/server/handler.go +++ b/server/handler.go @@ -3,9 +3,9 @@ package main import ( "log" "net/http" + "sort" "strconv" "strings" - "sort" "github.com/gin-gonic/gin" ) @@ -196,4 +196,67 @@ func PostRename(c *gin.Context) { response["message"] = "Rename successful" c.JSON(http.StatusOK, response) -} \ No newline at end of file +} + +// PostDelete 删除文件 +func PostDelete(c *gin.Context) { + var requestData struct { + FileName string `json:"file_name"` + } + + response := gin.H{ + "code": http.StatusOK, + "message": "Success", + "data": nil, + } + + if err := c.ShouldBindJSON(&requestData); err != nil { + response["code"] = http.StatusBadRequest + response["message"] = "Invalid request data: " + err.Error() + c.JSON(http.StatusBadRequest, response) + return + } + + if requestData.FileName == "" { + response["code"] = http.StatusBadRequest + response["message"] = "file_name is required" + c.JSON(http.StatusBadRequest, response) + return + } + + // 查找要删除的电影 + movie, exists := MovieDict[requestData.FileName] + if !exists { + response["code"] = http.StatusNotFound + response["message"] = "Movie not found" + c.JSON(http.StatusNotFound, response) + return + } + + // 执行删除 + if err := movie.DeleteVideo(); err != nil { + response["code"] = http.StatusInternalServerError + response["message"] = "Failed to delete video: " + err.Error() + c.JSON(http.StatusInternalServerError, response) + return + } + + // 从全局Movies切片中移除记录 + MovieDictLock.Lock() + defer MovieDictLock.Unlock() + + // 从MovieDict中删除 + delete(MovieDict, requestData.FileName) + + // 从Movies切片中移除记录 + for i, m := range Movies { + if m.FileName == requestData.FileName { + // 移除切片中的元素 + Movies = append(Movies[:i], Movies[i+1:]...) + break + } + } + + response["message"] = "Delete successful" + c.JSON(http.StatusOK, response) +} diff --git a/server/main.go b/server/main.go index 606197f..9ccd9e8 100644 --- a/server/main.go +++ b/server/main.go @@ -12,7 +12,7 @@ func main() { // 设置为发布模式 gin.SetMode(gin.ReleaseMode) - + eg := gin.Default() eg.Use(Cors()) eg.Static("/res", "movie/") @@ -38,6 +38,7 @@ func main() { movie.GET("/list/category", GetCategoryList) movie.GET("/list", MovieList) movie.POST("/rename", PostRename) + movie.POST("/delete", PostDelete) eg.Run("0.0.0.0:4444") -} \ No newline at end of file +} diff --git a/server/movie.go b/server/movie.go index 4783878..9aceeb4 100644 --- a/server/movie.go +++ b/server/movie.go @@ -71,6 +71,30 @@ func (m *Movie) RenameVideo(newName string) error { return nil } +// DeleteVideo 删除视频文件和对应的缩略图 +func (m *Movie) DeleteVideo() error { + m.mu.Lock() + defer m.mu.Unlock() + + // 删除视频文件 + if err := os.Remove(m.VideoPath); err != nil && !os.IsNotExist(err) { + return fmt.Errorf("删除视频文件失败: %w", err) + } + + // 删除缩略图文件 + dir := filepath.Dir(m.VideoPath) + imagePath := filepath.Join(dir, m.Image) + if _, err := os.Stat(imagePath); err == nil { + if err := os.Remove(imagePath); err != nil && !os.IsNotExist(err) { + log.Printf("警告:删除缩略图文件失败: %s, 错误: %v", imagePath, err) + // 不返回错误,因为主要文件已删除 + } + } + + log.Printf("成功删除视频文件: %s", m.FileName) + return nil +} + var Movies []*Movie // 存储所有电影信息的全局切片 var MovieDict = make(map[string]*Movie) // 存储需要处理缩略图的视频字典 var MovieDictLock sync.Mutex // 保护MovieDict的并发访问 @@ -331,4 +355,4 @@ func markRecentMovies() { for i := 0; i < len(Movies) && i < 20; i++ { Movies[i].TimeCategory = "最新添加" } -} \ No newline at end of file +} diff --git a/src/Main.jsx b/src/Main.jsx index 8e32fcb..51cd82a 100644 --- a/src/Main.jsx +++ b/src/Main.jsx @@ -11,6 +11,8 @@ 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'; @@ -55,6 +57,7 @@ const Main = () => { 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); @@ -410,7 +413,7 @@ const Main = () => { }, [activeCategory, persistedParams.categoryHistory, setPersistedParams, fetchMovies]); - // MODIFIED handleRename + // handleRename const handleRename = useCallback(async (oldName, newName) => { const scrollPosBeforeRename = window.scrollY; // 1. Capture scroll position @@ -432,7 +435,26 @@ const Main = () => { console.error('重命名错误:', error); throw error; // Re-throw for MovieCard to handle Snackbar } - }, [fetchMovies, activeCategory, currentPage, activeSearchQuery]); // Added fetchMovies dependency + }, [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) { @@ -463,6 +485,10 @@ const Main = () => { if (e.key === 'Enter') handleSearchSubmit(); }; + const handleSuccessSnackbarClose = () => { + setSuccessMessage(''); + }; + const paginationComponent = !loading && movies.length > 0 && pagination.pages > 1 && ( { style={{ textDecoration: 'none' }} onClick={handleMovieCardClick} > - + ))} @@ -531,8 +557,20 @@ const Main = () => { )} {paginationComponent} + + {/* 删除成功提示 */} + + + {successMessage} + + ); }; -export default Main; \ No newline at end of file +export default Main; diff --git a/src/components/MovieCard.jsx b/src/components/MovieCard.jsx index 80fa91b..5143f18 100644 --- a/src/components/MovieCard.jsx +++ b/src/components/MovieCard.jsx @@ -15,13 +15,17 @@ import { Alert } from '@mui/material'; import EditIcon from '@mui/icons-material/Edit'; +import DeleteIcon from '@mui/icons-material/Delete'; import { styled } from '@mui/system'; -const MovieCard = ({ movie, config, onRename }) => { +const MovieCard = ({ movie, config, onRename, onDelete }) => { const [newName, setNewName] = useState(''); const [openDialog, setOpenDialog] = useState(false); + const [deleteDialog, setDeleteDialog] = useState(false); + const [deleteConfirmText, setDeleteConfirmText] = useState(''); const [error, setError] = useState(null); const [openSnackbar, setOpenSnackbar] = useState(false); + const [isDeleting, setIsDeleting] = useState(false); const truncateFilename = (filename, maxLength) => { return filename.length > maxLength @@ -40,12 +44,21 @@ const MovieCard = ({ movie, config, onRename }) => { setOpenDialog(true); }; + const handleDeleteClick = (e) => { + e.stopPropagation(); + e.preventDefault(); + setDeleteDialog(true); + setDeleteConfirmText(''); + }; + const handleDialogClose = (e) => { if (e) { e.stopPropagation(); e.preventDefault(); } setOpenDialog(false); + setDeleteDialog(false); + setDeleteConfirmText(''); }; const handleRenameSubmit = async (e) => { @@ -71,7 +84,6 @@ const MovieCard = ({ movie, config, onRename }) => { await onRename(movie.filename, fullNewName); handleDialogClose(); } catch (error) { - // 统一错误处理,优先显示后端返回的 message,其次显示 error.message,最后显示默认错误 setError( error.response?.data?.message || @@ -82,6 +94,28 @@ const MovieCard = ({ movie, config, onRename }) => { } }; + const handleDeleteConfirm = async () => { + if (deleteConfirmText !== '确认删除') { + setError('请输入"确认删除"来确认操作'); + setOpenSnackbar(true); + return; + } + + setIsDeleting(true); + try { + await onDelete(movie.filename); + handleDialogClose(); + } catch (error) { + setError( + error.response?.data?.message || + error.message || + '删除失败,请稍后重试' + ); + setOpenSnackbar(true); + setIsDeleting(false); + } + }; + const handleCloseSnackbar = () => { setOpenSnackbar(false); }; @@ -94,6 +128,14 @@ const MovieCard = ({ movie, config, onRename }) => { } }; + const handleDeleteKeyPress = (e) => { + if (e.key === 'Enter') { + e.stopPropagation(); + e.preventDefault(); + handleDeleteConfirm(); + } + }; + const handleDialogContentClick = (e) => { e.stopPropagation(); }; @@ -124,13 +166,29 @@ const MovieCard = ({ movie, config, onRename }) => { style={{ position: 'absolute', top: 5, - right: 5, + right: 35, // 调整位置为删除按钮留出空间 backgroundColor: 'rgba(255, 255, 255, 0.7)', zIndex: 2, }} > + + + + + { + {/* 重命名对话框 */} { + {/* 删除确认对话框 */} + + 确认删除 + + + ⚠️ 此操作不可逆转! + + + 您即将删除文件:{movie.filename} + + + 为了确认删除,请输入:"确认删除" + + setDeleteConfirmText(e.target.value)} + onKeyPress={handleDeleteKeyPress} + onClick={(e) => e.stopPropagation()} + error={deleteConfirmText && deleteConfirmText !== '确认删除'} + helperText={deleteConfirmText && deleteConfirmText !== '确认删除' ? '请输入"确认删除"' : ''} + /> + + + + + + + { }; export default MovieCard; - -