/* eslint-disable @typescript-eslint/no-explicit-any, no-console */ '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 { ChevronDown, ChevronUp, 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 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; ImageProxy: string; DoubanProxy: string; } // 视频源数据类型 interface DataSource { name: string; key: string; api: string; detail?: 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 [userSettings, setUserSettings] = useState({ enableRegistration: false, }); 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; // 检测存储类型是否为 d1 const isD1Storage = typeof window !== 'undefined' && (window as any).RUNTIME_CONFIG?.STORAGE_TYPE === 'd1'; const isUpstashStorage = typeof window !== 'undefined' && (window as any).RUNTIME_CONFIG?.STORAGE_TYPE === 'upstash'; useEffect(() => { if (config?.UserConfig) { setUserSettings({ enableRegistration: config.UserConfig.AllowRegister, }); } }, [config]); // 切换允许注册设置 const toggleAllowRegister = async (value: boolean) => { try { // 先更新本地 UI setUserSettings((prev) => ({ ...prev, enableRegistration: value })); const res = await fetch('/api/admin/user', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'setAllowRegister', allowRegister: value, }), }); 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 : '操作失败'); // revert toggle UI setUserSettings((prev) => ({ ...prev, enableRegistration: !value })); } }; 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 [batchMode, setBatchMode] = useState(false); const [selectedSources, setSelectedSources] = useState>(new Set()); 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) => { // 检查是否为示例源 const source = sources.find(s => s.key === key); if (source?.from === 'config') { showError('示例源不可删除,这些源用于演示功能'); return; } 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 handleToggleBatchMode = () => { setBatchMode(!batchMode); setSelectedSources(new Set()); // 切换模式时清空选择 }; const handleSelectSource = (key: string, checked: boolean) => { const newSelected = new Set(selectedSources); if (checked) { newSelected.add(key); } else { newSelected.delete(key); } setSelectedSources(newSelected); }; const handleSelectAll = (checked: boolean) => { if (checked) { // 只选择可删除的视频源(排除示例源) const deletableSources = sources.filter(source => source.from !== 'config'); setSelectedSources(new Set(deletableSources.map(source => source.key))); } else { setSelectedSources(new Set()); } }; const handleBatchDelete = async () => { if (selectedSources.size === 0) { showError('请先选择要删除的视频源'); return; } const selectedArray = Array.from(selectedSources); const result = await Swal.fire({ title: '确认批量删除', text: `即将删除 ${selectedArray.length} 个视频源,此操作不可撤销!`, icon: 'warning', showCancelButton: true, confirmButtonText: '确认删除', cancelButtonText: '取消', confirmButtonColor: '#ef4444', cancelButtonColor: '#6b7280' }); if (!result.isConfirmed) return; // 批量删除逐个进行,显示进度 let successCount = 0; let errorCount = 0; const errors: string[] = []; for (let i = 0; i < selectedArray.length; i++) { const key = selectedArray[i]; try { await callSourceApi({ action: 'delete', key }); successCount++; // 显示进度 if (selectedArray.length > 1) { Swal.update({ title: '正在删除...', text: `进度: ${i + 1}/${selectedArray.length}`, showConfirmButton: false, showCancelButton: false, allowOutsideClick: false }); } } catch (error) { errorCount++; const sourceName = sources.find(s => s.key === key)?.name || key; errors.push(`${sourceName}: ${error instanceof Error ? error.message : '删除失败'}`); } } // 显示删除结果 if (errorCount === 0) { showSuccess(`成功删除 ${successCount} 个视频源`); setSelectedSources(new Set()); // 清空选择 setBatchMode(false); // 退出批量模式 } else { await Swal.fire({ title: '删除完成', html: `

✅ 成功删除: ${successCount} 个

❌ 删除失败: ${errorCount} 个

${errors.length > 0 ? `
查看错误详情
${errors.map(err => `
${err}
`).join('')}
` : ''}
`, icon: successCount > 0 ? 'warning' : 'error', confirmButtonText: '确定' }); // 清空已成功删除的选择项 const failedKeys = new Set( errors.map(err => { const keyMatch = err.split(':')[0]; return sources.find(s => s.name === keyMatch)?.key; }).filter((key): key is string => Boolean(key)) ); setSelectedSources(failedKeys); } await refreshConfig(); }; // 导出配置 const handleExportConfig = () => { try { // 构建符合要求的配置格式 const exportConfig = { cache_time: config?.SiteConfig?.SiteInterfaceCacheTime || 7200, api_site: {} as Record }; // 将视频源转换为config.json格式 sources.forEach(source => { if (!source.disabled) { exportConfig.api_site[source.key] = { api: source.api, name: source.name, ...(source.detail && { detail: source.detail }) }; } }); // 生成JSON文件并下载 const dataStr = JSON.stringify(exportConfig, null, 2); const dataBlob = new Blob([dataStr], { type: 'application/json' }); const url = URL.createObjectURL(dataBlob); const link = document.createElement('a'); link.href = url; link.download = `config_${new Date().toISOString().split('T')[0]}.json`; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(url); showSuccess('配置文件已导出到下载文件夹'); } catch (error) { showError('导出失败: ' + (error instanceof Error ? error.message : '未知错误')); } }; // 导入配置 const handleImportConfig = (event: React.ChangeEvent) => { const file = event.target.files?.[0]; if (!file) return; // 检查文件类型 if (!file.name.toLowerCase().endsWith('.json')) { showError('请选择JSON文件'); return; } const reader = new FileReader(); reader.onload = async (e) => { try { const content = e.target?.result as string; const importConfig = JSON.parse(content); // 验证配置格式 if (!importConfig.api_site || typeof importConfig.api_site !== 'object') { showError('配置文件格式错误:缺少 api_site 字段'); return; } // 确认导入 const result = await Swal.fire({ title: '确认导入', text: `检测到 ${Object.keys(importConfig.api_site).length} 个视频源,是否继续导入?`, icon: 'question', showCancelButton: true, confirmButtonText: '确认导入', cancelButtonText: '取消', confirmButtonColor: '#059669', cancelButtonColor: '#6b7280' }); if (!result.isConfirmed) return; // 批量导入视频源 let successCount = 0; let errorCount = 0; const errors: string[] = []; for (const [key, source] of Object.entries(importConfig.api_site)) { try { // 类型检查和验证 if (!source || typeof source !== 'object' || Array.isArray(source)) { throw new Error(`${key}: 无效的配置对象`); } const sourceObj = source as { api?: string; name?: string; detail?: string }; if (!sourceObj.api || !sourceObj.name) { throw new Error(`${key}: 缺少必要字段 api 或 name`); } await callSourceApi({ action: 'add', key: key, name: sourceObj.name, api: sourceObj.api, detail: sourceObj.detail || '' }); successCount++; } catch (error) { errorCount++; errors.push(`${key}: ${error instanceof Error ? error.message : '未知错误'}`); } } // 显示导入结果 if (errorCount === 0) { showSuccess(`成功导入 ${successCount} 个视频源`); } else { await Swal.fire({ title: '导入完成', html: `

✅ 成功导入: ${successCount} 个

❌ 导入失败: ${errorCount} 个

${errors.length > 0 ? `
查看错误详情
${errors.map(err => `
${err}
`).join('')}
` : ''}
`, icon: successCount > 0 ? 'warning' : 'error', confirmButtonText: '确定' }); } } catch (error) { showError('配置文件解析失败: ' + (error instanceof Error ? error.message : '文件格式错误')); } }; reader.onerror = () => { showError('文件读取失败'); }; reader.readAsText(file); // 清空input,允许重复选择同一文件 event.target.value = ''; }; 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 ( {/* 拖拽手柄 */} {/* 批量选择复选框 */} {batchMode && ( handleSelectSource(source.key, e.target.checked)} disabled={source.from === 'config'} // 禁用示例源选择 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 disabled:opacity-50' /> )}
{source.name} {source.from === 'config' && ( 示例源 )}
{source.key} {source.api} {source.detail || '-'} {!source.disabled ? '启用中' : '已禁用'} {source.from !== 'config' ? ( ) : ( 不可删除 )} ); }; if (!config) { return (
加载中...
); } return (
{/* 视频源管理工具栏 */}

视频源列表

{/* 批量操作区域 */} {!batchMode ? ( <> {/* 普通模式按钮 */} {/* 导入导出按钮 */}
{/* 添加视频源按钮 */} ) : ( <> {/* 批量模式按钮 */}
已选 {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) => ( ))}
{/* 批量选择列 */} {batchMode && ( 0 && selectedSources.size === sources.length} onChange={(e) => 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 && (
)}
); }; // 新增站点配置组件 const SiteConfigComponent = ({ config }: { config: AdminConfig | null }) => { const [siteSettings, setSiteSettings] = useState({ SiteName: '', Announcement: '', SearchDownstreamMaxPage: 1, SiteInterfaceCacheTime: 7200, ImageProxy: '', DoubanProxy: '', }); // 保存状态 const [saving, setSaving] = useState(false); // 检测存储类型是否为 d1 或 upstash const isD1Storage = typeof window !== 'undefined' && (window as any).RUNTIME_CONFIG?.STORAGE_TYPE === 'd1'; const isUpstashStorage = typeof window !== 'undefined' && (window as any).RUNTIME_CONFIG?.STORAGE_TYPE === 'upstash'; useEffect(() => { if (config?.SiteConfig) { setSiteSettings({ ...config.SiteConfig, ImageProxy: config.SiteConfig.ImageProxy || '', DoubanProxy: config.SiteConfig.DoubanProxy || '', }); } }, [config]); // 保存站点配置 const handleSave = async () => { try { setSaving(true); const resp = await fetch('/api/admin/site', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ...siteSettings }), }); if (!resp.ok) { const data = await resp.json().catch(() => ({})); throw new Error(data.error || `保存失败: ${resp.status}`); } showSuccess('保存成功, 请刷新页面'); } catch (err) { showError(err instanceof Error ? err.message : '保存失败'); } finally { setSaving(false); } }; if (!config) { return (
加载中...
); } return (
{/* 站点名称 */}
!isD1Storage && !isUpstashStorage && setSiteSettings((prev) => ({ ...prev, SiteName: e.target.value })) } disabled={isD1Storage || isUpstashStorage} 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-green-500 focus:border-transparent ${ isD1Storage || isUpstashStorage ? 'opacity-50 cursor-not-allowed' : '' }`} />
{/* 站点公告 */}