import { useState, useEffect, useCallback } from 'react'; import { Button, Space, Input, Select, TreeSelect, Tree, Upload, Modal, Form, Switch, message, Popconfirm, Empty, Card, Checkbox, Typography, Dropdown, Spin, } from 'antd'; import { UploadOutlined, FolderAddOutlined, EditOutlined, DeleteOutlined, EllipsisOutlined, InboxOutlined, ReloadOutlined, } from '@ant-design/icons'; import type { UploadProps } from 'antd'; import { resolveMediaUrl } from '../../utils/media'; import { mediaApi, mediaFolderApi, type MediaItem, type FolderItem } from '../../api/health/media'; import { AuthButton } from '../../components/AuthButton'; import { formatDateTime } from '../../utils/format'; // --- 工具函数 --- const CT_OPTIONS = [ { value: '', label: '全部类型' }, { value: 'image/', label: '图片' }, { value: 'video/', label: '视频' }, { value: 'application/', label: '文档' }, ]; const formatSize = (b: number) => b > 1048576 ? (b / 1048576).toFixed(1) + 'MB' : b > 1024 ? (b / 1024).toFixed(1) + 'KB' : b + 'B'; const isImage = (ct: string) => ct.startsWith('image/'); interface TreeNode { id: string; name: string; children: TreeNode[] } function buildTree(folders: FolderItem[]): TreeNode[] { return folders.map((f) => ({ id: f.id, name: `${f.name} (${f.item_count})`, children: buildTree(f.children) })); } interface EditFormValues { filename: string; alt_text?: string; is_public: boolean } interface FolderFormValues { name: string } // =========================================================================== export default function MediaLibrary() { const [items, setItems] = useState([]); const [total, setTotal] = useState(0); const [page, setPage] = useState(1); const pageSize = 24; const [loading, setLoading] = useState(false); const [keyword, setKeyword] = useState(''); const [contentType, setContentType] = useState(''); const [folderId, setFolderId] = useState(); const [folders, setFolders] = useState([]); const [foldersLoading, setFoldersLoading] = useState(false); const [selectedIds, setSelectedIds] = useState>(new Set()); const [uploadOpen, setUploadOpen] = useState(false); const [editOpen, setEditOpen] = useState(false); const [moveOpen, setMoveOpen] = useState(false); const [folderOpen, setFolderOpen] = useState(false); const [editingItem, setEditingItem] = useState(null); const [movingItem, setMovingItem] = useState(null); const [editingFolder, setEditingFolder] = useState(null); const [uploadForm] = Form.useForm(); const [editForm] = Form.useForm(); const [folderForm] = Form.useForm(); const [submitting, setSubmitting] = useState(false); const fetchMedia = useCallback(async () => { setLoading(true); try { const r = await mediaApi.list({ page, page_size: pageSize, folder_id: folderId, content_type: contentType || undefined, keyword: keyword || undefined }); setItems(r.data); setTotal(r.total); } catch { message.error('加载媒体列表失败'); } finally { setLoading(false); } }, [page, pageSize, folderId, contentType, keyword]); const fetchFolders = useCallback(async () => { setFoldersLoading(true); try { setFolders(await mediaFolderApi.tree()); } catch { message.error('加载文件夹失败'); } finally { setFoldersLoading(false); } }, []); useEffect(() => { fetchMedia(); }, [fetchMedia]); useEffect(() => { fetchFolders(); }, [fetchFolders]); const resetPage = () => { setPage(1); setSelectedIds(new Set()); }; const handleFolderSelect = (keys: React.Key[]) => { setFolderId(keys.length ? (keys[0] as string) : undefined); resetPage(); }; const handleUpload: UploadProps['customRequest'] = async (opts) => { const { file, onSuccess, onError } = opts; try { const fid = uploadForm.getFieldValue('folder_id') as string | undefined; const pub = uploadForm.getFieldValue('is_public') as boolean; const fd = new FormData(); fd.append('file', file as File); if (fid) fd.append('folder_id', fid); fd.append('is_public', String(pub ?? false)); await mediaApi.upload(fd); onSuccess?.(null); message.success('上传成功'); fetchMedia(); fetchFolders(); } catch (e) { onError?.(e as Error); message.error('上传失败'); } }; const openEdit = (item: MediaItem) => { setEditingItem(item); editForm.setFieldsValue({ filename: item.filename, alt_text: item.alt_text, is_public: item.is_public }); setEditOpen(true); }; const handleEditSubmit = async (v: EditFormValues) => { if (!editingItem) return; setSubmitting(true); try { await mediaApi.update(editingItem.id, { ...v, version: editingItem.version }); message.success('更新成功'); setEditOpen(false); fetchMedia(); } catch { message.error('更新失败'); } finally { setSubmitting(false); } }; const handleMove = async (targetId: string | undefined) => { if (!movingItem) return; try { await mediaApi.move(movingItem.id, { folder_id: targetId, version: movingItem.version }); message.success('移动成功'); setMoveOpen(false); fetchMedia(); fetchFolders(); } catch { message.error('移动失败'); } }; const handleDelete = async (item: MediaItem) => { try { await mediaApi.delete(item.id, item.version); message.success('删除成功'); fetchMedia(); fetchFolders(); } catch { message.error('删除失败'); } }; const handleBatchDelete = async () => { if (!selectedIds.size) return; try { await mediaApi.batchDelete([...selectedIds]); message.success(`已删除 ${selectedIds.size} 个文件`); setSelectedIds(new Set()); fetchMedia(); fetchFolders(); } catch { message.error('批量删除失败'); } }; const openCreateFolder = () => { setEditingFolder(null); folderForm.resetFields(); setFolderOpen(true); }; const openRenameFolder = (f: FolderItem) => { setEditingFolder(f); folderForm.setFieldsValue({ name: f.name }); setFolderOpen(true); }; const handleFolderSubmit = async (v: FolderFormValues) => { setSubmitting(true); try { if (editingFolder) { await mediaFolderApi.update(editingFolder.id, { name: v.name, version: editingFolder.version }); message.success('文件夹已重命名'); } else { await mediaFolderApi.create({ name: v.name, parent_id: folderId }); message.success('文件夹已创建'); } setFolderOpen(false); fetchFolders(); } catch { message.error(editingFolder ? '重命名失败' : '创建失败'); } finally { setSubmitting(false); } }; const handleDeleteFolder = async (f: FolderItem) => { try { await mediaFolderApi.delete(f.id, f.version); message.success('文件夹已删除'); if (folderId === f.id) setFolderId(undefined); fetchFolders(); fetchMedia(); } catch { message.error('删除文件夹失败(可能不为空)'); } }; const toggleSelect = (id: string) => setSelectedIds((prev) => { const n = new Set(prev); if (n.has(id)) { n.delete(id); } else { n.add(id); } return n; }); const renderCard = (item: MediaItem) => { const sel = selectedIds.has(item.id); const actions = [ { key: 'edit', label: '编辑信息', onClick: () => openEdit(item) }, { key: 'move', label: '移动到...', onClick: () => { setMovingItem(item); setMoveOpen(true); } }, { key: 'delete', label: '删除', danger: true as const, onClick: () => handleDelete(item) }, ]; return ( toggleSelect(item.id)} style={{ height: 140, display: 'flex', alignItems: 'center', justifyContent: 'center', background: 'var(--ant-color-fill-quaternary, #f5f5f5)', overflow: 'hidden', position: 'relative', cursor: 'pointer' }}> {isImage(item.content_type) ? ( {item.alt_text ) : ( )} {sel &&
}
}>
{item.filename} {formatSize(item.file_size)} · {formatDateTime(item.created_at)}
); }; const treeData = [{ id: '__all__', name: '全部文件', children: buildTree(folders) }]; const pageCount = Math.ceil(total / pageSize); return (
{/* 左侧 — 文件夹树 */}
文件夹
{foldersLoading ?
: ( { if (node.id === '__all__') return {node.name}; const matched = folders.find((f) => f.id === node.id); return (
{node.name} {matched &&
); }} /> )}
{/* 右侧 — 媒体列表 */}
{/* 工具栏 */}
{selectedIds.size > 0 && <> } { setKeyword(v); resetPage(); }} /> {/* 移动弹窗 */} setMoveOpen(false)} footer={null} destroyOnHidden>
选择目标文件夹:
{folders.map((f) => )}
{/* 文件夹创建/重命名弹窗 */} setFolderOpen(false)} onOk={() => folderForm.submit()} confirmLoading={submitting} destroyOnHidden>
); }