import { FormEvent, useEffect, useState } from 'react' import toast from 'react-hot-toast' import { Plus, RefreshCw, Save, Trash2 } from 'lucide-react' import { libraryAPI, type LibraryRootInput } from '../api/library' import type { Library, LibraryRoot } from '../types' import { confirmAction } from '../components/confirmAction' type RootDraft = LibraryRootInput const emptyRootDraft = (): RootDraft => ({ name: '', path: '', enabled: true }) export function AdminLibraryPanel() { const [libs, setLibs] = useState([]) const [name, setName] = useState('') const [roots, setRoots] = useState([emptyRootDraft()]) const [type, setType] = useState('movie') const [newRootByLibrary, setNewRootByLibrary] = useState>({}) const [rootDrafts, setRootDrafts] = useState>({}) const refresh = () => libraryAPI.list({ includeHidden: true }).then(setLibs) useEffect(() => { refresh().catch(() => undefined) }, []) const handleCreate = async (e: FormEvent) => { e.preventDefault() try { const payload = roots .map((root, index) => ({ ...root, path: root.path.trim(), name: root.name?.trim(), sort_order: index })) .filter((root) => root.path) if (payload.length === 0) { toast.error('请至少填写一个路径') return } await libraryAPI.createWithRoots(name, type, payload) toast.success('媒体库已创建') setName('') setRoots([emptyRootDraft()]) await refresh() } catch (err: unknown) { const msg = (err as { response?: { data?: { error?: string } } })?.response?.data?.error ?? '创建失败' toast.error(msg) } } const updateCreateRoot = (index: number, patch: Partial) => { setRoots((prev) => prev.map((root, i) => (i === index ? { ...root, ...patch } : root))) } const addCreateRoot = () => setRoots((prev) => [...prev, emptyRootDraft()]) const removeCreateRoot = (index: number) => { setRoots((prev) => (prev.length <= 1 ? prev : prev.filter((_, i) => i !== index))) } const newRootDraft = (libraryID: string) => newRootByLibrary[libraryID] ?? emptyRootDraft() const setNewRootDraft = (libraryID: string, patch: Partial) => { setNewRootByLibrary((prev) => ({ ...prev, [libraryID]: { ...newRootDraft(libraryID), ...patch } })) } const addLibraryRoot = async (libraryID: string) => { const draft = newRootDraft(libraryID) if (!draft.path?.trim()) { toast.error('请填写路径') return } await libraryAPI.addRoot(libraryID, { ...draft, path: draft.path.trim(), name: draft.name?.trim() }) setNewRootByLibrary((prev) => ({ ...prev, [libraryID]: emptyRootDraft() })) toast.success('路径已添加') await refresh() } const rootDraftKey = (libraryID: string, rootID: string) => `${libraryID}:${rootID}` const editableRootDraft = (libraryID: string, root: LibraryRoot): RootDraft => { const key = rootDraftKey(libraryID, root.id) return rootDrafts[key] ?? { name: root.name ?? '', path: root.path, enabled: root.enabled, sort_order: root.sort_order, } } const setEditableRootDraft = (libraryID: string, root: LibraryRoot, patch: Partial) => { const key = rootDraftKey(libraryID, root.id) setRootDrafts((prev) => ({ ...prev, [key]: { ...editableRootDraft(libraryID, root), ...patch } })) } const saveLibraryRoot = async (libraryID: string, root: LibraryRoot) => { const draft = editableRootDraft(libraryID, root) if (!draft.path?.trim()) { toast.error('请填写路径') return } await libraryAPI.updateRoot(libraryID, root.id, { name: draft.name?.trim(), path: draft.path.trim(), enabled: draft.enabled, sort_order: draft.sort_order, }) setRootDrafts((prev) => { const next = { ...prev } delete next[rootDraftKey(libraryID, root.id)] return next }) toast.success('路径已保存') await refresh() } return (
setName(e.target.value)} />
{roots.map((root, index) => (
updateCreateRoot(index, { name: e.target.value })} /> updateCreateRoot(index, { path: e.target.value })} />
))}

Docker 部署时请优先填写容器内路径,例如 /media/电影、/media/电视剧/国产剧;如果误填 NAS 宿主机路径,系统会尝试按 compose 挂载自动转换。

{libs.map((l) => ( ))}
名称 路径 类型 操作
{l.name}
{(l.roots?.length ? l.roots : [{ id: '', library_id: l.id, path: l.path, enabled: l.enabled, name: '', sort_order: 0, created_at: l.created_at, updated_at: l.updated_at, }]).map((root) => (
{root.id ? ( <> setEditableRootDraft(l.id, root, { name: e.target.value })} /> setEditableRootDraft(l.id, root, { path: e.target.value })} /> ) : ( {root.name ? `${root.name}:${root.path}` : root.path} )}
{root.id && ( )} {root.id && ( )} {root.id && ( )}
))}
setNewRootDraft(l.id, { name: e.target.value })} /> setNewRootDraft(l.id, { path: e.target.value })} />
{l.type}
) }