import { useEffect, useState } from 'react' import { FolderInput, X } from 'lucide-react' import toast from 'react-hot-toast' import { toolsAPI, type OrganizeOverrides } from '../api/tools' import type { Media } from '../types' interface OrganizeMediaDialogProps { open: boolean media: Media | null onClose: () => void onOrganized?: () => void | Promise } // OrganizeMediaDialog 对单个媒体执行「手动整理入库」:把该媒体按命名规范转移到 // 目标媒体目录,并可手动覆盖类型/二级分类/目标路径/转移方式。后端 handler 为 // POST /admin/media/:id/organize(见 organizeMediaHandler)。云盘媒体不支持本地整理。 export function OrganizeMediaDialog({ open, media, onClose, onOrganized }: OrganizeMediaDialogProps) { const [form, setForm] = useState({ media_type: '', media_category: '', dest_path: '', transfer_mode: 'hardlink', scan_after: true, scrape_after: true, dry_run: false, }) const [busy, setBusy] = useState(false) const [preview, setPreview] = useState('') useEffect(() => { if (!open) return setPreview('') setForm((prev) => ({ ...prev, media_type: '', media_category: '', dest_path: '', dry_run: false })) }, [open, media]) if (!open || !media) return null const isCloud = (media.path || '').toLowerCase().startsWith('cloud://') const set = (key: keyof typeof form, value: string | boolean) => { setForm((prev) => ({ ...prev, [key]: value })) } const run = async (dryRun: boolean) => { if (busy) return setBusy(true) setPreview('') try { const opts: OrganizeOverrides = { transfer_mode: form.transfer_mode, scan_after: form.scan_after, scrape_after: form.scrape_after, dry_run: dryRun, } if (form.media_type.trim()) opts.media_type = form.media_type.trim() if (form.media_category.trim()) opts.media_category = form.media_category.trim() if (form.dest_path.trim()) opts.dest_path = form.dest_path.trim() const r = await toolsAPI.organizeMedia(media.id, opts) if (dryRun) { setPreview(r.path || '(未返回目标路径)') toast.success('已生成整理预览') } else { toast.success('整理完成') await onOrganized?.() onClose() } } catch (err: unknown) { const msg = (err as { response?: { data?: { error?: string } } })?.response?.data?.error || '整理失败' toast.error(msg) } finally { setBusy(false) } } return (

整理入库

把「{media.title}」按命名规范转移到媒体目录。留空则自动识别类型与二级分类。

{isCloud && (
该媒体位于云盘库,本地整理无法直接移动云盘文件。请在外部存储中启用云盘转移,或对本地媒体使用本功能。
)} set('media_category', v)} placeholder="如 华语电影 / 国产剧 / 日番" /> set('dest_path', v)} placeholder="如 /media" /> {preview && (
预览目标路径: {preview}
)}
) } function Field({ label, value, onChange, placeholder, }: { label: string value: string onChange: (value: string) => void placeholder?: string }) { return ( ) }