/* 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, Check, CheckCircle, ChevronDown, ChevronUp, Database, ExternalLink, FileText, FolderOpen, Settings, Tv, Users, Video, } from 'lucide-react'; import { GripVertical } from 'lucide-react'; import { Suspense, useCallback, useEffect, useMemo, useState } from 'react'; import { createPortal } from 'react-dom'; import { AdminConfig, AdminConfigResult } from '@/lib/admin.types'; import { getAuthInfoFromBrowserCookie } from '@/lib/auth'; 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', }; // 通用弹窗组件 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) => { 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 && ( )}
, document.body ); }; // 弹窗状态管理 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; } // 视频源数据类型 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'; } // 自定义分类数据类型 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; } const UserConfig = ({ config, role, refreshConfig }: 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) => ( ))} {userGroups.length === 0 && ( )}
用户组名称 可用视频源 操作
{group.name}
{group.enabledApis && group.enabledApis.length > 0 ? `${group.enabledApis.length} 个源` : '无限制'}
暂无用户组,请添加用户组来管理用户权限
{/* 用户列表 */}

用户列表

{/* 批量操作按钮 */} {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' />
)} {/* 用户列表 */}
{/* 按规则排序用户:自己 -> 站长(若非自己) -> 管理员 -> 其他 */} {(() => { 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 ( ); })} ); })()}
{(() => { // 检查是否有权限操作任何用户 const hasAnyPermission = config?.UserConfig?.Users?.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' /> ) : (
); })()}
用户名 角色 状态 用户组 采集源权限 操作
{(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.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 && ( )}
{/* 配置用户采集源权限弹窗 */} {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: [] }); }}>
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' />
{/* 可用视频源 */}
{config?.SourceConfig?.map((source) => ( ))}
{/* 快速操作按钮 */}
{/* 操作按钮 */}
, document.body )} {/* 编辑用户组弹窗 */} {showEditUserGroupForm && editingUserGroup && createPortal(
{ setShowEditUserGroupForm(false); setEditingUserGroup(null); }}>
e.stopPropagation()}>

编辑用户组 - {editingUserGroup.name}

{/* 可用视频源 */}
{config?.SourceConfig?.map((source) => ( ))}
{/* 快速操作按钮 */}
{/* 操作按钮 */}
, 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 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 [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>([]); // 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); }).catch(() => { console.error('操作失败', 'add', newSource); }); }; 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: '搜索关键词不能为空' }); 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: '连接错误,请重试' }); }; // 设置超时,防止长时间等待 setTimeout(() => { if (eventSource.readyState === EventSource.OPEN) { eventSource.close(); setIsValidating(false); showAlert({ type: 'warning', title: '验证超时', message: '检测超时,请重试' }); } }, 60000); // 60秒超时 } catch (error) { setIsValidating(false); showAlert({ type: 'error', title: '验证失败', message: error instanceof Error ? error.message : '未知错误' }); throw error; } }); }; // 获取有效性状态显示 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-blue-100 dark:bg-blue-900/20 text-blue-800 dark:text-blue-300', icon: '⟳', message: result.message }; case 'valid': return { text: '有效', className: 'bg-green-100 dark:bg-green-900/20 text-green-800 dark:text-green-300', 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, 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' /> {source.name} {source.key} {source.api} {source.detail || '-'} {!source.disabled ? '启用中' : '已禁用'} {(() => { const status = getValidationStatus(source.key); if (!status) { return ( 未检测 ); } return ( {status.icon} {status.text} ); })()} {source.from !== 'config' && ( )} ); }; // 全选/取消全选 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: '请选择至少一个视频源' }); 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 : '操作失败' }); } 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 && (
setNewSource((prev) => ({ ...prev, name: 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' /> setNewSource((prev) => ({ ...prev, key: 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' /> setNewSource((prev) => ({ ...prev, api: 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' /> setNewSource((prev) => ({ ...prev, detail: 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' />
)} {/* 视频源表格 */}
s.key)} strategy={verticalListSortingStrategy} > {sources.map((source) => ( ))}
handleSelectAll(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' /> 名称 Key API 地址 Detail 地址 状态 有效性 操作
{/* 保存排序按钮 */} {orderChanged && (
)} {/* 有效性检测弹窗 */} {showValidationModal && createPortal(
setShowValidationModal(false)}>
e.stopPropagation()}>

视频源有效性检测

请输入检测用的搜索关键词

setSearchKeyword(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' onKeyPress={(e) => e.key === 'Enter' && handleValidateSources()} />
, document.body )} {/* 通用弹窗组件 */} {/* 批量操作确认弹窗 */} {confirmModal.isOpen && createPortal(
e.stopPropagation()}>

{confirmModal.title}

{confirmModal.message}

{/* 操作按钮 */}
, document.body )}
); }; // 分类配置组件 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 })) } 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' /> setNewCategory((prev) => ({ ...prev, query: 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' />
)} {/* 分类表格 */}
`${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') : '从未更新'}
{/* 订阅URL输入 */}
setSubscriptionUrl(e.target.value)} placeholder='https://example.com/config.json' disabled={false} className='w-full px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-green-500 focus:border-transparent transition-all duration-200 shadow-sm hover:border-gray-400 dark:hover:border-gray-500' />

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

{/* 拉取配置按钮 */}
{/* 自动更新开关 */}

启用后系统将定期自动拉取最新配置

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