import { FormEvent, useEffect, useState } from 'react' import { Loader2, Pencil, Plus, Send, Server, Trash2 } from 'lucide-react' import toast from 'react-hot-toast' import { downloadClientsAPI, type DownloadClient, type DownloadClientInput, type DownloadClientType, } from '../api/download_clients' // DownloadClientsPage manages multiple downloader integrations. // Replaces the Vue UI's DownloadView "clients" tab with a typed CRUD // surface and a per-client Test button. export function DownloadClientsPage() { const [clients, setClients] = useState([]) const [loading, setLoading] = useState(true) const [editing, setEditing] = useState(null) const [showForm, setShowForm] = useState(false) const refresh = async () => { setLoading(true) try { setClients(await downloadClientsAPI.list()) } finally { setLoading(false) } } useEffect(() => { refresh().catch(() => undefined) }, []) const onTest = async (id: string) => { try { const r = await downloadClientsAPI.test(id) if (r.ok) toast.success('连接成功') else toast.error(r.error ?? '连接失败') } catch (err: unknown) { const msg = (err as { response?: { data?: { error?: string } } })?.response?.data?.error ?? '测试失败' toast.error(msg) } } const onDelete = async (c: DownloadClient) => { if (!confirm(`确定删除「${c.name}」?`)) return try { await downloadClientsAPI.remove(c.id) toast.success('已删除') await refresh() } catch (err: unknown) { const msg = (err as { response?: { data?: { error?: string } } })?.response?.data?.error ?? '删除失败' toast.error(msg) } } return (

下载器管理

qBittorrent / Aria2 / Transmission · 多客户端 + 连接测试

{loading && (
)} {!loading && clients.length === 0 && (
暂无下载器
)} {!loading && clients.length > 0 && (
{clients.map((c) => (
{c.name} {c.type} {c.is_default && ( 默认 )} {!c.enabled && ( 已禁用 )}
{c.host} {c.username && ` · ${c.username}`}
))}
)} {showForm && ( setShowForm(false)} onSaved={async () => { setShowForm(false) await refresh() }} /> )}
) } function ClientFormModal({ editing, onClose, onSaved, }: { editing: DownloadClient | null onClose: () => void onSaved: () => void | Promise }) { const [form, setForm] = useState(() => ({ name: editing?.name ?? '', type: editing?.type ?? 'qbittorrent', host: editing?.host ?? '', username: editing?.username ?? '', password: '', is_default: editing?.is_default ?? false, enabled: editing?.enabled ?? true, })) const [saving, setSaving] = useState(false) const onSubmit = async (e: FormEvent) => { e.preventDefault() setSaving(true) try { if (editing) await downloadClientsAPI.update(editing.id, form) else await downloadClientsAPI.create(form) toast.success('已保存') await onSaved() } catch (err: unknown) { const msg = (err as { response?: { data?: { error?: string } } })?.response?.data?.error ?? '保存失败' toast.error(msg) } finally { setSaving(false) } } const update = (k: K, v: DownloadClientInput[K]) => setForm((f) => ({ ...f, [k]: v })) const placeholder = ( { qbittorrent: 'http://127.0.0.1:8080', aria2: 'http://127.0.0.1:6800/jsonrpc', transmission: 'http://127.0.0.1:9091/transmission/rpc', } as Record )[form.type] return (

{editing ? '编辑下载器' : '添加下载器'}

update('name', e.target.value)} /> update('host', e.target.value)} /> {form.type !== 'aria2' && ( <> update('username', e.target.value)} /> update('password', e.target.value)} /> )} {form.type === 'aria2' && ( update('password', e.target.value)} /> )}
) } function Field({ label, children }: { label: string; children: React.ReactNode }) { return ( ) }