diff --git a/.gitignore b/.gitignore index dcf2e2b0a7..74b904d433 100644 --- a/.gitignore +++ b/.gitignore @@ -49,3 +49,7 @@ public/sw.js public/sw.js.map public/workbox-*.js public/workbox-*.js.map +.env* + +# Local Netlify folder +.netlify diff --git a/package.json b/package.json index c8c89f9f77..55809f806a 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "bs58": "^6.0.0", "clsx": "^2.0.0", "crypto-js": "^4.2.0", + "gsap": "^3.15.0", "he": "^1.2.0", "hls.js": "^1.6.10", "lucide-react": "^0.438.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 07a1699412..f8d935e3c8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -41,6 +41,9 @@ importers: crypto-js: specifier: ^4.2.0 version: 4.2.0 + gsap: + specifier: ^3.15.0 + version: 3.15.0 he: specifier: ^1.2.0 version: 1.2.0 @@ -2919,6 +2922,9 @@ packages: graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + gsap@3.15.0: + resolution: {integrity: sha512-dMW4CWBTUK1AEEDeZc1g4xpPGIrSf9fJF960qbTZmN/QwZIWY5wgliS6JWl9/25fpTGJrMRtSjGtOmPnfjZB+A==} + hard-rejection@2.1.0: resolution: {integrity: sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==} engines: {node: '>=6'} @@ -8445,6 +8451,8 @@ snapshots: graphemer@1.4.0: {} + gsap@3.15.0: {} + hard-rejection@2.1.0: {} has-bigints@1.1.0: {} diff --git a/public/icons/icon-192x192.png b/public/icons/icon-192x192.png index 7a7f4937cf..58a5940055 100644 Binary files a/public/icons/icon-192x192.png and b/public/icons/icon-192x192.png differ diff --git a/public/icons/icon-256x256.png b/public/icons/icon-256x256.png index c93533462d..c6375f4363 100644 Binary files a/public/icons/icon-256x256.png and b/public/icons/icon-256x256.png differ diff --git a/public/icons/icon-384x384.png b/public/icons/icon-384x384.png index a627740364..f8591871a7 100644 Binary files a/public/icons/icon-384x384.png and b/public/icons/icon-384x384.png differ diff --git a/public/icons/icon-512x512.png b/public/icons/icon-512x512.png index 784f5176cd..67b96d1666 100644 Binary files a/public/icons/icon-512x512.png and b/public/icons/icon-512x512.png differ diff --git a/public/logo.png b/public/logo.png index 184fdaca37..65428c494f 100644 Binary files a/public/logo.png and b/public/logo.png differ diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index 830fffee7d..4518e0ce42 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -2032,6 +2032,8 @@ const VideoSourceConfig = ({ const { isLoading, withLoading } = useLoadingState(); const [sources, setSources] = useState([]); const [showAddForm, setShowAddForm] = useState(false); + const [showBatchForm, setShowBatchForm] = useState(false); + const [batchText, setBatchText] = useState(''); const [orderChanged, setOrderChanged] = useState(false); const [newSource, setNewSource] = useState({ name: '', @@ -2164,6 +2166,67 @@ const VideoSourceConfig = ({ }); }; + const handleBatchImport = () => { + if (!batchText.trim()) return; + withLoading('batchAddSources', async () => { + let parsed: unknown; + try { + parsed = JSON.parse(batchText); + } catch { + throw new Error('JSON 格式不正确'); + } + + let sources: { key: string; name: string; api: string; detail?: string }[] = []; + if (Array.isArray(parsed)) { + sources = parsed as { key: string; name: string; api: string; detail?: string }[]; + } else if (parsed && typeof parsed === 'object') { + const map = + 'api_site' in parsed && + parsed.api_site && + typeof parsed.api_site === 'object' + ? (parsed as { api_site: Record }).api_site + : (parsed as Record); + sources = Object.entries(map).map(([key, site]) => { + const item = + site && typeof site === 'object' + ? site + : { name: key, api: '' }; + return { + key, + name: item.name || key, + api: item.api || '', + detail: item.detail, + }; + }); + } + + const resp = await fetch('/api/admin/source', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ action: 'batch_add', sources }), + }); + const data = await resp.json().catch(() => ({})); + if (!resp.ok) { + throw new Error(data.error || `导入失败: ${resp.status}`); + } + await refreshConfig(); + setBatchText(''); + setShowBatchForm(false); + showAlert({ + type: 'success', + title: '批量导入完成', + message: `新增 ${data.added || 0} 个,跳过 ${data.skipped || 0} 个`, + timer: 2500, + }); + }).catch((err) => { + showAlert({ + type: 'error', + title: '批量导入失败', + message: err instanceof Error ? err.message : '导入失败', + }); + }); + }; + const handleDragEnd = (event: any) => { const { active, over } = event; if (!over || active.id === over.id) return; @@ -2561,7 +2624,19 @@ const VideoSourceConfig = ({ )} +