// src/pages/gallery/index.tsx import { ImageItem } from '@/pages/gallery/typings'; import { deleteImage, fetchImage, getImagesList, getUploadUrl, getVideoUrl, saveFileMetadata, uploadImage, } from '@/services/file/api'; import { getAuthization } from '@/utils/userUtils'; import { PageContainer } from '@ant-design/pro-components'; import { useRequest } from '@umijs/max'; import type { RadioChangeEvent } from 'antd'; import { Empty, Image, message, Modal, Spin } from 'antd'; import type { UploadFile } from 'antd/es/upload/interface'; import React, { FC, useCallback, useEffect, useMemo, useState } from 'react'; import GalleryTable from './components/GalleryTable'; import GalleryToolbar from './components/GalleryToolbar'; import GridView from './components/GridView'; import ListView from './components/ListView'; import './index.css'; const Gallery: FC = () => { const [viewMode, setViewMode] = useState<'small' | 'large' | 'list' | 'table'>('small'); const [page, setPage] = useState(1); const [previewVisible, setPreviewVisible] = useState(false); const [previewCurrent, setPreviewCurrent] = useState(0); const [selectedRowKeys, setSelectedRowKeys] = useState([]); const [batchMode, setBatchMode] = useState(false); const [uploading, setUploading] = useState(false); const [videoPreviewVisible, setVideoPreviewVisible] = useState(false); const [currentVideo, setCurrentVideo] = useState<{ videoUrl: string; thumbnailUrl?: string; } | null>(null); const pageSize = 50; const initPagination = { current: 1, pageSize: 5 }; // 表格模式使用不同的分页状态 const [tablePagination, setTablePagination] = useState(initPagination); // 非表格模式的数据请求 const { data, loading, loadingMore, refresh } = useRequest( () => { return getImagesList({ current: page, pageSize }); }, { loadMore: true, refreshDeps: [page], ready: viewMode !== 'table', // 非表格模式才启用 }, ); // 表格模式的数据请求 const { data: tableData, loading: tableLoading, run: fetchTableData, } = useRequest(async (params) => await getImagesList(params), { manual: true, }); // 当视图模式改变时重置分页 const handleViewModeChange = (e: RadioChangeEvent) => { const newViewMode = e.target.value; setViewMode(newViewMode); // 重置分页 if (newViewMode === 'table') { setTablePagination(initPagination); fetchTableData(initPagination); } else { setPage(1); } // 清除选择 setSelectedRowKeys([]); setBatchMode(false); }; // 处理表格分页变化 const handleTableChange = useCallback( (pagination: any) => { const { current, pageSize } = pagination; setTablePagination({ current, pageSize }); fetchTableData({ current: current, pageSize }); }, [fetchTableData], ); // 当表格分页变化时重新获取数据 useEffect(() => { if (viewMode === 'table') { fetchTableData({ current: tablePagination.current, pageSize: tablePagination.pageSize, }); } }, [viewMode, tablePagination, fetchTableData]); const imageList = useMemo((): ImageItem[] => { if (viewMode === 'table') { return tableData?.list || []; } return data?.list || []; }, [data, tableData, viewMode]); const handleScroll = useCallback( (e: React.UIEvent) => { // 表格模式不需要滚动加载 if (viewMode === 'table') return; const { scrollTop, scrollHeight, clientHeight } = e.currentTarget; if (scrollTop + clientHeight >= scrollHeight - 10 && !loading && !loadingMore) { if (data?.total && imageList.length < data.total) { setPage((prev) => prev + 1); } } }, [loading, loadingMore, data?.total, imageList.length, viewMode], ); // 处理图片预览 const handlePreview = useCallback( async (index: number) => { const item = imageList[index]; if (item.duration || item.thumbnailInstanceId) { // Check if it's a video try { const res = await getVideoUrl(item.instanceId); if (res.code === 200 && res.data) { setCurrentVideo({ videoUrl: res.data, thumbnailUrl: item.thumbnailInstanceId, }); setVideoPreviewVisible(true); } else { message.error('获取视频地址失败'); } } catch (e) { console.error(e); message.error('获取视频地址失败'); } } else { setPreviewCurrent(index); setPreviewVisible(true); } }, [imageList], ); // 关闭预览 const handlePreviewClose = useCallback(() => { setPreviewVisible(false); }, []); // 切换图片 const handlePreviewChange = useCallback((current: number) => { setPreviewCurrent(current); }, []); // 下载图片 const handleDownload = useCallback(async (instanceId: string, imageName?: string) => { try { // 使用项目中已有的fetchImage API,它会自动通过请求拦截器添加认证token const { data: response } = await fetchImage(instanceId); if (response) { // 创建一个临时的URL用于下载 const blob = response; const url = URL.createObjectURL(blob); // 创建一个临时的下载链接 const link = document.createElement('a'); link.href = url; link.download = imageName ?? 'image'; link.click(); // 清理临时URL URL.revokeObjectURL(url); } } catch (error) { console.error('Failed to download image:', error); message.error('下载失败,请重试'); } }, []); const handleVideoUpload = async (file: UploadFile) => { setUploading(true); const hide = message.loading('正在处理视频...', 0); try { // 1. 获取上传 URL const fileName = `${Date.now()}-${file.name}`; const uploadUrlRes = await getUploadUrl(fileName); if (uploadUrlRes.code !== 200) throw new Error('获取上传链接失败'); const uploadUrl = uploadUrlRes.data; // 2. 上传视频到 MinIO await fetch(uploadUrl, { method: 'PUT', body: file as any, }); // 3. 生成缩略图 let thumbnailInstanceId = ''; let duration = 0; try { const videoEl = document.createElement('video'); videoEl.preload = 'metadata'; videoEl.src = URL.createObjectURL(file as any); videoEl.muted = true; videoEl.playsInline = true; await new Promise((resolve, reject) => { videoEl.onloadedmetadata = () => { // 尝试跳转到第1秒以获取更好的缩略图 videoEl.currentTime = Math.min(1, videoEl.duration); }; videoEl.onseeked = () => resolve(true); videoEl.onerror = reject; }); duration = Math.floor(videoEl.duration); const canvas = document.createElement('canvas'); canvas.width = videoEl.videoWidth; canvas.height = videoEl.videoHeight; const ctx = canvas.getContext('2d'); if (ctx) { ctx.drawImage(videoEl, 0, 0, canvas.width, canvas.height); const thumbnailBlob = await new Promise((r) => canvas.toBlob(r, 'image/jpeg', 0.7), ); if (thumbnailBlob) { const thumbName = `thumb-${Date.now()}.jpg`; const thumbUrlRes = await getUploadUrl(thumbName); if (thumbUrlRes.code === 200) { await fetch(thumbUrlRes.data, { method: 'PUT', body: thumbnailBlob }); const thumbMetaRes = await saveFileMetadata({ imageName: thumbName, objectKey: thumbName, size: thumbnailBlob.size, contentType: 'image/jpeg', }); if (thumbMetaRes.code === 200) { thumbnailInstanceId = thumbMetaRes.data; } } } } URL.revokeObjectURL(videoEl.src); } catch (e) { console.warn('缩略图生成失败', e); } // 4. 保存视频元数据 await saveFileMetadata({ imageName: file.name, objectKey: fileName, size: file.size, contentType: file.type || 'video/mp4', thumbnailInstanceId: thumbnailInstanceId, duration: duration, }); message.success('视频上传成功'); // 刷新列表 if (viewMode === 'table') { fetchTableData({ current: tablePagination.current, pageSize: tablePagination.pageSize, }); } else { refresh(); } } catch (error) { console.error(error); message.error('视频上传失败'); } finally { hide(); setUploading(false); } }; // 上传图片/视频 const handleUpload = useCallback( async (file: UploadFile) => { // 检查文件类型 // 优先检查 mime type let isVideo = file.type?.startsWith('video/'); // 如果 mime type 不明确,检查扩展名 if (!isVideo && file.name) { const ext = file.name.split('.').pop()?.toLowerCase(); if (ext && ['mp4', 'mov', 'avi', 'mkv', 'webm', 'flv', 'wmv'].includes(ext)) { isVideo = true; } } if (isVideo) { await handleVideoUpload(file); return; } if (!file.type?.startsWith('image/')) { message.error('只能上传图片或视频文件!'); return; } setUploading(true); try { const formData = new FormData(); formData.append('image', file as any); // 调用上传API await uploadImage(formData); message.success(`${file.name} 上传成功`); // 上传成功后刷新列表 if (viewMode === 'table') { fetchTableData({ current: tablePagination.current, pageSize: tablePagination.pageSize, }); } else { refresh(); } } catch (error) { message.error(`${file.name} 上传失败`); } finally { setUploading(false); } }, [viewMode, tablePagination, fetchTableData, refresh], ); // 删除图片确认 const handleDelete = useCallback( (instanceId: string, imageName: string) => { Modal.confirm({ title: '确认删除', content: `确定要删除图片 "${imageName}" 吗?此操作不可恢复。`, okText: '删除', okType: 'danger', cancelText: '取消', onOk: async () => { try { await deleteImage({ instanceId }); message.success('删除成功'); // 表格模式也需要刷新数据 if (viewMode === 'table') { fetchTableData({ current: tablePagination.current, pageSize: tablePagination.pageSize, }); } else { refresh(); } // 如果在批量选择中,也需要移除 setSelectedRowKeys((prev) => prev.filter((key) => key !== instanceId)); } catch (error) { message.error('删除失败'); } }, }); }, [refresh, viewMode, tablePagination, fetchTableData], ); // 批量删除 const handleBatchDelete = useCallback(() => { if (selectedRowKeys.length === 0) { message.warning('请先选择要删除的图片'); return; } Modal.confirm({ title: '确认批量删除', content: `确定要删除选中的 ${selectedRowKeys.length} 张图片吗?此操作不可恢复。`, okText: '删除', okType: 'danger', cancelText: '取消', onOk: async () => { try { const promises = selectedRowKeys.map((id) => deleteImage({ instanceId: id })); await Promise.all(promises); message.success(`成功删除 ${selectedRowKeys.length} 张图片`); setSelectedRowKeys([]); setBatchMode(false); // 表格模式也需要刷新数据 if (viewMode === 'table') { fetchTableData({ current: tablePagination.current, pageSize: tablePagination.pageSize, }); } else { refresh(); } } catch (error) { message.error('批量删除失败'); } }, }); }, [selectedRowKeys, refresh, viewMode, tablePagination, fetchTableData]); // 批量下载 const handleBatchDownload = useCallback(() => { if (selectedRowKeys.length === 0) { message.warning('请先选择要下载的图片'); return; } selectedRowKeys.forEach((id, index) => { // 添加延迟避免浏览器限制 setTimeout(async () => { const item = imageList.find((img) => img.instanceId === id); if (item) { await handleDownload(id, item.imageName); } }, index * 100); }); message.success(`开始下载 ${selectedRowKeys.length} 张图片`); }, [selectedRowKeys, imageList, handleDownload]); // 切换选择 const handleSelect = useCallback((instanceId: string, checked: boolean) => { setSelectedRowKeys((prev) => { if (checked) { return [...prev, instanceId]; } else { return prev.filter((key) => key !== instanceId); } }); }, []); // 全选/取消全选 const handleSelectAll = useCallback( (checked: boolean) => { if (checked) { setSelectedRowKeys(imageList.map((item) => item.instanceId)); } else { setSelectedRowKeys([]); } }, [imageList], ); // 取消批量操作 const handleCancelBatch = useCallback(() => { setSelectedRowKeys([]); setBatchMode(false); }, []); // 根据视图模式获取当前数据和加载状态 const getCurrentDataAndLoading = () => { if (viewMode === 'table') { return { currentData: tableData, currentLoading: tableLoading }; } else { return { currentData: data, currentLoading: loading }; } }; const { currentLoading } = getCurrentDataAndLoading(); // 渲染视图 const renderView = () => { switch (viewMode) { case 'table': return ( ); case 'list': return ( ); default: return ( ); } }; return (
setBatchMode(true)} onCancelBatch={handleCancelBatch} onBatchDownload={handleBatchDownload} onBatchDelete={handleBatchDelete} onUpload={handleUpload} uploading={uploading} /> } > {currentLoading && ((viewMode === 'table' && tablePagination.current === 1) || (viewMode !== 'table' && page === 1)) ? (
) : imageList.length === 0 ? ( ) : ( renderView() )} {/* 视频预览 Modal */} { setVideoPreviewVisible(false); setCurrentVideo(null); }} width={800} destroyOnClose centered > {currentVideo && ( {/* 预览组件 - 使用认证后的图像URL */} setPreviewVisible(visible), onChange: handlePreviewChange, }} > {imageList.map((item: ImageItem) => ( {item.imageName} ))}
); }; export default Gallery;