/* eslint-disable @typescript-eslint/no-explicit-any */ 'use client'; import { AlertCircle, Loader2, Plus, RefreshCw, Trash2, X } from 'lucide-react'; import { useEffect, useState } from 'react'; import { createPortal } from 'react-dom'; import { AdminConfig } from '@/lib/admin.types'; import { AnimeSubscription, AnimeSubscriptionDownloadTool } from '@/types/anime-subscription'; interface AnimeSubscriptionComponentProps { config: AdminConfig | null; refreshConfig: () => Promise; } const downloadToolOptions: Array<{ value: AnimeSubscriptionDownloadTool; label: string }> = [ { value: 'aria2', label: 'aria2' }, { value: 'qBittorrent', label: 'qBittorrent' }, { value: 'Transmission', label: 'Transmission' }, ]; // Switch 组件 const Switch = ({ checked, onChange, disabled }: { checked: boolean; onChange: (checked: boolean) => void; disabled?: boolean }) => ( ); // AlertModal 组件 interface AlertModalProps { isOpen: boolean; onClose: () => void; type: 'success' | 'error' | 'warning' | 'info'; title: string; message?: string; confirmText?: string; onConfirm?: () => void; showConfirm?: boolean; } const AlertModal = ({ isOpen, onClose, type, title, message, confirmText = '确定', onConfirm, showConfirm = false, }: AlertModalProps) => { const [isVisible, setIsVisible] = useState(false); useEffect(() => { if (isOpen) { setIsVisible(true); } else { setIsVisible(false); } }, [isOpen]); if (!isOpen) return null; const icons = { success: , error: , warning: , info: , }; return createPortal(
{icons[type]}

{title}

{message && (

{message}

)}
{showConfirm && onConfirm ? ( <> ) : ( )}
, document.body ); }; export default function AnimeSubscriptionComponent({ config, refreshConfig, }: AnimeSubscriptionComponentProps) { const [enabled, setEnabled] = useState(false); const [downloadTool, setDownloadTool] = useState('aria2'); const [subscriptions, setSubscriptions] = useState([]); const [loading, setLoading] = useState(false); const [showAddForm, setShowAddForm] = useState(false); const [editingSubscription, setEditingSubscription] = useState(null); const [checkingId, setCheckingId] = useState(null); const [alertModal, setAlertModal] = useState<{ isOpen: boolean; type: 'success' | 'error' | 'warning' | 'info'; title: string; message?: string; confirmText?: string; onConfirm?: () => void; showConfirm?: boolean; }>({ isOpen: false, type: 'success', title: '', }); const showAlert = (config: Omit) => { setAlertModal({ ...config, isOpen: true }); }; const hideAlert = () => { setAlertModal(prev => ({ ...prev, isOpen: false })); }; // 表单状态 const [formData, setFormData] = useState({ title: '', filterText: '', source: 'mikan' as 'acgrip' | 'mikan' | 'dmhy', lastEpisode: 0, enabled: true, }); // 加载配置 useEffect(() => { if (config?.AnimeSubscriptionConfig) { setEnabled(config.AnimeSubscriptionConfig.Enabled || false); setDownloadTool(config.AnimeSubscriptionConfig.DownloadTool || 'aria2'); setSubscriptions(config.AnimeSubscriptionConfig.Subscriptions || []); } }, [config]); // 重置表单 const resetForm = () => { setFormData({ title: '', filterText: '', source: 'mikan', lastEpisode: 0, enabled: true, }); setEditingSubscription(null); setShowAddForm(false); }; // 切换启用状态 const handleToggleEnabled = async (newEnabled: boolean) => { try { setLoading(true); const response = await fetch('/api/admin/anime-subscription/toggle', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ enabled: newEnabled, downloadTool }), }); if (!response.ok) { throw new Error('切换状态失败'); } setEnabled(newEnabled); await refreshConfig(); } catch (error) { showAlert({ type: 'error', title: '切换状态失败', message: error instanceof Error ? error.message : '切换状态失败', }); } finally { setLoading(false); } }; const handleDownloadToolChange = async (newDownloadTool: AnimeSubscriptionDownloadTool) => { const previousDownloadTool = downloadTool; setDownloadTool(newDownloadTool); try { setLoading(true); const response = await fetch('/api/admin/anime-subscription/toggle', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ enabled, downloadTool: newDownloadTool }), }); if (!response.ok) { throw new Error('保存下载方式失败'); } await refreshConfig(); } catch (error) { setDownloadTool(previousDownloadTool); showAlert({ type: 'error', title: '保存失败', message: error instanceof Error ? error.message : '保存下载方式失败', }); } finally { setLoading(false); } }; // 开始添加 const handleAdd = () => { resetForm(); setShowAddForm(true); }; // 开始编辑 const handleEdit = (sub: AnimeSubscription) => { setFormData({ title: sub.title, filterText: sub.filterText, source: sub.source, lastEpisode: sub.lastEpisode, enabled: sub.enabled, }); setEditingSubscription(sub); setShowAddForm(false); }; // 保存订阅 const handleSave = async () => { if (!formData.title.trim() || !formData.filterText.trim()) { showAlert({ type: 'warning', title: '请填写必填字段', message: '番剧名称和过滤关键词不能为空', }); return; } try { setLoading(true); if (editingSubscription) { // 更新 const response = await fetch(`/api/admin/anime-subscription/${editingSubscription.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(formData), }); if (!response.ok) { throw new Error('更新订阅失败'); } } else { // 创建 const response = await fetch('/api/admin/anime-subscription', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(formData), }); if (!response.ok) { throw new Error('创建订阅失败'); } } resetForm(); await refreshConfig(); showAlert({ type: 'success', title: editingSubscription ? '订阅已更新' : '订阅已创建', }); } catch (error) { showAlert({ type: 'error', title: '保存失败', message: error instanceof Error ? error.message : '保存失败', }); } finally { setLoading(false); } }; // 删除订阅 const handleDelete = async (id: string, title: string) => { showAlert({ type: 'warning', title: '确认删除', message: `确定要删除订阅"${title}"吗?`, confirmText: '删除', showConfirm: true, onConfirm: async () => { try { setLoading(true); const response = await fetch(`/api/admin/anime-subscription/${id}`, { method: 'DELETE', }); if (!response.ok) { throw new Error('删除订阅失败'); } await refreshConfig(); showAlert({ type: 'success', title: '订阅已删除', }); } catch (error) { showAlert({ type: 'error', title: '删除失败', message: error instanceof Error ? error.message : '删除失败', }); } finally { setLoading(false); } }, }); }; // 手动检查更新 const handleCheckSubscription = async (id: string) => { try { setCheckingId(id); const response = await fetch(`/api/admin/anime-subscription/${id}/check`, { method: 'POST', }); if (!response.ok) { throw new Error('检查失败'); } const result = await response.json(); showAlert({ type: 'success', title: '检查完成', message: `发现 ${result.found} 个新集数,已下载 ${result.downloaded} 个`, }); await refreshConfig(); } catch (error) { showAlert({ type: 'error', title: '检查失败', message: error instanceof Error ? error.message : '检查失败', }); } finally { setCheckingId(null); } }; // 切换订阅启用状态 const handleToggleSubscription = async (sub: AnimeSubscription) => { try { setLoading(true); const response = await fetch(`/api/admin/anime-subscription/${sub.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ enabled: !sub.enabled }), }); if (!response.ok) { throw new Error('切换状态失败'); } await refreshConfig(); } catch (error) { showAlert({ type: 'error', title: '切换状态失败', message: error instanceof Error ? error.message : '切换状态失败', }); } finally { setLoading(false); } }; const formatTime = (timestamp: number) => { if (!timestamp) return '从未'; const now = Date.now(); const diff = now - timestamp; const minutes = Math.floor(diff / 60000); if (minutes < 1) return '刚刚'; if (minutes < 60) return `${minutes}分钟前`; const hours = Math.floor(minutes / 60); if (hours < 24) return `${hours}小时前`; const days = Math.floor(hours / 24); return `${days}天前`; }; return (
{/* 顶部控制 */}
启用追番功能
{/* 说明 */}

• 定时任务会自动检查订阅更新

• 下载路径:OpenList离线下载根目录/番剧名称/

• 过滤关键词支持多个,用逗号分隔,只会下载包含这些关键字的资源,可以用来过滤字幕组或是字幕种类

• 当前集数:已看到第几集,只下载更新的集数

{/* 添加/编辑表单 */} {(showAddForm || editingSubscription) && (

{editingSubscription ? '编辑订阅' : '添加订阅'}

setFormData({ ...formData, title: e.target.value })} placeholder='葬送的芙莉莲' className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-green-500' />
setFormData({ ...formData, filterText: e.target.value })} placeholder='简体,喵萌奶茶屋' className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-green-500' />

多个关键词用逗号分隔

setFormData({ ...formData, lastEpisode: parseInt(e.target.value) || 0 })} className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-green-500' />

已看到第几集

启用此订阅 setFormData({ ...formData, enabled: checked })} />
)} {/* 订阅列表 */} {subscriptions.length === 0 ? (
暂无订阅,点击"添加订阅"开始追番
) : (
{subscriptions.map((sub) => (
{/* 桌面端布局 */}

{sub.title}

{sub.source === 'acgrip' ? 'ACG.RIP' : sub.source === 'mikan' ? '蜜柑' : '动漫花园'}

过滤条件:{sub.filterText}

当前集数:第 {sub.lastEpisode} 集

上次检查:{formatTime(sub.lastCheckTime)}

handleToggleSubscription(sub)} disabled={loading} />
{/* 移动端布局 */}

{sub.title}

{sub.source === 'acgrip' ? 'ACG.RIP' : sub.source === 'mikan' ? '蜜柑' : '动漫花园'}
handleToggleSubscription(sub)} disabled={loading} />

过滤:{sub.filterText}

集数:第 {sub.lastEpisode} 集 · {formatTime(sub.lastCheckTime)}

))}
)} {/* AlertModal */}
); }