/* eslint-disable @typescript-eslint/no-explicit-any, no-console, @typescript-eslint/no-non-null-assertion,react-hooks/exhaustive-deps,@typescript-eslint/no-empty-function */ 'use client'; import { closestCenter, DndContext, PointerSensor, TouchSensor, useSensor, useSensors, } from '@dnd-kit/core'; import { restrictToParentElement, restrictToVerticalAxis, } from '@dnd-kit/modifiers'; import { arrayMove, SortableContext, useSortable, verticalListSortingStrategy, } from '@dnd-kit/sortable'; import { CSS } from '@dnd-kit/utilities'; import { AlertCircle, AlertTriangle, BookMarked, BookOpen, Bot, Cat, Check, CheckCircle, ChevronDown, ChevronUp, Cloud, Database, ExternalLink, FileText, FolderOpen, Globe, Mail, Palette, Plus, Search, Settings, Trash2, Tv, UserPlus, Users, Video, X, } from 'lucide-react'; import { GripVertical } from 'lucide-react'; import { Fragment, memo, Suspense, useCallback, useEffect, useMemo, useRef, useState, } from 'react'; import { createPortal } from 'react-dom'; import { AdminConfig, AdminConfigResult } from '@/lib/admin.types'; import { getAuthInfoFromBrowserCookie } from '@/lib/auth'; import { BookSource } from '@/lib/book.types'; import { ALL_FEATURE_PERMISSION_KEYS, FEATURE_PERMISSION_OPTIONS, } from '@/lib/feature-permissions'; import AnimeSubscriptionComponent from '@/components/AnimeSubscriptionComponent'; import CorrectDialog from '@/components/CorrectDialog'; import DataMigration from '@/components/DataMigration'; import PageLayout from '@/components/PageLayout'; // 统一按钮样式系统 const buttonStyles = { // 主要操作按钮(蓝色)- 用于配置、设置、确认等 primary: 'px-3 py-1.5 text-sm font-medium bg-blue-600 hover:bg-blue-700 dark:bg-blue-600 dark:hover:bg-blue-700 text-white rounded-lg transition-colors', // 成功操作按钮(绿色)- 用于添加、启用、保存等 success: 'px-3 py-1.5 text-sm font-medium bg-green-600 hover:bg-green-700 dark:bg-green-600 dark:hover:bg-green-700 text-white rounded-lg transition-colors', // 危险操作按钮(红色)- 用于删除、禁用、重置等 danger: 'px-3 py-1.5 text-sm font-medium bg-red-600 hover:bg-red-700 dark:bg-red-600 dark:hover:bg-red-700 text-white rounded-lg transition-colors', // 次要操作按钮(灰色)- 用于取消、关闭等 secondary: 'px-3 py-1.5 text-sm font-medium bg-gray-600 hover:bg-gray-700 dark:bg-gray-600 dark:hover:bg-gray-700 text-white rounded-lg transition-colors', // 警告操作按钮(黄色)- 用于批量禁用等 warning: 'px-3 py-1.5 text-sm font-medium bg-yellow-600 hover:bg-yellow-700 dark:bg-yellow-600 dark:hover:bg-yellow-700 text-white rounded-lg transition-colors', // 小尺寸主要按钮 primarySmall: 'px-2 py-1 text-xs font-medium bg-blue-600 hover:bg-blue-700 dark:bg-blue-600 dark:hover:bg-blue-700 text-white rounded-md transition-colors', // 小尺寸成功按钮 successSmall: 'px-2 py-1 text-xs font-medium bg-green-600 hover:bg-green-700 dark:bg-green-600 dark:hover:bg-green-700 text-white rounded-md transition-colors', // 小尺寸危险按钮 dangerSmall: 'px-2 py-1 text-xs font-medium bg-red-600 hover:bg-red-700 dark:bg-red-600 dark:hover:bg-red-700 text-white rounded-md transition-colors', // 小尺寸次要按钮 secondarySmall: 'px-2 py-1 text-xs font-medium bg-gray-600 hover:bg-gray-700 dark:bg-gray-600 dark:hover:bg-gray-700 text-white rounded-md transition-colors', // 小尺寸警告按钮 warningSmall: 'px-2 py-1 text-xs font-medium bg-yellow-600 hover:bg-yellow-700 dark:bg-yellow-600 dark:hover:bg-yellow-700 text-white rounded-md transition-colors', // 圆角小按钮(用于表格操作) roundedPrimary: 'inline-flex items-center px-3 py-1.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800 hover:bg-blue-200 dark:bg-blue-900/40 dark:hover:bg-blue-900/60 dark:text-blue-200 transition-colors', roundedSuccess: 'inline-flex items-center px-3 py-1.5 rounded-full text-xs font-medium bg-green-100 text-green-800 hover:bg-green-200 dark:bg-green-900/40 dark:hover:bg-green-900/60 dark:text-green-200 transition-colors', roundedDanger: 'inline-flex items-center px-3 py-1.5 rounded-full text-xs font-medium bg-red-100 text-red-800 hover:bg-red-200 dark:bg-red-900/40 dark:hover:bg-red-900/60 dark:text-red-200 transition-colors', roundedSecondary: 'inline-flex items-center px-3 py-1.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800 hover:bg-gray-200 dark:bg-gray-700/40 dark:hover:bg-gray-700/60 dark:text-gray-200 transition-colors', roundedWarning: 'inline-flex items-center px-3 py-1.5 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800 hover:bg-yellow-200 dark:bg-yellow-900/40 dark:hover:bg-yellow-900/60 dark:text-yellow-200 transition-colors', roundedPurple: 'inline-flex items-center px-3 py-1.5 rounded-full text-xs font-medium bg-purple-100 text-purple-800 hover:bg-purple-200 dark:bg-purple-900/40 dark:hover:bg-purple-900/60 dark:text-purple-200 transition-colors', // 禁用状态 disabled: 'px-3 py-1.5 text-sm font-medium bg-gray-400 dark:bg-gray-600 cursor-not-allowed text-white rounded-lg transition-colors', disabledSmall: 'px-2 py-1 text-xs font-medium bg-gray-400 dark:bg-gray-600 cursor-not-allowed text-white rounded-md transition-colors', // 开关按钮样式 toggleOn: 'bg-green-600 dark:bg-green-600', toggleOff: 'bg-gray-200 dark:bg-gray-700', toggleThumb: 'bg-white', toggleThumbOn: 'translate-x-6', toggleThumbOff: 'translate-x-1', // 快速操作按钮样式 quickAction: 'px-3 py-1.5 text-xs font-medium text-gray-600 dark:text-gray-400 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700 rounded-md transition-colors', }; const DEFAULT_GROUP_PERMISSIONS = [...ALL_FEATURE_PERMISSION_KEYS]; // 通用弹窗组件 interface AlertModalProps { isOpen: boolean; onClose: () => void; type: 'success' | 'error' | 'warning'; title: string; message?: string; timer?: number; showConfirm?: boolean; onConfirm?: () => void; } const AlertModal = ({ isOpen, onClose, type, title, message, timer, showConfirm = false, onConfirm, }: AlertModalProps) => { const [isVisible, setIsVisible] = useState(false); useEffect(() => { if (isOpen) { setIsVisible(true); if (timer) { setTimeout(() => { onClose(); }, timer); } } else { setIsVisible(false); } }, [isOpen, timer, onClose]); if (!isOpen) return null; const getIcon = () => { switch (type) { case 'success': return ; case 'error': return ; case 'warning': return ; default: return null; } }; const getBgColor = () => { switch (type) { case 'success': return 'bg-green-50 dark:bg-green-900/20 border-green-200 dark:border-green-800'; case 'error': return 'bg-red-50 dark:bg-red-900/20 border-red-200 dark:border-red-800'; case 'warning': return 'bg-yellow-50 dark:bg-yellow-900/20 border-yellow-200 dark:border-yellow-800'; default: return 'bg-blue-50 dark:bg-blue-900/20 border-blue-200 dark:border-blue-800'; } }; return createPortal(
{getIcon()}

{title}

{message && (

{message}

)} {showConfirm ? ( onConfirm ? ( // 确认操作:显示取消和确定按钮
) : ( // 普通提示:只显示确定按钮 ) ) : null}
, document.body ); }; // 弹窗状态管理 const useAlertModal = () => { const [alertModal, setAlertModal] = useState<{ isOpen: boolean; type: 'success' | 'error' | 'warning'; title: string; message?: string; timer?: number; showConfirm?: boolean; onConfirm?: () => void; }>({ isOpen: false, type: 'success', title: '', }); const showAlert = (config: Omit) => { setAlertModal({ ...config, isOpen: true }); }; const hideAlert = () => { setAlertModal((prev) => ({ ...prev, isOpen: false })); }; return { alertModal, showAlert, hideAlert }; }; // 统一弹窗方法(必须在首次使用前定义) const showError = (message: string, showAlert?: (config: any) => void) => { if (showAlert) { showAlert({ type: 'error', title: '错误', message, showConfirm: true }); } else { console.error(message); } }; const showSuccess = (message: string, showAlert?: (config: any) => void) => { if (showAlert) { showAlert({ type: 'success', title: '成功', message, timer: 2000 }); } else { console.log(message); } }; // 通用加载状态管理系统 interface LoadingState { [key: string]: boolean; } const useLoadingState = () => { const [loadingStates, setLoadingStates] = useState({}); const setLoading = (key: string, loading: boolean) => { setLoadingStates((prev) => ({ ...prev, [key]: loading })); }; const isLoading = (key: string) => loadingStates[key] || false; const withLoading = async ( key: string, operation: () => Promise ): Promise => { setLoading(key, true); try { const result = await operation(); return result; } finally { setLoading(key, false); } }; return { loadingStates, setLoading, isLoading, withLoading }; }; interface StandaloneSourceScript { id: string; key: string; name: string; description?: string; enabled: boolean; version: string; code: string; createdAt: number; updatedAt: number; } // 新增站点配置类型 interface SiteConfig { SiteName: string; Announcement: string; SearchDownstreamMaxPage: number; SiteInterfaceCacheTime: number; DoubanProxyType: string; DoubanProxy: string; DoubanImageProxyType: string; DoubanImageProxy: string; DisableYellowFilter: boolean; FluidSearch: boolean; DanmakuSourceType?: 'builtin' | 'custom'; DanmakuApiBase: string; DanmakuApiToken: string; DanmakuAutoLoadDefault?: boolean; TMDBApiKey?: string; TMDBProxy?: string; TMDBReverseProxy?: string; BannerDataSource?: string; RecommendationDataSource?: string; PansouApiUrl?: string; PansouUsername?: string; PansouPassword?: string; PansouKeywordBlocklist?: string; MagnetProxy?: string; MagnetMikanReverseProxy?: string; MagnetDmhyReverseProxy?: string; MagnetAcgripReverseProxy?: string; EnableComments: boolean; EnableRegistration?: boolean; RequireRegistrationInviteCode?: boolean; RegistrationInviteCode?: string; RegistrationRequireTurnstile?: boolean; LoginRequireTurnstile?: boolean; TurnstileSiteKey?: string; TurnstileSecretKey?: string; DefaultUserTags?: string[]; EnableOIDCLogin?: boolean; EnableOIDCRegistration?: boolean; OIDCIssuer?: string; OIDCAuthorizationEndpoint?: string; OIDCTokenEndpoint?: string; OIDCUserInfoEndpoint?: string; OIDCClientId?: string; OIDCClientSecret?: string; OIDCButtonText?: string; } // 视频源数据类型 interface DataSource { name: string; key: string; api: string; detail?: string; disabled?: boolean; from: 'config' | 'custom'; proxyMode?: boolean; weight?: number; } // 直播源数据类型 interface LiveDataSource { name: string; key: string; url: string; ua?: string; epg?: string; channelNumber?: number; disabled?: boolean; from: 'config' | 'custom'; proxyMode?: 'full' | 'm3u8-only' | 'direct'; // 代理模式 } // 自定义分类数据类型 interface CustomCategory { name?: string; type: 'movie' | 'tv'; query: string; disabled?: boolean; from: 'config' | 'custom'; } // 可折叠标签组件 interface CollapsibleTabProps { title: string; icon?: React.ReactNode; isExpanded: boolean; onToggle: () => void; children: React.ReactNode; isParent?: boolean; } const CollapsibleTab = ({ title, icon, isExpanded, onToggle, children, isParent = false, }: CollapsibleTabProps) => { return (
{isExpanded && (
{children}
)}
); }; // 用户配置组件 interface UserConfigProps { config: AdminConfig | null; role: 'owner' | 'admin' | null; refreshConfig: () => Promise; usersV2: Array<{ username: string; role: 'owner' | 'admin' | 'user'; banned: boolean; tags?: string[]; oidcSub?: string; enabledApis?: string[]; created_at: number; }> | null; userPage: number; userTotalPages: number; userTotal: number; fetchUsersV2: (page: number, search?: string) => Promise; userListLoading: boolean; userSearch: string; setUserSearch: (value: string) => void; } const UserConfig = ({ config, role, refreshConfig, usersV2, userPage, userTotalPages, userTotal, fetchUsersV2, userListLoading, userSearch, setUserSearch, }: UserConfigProps) => { const { alertModal, showAlert, hideAlert } = useAlertModal(); const { isLoading, withLoading } = useLoadingState(); const [showAddUserForm, setShowAddUserForm] = useState(false); const [showChangePasswordForm, setShowChangePasswordForm] = useState(false); const [showAddUserGroupForm, setShowAddUserGroupForm] = useState(false); const [showEditUserGroupForm, setShowEditUserGroupForm] = useState(false); const [newUser, setNewUser] = useState({ username: '', password: '', userGroup: '', // 新增用户组字段 }); const [changePasswordUser, setChangePasswordUser] = useState({ username: '', password: '', }); const [newUserGroup, setNewUserGroup] = useState({ name: '', enabledApis: [] as string[], permissions: [...DEFAULT_GROUP_PERMISSIONS] as string[], }); const [editingUserGroup, setEditingUserGroup] = useState<{ name: string; enabledApis: string[]; permissions: string[]; } | null>(null); const [showConfigureApisModal, setShowConfigureApisModal] = useState(false); const [selectedUser, setSelectedUser] = useState<{ username: string; role: 'user' | 'admin' | 'owner'; enabledApis?: string[]; tags?: string[]; } | null>(null); const [selectedApis, setSelectedApis] = useState([]); const [showConfigureUserGroupModal, setShowConfigureUserGroupModal] = useState(false); const [selectedUserForGroup, setSelectedUserForGroup] = useState<{ username: string; role: 'user' | 'admin' | 'owner'; tags?: string[]; } | null>(null); const [selectedUserGroups, setSelectedUserGroups] = useState([]); const [selectedUsers, setSelectedUsers] = useState>(new Set()); const [showBatchUserGroupModal, setShowBatchUserGroupModal] = useState(false); const [selectedUserGroup, setSelectedUserGroup] = useState(''); const [showDeleteUserGroupModal, setShowDeleteUserGroupModal] = useState(false); const [deletingUserGroup, setDeletingUserGroup] = useState<{ name: string; affectedUsers: Array<{ username: string; role: 'user' | 'admin' | 'owner'; }>; } | null>(null); const [showDeleteUserModal, setShowDeleteUserModal] = useState(false); const [deletingUser, setDeletingUser] = useState(null); const trimmedUserSearch = userSearch.trim(); // 当前登录用户名 const currentUsername = getAuthInfoFromBrowserCookie()?.username || null; // 判断是否有旧版用户数据需要迁移 const hasOldUserData = config?.UserConfig?.Users?.filter((u: any) => u.role !== 'owner').length ?? 0 > 0; // 使用新版本用户列表(如果可用且没有旧数据),否则使用配置中的用户列表 const displayUsers: Array<{ username: string; role: 'owner' | 'admin' | 'user'; banned?: boolean; enabledApis?: string[]; tags?: string[]; created_at?: number; oidcSub?: string; }> = !hasOldUserData && usersV2 ? usersV2 : config?.UserConfig?.Users || []; // 使用 useMemo 计算全选状态,避免每次渲染都重新计算 const selectAllUsers = useMemo(() => { const selectableUserCount = displayUsers?.filter( (user) => role === 'owner' || (role === 'admin' && (user.role === 'user' || user.username === currentUsername)) ).length || 0; return selectedUsers.size === selectableUserCount && selectedUsers.size > 0; }, [selectedUsers.size, displayUsers, role, currentUsername]); // 获取用户组列表 const userGroups = config?.UserConfig?.Tags || []; // 处理用户组相关操作 const handleUserGroupAction = async ( action: 'add' | 'edit' | 'delete', groupName: string, enabledApis?: string[], permissions?: string[] ) => { return withLoading(`userGroup_${action}_${groupName}`, async () => { try { const res = await fetch('/api/admin/user', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'userGroup', groupAction: action, groupName, enabledApis, permissions, }), }); if (!res.ok) { const data = await res.json().catch(() => ({})); throw new Error(data.error || `操作失败: ${res.status}`); } await refreshConfig(); if (action === 'add') { setNewUserGroup({ name: '', enabledApis: [], permissions: [...DEFAULT_GROUP_PERMISSIONS], }); setShowAddUserGroupForm(false); } else if (action === 'edit') { setEditingUserGroup(null); setShowEditUserGroupForm(false); } showSuccess( action === 'add' ? '用户组添加成功' : action === 'edit' ? '用户组更新成功' : '用户组删除成功', showAlert ); } catch (err) { showError(err instanceof Error ? err.message : '操作失败', showAlert); throw err; } }); }; const handleAddUserGroup = () => { if (!newUserGroup.name.trim()) return; handleUserGroupAction( 'add', newUserGroup.name, newUserGroup.enabledApis, newUserGroup.permissions ); }; const handleEditUserGroup = () => { if (!editingUserGroup?.name.trim()) return; handleUserGroupAction( 'edit', editingUserGroup.name, editingUserGroup.enabledApis, editingUserGroup.permissions ); }; const handleDeleteUserGroup = (groupName: string) => { // 计算会受影响的用户数量 const affectedUsers = config?.UserConfig?.Users?.filter( (user) => user.tags && user.tags.includes(groupName) ) || []; setDeletingUserGroup({ name: groupName, affectedUsers: affectedUsers.map((u) => ({ username: u.username, role: u.role, })), }); setShowDeleteUserGroupModal(true); }; const handleConfirmDeleteUserGroup = async () => { if (!deletingUserGroup) return; try { await handleUserGroupAction('delete', deletingUserGroup.name); setShowDeleteUserGroupModal(false); setDeletingUserGroup(null); } catch (err) { // 错误处理已在 handleUserGroupAction 中处理 } }; const handleStartEditUserGroup = (group: { name: string; enabledApis: string[]; permissions?: string[]; }) => { setEditingUserGroup({ ...group, permissions: group.permissions || [], }); setShowEditUserGroupForm(true); setShowAddUserGroupForm(false); }; // 为用户分配用户组 const handleAssignUserGroup = async ( username: string, userGroups: string[] ) => { return withLoading(`assignUserGroup_${username}`, async () => { try { const res = await fetch('/api/admin/user', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ targetUsername: username, action: 'updateUserGroups', userGroups, }), }); if (!res.ok) { const data = await res.json().catch(() => ({})); throw new Error(data.error || `操作失败: ${res.status}`); } await refreshConfig(); showSuccess('用户组分配成功', showAlert); } catch (err) { showError(err instanceof Error ? err.message : '操作失败', showAlert); throw err; } }); }; const handleBanUser = async (uname: string) => { await withLoading(`banUser_${uname}`, () => handleUserAction('ban', uname)); }; const handleUnbanUser = async (uname: string) => { await withLoading(`unbanUser_${uname}`, () => handleUserAction('unban', uname) ); }; const handleSetAdmin = async (uname: string) => { await withLoading(`setAdmin_${uname}`, () => handleUserAction('setAdmin', uname) ); }; const handleRemoveAdmin = async (uname: string) => { await withLoading(`removeAdmin_${uname}`, () => handleUserAction('cancelAdmin', uname) ); }; const handleAddUser = async () => { if (!newUser.username || !newUser.password) return; await withLoading('addUser', async () => { await handleUserAction( 'add', newUser.username, newUser.password, newUser.userGroup ); setNewUser({ username: '', password: '', userGroup: '' }); setShowAddUserForm(false); }); }; const handleChangePassword = async () => { if (!changePasswordUser.username || !changePasswordUser.password) return; await withLoading( `changePassword_${changePasswordUser.username}`, async () => { await handleUserAction( 'changePassword', changePasswordUser.username, changePasswordUser.password ); setChangePasswordUser({ username: '', password: '' }); setShowChangePasswordForm(false); } ); }; const handleShowChangePasswordForm = (username: string) => { setChangePasswordUser({ username, password: '' }); setShowChangePasswordForm(true); setShowAddUserForm(false); // 关闭添加用户表单 }; const handleDeleteUser = (username: string) => { setDeletingUser(username); setShowDeleteUserModal(true); }; const handleConfigureUserApis = (user: { username: string; role: 'user' | 'admin' | 'owner'; enabledApis?: string[]; }) => { setSelectedUser(user); setSelectedApis(user.enabledApis || []); setShowConfigureApisModal(true); }; const handleConfigureUserGroup = (user: { username: string; role: 'user' | 'admin' | 'owner'; tags?: string[]; }) => { setSelectedUserForGroup(user); setSelectedUserGroups(user.tags || []); setShowConfigureUserGroupModal(true); }; const handleSaveUserGroups = async () => { if (!selectedUserForGroup) return; await withLoading( `saveUserGroups_${selectedUserForGroup.username}`, async () => { try { await handleAssignUserGroup( selectedUserForGroup.username, selectedUserGroups ); setShowConfigureUserGroupModal(false); setSelectedUserForGroup(null); setSelectedUserGroups([]); } catch (err) { // 错误处理已在 handleAssignUserGroup 中处理 } } ); }; // 处理用户选择 const handleSelectUser = useCallback((username: string, checked: boolean) => { setSelectedUsers((prev) => { const newSelectedUsers = new Set(prev); if (checked) { newSelectedUsers.add(username); } else { newSelectedUsers.delete(username); } return newSelectedUsers; }); }, []); const handleSelectAllUsers = useCallback( (checked: boolean) => { if (checked) { // 只选择自己有权限操作的用户 const selectableUsernames = displayUsers?.filter( (user) => role === 'owner' || (role === 'admin' && (user.role === 'user' || user.username === currentUsername)) ).map((u) => u.username) || []; setSelectedUsers(new Set(selectableUsernames)); } else { setSelectedUsers(new Set()); } }, [displayUsers, role, currentUsername] ); // 批量设置用户组 const handleBatchSetUserGroup = async (userGroup: string) => { if (selectedUsers.size === 0) return; await withLoading('batchSetUserGroup', async () => { try { const res = await fetch('/api/admin/user', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'batchUpdateUserGroups', usernames: Array.from(selectedUsers), userGroups: userGroup === '' ? [] : [userGroup], }), }); if (!res.ok) { const data = await res.json().catch(() => ({})); throw new Error(data.error || `操作失败: ${res.status}`); } const userCount = selectedUsers.size; setSelectedUsers(new Set()); setShowBatchUserGroupModal(false); setSelectedUserGroup(''); showSuccess( `已为 ${userCount} 个用户设置用户组: ${userGroup}`, showAlert ); // 刷新配置 await refreshConfig(); } catch (err) { showError('批量设置用户组失败', showAlert); throw err; } }); }; // 提取URL域名的辅助函数 const extractDomain = (url: string): string => { try { const urlObj = new URL(url); return urlObj.hostname; } catch { // 如果URL格式不正确,返回原字符串 return url; } }; const handleSaveUserApis = async () => { if (!selectedUser) return; await withLoading(`saveUserApis_${selectedUser.username}`, async () => { try { const res = await fetch('/api/admin/user', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ targetUsername: selectedUser.username, action: 'updateUserApis', enabledApis: selectedApis, }), }); if (!res.ok) { const data = await res.json().catch(() => ({})); throw new Error(data.error || `操作失败: ${res.status}`); } // 成功后刷新配置 await refreshConfig(); setShowConfigureApisModal(false); setSelectedUser(null); setSelectedApis([]); } catch (err) { showError(err instanceof Error ? err.message : '操作失败', showAlert); throw err; } }); }; // 通用请求函数 const handleUserAction = async ( action: | 'add' | 'ban' | 'unban' | 'setAdmin' | 'cancelAdmin' | 'changePassword' | 'deleteUser', targetUsername: string, targetPassword?: string, userGroup?: string ) => { try { const res = await fetch('/api/admin/user', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ targetUsername, ...(targetPassword ? { targetPassword } : {}), ...(userGroup ? { userGroup } : {}), action, }), }); if (!res.ok) { const data = await res.json().catch(() => ({})); throw new Error(data.error || `操作失败: ${res.status}`); } // 成功后刷新配置和用户列表(refreshConfig 已经是 refreshConfigAndUsers) await refreshConfig(); } catch (err) { showError(err instanceof Error ? err.message : '操作失败', showAlert); } }; const handleConfirmDeleteUser = async () => { if (!deletingUser) return; await withLoading(`deleteUser_${deletingUser}`, async () => { try { await handleUserAction('deleteUser', deletingUser); setShowDeleteUserModal(false); setDeletingUser(null); } catch (err) { // 错误处理已在 handleUserAction 中处理 } }); }; if (!config) { return (
加载中...
); } return (
{/* 用户统计 */}

用户统计

{!hasOldUserData && usersV2 ? userTotal : displayUsers.length}
总用户数
{/* 数据迁移提示 */} {config.UserConfig.Users && config.UserConfig.Users.filter((u) => u.role !== 'owner').length > 0 && (
检测到旧版用户数据

建议迁移到新的用户存储结构,以获得更好的性能和安全性。迁移后用户密码将使用SHA256加密。

)}
{/* 用户组管理 */}

用户组管理

{/* 用户组列表 */}
{userGroups.map((group) => ( ))} {userGroups.length === 0 && ( )}
用户组名称 可用视频源 功能权限 操作
{group.name}
{group.enabledApis && group.enabledApis.length > 0 ? `${group.enabledApis.length} 个源` : '无限制'}
{group.permissions && group.permissions.length > 0 ? `${group.permissions.length} 项` : '无'}
暂无用户组,请添加用户组来管理用户权限
{/* 用户列表 */}

用户列表

{!hasOldUserData && usersV2 && (
{ e.preventDefault(); setSelectedUsers(new Set()); fetchUsersV2(1, trimmedUserSearch); }} className='ml-auto flex min-w-0 items-center gap-2' >
setUserSearch(e.target.value)} placeholder='按用户名搜索' className='w-full pl-9 pr-3 py-1.5 text-sm border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent' />
{trimmedUserSearch && ( )}
)} {/* 批量操作按钮 */} {selectedUsers.size > 0 && ( <>
已选择 {selectedUsers.size} 个用户
)}
{/* 添加用户表单 */} {showAddUserForm && (
setNewUser((prev) => ({ ...prev, username: e.target.value, })) } className='px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-green-500 focus:border-transparent' /> setNewUser((prev) => ({ ...prev, password: e.target.value, })) } className='px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-green-500 focus:border-transparent' />
)} {/* 修改密码表单 */} {showChangePasswordForm && (
修改用户密码
setChangePasswordUser((prev) => ({ ...prev, password: e.target.value, })) } className='flex-1 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent' />
)} {/* 用户列表 */}
{/* 迁移遮罩层 */} {config.UserConfig.Users && config.UserConfig.Users.filter((u) => u.role !== 'owner').length > 0 && (

需要迁移数据

检测到旧版用户数据,请先迁移到新的存储结构后再进行用户管理操作。

请在上方的"用户统计"区域点击"立即迁移"按钮完成数据迁移。

)}
{/* 按规则排序用户:自己 -> 站长(若非自己) -> 管理员 -> 其他 */} {(() => { // 如果正在加载,显示加载状态 if (userListLoading) { return ( ); } const sortedUsers = [...displayUsers].sort((a, b) => { type UserInfo = (typeof displayUsers)[number]; const priority = (u: UserInfo) => { if (u.username === currentUsername) return 0; if (u.role === 'owner') return 1; if (u.role === 'admin') return 2; return 3; }; return priority(a) - priority(b); }); if (sortedUsers.length === 0) { return ( ); } return ( {sortedUsers.map((user) => { // 修改密码权限:站长可修改管理员和普通用户密码,管理员可修改普通用户和自己的密码,但任何人都不能修改站长密码 const canChangePassword = user.role !== 'owner' && // 不能修改站长密码 (role === 'owner' || // 站长可以修改管理员和普通用户密码 (role === 'admin' && (user.role === 'user' || user.username === currentUsername))); // 管理员可以修改普通用户和自己的密码 // 删除用户权限:站长可删除除自己外的所有用户,管理员仅可删除普通用户 const canDeleteUser = user.username !== currentUsername && (role === 'owner' || // 站长可以删除除自己外的所有用户 (role === 'admin' && user.role === 'user')); // 管理员仅可删除普通用户 // 其他操作权限:不能操作自己,站长可操作所有用户,管理员可操作普通用户 const canOperate = user.username !== currentUsername && (role === 'owner' || (role === 'admin' && user.role === 'user')); return ( ); })} ); })()}
{(() => { // 检查是否有权限操作任何用户 const hasAnyPermission = displayUsers?.some( (user) => role === 'owner' || (role === 'admin' && (user.role === 'user' || user.username === currentUsername)) ); return hasAnyPermission ? ( handleSelectAllUsers(e.target.checked) } className='w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600' /> ) : (
); })()}
用户名 角色 状态 用户组 采集源权限 操作
加载中...
{trimmedUserSearch ? `未找到用户名包含“${trimmedUserSearch}”的用户` : '暂无用户'}
{role === 'owner' || (role === 'admin' && (user.role === 'user' || user.username === currentUsername)) ? ( handleSelectUser( user.username, e.target.checked ) } className='w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600' /> ) : (
)}
{user.username} {user.oidcSub && ( OIDC )}
{user.role === 'owner' ? '站长' : user.role === 'admin' ? '管理员' : '普通用户'} {!user.banned ? '正常' : '已封禁'}
{user.tags && user.tags.length > 0 ? user.tags.join(', ') : '无用户组'} {/* 配置用户组按钮 */} {(role === 'owner' || (role === 'admin' && (user.role === 'user' || user.username === currentUsername))) && ( )}
{user.enabledApis && user.enabledApis.length > 0 ? `${user.enabledApis.length} 个源` : '无限制'} {/* 配置采集源权限按钮 */} {(role === 'owner' || (role === 'admin' && (user.role === 'user' || user.username === currentUsername))) && ( )}
{/* 修改密码按钮 */} {canChangePassword && ( )} {canOperate && ( <> {/* 其他操作按钮 */} {user.role === 'user' && ( )} {user.role === 'admin' && ( )} {user.role !== 'owner' && (!user.banned ? ( ) : ( ))} )} {/* 删除用户按钮 - 放在最后,使用更明显的红色样式 */} {canDeleteUser && ( )}
{/* 用户列表分页 */} {!hasOldUserData && usersV2 && userTotalPages > 1 && (
{trimmedUserSearch ? `搜索结果 ${userTotal} 个用户` : `共 ${userTotal} 个用户`} ,第 {userPage} / {userTotalPages} 页
)}
{/* 配置用户采集源权限弹窗 */} {showConfigureApisModal && selectedUser && createPortal(
{ setShowConfigureApisModal(false); setSelectedUser(null); setSelectedApis([]); }} >
e.stopPropagation()} >

配置用户采集源权限 - {selectedUser.username}

配置说明

提示:全不选为无限制,选中的采集源将限制用户只能访问这些源

{/* 采集源选择 - 多列布局 */}

选择可用的采集源:

{config?.SourceConfig?.map((source) => ( ))}
{/* 快速操作按钮 */}
已选择: {selectedApis.length > 0 ? `${selectedApis.length} 个源` : '无限制'}
{/* 操作按钮 */}
, document.body )} {/* 添加用户组弹窗 */} {showAddUserGroupForm && createPortal(
{ setShowAddUserGroupForm(false); setNewUserGroup({ name: '', enabledApis: [], permissions: [...DEFAULT_GROUP_PERMISSIONS], }); }} >
e.stopPropagation()} >

添加新用户组

{/* 用户组名称 */}
setNewUserGroup((prev) => ({ ...prev, name: e.target.value, })) } className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent' />
{FEATURE_PERMISSION_OPTIONS.map((permission) => ( ))}
{/* 可用视频源 */}
{config?.SourceConfig?.map((source) => ( ))}
{/* 快速操作按钮 */}
{/* 操作按钮 */}
, document.body )} {/* 编辑用户组弹窗 */} {showEditUserGroupForm && editingUserGroup && createPortal(
{ setShowEditUserGroupForm(false); setEditingUserGroup(null); }} >
e.stopPropagation()} >

编辑用户组 - {editingUserGroup.name}

{/* 可用视频源 */}
{config?.SourceConfig?.map((source) => ( ))}
{/* 快速操作按钮 */}
{FEATURE_PERMISSION_OPTIONS.map((permission) => ( ))}
{/* 操作按钮 */}
, document.body )} {/* 配置用户组弹窗 */} {showConfigureUserGroupModal && selectedUserForGroup && createPortal(
{ setShowConfigureUserGroupModal(false); setSelectedUserForGroup(null); setSelectedUserGroups([]); }} >
e.stopPropagation()} >

配置用户组 - {selectedUserForGroup.username}

配置说明

提示:选择"无用户组"为无限制,选择特定用户组将限制用户只能访问该用户组允许的采集源

{/* 用户组选择 - 下拉选择器 */}

选择"无用户组"为无限制,选择特定用户组将限制用户只能访问该用户组允许的采集源

{/* 操作按钮 */}
, document.body )} {/* 删除用户组确认弹窗 */} {showDeleteUserGroupModal && deletingUserGroup && createPortal(
{ setShowDeleteUserGroupModal(false); setDeletingUserGroup(null); }} >
e.stopPropagation()} >

确认删除用户组

危险操作警告

删除用户组 {deletingUserGroup.name}{' '} 将影响所有使用该组的用户,此操作不可恢复!

{deletingUserGroup.affectedUsers.length > 0 ? (
⚠️ 将影响 {deletingUserGroup.affectedUsers.length}{' '} 个用户:
{deletingUserGroup.affectedUsers.map((user, index) => (
• {user.username} ({user.role})
))}

这些用户的用户组将被自动移除

) : (
✅ 当前没有用户使用此用户组
)}
{/* 操作按钮 */}
, document.body )} {/* 删除用户确认弹窗 */} {showDeleteUserModal && deletingUser && createPortal(
{ setShowDeleteUserModal(false); setDeletingUser(null); }} >
e.stopPropagation()} >

确认删除用户

危险操作警告

删除用户 {deletingUser}{' '} 将同时删除其搜索历史、播放记录和收藏夹,此操作不可恢复!

{/* 操作按钮 */}
, document.body )} {/* 批量设置用户组弹窗 */} {showBatchUserGroupModal && createPortal(
{ setShowBatchUserGroupModal(false); setSelectedUserGroup(''); }} >
e.stopPropagation()} >

批量设置用户组

批量操作说明

将为选中的 {selectedUsers.size} 个用户{' '} 设置用户组,选择"无用户组"为无限制

选择"无用户组"为无限制,选择特定用户组将限制用户只能访问该用户组允许的采集源

{/* 操作按钮 */}
, document.body )} {/* 通用弹窗组件 */}
); }; // 私人影库配置组件 const OpenListConfigComponent = ({ config, refreshConfig, }: { config: AdminConfig | null; refreshConfig: () => Promise; }) => { const { alertModal, showAlert, hideAlert } = useAlertModal(); const { isLoading, withLoading } = useLoadingState(); const [enabled, setEnabled] = useState(false); const [url, setUrl] = useState(''); const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); const [rootPaths, setRootPaths] = useState(['/']); const [offlineDownloadPath, setOfflineDownloadPath] = useState('/'); const [scanInterval, setScanInterval] = useState(0); const [scanMode, setScanMode] = useState<'torrent' | 'name' | 'hybrid'>( 'hybrid' ); const [disableVideoPreview, setDisableVideoPreview] = useState(false); const [videos, setVideos] = useState([]); const [refreshing, setRefreshing] = useState(false); const [scanProgress, setScanProgress] = useState<{ current: number; total: number; currentFolder?: string; } | null>(null); const [correctDialogOpen, setCorrectDialogOpen] = useState(false); const [selectedVideo, setSelectedVideo] = useState(null); useEffect(() => { if (config?.OpenListConfig) { setEnabled(config.OpenListConfig.Enabled || false); setUrl(config.OpenListConfig.URL || ''); setUsername(config.OpenListConfig.Username || ''); setPassword(config.OpenListConfig.Password || ''); setRootPaths( config.OpenListConfig.RootPaths || (config.OpenListConfig.RootPath ? [config.OpenListConfig.RootPath] : ['/']) ); setOfflineDownloadPath(config.OpenListConfig.OfflineDownloadPath || '/'); setScanInterval(config.OpenListConfig.ScanInterval || 0); setScanMode(config.OpenListConfig.ScanMode || 'hybrid'); setDisableVideoPreview( config.OpenListConfig.DisableVideoPreview || false ); } }, [config]); useEffect(() => { if ( config?.OpenListConfig?.URL && config?.OpenListConfig?.Username && config?.OpenListConfig?.Password ) { fetchVideos(); } }, [config]); const fetchVideos = async (noCache = false) => { try { setRefreshing(true); const url = `/api/openlist/list?page=1&pageSize=100&includeFailed=true${ noCache ? '&noCache=true' : '' }`; const response = await fetch(url); if (response.ok) { const data = await response.json(); setVideos(data.list || []); } } catch (error) { console.error('获取视频列表失败:', error); } finally { setRefreshing(false); } }; const handleSave = async () => { await withLoading('saveOpenList', async () => { try { const response = await fetch('/api/admin/openlist', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'save', Enabled: enabled, URL: url, Username: username, Password: password, RootPaths: rootPaths, OfflineDownloadPath: offlineDownloadPath, ScanInterval: scanInterval, ScanMode: scanMode, DisableVideoPreview: disableVideoPreview, }), }); if (!response.ok) { const data = await response.json(); throw new Error(data.error || '保存失败'); } showSuccess('保存成功', showAlert); await refreshConfig(); } catch (error) { showError( error instanceof Error ? error.message : '保存失败', showAlert ); throw error; } }); }; const handleRefresh = async (clearMetaInfo = false) => { setRefreshing(true); setScanProgress(null); try { const response = await fetch('/api/openlist/refresh', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ clearMetaInfo }), }); if (!response.ok) { const data = await response.json(); throw new Error(data.error || '刷新失败'); } const result = await response.json(); const taskId = result.taskId; if (!taskId) { throw new Error('未获取到任务ID'); } // 轮询任务进度 const pollInterval = setInterval(async () => { try { const progressResponse = await fetch( `/api/openlist/scan-progress?taskId=${taskId}` ); if (!progressResponse.ok) { clearInterval(pollInterval); throw new Error('获取进度失败'); } const progressData = await progressResponse.json(); const task = progressData.task; if (task.status === 'running') { setScanProgress(task.progress); } else if (task.status === 'completed') { clearInterval(pollInterval); setScanProgress(null); setRefreshing(false); showSuccess( `扫描完成!新增 ${task.result.new} 个,已存在 ${task.result.existing} 个,失败 ${task.result.errors} 个`, showAlert ); // 先强制从数据库读取视频列表(这会更新缓存) await fetchVideos(true); // 然后再刷新配置(这会触发 useEffect,但此时缓存已经是新的了) await refreshConfig(); } else if (task.status === 'failed') { clearInterval(pollInterval); setScanProgress(null); setRefreshing(false); throw new Error(task.error || '扫描失败'); } } catch (error) { clearInterval(pollInterval); setScanProgress(null); setRefreshing(false); showError( error instanceof Error ? error.message : '获取进度失败', showAlert ); } }, 1000); } catch (error) { setScanProgress(null); setRefreshing(false); showError(error instanceof Error ? error.message : '刷新失败', showAlert); } }; const handleRefreshVideo = async (folder: string) => { try { const response = await fetch('/api/openlist/refresh-video', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ folder }), }); if (!response.ok) { const data = await response.json(); throw new Error(data.error || '刷新失败'); } showSuccess('刷新成功', showAlert); } catch (error) { showError(error instanceof Error ? error.message : '刷新失败', showAlert); } }; const handleCorrectSuccess = () => { fetchVideos(true); // 强制从数据库重新读取,不使用缓存 }; const handleCheckConnectivity = async () => { await withLoading('checkOpenList', async () => { try { const response = await fetch('/api/openlist/check', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ url, username, password, }), }); const data = await response.json(); if (response.ok && data.success) { showSuccess('连接成功', showAlert); } else { throw new Error(data.error || '连接失败'); } } catch (error) { showError( error instanceof Error ? error.message : '连接失败', showAlert ); throw error; } }); }; const handleDeleteVideo = async (key: string, title: string) => { // 显示确认对话框,直接在 onConfirm 中执行删除操作 showAlert({ type: 'warning', title: '确认删除', message: `确定要删除视频记录"${title}"吗?此操作不会删除实际文件,只会从列表中移除。`, showConfirm: true, onConfirm: async () => { try { const response = await fetch('/api/openlist/delete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ key }), }); if (!response.ok) { const data = await response.json(); throw new Error(data.error || '删除失败'); } showSuccess('删除成功', showAlert); await fetchVideos(true); // 强制从数据库重新读取 refreshConfig(); // 异步刷新配置以更新资源数量(不等待,避免重复刷新) } catch (error) { showError( error instanceof Error ? error.message : '删除失败', showAlert ); } }, }); }; const formatDate = (timestamp?: number) => { if (!timestamp) return '未刷新'; return new Date(timestamp).toLocaleString('zh-CN'); }; return (
{/* 使用说明 */}
使用说明

• 私人影库功能需要配合 OpenList 使用,用于管理和播放您自己的视频文件

• OpenList 是一个开源的网盘聚合程序,支持多种存储后端(本地、阿里云盘、OneDrive 等)

• 配置后,系统会自动扫描指定目录下的视频文件夹,并通过 TMDB 匹配元数据信息

• 定时扫描间隔设置为 0 表示关闭自动扫描,最低间隔为 60 分钟

• 视频文件夹名称为影片名称,精准命名可以提高 TMDB 匹配准确率

{/* 功能开关 */}

启用私人影库功能

关闭后将不显示私人影库入口,也不会执行定时扫描

{/* 配置表单 */}
setUrl(e.target.value)} disabled={!enabled} placeholder='https://your-openlist-server.com' className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:opacity-50 disabled:cursor-not-allowed' />
setUsername(e.target.value)} disabled={!enabled} placeholder='admin' className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:opacity-50 disabled:cursor-not-allowed' />
setPassword(e.target.value)} disabled={!enabled} placeholder='password' className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:opacity-50 disabled:cursor-not-allowed' />
{rootPaths.map((path, index) => (
{ const newPaths = [...rootPaths]; newPaths[index] = e.target.value; setRootPaths(newPaths); }} disabled={!enabled} placeholder='/' className='flex-1 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:opacity-50 disabled:cursor-not-allowed' /> {rootPaths.length > 1 && ( )}
))}

OpenList 中的视频文件夹路径,可以配置多个根目录

setOfflineDownloadPath(e.target.value)} disabled={!enabled} placeholder='/' className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:opacity-50 disabled:cursor-not-allowed' />

动漫磁力等离线下载任务的保存目录,默认为根目录 /

setScanInterval(parseInt(e.target.value) || 0)} disabled={!enabled} placeholder='0' min='0' className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:opacity-50 disabled:cursor-not-allowed' />

设置为 0 关闭定时扫描,最低 60 分钟

混合模式:先用种子库匹配,失败后降级为名字匹配

禁用预览视频

开启后将直接返回直连链接,不使用视频预览流

{/* 视频列表区域 */} {enabled && config?.OpenListConfig?.URL && config?.OpenListConfig?.Username && config?.OpenListConfig?.Password && (

视频列表

资源数: {config.OpenListConfig.ResourceCount || 0} | 上次更新:{' '} {formatDate(config.OpenListConfig.LastRefreshTime)}
{refreshing && scanProgress && (
扫描进度: {scanProgress.current} / {scanProgress.total} {scanProgress.total > 0 ? Math.round( (scanProgress.current / scanProgress.total) * 100 ) : 0} %
0 ? (scanProgress.current / scanProgress.total) * 100 : 0 }%`, }} />
{scanProgress.currentFolder && (

正在处理: {scanProgress.currentFolder}

)}
)} {refreshing ? (
加载中...
) : videos.length > 0 ? (
{videos.map((video) => ( ))}
标题 状态 类型 季度 年份 评分 操作
{video.title} {video.failed ? ( 匹配失败 ) : ( 正常 )} {video.mediaType === 'movie' ? '电影' : '剧集'} {video.seasonNumber ? ( S{video.seasonNumber} ) : ( '-' )} {video.releaseDate ? video.releaseDate.split('-')[0] : '-'} {video.voteAverage > 0 ? video.voteAverage.toFixed(1) : '-'}
{!video.failed && ( )}
) : (
暂无视频,请点击"立即扫描"扫描视频库
)}
)} {/* 纠错对话框 */} {selectedVideo && ( setCorrectDialogOpen(false)} videoKey={selectedVideo.id} currentTitle={selectedVideo.title} currentVideo={{ tmdbId: selectedVideo.tmdbId, doubanId: selectedVideo.doubanId, poster: selectedVideo.poster, releaseDate: selectedVideo.releaseDate, overview: selectedVideo.overview, voteAverage: selectedVideo.voteAverage, mediaType: selectedVideo.mediaType, seasonNumber: selectedVideo.seasonNumber, seasonName: selectedVideo.seasonName, }} onCorrect={handleCorrectSuccess} /> )}
); }; const NetDiskConfigComponent = ({ config, refreshConfig, }: { config: AdminConfig | null; refreshConfig: () => Promise; }) => { const { alertModal, showAlert, hideAlert } = useAlertModal(); const { isLoading, withLoading } = useLoadingState(); const [enabled, setEnabled] = useState(false); const [cookie, setCookie] = useState(''); const [savePath, setSavePath] = useState('/'); const [quarkPlayMode, setQuarkPlayMode] = useState<'direct_first' | 'transcode_first'>('transcode_first'); const [quarkMultiThreadPlayback, setQuarkMultiThreadPlayback] = useState(false); const [mobileEnabled, setMobileEnabled] = useState(false); const [mobileAuthorization, setMobileAuthorization] = useState(''); const [baiduEnabled, setBaiduEnabled] = useState(false); const [baiduCookie, setBaiduCookie] = useState(''); const [tianyiEnabled, setTianyiEnabled] = useState(false); const [tianyiAccount, setTianyiAccount] = useState(''); const [tianyiPassword, setTianyiPassword] = useState(''); const [pan123Enabled, setPan123Enabled] = useState(false); const [pan123Account, setPan123Account] = useState(''); const [pan123Password, setPan123Password] = useState(''); const [ucEnabled, setUcEnabled] = useState(false); const [ucCookie, setUcCookie] = useState(''); const [ucToken, setUcToken] = useState(''); const [ucSavePath, setUcSavePath] = useState('/'); const [pan115Enabled, setPan115Enabled] = useState(false); const [pan115Cookie, setPan115Cookie] = useState(''); useEffect(() => { const quark = config?.NetDiskConfig?.Quark; const mobile = config?.NetDiskConfig?.Mobile; setEnabled(quark?.Enabled || false); setCookie(quark?.Cookie || ''); setSavePath(quark?.SavePath || '/'); setQuarkPlayMode(quark?.PlayMode === 'direct_first' ? 'direct_first' : 'transcode_first'); setQuarkMultiThreadPlayback(Boolean(quark?.MultiThreadPlayback)); setMobileEnabled(mobile?.Enabled || false); setMobileAuthorization(mobile?.Authorization || ''); setBaiduEnabled(config?.NetDiskConfig?.Baidu?.Enabled || false); setBaiduCookie(config?.NetDiskConfig?.Baidu?.Cookie || ''); setTianyiEnabled(config?.NetDiskConfig?.Tianyi?.Enabled || false); setTianyiAccount(config?.NetDiskConfig?.Tianyi?.Account || ''); setTianyiPassword(config?.NetDiskConfig?.Tianyi?.Password || ''); setPan123Enabled(config?.NetDiskConfig?.Pan123?.Enabled || false); setPan123Account(config?.NetDiskConfig?.Pan123?.Account || ''); setPan123Password(config?.NetDiskConfig?.Pan123?.Password || ''); setUcEnabled(config?.NetDiskConfig?.UC?.Enabled || false); setUcCookie(config?.NetDiskConfig?.UC?.Cookie || ''); setUcToken(config?.NetDiskConfig?.UC?.Token || ''); setUcSavePath(config?.NetDiskConfig?.UC?.SavePath || '/'); setPan115Enabled(config?.NetDiskConfig?.Pan115?.Enabled || false); setPan115Cookie(config?.NetDiskConfig?.Pan115?.Cookie || ''); }, [config]); const handleSave = async () => { await withLoading('saveNetDisk', async () => { const response = await fetch('/api/admin/netdisk', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'save', Quark: { Enabled: enabled, Cookie: cookie, SavePath: savePath, PlayMode: quarkPlayMode, MultiThreadPlayback: quarkMultiThreadPlayback, }, Mobile: { Enabled: mobileEnabled, Authorization: mobileAuthorization, }, Baidu: { Enabled: baiduEnabled, Cookie: baiduCookie, }, Tianyi: { Enabled: tianyiEnabled, Account: tianyiAccount, Password: tianyiPassword, }, Pan123: { Enabled: pan123Enabled, Account: pan123Account, Password: pan123Password, }, UC: { Enabled: ucEnabled, Cookie: ucCookie, Token: ucToken, SavePath: ucSavePath, }, Pan115: { Enabled: pan115Enabled, Cookie: pan115Cookie, }, }), }); const data = await response.json(); if (!response.ok) { throw new Error(data.error || '保存失败'); } showSuccess('保存成功', showAlert); await refreshConfig(); }); }; const handleValidate = async () => { await withLoading('validateNetDisk', async () => { try { const response = await fetch('/api/admin/netdisk', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'validate', Quark: { Cookie: cookie, SavePath: savePath, }, }), }); const data = await response.json(); if (!response.ok) { throw new Error(data.error || '校验失败'); } showSuccess(data.message || '夸克 Cookie 可读', showAlert); } catch (error) { showError( error instanceof Error ? error.message : '校验失败', showAlert ); throw error; } }); }; const handleValidateMobile = async () => { await withLoading('validateMobileNetDisk', async () => { try { const response = await fetch('/api/admin/netdisk', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'validate', provider: 'mobile', Mobile: { Authorization: mobileAuthorization, }, }), }); const data = await response.json(); if (!response.ok) { throw new Error(data.error || '校验失败'); } showSuccess(data.message || '移动云盘验证头格式正常', showAlert); } catch (error) { showError( error instanceof Error ? error.message : '校验失败', showAlert ); throw error; } }); }; const handleValidateBaidu = async () => { await withLoading('validateBaiduNetDisk', async () => { try { const response = await fetch('/api/admin/netdisk', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'validate', provider: 'baidu', Baidu: { Cookie: baiduCookie, }, }), }); const data = await response.json(); if (!response.ok) { throw new Error(data.error || '校验失败'); } showSuccess(data.message || '百度网盘 Cookie 格式正常', showAlert); } catch (error) { showError( error instanceof Error ? error.message : '校验失败', showAlert ); throw error; } }); }; const handleValidateTianyi = async () => { await withLoading('validateTianyiNetDisk', async () => { try { const response = await fetch('/api/admin/netdisk', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'validate', provider: 'tianyi', Tianyi: { Account: tianyiAccount, Password: tianyiPassword, }, }), }); const data = await response.json(); if (!response.ok) { throw new Error(data.error || '校验失败'); } showSuccess(data.message || '天翼云盘账号密码可用', showAlert); } catch (error) { showError( error instanceof Error ? error.message : '校验失败', showAlert ); throw error; } }); }; const handleValidatePan123 = async () => { await withLoading('validatePan123NetDisk', async () => { try { const response = await fetch('/api/admin/netdisk', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'validate', provider: 'pan123', Pan123: { Account: pan123Account, Password: pan123Password, }, }), }); const data = await response.json(); if (!response.ok) { throw new Error(data.error || '校验失败'); } showSuccess(data.message || '123网盘账号密码可用', showAlert); } catch (error) { showError( error instanceof Error ? error.message : '校验失败', showAlert ); throw error; } }); }; const handleValidateUC = async () => { await withLoading('validateUCNetDisk', async () => { try { const response = await fetch('/api/admin/netdisk', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'validate', provider: 'uc', UC: { Cookie: ucCookie, Token: ucToken, SavePath: ucSavePath, }, }), }); const data = await response.json(); if (!response.ok) { throw new Error(data.error || '校验失败'); } showSuccess(data.message || 'UC Cookie 可读', showAlert); } catch (error) { showError( error instanceof Error ? error.message : '校验失败', showAlert ); throw error; } }); }; const handleValidatePan115 = async () => { await withLoading('validatePan115NetDisk', async () => { try { const response = await fetch('/api/admin/netdisk', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'validate', provider: 'pan115', Pan115: { Cookie: pan115Cookie, }, }), }); const data = await response.json(); if (!response.ok) { throw new Error(data.error || '校验失败'); } showSuccess(data.message || '115 Cookie 格式正常', showAlert); } catch (error) { showError( error instanceof Error ? error.message : '校验失败', showAlert ); throw error; } }); }; return (
夸克网盘

启用夸克网盘

开启后,网盘搜索中的夸克资源会显示“立即播放”和“转存”按钮