/* 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 { Alert, Avatar, Button, Card, Checkbox, Chip, Input, Label, Skeleton, Switch, Table, TextArea, TextField } from '@heroui/react'; import { AlertCircle, AlertTriangle, CheckCircle, ChevronDown, ChevronUp, Database, ExternalLink, FileText, FolderOpen, Settings, Tv, User, Users, Video, X, } from 'lucide-react'; import { GripVertical, Palette } from 'lucide-react'; import { Suspense, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { AdminConfig, AdminConfigResult } from '@/lib/admin.types'; import { getAuthInfoFromBrowserCookie } from '@/lib/auth'; import DataMigration from '@/components/DataMigration'; import PageLayout from '@/components/PageLayout'; import ThemeManager from '@/components/ThemeManager'; import { AppDialog, AppFilterSelect } from '@/components/ui/HeroPrimitives'; const AdminTable = ({ ariaLabel, minWidth = 'min-w-[900px]', children, }: { ariaLabel: string; minWidth?: string; children: React.ReactNode; }) => ( {children}
); const AdminCheckbox = ({ ariaLabel, isSelected, onChange, isDisabled, }: { ariaLabel: string; isSelected: boolean; onChange: (isSelected: boolean) => void; isDisabled?: boolean; }) => ( ); const AdminChip = ({ children, color = 'default', }: { children: React.ReactNode; color?: 'default' | 'accent' | 'success' | 'warning' | 'danger'; }) => ( {children} ); const AdminSettingSwitch = ({ label, description, isSelected, onChange, }: { label: string; description: string; isSelected: boolean; onChange: (isSelected: boolean) => void; }) => (

{description}

); // 获取用户头像的函数 const getUserAvatar = async (username: string): Promise => { try { const response = await fetch(`/api/avatar?user=${encodeURIComponent(username)}`); if (response.ok) { const data = await response.json(); return data.avatar || null; } } catch (error) { console.error('获取头像失败:', error); } return null; }; // 用户头像组件 interface UserAvatarProps { username: string; size?: 'sm' | 'md' | 'lg'; } const UserAvatar = ({ username, size = 'sm' }: UserAvatarProps) => { const [avatarUrl, setAvatarUrl] = useState(null); const [loading, setLoading] = useState(true); useEffect(() => { const fetchAvatar = async () => { setLoading(true); const avatar = await getUserAvatar(username); setAvatarUrl(avatar); setLoading(false); }; fetchAvatar(); }, [username]); return ( {loading ? ( ) : avatarUrl ? ( ) : ( )} ); }; // 机器码单元格组件 interface MachineCodeCellProps { username: string; canManage: boolean; machineCodeData: Record; onRefresh: () => void; showAlert: (config: any) => void; } const MachineCodeCell = ({ username, canManage, machineCodeData, onRefresh, showAlert }: MachineCodeCellProps) => { const [unbinding, setUnbinding] = useState(false); const [tooltipPosition, setTooltipPosition] = useState<'top' | 'bottom'>('bottom'); const tooltipRef = useRef(null); const codeRef = useRef(null); const machineCodeInfo = machineCodeData[username] || null; // 智能定位逻辑 const handleMouseEnter = useCallback(() => { if (!codeRef.current) return; const element = codeRef.current; const rect = element.getBoundingClientRect(); const viewportHeight = window.innerHeight; if (rect.top < viewportHeight / 2) { setTooltipPosition('bottom'); } else { setTooltipPosition('top'); } }, []); // 解绑机器码 const handleUnbind = async () => { if (!machineCodeInfo || !canManage) return; try { setUnbinding(true); const response = await fetch('/api/machine-code', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ action: 'unbind', targetUser: username, }), }); if (response.ok) { showSuccess('机器码解绑成功', showAlert); onRefresh(); // 刷新数据 } else { const error = await response.json(); showError(`解绑失败: ${error.error || '未知错误'}`, showAlert); } } catch (error) { console.error('解绑机器码失败:', error); showError('解绑失败,请重试', showAlert); } finally { setUnbinding(false); } }; const formatMachineCode = (code: string) => { if (code.length !== 32) return code; return code.match(/.{1,4}/g)?.join('-') || code; }; const formatDate = (timestamp: number) => { return new Date(timestamp).toLocaleDateString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', }); }; if (!machineCodeInfo) { return (
未绑定
); } return (
{formatMachineCode(machineCodeInfo.machineCode).substring(0, 12)}... {/* 悬停显示完整机器码 - 智能定位 */}
{formatMachineCode(machineCodeInfo.machineCode)}
{machineCodeInfo.deviceInfo && (
{machineCodeInfo.deviceInfo}
)}
绑定时间: {formatDate(machineCodeInfo.bindTime)}
{/* 箭头 - 根据位置动态调整 */}
{canManage && ( )}
已绑定
); }; // 通用弹窗组件 interface AlertModalProps { isOpen: boolean; onClose: () => void; type: 'success' | 'error' | 'warning'; title: string; message?: string; timer?: number; showConfirm?: boolean; } const AlertModal = ({ isOpen, onClose, type, title, message, timer, showConfirm = false }: AlertModalProps) => { useEffect(() => { if (isOpen && timer) { const timeout = setTimeout(onClose, timer); return () => clearTimeout(timeout); } }, [isOpen, timer, onClose]); const getIcon = () => { switch (type) { case 'success': return ; case 'error': return ; case 'warning': return ; default: return null; } }; const getStatus = () => { switch (type) { case 'success': return 'success'; case 'error': return 'danger'; case 'warning': return 'warning'; default: return 'accent'; } }; return ( { if (!open) onClose(); }} title={title} icon={getIcon()} footer={ showConfirm ? ( ) : null } > {message ? {message} : null} ); }; // 弹窗状态管理 const useAlertModal = () => { const [alertModal, setAlertModal] = useState<{ isOpen: boolean; type: 'success' | 'error' | 'warning'; title: string; message?: string; timer?: number; showConfirm?: boolean; }>({ 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 SiteConfig { SiteName: string; Announcement: string; SearchDownstreamMaxPage: number; SiteInterfaceCacheTime: number; DoubanProxyType: string; DoubanProxy: string; DoubanImageProxyType: string; DoubanImageProxy: string; DisableYellowFilter: boolean; FluidSearch: boolean; RequireDeviceCode: boolean; } // 视频源数据类型 interface DataSource { name: string; key: string; api: string; detail?: string; disabled?: boolean; from: 'config' | 'custom'; } // 直播源数据类型 interface LiveDataSource { name: string; key: string; url: string; ua?: string; epg?: string; channelNumber?: number; disabled?: boolean; from: 'config' | 'custom'; } type ValidationResult = { status: 'valid' | 'invalid' | 'no_results' | 'validating' | null; message: string; details?: { responseTime?: number; resultCount?: number; error?: string; searchKeyword?: string; }; }; const SourceValidationSummary = ({ result }: { result: ValidationResult }) => { if (!result.status) return null; const statusMeta = { valid: { chipColor: 'success' as const, icon: CheckCircle, label: '检测通过', }, validating: { chipColor: 'accent' as const, icon: Settings, label: '检测中', }, no_results: { chipColor: 'warning' as const, icon: AlertTriangle, label: '无搜索结果', }, invalid: { chipColor: 'danger' as const, icon: AlertCircle, label: '检测失败', }, }[result.status]; const StatusIcon = statusMeta.icon; return (
检测结果 {statusMeta.label} {result.message}
{result.details && (
{result.details.searchKeyword && (
测试关键词: {result.details.searchKeyword}
)} {result.details.responseTime && (
响应时间: {result.details.responseTime}ms
)} {result.details.resultCount !== undefined && (
搜索结果数: {result.details.resultCount}
)} {result.details.error && (
错误信息: {result.details.error}
)}
)}
); }; // 自定义分类数据类型 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; } const CollapsibleTab = ({ title, icon, isExpanded, onToggle, children, }: CollapsibleTabProps) => { return ( {isExpanded &&
{children}
}
); }; // 用户配置组件 interface UserConfigProps { config: AdminConfig | null; role: 'owner' | 'admin' | null; refreshConfig: () => Promise; machineCodeUsers: Record; fetchMachineCodeUsers: () => Promise; } const UserConfig = ({ config, role, refreshConfig, machineCodeUsers, fetchMachineCodeUsers }: 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[], }); const [editingUserGroup, setEditingUserGroup] = useState<{ name: string; enabledApis: 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 currentUsername = getAuthInfoFromBrowserCookie()?.username || null; // 使用 useMemo 计算全选状态,避免每次渲染都重新计算 const selectAllUsers = useMemo(() => { const selectableUserCount = config?.UserConfig?.Users?.filter(user => (role === 'owner' || (role === 'admin' && (user.role === 'user' || user.username === currentUsername))) ).length || 0; return selectedUsers.size === selectableUserCount && selectedUsers.size > 0; }, [selectedUsers.size, config?.UserConfig?.Users, role, currentUsername]); // 获取用户组列表 const userGroups = config?.UserConfig?.Tags || []; // 处理用户组相关操作 const handleUserGroupAction = async ( action: 'add' | 'edit' | 'delete', groupName: string, enabledApis?: 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, }), }); if (!res.ok) { const data = await res.json().catch(() => ({})); throw new Error(data.error || `操作失败: ${res.status}`); } await refreshConfig(); if (action === 'add') { setNewUserGroup({ name: '', enabledApis: [] }); 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); }; const handleEditUserGroup = () => { if (!editingUserGroup?.name.trim()) return; handleUserGroupAction('edit', editingUserGroup.name, editingUserGroup.enabledApis); }; 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[] }) => { setEditingUserGroup({ ...group }); 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 = config?.UserConfig?.Users?.filter(user => (role === 'owner' || (role === 'admin' && (user.role === 'user' || user.username === currentUsername))) ).map(u => u.username) || []; setSelectedUsers(new Set(selectableUsernames)); } else { setSelectedUsers(new Set()); } }, [config?.UserConfig?.Users, 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}`); } // 成功后刷新配置(无需整页刷新) 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 (
{/* 用户统计 */}

用户统计

{config.UserConfig.Users.length}
总用户数
{/* 用户组管理 */}

用户组管理

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

用户列表

{/* 批量操作按钮 */} {selectedUsers.size > 0 && ( <>
已选择 {selectedUsers.size} 个用户
)}
{/* 添加用户表单 */} {showAddUserForm && (
setNewUser((prev) => ({ ...prev, username: e.target.value })) } /> setNewUser((prev) => ({ ...prev, password: e.target.value })) } />
({ label: `${group.name} (${group.enabledApis && group.enabledApis.length > 0 ? `${group.enabledApis.length} 个源` : '无限制'})`, value: group.name, })), ]} onChange={(value) => setNewUser((prev) => ({ ...prev, userGroup: value })) } />
)} {/* 修改密码表单 */} {showChangePasswordForm && (
修改用户密码
setChangePasswordUser((prev) => ({ ...prev, password: e.target.value, })) } />
)} {/* 用户列表 */} {(() => { // 检查是否有权限操作任何用户 const hasAnyPermission = config?.UserConfig?.Users?.some(user => (role === 'owner' || (role === 'admin' && (user.role === 'user' || user.username === currentUsername))) ); return hasAnyPermission ? ( ) : (
); })()} 用户名 角色 状态 用户组 采集源权限 机器码 操作 {/* 按规则排序用户:自己 -> 站长(若非自己) -> 管理员 -> 其他 */} {(() => { const sortedUsers = [...config.UserConfig.Users].sort((a, b) => { type UserInfo = (typeof config.UserConfig.Users)[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); }); 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 ( {(role === 'owner' || (role === 'admin' && (user.role === 'user' || user.username === currentUsername))) ? ( handleSelectUser(user.username, selected)} /> ) : (
)}
{user.username}
{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 && ( )}
); })} ); })()}
{/* 配置用户采集源权限弹窗 */} { if (!open) { setShowConfigureApisModal(false); setSelectedUser(null); setSelectedApis([]); } }} title={`配置用户采集源权限${selectedUser ? ` - ${selectedUser.username}` : ''}`} size='lg' footer={ selectedUser ? ( <> ) : null } >
配置说明 提示:全不选为无限制,选中的采集源将限制用户只能访问这些源

选择可用的采集源:

{config?.SourceConfig?.map((source) => ( { if (isSelected) { setSelectedApis([...selectedApis, source.key]); } else { setSelectedApis(selectedApis.filter(api => api !== source.key)); } }} >
{source.name}
{source.api ? (
{extractDomain(source.api)}
) : null}
))}
已选择: {selectedApis.length > 0 ? `${selectedApis.length} 个源` : '无限制'}
{/* 添加用户组弹窗 */} { if (!open) { setShowAddUserGroupForm(false); setNewUserGroup({ name: '', enabledApis: [] }); } }} title='添加新用户组' size='lg' footer={ <> } >
setNewUserGroup((prev) => ({ ...prev, name: e.target.value })) } />
{config?.SourceConfig?.map((source) => ( { if (isSelected) { setNewUserGroup(prev => ({ ...prev, enabledApis: [...prev.enabledApis, source.key] })); } else { setNewUserGroup(prev => ({ ...prev, enabledApis: prev.enabledApis.filter(api => api !== source.key) })); } }} >
{source.name}
{source.api ? (
{extractDomain(source.api)}
) : null}
))}
{/* 编辑用户组弹窗 */} { if (!open) { setShowEditUserGroupForm(false); setEditingUserGroup(null); } }} title={`编辑用户组${editingUserGroup ? ` - ${editingUserGroup.name}` : ''}`} size='lg' footer={ editingUserGroup ? ( <> ) : null } > {editingUserGroup ? (
{config?.SourceConfig?.map((source) => ( { if (isSelected) { setEditingUserGroup(prev => prev ? { ...prev, enabledApis: [...prev.enabledApis, source.key] } : null); } else { setEditingUserGroup(prev => prev ? { ...prev, enabledApis: prev.enabledApis.filter(api => api !== source.key) } : null); } }} >
{source.name}
{source.api ? (
{extractDomain(source.api)}
) : null}
))}
) : null}
{/* 配置用户组弹窗 */} { if (!open) { setShowConfigureUserGroupModal(false); setSelectedUserForGroup(null); setSelectedUserGroups([]); } }} title={`配置用户组${selectedUserForGroup ? ` - ${selectedUserForGroup.username}` : ''}`} size='lg' footer={ selectedUserForGroup ? ( <> ) : null } >
配置说明 提示:选择"无用户组"为无限制,选择特定用户组将限制用户只能访问该用户组允许的采集源 0 ? selectedUserGroups[0] : ''} options={[ { value: '', label: '无用户组(无限制)' }, ...userGroups.map((group) => ({ value: group.name, label: `${group.name} ${group.enabledApis && group.enabledApis.length > 0 ? `(${group.enabledApis.length} 个源)` : ''}`, })), ]} onChange={(value) => setSelectedUserGroups(value ? [value] : [])} />

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

{/* 删除用户组确认弹窗 */} { if (!open) { setShowDeleteUserGroupModal(false); setDeletingUserGroup(null); } }} title='确认删除用户组' footer={ deletingUserGroup ? ( <> ) : null } > {deletingUserGroup ? (
危险操作警告 删除用户组 {deletingUserGroup.name} 将影响所有使用该组的用户,此操作不可恢复! {deletingUserGroup.affectedUsers.length > 0 ? ( 将影响 {deletingUserGroup.affectedUsers.length} 个用户
{deletingUserGroup.affectedUsers.map((user, index) => (
• {user.username} ({user.role})
))}

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

) : ( 当前没有用户使用此用户组 )}
) : null}
{/* 删除用户确认弹窗 */} { if (!open) { setShowDeleteUserModal(false); setDeletingUser(null); } }} title='确认删除用户' footer={ <> } > 危险操作警告 删除用户 {deletingUser} 将同时删除其搜索历史、播放记录和收藏夹,此操作不可恢复! {/* 批量设置用户组弹窗 */} { if (!open) { setShowBatchUserGroupModal(false); setSelectedUserGroup(''); } }} title='批量设置用户组' footer={ <> } >
批量操作说明 将为选中的 {selectedUsers.size} 个用户 设置用户组,选择"无用户组"为无限制 ({ value: group.name, label: `${group.name} ${group.enabledApis && group.enabledApis.length > 0 ? `(${group.enabledApis.length} 个源)` : ''}`, })), ]} onChange={setSelectedUserGroup} />

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

{/* 通用弹窗组件 */}
); } // 视频源配置组件 const VideoSourceConfig = ({ config, refreshConfig, }: { config: AdminConfig | null; refreshConfig: () => Promise; }) => { const { alertModal, showAlert, hideAlert } = useAlertModal(); const { isLoading, withLoading } = useLoadingState(); const [sources, setSources] = useState([]); const [showAddForm, setShowAddForm] = useState(false); const [editingSource, setEditingSource] = useState(null); const [orderChanged, setOrderChanged] = useState(false); const [newSource, setNewSource] = useState({ name: '', key: '', api: '', detail: '', disabled: false, from: 'config', }); // 批量操作相关状态 const [selectedSources, setSelectedSources] = useState>(new Set()); // 使用 useMemo 计算全选状态,避免每次渲染都重新计算 const selectAll = useMemo(() => { return selectedSources.size === sources.length && selectedSources.size > 0; }, [selectedSources.size, sources.length]); // 确认弹窗状态 const [confirmModal, setConfirmModal] = useState<{ isOpen: boolean; title: string; message: string; onConfirm: () => void; onCancel: () => void; }>({ isOpen: false, title: '', message: '', onConfirm: () => { }, onCancel: () => { } }); // 有效性检测相关状态 const [showValidationModal, setShowValidationModal] = useState(false); const [searchKeyword, setSearchKeyword] = useState(''); const [isValidating, setIsValidating] = useState(false); const [validationResults, setValidationResults] = useState>([]); // 单个视频源验证状态 const [singleValidationResult, setSingleValidationResult] = useState<{ status: 'valid' | 'invalid' | 'no_results' | 'validating' | null; message: string; details?: { responseTime?: number; resultCount?: number; error?: string; searchKeyword?: string; }; }>({ status: null, message: '' }); const [isSingleValidating, setIsSingleValidating] = useState(false); // 新增视频源验证状态 const [newSourceValidationResult, setNewSourceValidationResult] = useState<{ status: 'valid' | 'invalid' | 'no_results' | 'validating' | null; message: string; details?: { responseTime?: number; resultCount?: number; error?: string; searchKeyword?: string; }; }>({ status: null, message: '' }); const [isNewSourceValidating, setIsNewSourceValidating] = useState(false); // dnd-kit 传感器 const sensors = useSensors( useSensor(PointerSensor, { activationConstraint: { distance: 5, // 轻微位移即可触发 }, }), useSensor(TouchSensor, { activationConstraint: { delay: 150, // 长按 150ms 后触发,避免与滚动冲突 tolerance: 5, }, }) ); // 初始化 useEffect(() => { if (config?.SourceConfig) { setSources(config.SourceConfig); // 进入时重置 orderChanged setOrderChanged(false); // 重置选择状态 setSelectedSources(new Set()); } }, [config]); // 通用 API 请求 const callSourceApi = async (body: Record) => { try { const resp = await fetch('/api/admin/source', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ...body }), }); if (!resp.ok) { const data = await resp.json().catch(() => ({})); throw new Error(data.error || `操作失败: ${resp.status}`); } // 成功后刷新配置 await refreshConfig(); } catch (err) { showError(err instanceof Error ? err.message : '操作失败', showAlert); throw err; // 向上抛出方便调用处判断 } }; const handleToggleEnable = (key: string) => { const target = sources.find((s) => s.key === key); if (!target) return; const action = target.disabled ? 'enable' : 'disable'; withLoading(`toggleSource_${key}`, () => callSourceApi({ action, key })).catch(() => { console.error('操作失败', action, key); }); }; const handleDelete = (key: string) => { withLoading(`deleteSource_${key}`, () => callSourceApi({ action: 'delete', key })).catch(() => { console.error('操作失败', 'delete', key); }); }; const handleAddSource = () => { if (!newSource.name || !newSource.key || !newSource.api) return; withLoading('addSource', async () => { await callSourceApi({ action: 'add', key: newSource.key, name: newSource.name, api: newSource.api, detail: newSource.detail, }); setNewSource({ name: '', key: '', api: '', detail: '', disabled: false, from: 'custom', }); setShowAddForm(false); // 清除检测结果 clearNewSourceValidation(); }).catch(() => { console.error('操作失败', 'add', newSource); }); }; const handleEditSource = () => { if (!editingSource || !editingSource.name || !editingSource.api) return; withLoading('editSource', async () => { await callSourceApi({ action: 'edit', key: editingSource.key, name: editingSource.name, api: editingSource.api, detail: editingSource.detail, }); setEditingSource(null); }).catch(() => { console.error('操作失败', 'edit', editingSource); }); }; const handleCancelEdit = () => { setEditingSource(null); // 清除单个源的检测结果 setSingleValidationResult({ status: null, message: '' }); setIsSingleValidating(false); }; // 清除新增视频源检测结果 const clearNewSourceValidation = () => { setNewSourceValidationResult({ status: null, message: '' }); setIsNewSourceValidating(false); }; const handleDragEnd = (event: any) => { const { active, over } = event; if (!over || active.id === over.id) return; const oldIndex = sources.findIndex((s) => s.key === active.id); const newIndex = sources.findIndex((s) => s.key === over.id); setSources((prev) => arrayMove(prev, oldIndex, newIndex)); setOrderChanged(true); }; const handleSaveOrder = () => { const order = sources.map((s) => s.key); withLoading('saveSourceOrder', () => callSourceApi({ action: 'sort', order })) .then(() => { setOrderChanged(false); }) .catch(() => { console.error('操作失败', 'sort', order); }); }; // 有效性检测函数 const handleValidateSources = async () => { if (!searchKeyword.trim()) { showAlert({ type: 'warning', title: '请输入搜索关键词', message: '搜索关键词不能为空', showConfirm: true }); return; } await withLoading('validateSources', async () => { setIsValidating(true); setValidationResults([]); // 清空之前的结果 setShowValidationModal(false); // 立即关闭弹窗 // 初始化所有视频源为检测中状态 const initialResults = sources.map(source => ({ key: source.key, name: source.name, status: 'validating' as const, message: '检测中...', resultCount: 0 })); setValidationResults(initialResults); try { // 使用EventSource接收流式数据 const eventSource = new EventSource(`/api/admin/source/validate?q=${encodeURIComponent(searchKeyword.trim())}`); eventSource.onmessage = (event) => { try { const data = JSON.parse(event.data); switch (data.type) { case 'start': console.log(`开始检测 ${data.totalSources} 个视频源`); break; case 'source_result': case 'source_error': // 更新验证结果 setValidationResults(prev => { const existing = prev.find(r => r.key === data.source); if (existing) { return prev.map(r => r.key === data.source ? { key: data.source, name: sources.find(s => s.key === data.source)?.name || data.source, status: data.status, message: data.status === 'valid' ? '搜索正常' : data.status === 'no_results' ? '无法搜索到结果' : '连接失败', resultCount: data.status === 'valid' ? 1 : 0 } : r); } else { return [...prev, { key: data.source, name: sources.find(s => s.key === data.source)?.name || data.source, status: data.status, message: data.status === 'valid' ? '搜索正常' : data.status === 'no_results' ? '无法搜索到结果' : '连接失败', resultCount: data.status === 'valid' ? 1 : 0 }]; } }); break; case 'complete': console.log(`检测完成,共检测 ${data.completedSources} 个视频源`); eventSource.close(); setIsValidating(false); break; } } catch (error) { console.error('解析EventSource数据失败:', error); } }; eventSource.onerror = (error) => { console.error('EventSource错误:', error); eventSource.close(); setIsValidating(false); showAlert({ type: 'error', title: '验证失败', message: '连接错误,请重试', showConfirm: true }); }; // 设置超时,防止长时间等待 setTimeout(() => { if (eventSource.readyState === EventSource.OPEN) { eventSource.close(); setIsValidating(false); showAlert({ type: 'warning', title: '验证超时', message: '检测超时,请重试', showConfirm: true }); } }, 60000); // 60秒超时 } catch (error) { setIsValidating(false); showAlert({ type: 'error', title: '验证失败', message: error instanceof Error ? error.message : '未知错误', showConfirm: true }); throw error; } }); }; // 通用视频源有效性检测函数 const handleValidateSource = async ( api: string, name: string, isNewSource: boolean = false ) => { if (!api.trim()) { showAlert({ type: 'warning', title: 'API地址不能为空', message: '请输入有效的API地址', showConfirm: true }); return; } const validationKey = isNewSource ? 'validateNewSource' : 'validateSingleSource'; const setValidating = isNewSource ? setIsNewSourceValidating : setIsSingleValidating; const setResult = isNewSource ? setNewSourceValidationResult : setSingleValidationResult; await withLoading(validationKey, async () => { setValidating(true); setResult({ status: 'validating', message: '检测中...' }); const startTime = Date.now(); const testKeyword = '灵笼'; try { // 构建检测 URL,使用临时 API 地址 const eventSource = new EventSource(`/api/admin/source/validate?q=${encodeURIComponent(testKeyword)}&tempApi=${encodeURIComponent(api.trim())}&tempName=${encodeURIComponent(name)}`); eventSource.onmessage = (event) => { try { const data = JSON.parse(event.data); const responseTime = Date.now() - startTime; switch (data.type) { case 'start': console.log(`开始检测视频源: ${name}`); break; case 'source_result': case 'source_error': if (data.source === 'temp') { let message = ''; let details: any = { responseTime, searchKeyword: testKeyword }; if (data.status === 'valid') { message = '搜索正常'; details.resultCount = data.resultCount || 0; } else if (data.status === 'no_results') { message = '无法搜索到结果'; details.resultCount = 0; } else { message = '连接失败'; details.error = data.error || '未知错误'; } setResult({ status: data.status, message, details }); } break; case 'complete': console.log(`检测完成: ${name}`); eventSource.close(); setValidating(false); break; } } catch (error) { console.error('解析EventSource数据失败:', error); } }; eventSource.onerror = (error) => { console.error('EventSource错误:', error); eventSource.close(); setValidating(false); const responseTime = Date.now() - startTime; setResult({ status: 'invalid', message: '连接错误,请重试', details: { responseTime, error: '网络连接失败', searchKeyword: testKeyword } }); }; // 设置超时,防止长时间等待 setTimeout(() => { if (eventSource.readyState === EventSource.OPEN) { eventSource.close(); setValidating(false); const responseTime = Date.now() - startTime; setResult({ status: 'invalid', message: '检测超时,请重试', details: { responseTime, error: '请求超时(30秒)', searchKeyword: testKeyword } }); } }, 30000); // 30秒超时 } catch (error) { setValidating(false); const responseTime = Date.now() - startTime; setResult({ status: 'invalid', message: error instanceof Error ? error.message : '未知错误', details: { responseTime, error: error instanceof Error ? error.message : '未知错误', searchKeyword: testKeyword } }); } }); }; // 单个视频源有效性检测函数 const handleValidateSingleSource = async () => { if (!editingSource) { showAlert({ type: 'warning', title: '没有可检测的视频源', message: '请确保正在编辑视频源', showConfirm: true }); return; } await handleValidateSource(editingSource.api, editingSource.name, false); }; // 新增视频源有效性检测函数 const handleValidateNewSource = async () => { if (!newSource.name.trim()) { showAlert({ type: 'warning', title: '视频源名称不能为空', message: '请输入视频源名称', showConfirm: true }); return; } await handleValidateSource(newSource.api, newSource.name, true); }; // 获取有效性状态显示 const getValidationStatus = (sourceKey: string) => { const result = validationResults.find(r => r.key === sourceKey); if (!result) return null; switch (result.status) { case 'validating': return { text: '检测中', className: 'bg-accent/10 text-accent', icon: '⟳', message: result.message }; case 'valid': return { text: '有效', className: 'bg-accent/10 text-accent', icon: '✓', message: result.message }; case 'no_results': return { text: '无法搜索', className: 'bg-yellow-100 dark:bg-yellow-900/20 text-yellow-800 dark:text-yellow-300', icon: '⚠', message: result.message }; case 'invalid': return { text: '无效', className: 'bg-red-100 dark:bg-red-900/20 text-red-800 dark:text-red-300', icon: '✗', message: result.message }; default: return null; } }; // 可拖拽行封装 (dnd-kit) const DraggableRow = ({ source }: { source: DataSource }) => { const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id: source.key }); const style = { transform: CSS.Transform.toString(transform), transition, } as React.CSSProperties; return ( handleSelectSource(source.key, selected)} /> {source.name} {source.key} {source.api} {source.detail || '-'} {!source.disabled ? '启用中' : '已禁用'} {(() => { const status = getValidationStatus(source.key); if (!status) { return ( 未检测 ); } return ( {status.icon} {status.text} ); })()}
); }; // 全选/取消全选 const handleSelectAll = useCallback((checked: boolean) => { if (checked) { const allKeys = sources.map(s => s.key); setSelectedSources(new Set(allKeys)); } else { setSelectedSources(new Set()); } }, [sources]); // 单个选择 const handleSelectSource = useCallback((key: string, checked: boolean) => { setSelectedSources(prev => { const newSelected = new Set(prev); if (checked) { newSelected.add(key); } else { newSelected.delete(key); } return newSelected; }); }, []); // 批量操作 const handleBatchOperation = async (action: 'batch_enable' | 'batch_disable' | 'batch_delete') => { if (selectedSources.size === 0) { showAlert({ type: 'warning', title: '请先选择要操作的视频源', message: '请选择至少一个视频源', showConfirm: true }); return; } const keys = Array.from(selectedSources); let confirmMessage = ''; let actionName = ''; switch (action) { case 'batch_enable': confirmMessage = `确定要启用选中的 ${keys.length} 个视频源吗?`; actionName = '批量启用'; break; case 'batch_disable': confirmMessage = `确定要禁用选中的 ${keys.length} 个视频源吗?`; actionName = '批量禁用'; break; case 'batch_delete': confirmMessage = `确定要删除选中的 ${keys.length} 个视频源吗?此操作不可恢复!`; actionName = '批量删除'; break; } // 显示确认弹窗 setConfirmModal({ isOpen: true, title: '确认操作', message: confirmMessage, onConfirm: async () => { try { await withLoading(`batchSource_${action}`, () => callSourceApi({ action, keys })); showAlert({ type: 'success', title: `${actionName}成功`, message: `${actionName}了 ${keys.length} 个视频源`, timer: 2000 }); // 重置选择状态 setSelectedSources(new Set()); } catch (err) { showAlert({ type: 'error', title: `${actionName}失败`, message: err instanceof Error ? err.message : '操作失败', showConfirm: true }); } setConfirmModal({ isOpen: false, title: '', message: '', onConfirm: () => { }, onCancel: () => { } }); }, onCancel: () => { setConfirmModal({ isOpen: false, title: '', message: '', onConfirm: () => { }, onCancel: () => { } }); } }); }; if (!config) { return (
加载中...
); } return (
{/* 添加视频源表单 */}

视频源列表

{/* 批量操作按钮 - 移动端显示在下一行,PC端显示在左侧 */} {selectedSources.size > 0 && ( <>
已选 {selectedSources.size} 已选择 {selectedSources.size} 个视频源
)}
{showAddForm && (
添加视频源 填写资源站名称、唯一 Key 和采集 API 地址。
setNewSource((prev) => ({ ...prev, name: e.target.value })) } variant='secondary' /> setNewSource((prev) => ({ ...prev, key: e.target.value })) } variant='secondary' /> setNewSource((prev) => ({ ...prev, api: e.target.value })) } variant='secondary' /> setNewSource((prev) => ({ ...prev, detail: e.target.value })) } variant='secondary' />
)} {/* 编辑视频源表单 */} {editingSource && (
编辑视频源 {editingSource.name} · {editingSource.key}
setEditingSource((prev) => prev ? ({ ...prev, name: e.target.value }) : null) } variant='secondary' />

Key 已用于数据关联,不能编辑。

setEditingSource((prev) => prev ? ({ ...prev, api: e.target.value }) : null) } variant='secondary' /> setEditingSource((prev) => prev ? ({ ...prev, detail: e.target.value }) : null) } variant='secondary' />
)} {/* 视频源表格 */} 名称 Key API 地址 Detail 地址 状态 有效性 操作 s.key)} strategy={verticalListSortingStrategy} > {sources.map((source) => ( ))} {/* 保存排序按钮 */} {orderChanged && (
)} {/* 有效性检测弹窗 */} } > setSearchKeyword(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && handleValidateSources()} /> {/* 通用弹窗组件 */} {/* 批量操作确认弹窗 */} { if (!open) confirmModal.onCancel(); }} title={confirmModal.title} description={confirmModal.message} footer={ <> } />
); }; // 分类配置组件 const CategoryConfig = ({ config, refreshConfig, }: { config: AdminConfig | null; refreshConfig: () => Promise; }) => { const { alertModal, showAlert, hideAlert } = useAlertModal(); const { isLoading, withLoading } = useLoadingState(); const [categories, setCategories] = useState([]); const [showAddForm, setShowAddForm] = useState(false); const [orderChanged, setOrderChanged] = useState(false); const [newCategory, setNewCategory] = useState({ name: '', type: 'movie', query: '', disabled: false, from: 'config', }); // dnd-kit 传感器 const sensors = useSensors( useSensor(PointerSensor, { activationConstraint: { distance: 5, // 轻微位移即可触发 }, }), useSensor(TouchSensor, { activationConstraint: { delay: 150, // 长按 150ms 后触发,避免与滚动冲突 tolerance: 5, }, }) ); // 初始化 useEffect(() => { if (config?.CustomCategories) { setCategories(config.CustomCategories); // 进入时重置 orderChanged setOrderChanged(false); } }, [config]); // 通用 API 请求 const callCategoryApi = async (body: Record) => { try { const resp = await fetch('/api/admin/category', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ...body }), }); if (!resp.ok) { const data = await resp.json().catch(() => ({})); throw new Error(data.error || `操作失败: ${resp.status}`); } // 成功后刷新配置 await refreshConfig(); } catch (err) { showError(err instanceof Error ? err.message : '操作失败', showAlert); throw err; // 向上抛出方便调用处判断 } }; const handleToggleEnable = (query: string, type: 'movie' | 'tv') => { const target = categories.find((c) => c.query === query && c.type === type); if (!target) return; const action = target.disabled ? 'enable' : 'disable'; withLoading(`toggleCategory_${query}_${type}`, () => callCategoryApi({ action, query, type })).catch(() => { console.error('操作失败', action, query, type); }); }; const handleDelete = (query: string, type: 'movie' | 'tv') => { withLoading(`deleteCategory_${query}_${type}`, () => callCategoryApi({ action: 'delete', query, type })).catch(() => { console.error('操作失败', 'delete', query, type); }); }; const handleAddCategory = () => { if (!newCategory.name || !newCategory.query) return; withLoading('addCategory', async () => { await callCategoryApi({ action: 'add', name: newCategory.name, type: newCategory.type, query: newCategory.query, }); setNewCategory({ name: '', type: 'movie', query: '', disabled: false, from: 'custom', }); setShowAddForm(false); }).catch(() => { console.error('操作失败', 'add', newCategory); }); }; const handleDragEnd = (event: any) => { const { active, over } = event; if (!over || active.id === over.id) return; const oldIndex = categories.findIndex( (c) => `${c.query}:${c.type}` === active.id ); const newIndex = categories.findIndex( (c) => `${c.query}:${c.type}` === over.id ); setCategories((prev) => arrayMove(prev, oldIndex, newIndex)); setOrderChanged(true); }; const handleSaveOrder = () => { const order = categories.map((c) => `${c.query}:${c.type}`); withLoading('saveCategoryOrder', () => callCategoryApi({ action: 'sort', order })) .then(() => { setOrderChanged(false); }) .catch(() => { console.error('操作失败', 'sort', order); }); }; // 可拖拽行封装 (dnd-kit) const DraggableRow = ({ category }: { category: CustomCategory }) => { const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id: `${category.query}:${category.type}` }); const style = { transform: CSS.Transform.toString(transform), transition, } as React.CSSProperties; return ( {category.name || '-'} {category.type === 'movie' ? '电影' : '电视剧'} {category.query} {!category.disabled ? '启用中' : '已禁用'}
{category.from !== 'config' && ( )}
); }; if (!config) { return (
加载中...
); } return (
{/* 添加分类表单 */}

自定义分类列表

{showAddForm && (
setNewCategory((prev) => ({ ...prev, name: e.target.value })) } /> setNewCategory((prev) => ({ ...prev, type: value as 'movie' | 'tv', })) } /> setNewCategory((prev) => ({ ...prev, query: e.target.value })) } />
)} {/* 分类表格 */} 分类名称 类型 搜索关键词 状态 操作 `${c.query}:${c.type}`)} strategy={verticalListSortingStrategy} > {categories.map((category) => ( ))} {/* 保存排序按钮 */} {orderChanged && (
)} {/* 通用弹窗组件 */}
); }; // 新增配置文件组件 const ConfigFileComponent = ({ config, refreshConfig }: { config: AdminConfig | null; refreshConfig: () => Promise }) => { const { alertModal, showAlert, hideAlert } = useAlertModal(); const { isLoading, withLoading } = useLoadingState(); const [configContent, setConfigContent] = useState(''); const [subscriptionUrl, setSubscriptionUrl] = useState(''); const [autoUpdate, setAutoUpdate] = useState(false); const [lastCheckTime, setLastCheckTime] = useState(''); useEffect(() => { if (config?.ConfigFile) { setConfigContent(config.ConfigFile); } if (config?.ConfigSubscribtion) { setSubscriptionUrl(config.ConfigSubscribtion.URL); setAutoUpdate(config.ConfigSubscribtion.AutoUpdate); setLastCheckTime(config.ConfigSubscribtion.LastCheck || ''); } }, [config]); // 拉取订阅配置 const handleFetchConfig = async () => { if (!subscriptionUrl.trim()) { showError('请输入订阅URL', showAlert); return; } await withLoading('fetchConfig', async () => { try { const resp = await fetch('/api/admin/config_subscription/fetch', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ url: subscriptionUrl }), }); if (!resp.ok) { const data = await resp.json().catch(() => ({})); throw new Error(data.error || `拉取失败: ${resp.status}`); } const data = await resp.json(); if (data.configContent) { setConfigContent(data.configContent); // 更新本地配置的最后检查时间 const currentTime = new Date().toISOString(); setLastCheckTime(currentTime); showSuccess('配置拉取成功', showAlert); } else { showError('拉取失败:未获取到配置内容', showAlert); } } catch (err) { showError(err instanceof Error ? err.message : '拉取失败', showAlert); throw err; } }); }; // 保存配置文件 const handleSave = async () => { await withLoading('saveConfig', async () => { try { const resp = await fetch('/api/admin/config_file', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ configFile: configContent, subscriptionUrl, autoUpdate, lastCheckTime: lastCheckTime || new Date().toISOString() }), }); if (!resp.ok) { const data = await resp.json().catch(() => ({})); throw new Error(data.error || `保存失败: ${resp.status}`); } showSuccess('配置文件保存成功', showAlert); await refreshConfig(); } catch (err) { showError(err instanceof Error ? err.message : '保存失败', showAlert); throw err; } }); }; if (!config) { return (
加载中...
); } return (
{/* 配置订阅区域 */} 配置订阅 最后更新: {lastCheckTime ? new Date(lastCheckTime).toLocaleString('zh-CN') : '从未更新'} setSubscriptionUrl(e.target.value)} placeholder='https://example.com/config.json' variant='secondary' />

输入配置文件的订阅地址,要求 JSON 格式,且使用 Base58 编码

{/* 配置文件编辑区域 */}