/* eslint-disable @typescript-eslint/no-explicit-any, no-console, @typescript-eslint/no-non-null-assertion */ '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 { Check, ChevronDown, ChevronUp, Database, ExternalLink, FileText, FolderOpen, Settings, Users, Video, } from 'lucide-react'; import { GripVertical } from 'lucide-react'; import { Suspense, useCallback, useEffect, useState } from 'react'; import Swal from 'sweetalert2'; import { AdminConfig, AdminConfigResult } from '@/lib/admin.types'; import { getAuthInfoFromBrowserCookie } from '@/lib/auth'; import DataMigration from '@/components/DataMigration'; import PageLayout from '@/components/PageLayout'; // 统一弹窗方法(必须在首次使用前定义) const showError = (message: string) => Swal.fire({ icon: 'error', title: '错误', text: message }); const showSuccess = (message: string) => Swal.fire({ icon: 'success', title: '成功', text: message, timer: 2000, showConfirmButton: false, }); // 新增站点配置类型 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 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 [showAddUserForm, setShowAddUserForm] = useState(false); const [showChangePasswordForm, setShowChangePasswordForm] = useState(false); const [newUser, setNewUser] = useState({ username: '', password: '', }); const [changePasswordUser, setChangePasswordUser] = useState({ username: '', password: '', }); // 当前登录用户名 const currentUsername = getAuthInfoFromBrowserCookie()?.username || null; const handleBanUser = async (uname: string) => { await handleUserAction('ban', uname); }; const handleUnbanUser = async (uname: string) => { await handleUserAction('unban', uname); }; const handleSetAdmin = async (uname: string) => { await handleUserAction('setAdmin', uname); }; const handleRemoveAdmin = async (uname: string) => { await handleUserAction('cancelAdmin', uname); }; const handleAddUser = async () => { if (!newUser.username || !newUser.password) return; await handleUserAction('add', newUser.username, newUser.password); setNewUser({ username: '', password: '' }); setShowAddUserForm(false); }; const handleChangePassword = async () => { if (!changePasswordUser.username || !changePasswordUser.password) return; 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 = async (username: string) => { const { isConfirmed } = await Swal.fire({ title: '确认删除用户', text: `删除用户 ${username} 将同时删除其搜索历史、播放记录和收藏夹,此操作不可恢复!`, icon: 'warning', showCancelButton: true, confirmButtonText: '确认删除', cancelButtonText: '取消', confirmButtonColor: '#dc2626', }); if (!isConfirmed) return; await handleUserAction('deleteUser', username); }; // 通用请求函数 const handleUserAction = async ( action: | 'add' | 'ban' | 'unban' | 'setAdmin' | 'cancelAdmin' | 'changePassword' | 'deleteUser', targetUsername: string, targetPassword?: string ) => { try { const res = await fetch('/api/admin/user', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ targetUsername, ...(targetPassword ? { targetPassword } : {}), 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 : '操作失败'); } }; if (!config) { return (
加载中...
); } return (
{/* 用户统计 */}

用户统计

{config.UserConfig.Users.length}
总用户数
{/* 用户列表 */}

用户列表

{/* 添加用户表单 */} {showAddUserForm && (
setNewUser((prev) => ({ ...prev, username: 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-green-500 focus:border-transparent' /> setNewUser((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-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 ( ); })} ); })()}
用户名 角色 状态 操作
{user.username} {user.role === 'owner' ? '站长' : user.role === 'admin' ? '管理员' : '普通用户'} {!user.banned ? '正常' : '已封禁'} {/* 修改密码按钮 */} {canChangePassword && ( )} {canOperate && ( <> {/* 其他操作按钮 */} {user.role === 'user' && ( )} {user.role === 'admin' && ( )} {user.role !== 'owner' && (!user.banned ? ( ) : ( ))} )} {/* 删除用户按钮 - 放在最后,使用更明显的红色样式 */} {canDeleteUser && ( )}
); }; // 视频源配置组件 const VideoSourceConfig = ({ config, refreshConfig, }: { config: AdminConfig | null; refreshConfig: () => Promise; }) => { 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', }); // 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); } }, [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 : '操作失败'); throw err; // 向上抛出方便调用处判断 } }; const handleToggleEnable = (key: string) => { const target = sources.find((s) => s.key === key); if (!target) return; const action = target.disabled ? 'enable' : 'disable'; callSourceApi({ action, key }).catch(() => { console.error('操作失败', action, key); }); }; const handleDelete = (key: string) => { callSourceApi({ action: 'delete', key }).catch(() => { console.error('操作失败', 'delete', key); }); }; const handleAddSource = () => { if (!newSource.name || !newSource.key || !newSource.api) return; callSourceApi({ action: 'add', key: newSource.key, name: newSource.name, api: newSource.api, detail: newSource.detail, }) .then(() => { 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); callSourceApi({ action: 'sort', order }) .then(() => { setOrderChanged(false); }) .catch(() => { console.error('操作失败', 'sort', order); }); }; // 可拖拽行封装 (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 ( {source.name} {source.key} {source.api} {source.detail || '-'} {!source.disabled ? '启用中' : '已禁用'} {source.from !== 'config' && ( )} ); }; if (!config) { return (
加载中...
); } return (
{/* 添加视频源表单 */}

视频源列表

{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) => ( ))}
名称 Key API 地址 Detail 地址 状态 操作
{/* 保存排序按钮 */} {orderChanged && (
)}
); }; // 分类配置组件 const CategoryConfig = ({ config, refreshConfig, }: { config: AdminConfig | null; refreshConfig: () => Promise; }) => { 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 : '操作失败'); 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'; callCategoryApi({ action, query, type }).catch(() => { console.error('操作失败', action, query, type); }); }; const handleDelete = (query: string, type: 'movie' | 'tv') => { callCategoryApi({ action: 'delete', query, type }).catch(() => { console.error('操作失败', 'delete', query, type); }); }; const handleAddCategory = () => { if (!newCategory.name || !newCategory.query) return; callCategoryApi({ action: 'add', name: newCategory.name, type: newCategory.type, query: newCategory.query, }) .then(() => { 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}`); 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 [configContent, setConfigContent] = useState(''); const [saving, setSaving] = useState(false); const [subscriptionUrl, setSubscriptionUrl] = useState(''); const [autoUpdate, setAutoUpdate] = useState(false); const [fetching, setFetching] = 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'); return; } try { setFetching(true); 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('配置拉取成功'); } else { showError('拉取失败:未获取到配置内容'); } } catch (err) { showError(err instanceof Error ? err.message : '拉取失败'); } finally { setFetching(false); } }; // 保存配置文件 const handleSave = async () => { try { setSaving(true); 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('配置文件保存成功'); await refreshConfig(); } catch (err) { showError(err instanceof Error ? err.message : '保存失败'); } finally { setSaving(false); } }; 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 编码

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

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

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