From a4c56e2cdd1ece6fdbb25e5708355128e89f5405 Mon Sep 17 00:00:00 2001 From: shinya Date: Sat, 5 Jul 2025 00:29:43 +0800 Subject: [PATCH] feat: roughly admin info struct and interface --- package.json | 4 + pnpm-lock.yaml | 72 +++ src/app/admin/page.tsx | 910 ++++++++++++++++++++++++++++++ src/app/api/admin/config/route.ts | 60 ++ src/app/api/detail/route.ts | 4 +- src/app/api/register/route.ts | 4 +- src/app/api/search/route.ts | 4 +- src/app/layout.tsx | 15 +- src/app/search/page.tsx | 11 +- src/lib/admin.types.ts | 30 + src/lib/config.ts | 183 +++++- src/lib/db.ts | 34 ++ src/lib/downstream.ts | 7 +- src/lib/fetchVideoDetail.ts | 4 +- src/lib/runtime.ts | 16 +- 15 files changed, 1305 insertions(+), 53 deletions(-) create mode 100644 src/app/admin/page.tsx create mode 100644 src/app/api/admin/config/route.ts create mode 100644 src/lib/admin.types.ts diff --git a/package.json b/package.json index e35cf07..9435622 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,10 @@ }, "dependencies": { "@cloudflare/next-on-pages": "^1.13.12", + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/modifiers": "^9.0.0", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", "@headlessui/react": "^2.2.4", "@heroicons/react": "^2.2.0", "@upstash/redis": "^1.25.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9e0ff3f..f3a3560 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,18 @@ importers: '@cloudflare/next-on-pages': specifier: ^1.13.12 version: 1.13.12(vercel@44.2.7(rollup@2.79.2))(wrangler@4.22.0) + '@dnd-kit/core': + specifier: ^6.3.1 + version: 6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@dnd-kit/modifiers': + specifier: ^9.0.0 + version: 9.0.0(@dnd-kit/core@6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) + '@dnd-kit/sortable': + specifier: ^10.0.0 + version: 10.0.0(@dnd-kit/core@6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) + '@dnd-kit/utilities': + specifier: ^3.2.2 + version: 3.2.2(react@18.3.1) '@headlessui/react': specifier: ^2.2.4 version: 2.2.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -951,6 +963,34 @@ packages: resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} + '@dnd-kit/accessibility@3.1.1': + resolution: {integrity: sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==} + peerDependencies: + react: '>=16.8.0' + + '@dnd-kit/core@6.3.1': + resolution: {integrity: sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@dnd-kit/modifiers@9.0.0': + resolution: {integrity: sha512-ybiLc66qRGuZoC20wdSSG6pDXFikui/dCNGthxv4Ndy8ylErY0N3KVxY2bgo7AWwIbxDmXDg3ylAFmnrjcbVvw==} + peerDependencies: + '@dnd-kit/core': ^6.3.0 + react: '>=16.8.0' + + '@dnd-kit/sortable@10.0.0': + resolution: {integrity: sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==} + peerDependencies: + '@dnd-kit/core': ^6.3.0 + react: '>=16.8.0' + + '@dnd-kit/utilities@3.2.2': + resolution: {integrity: sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==} + peerDependencies: + react: '>=16.8.0' + '@edge-runtime/format@2.2.1': resolution: {integrity: sha512-JQTRVuiusQLNNLe2W9tnzBlV/GvSVcozLl4XZHk5swnRZ/v6jp8TqR8P7sqmJsQqblDZ3EztcWmLDbhRje/+8g==} engines: {node: '>=16'} @@ -7306,6 +7346,38 @@ snapshots: dependencies: '@jridgewell/trace-mapping': 0.3.9 + '@dnd-kit/accessibility@3.1.1(react@18.3.1)': + dependencies: + react: 18.3.1 + tslib: 2.8.1 + + '@dnd-kit/core@6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@dnd-kit/accessibility': 3.1.1(react@18.3.1) + '@dnd-kit/utilities': 3.2.2(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + tslib: 2.8.1 + + '@dnd-kit/modifiers@9.0.0(@dnd-kit/core@6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)': + dependencies: + '@dnd-kit/core': 6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@dnd-kit/utilities': 3.2.2(react@18.3.1) + react: 18.3.1 + tslib: 2.8.1 + + '@dnd-kit/sortable@10.0.0(@dnd-kit/core@6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)': + dependencies: + '@dnd-kit/core': 6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@dnd-kit/utilities': 3.2.2(react@18.3.1) + react: 18.3.1 + tslib: 2.8.1 + + '@dnd-kit/utilities@3.2.2(react@18.3.1)': + dependencies: + react: 18.3.1 + tslib: 2.8.1 + '@edge-runtime/format@2.2.1': {} '@edge-runtime/node-utils@2.3.0': {} diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx new file mode 100644 index 0000000..bbecc0e --- /dev/null +++ b/src/app/admin/page.tsx @@ -0,0 +1,910 @@ +/* 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 { useEffect, useState } from 'react'; + +import { AdminConfig, AdminConfigResult } from '@/lib/admin.types'; + +import PageLayout from '@/components/PageLayout'; + +// 新增站点配置类型 +interface SiteConfig { + SiteName: string; + Announcement: string; + SearchDownstreamMaxPage: number; + SiteInterfaceCacheTime: number; + SearchResultDefaultAggregate: boolean; +} + +// 视频源数据类型 +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}
} +
+ ); +}; + +// 用户配置组件 +const UserConfig = ({ config }: { config: AdminConfig | null }) => { + const [userSettings, setUserSettings] = useState({ + enableRegistration: false, + }); + const [showAddUserForm, setShowAddUserForm] = useState(false); + const [newUser, setNewUser] = useState({ + username: '', + password: '', + }); + + useEffect(() => { + if (config?.UserConfig) { + setUserSettings({ + enableRegistration: config.UserConfig.AllowRegister, + }); + } + }, [config]); + + const handleBanUser = (username: string) => { + // 这里应该调用API来封禁用户 + console.log('封禁用户:', username); + }; + + const handleUnbanUser = (username: string) => { + // 这里应该调用API来解封用户 + console.log('解封用户:', username); + }; + + const handleSetAdmin = (username: string) => { + // 这里应该调用API来设为管理员 + console.log('设为管理员:', username); + }; + + const handleRemoveAdmin = (username: string) => { + // 这里应该调用API来取消管理员 + console.log('取消管理员:', username); + }; + + const handleAddUser = () => { + // 这里应该调用API来添加用户,默认角色为 user + console.log('添加用户:', { ...newUser, role: 'user' }); + setNewUser({ username: '', password: '' }); + setShowAddUserForm(false); + }; + + 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' + /> + +
+
+ )} + + {/* 用户列表 */} +
+ + + + + + + + + + + {config.UserConfig.Users.map((user) => ( + + + + + + + ))} + +
+ 用户名 + + 角色 + + 状态 + + 操作 +
+ {user.username} + + + {user.role === 'owner' + ? '站长' + : user.role === 'admin' + ? '管理员' + : '普通用户'} + + + + {!user.banned ? '正常' : '已封禁'} + + + {user.role === 'user' ? ( + + ) : user.role === 'admin' ? ( + + ) : null} + {user.role !== 'owner' && + (!user.banned ? ( + + ) : ( + + ))} +
+
+
+
+ ); +}; + +// 视频源配置组件 +const VideoSourceConfig = ({ config }: { config: AdminConfig | null }) => { + 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); + } + }, [config]); + + const handleToggleEnable = (key: string) => { + setSources((prev) => + prev.map((source) => + source.key === key ? { ...source, disabled: !source.disabled } : source + ) + ); + }; + + const handleDelete = (key: string) => { + setSources((prev) => prev.filter((source) => source.key !== key)); + }; + + const handleAddSource = () => { + if (!newSource.name || !newSource.key || !newSource.api) return; + setSources((prev) => [...prev, newSource]); + setNewSource({ + name: '', + key: '', + api: '', + detail: '', + disabled: false, + from: 'custom', + }); + setShowAddForm(false); + setOrderChanged(true); + }; + + 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 = () => { + console.log('保存排序:', sources); + // TODO: 调用 API 保存排序 + setOrderChanged(false); + }; + + // 可拖拽行封装 (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 SiteConfigComponent = ({ config }: { config: AdminConfig | null }) => { + const [siteSettings, setSiteSettings] = useState({ + SiteName: '', + Announcement: '', + SearchDownstreamMaxPage: 1, + SiteInterfaceCacheTime: 7200, + SearchResultDefaultAggregate: false, + }); + + useEffect(() => { + if (config?.SiteConfig) { + setSiteSettings(config.SiteConfig); + } + }, [config]); + + if (!config) { + return ( +
+ 加载中... +
+ ); + } + + return ( +
+ {/* 站点名称 */} +
+ + + setSiteSettings((prev) => ({ ...prev, SiteName: 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-green-500 focus:border-transparent' + /> +
+ + {/* 站点公告 */} +
+ +