diff --git a/src/components/PansouSearch.tsx b/src/components/PansouSearch.tsx index fbcbb79..e2d222a 100644 --- a/src/components/PansouSearch.tsx +++ b/src/components/PansouSearch.tsx @@ -1,9 +1,15 @@ /* eslint-disable @typescript-eslint/no-explicit-any,no-console */ 'use client'; -import { AlertCircle, Copy, ExternalLink, Loader2, RefreshCw } from 'lucide-react'; +import { + AlertCircle, + Copy, + ExternalLink, + Loader2, + RefreshCw, +} from 'lucide-react'; import { useRouter } from 'next/navigation'; -import { useCallback, useEffect, useState } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; import { PansouLink, PansouSearchResult } from '@/lib/pansou.client'; @@ -35,13 +41,17 @@ const CLOUD_TYPE_NAMES: Record = { // 网盘类型颜色 const CLOUD_TYPE_COLORS: Record = { baidu: 'bg-blue-100 text-blue-800 dark:bg-blue-900/40 dark:text-blue-200', - aliyun: 'bg-orange-100 text-orange-800 dark:bg-orange-900/40 dark:text-orange-200', - quark: 'bg-purple-100 text-purple-800 dark:bg-purple-900/40 dark:text-purple-200', + aliyun: + 'bg-orange-100 text-orange-800 dark:bg-orange-900/40 dark:text-orange-200', + quark: + 'bg-purple-100 text-purple-800 dark:bg-purple-900/40 dark:text-purple-200', tianyi: 'bg-red-100 text-red-800 dark:bg-red-900/40 dark:text-red-200', uc: 'bg-green-100 text-green-800 dark:bg-green-900/40 dark:text-green-200', mobile: 'bg-pink-100 text-pink-800 dark:bg-pink-900/40 dark:text-pink-200', - '115': 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/40 dark:text-yellow-200', - pikpak: 'bg-indigo-100 text-indigo-800 dark:bg-indigo-900/40 dark:text-indigo-200', + '115': + 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/40 dark:text-yellow-200', + pikpak: + 'bg-indigo-100 text-indigo-800 dark:bg-indigo-900/40 dark:text-indigo-200', xunlei: 'bg-cyan-100 text-cyan-800 dark:bg-cyan-900/40 dark:text-cyan-200', '123': 'bg-teal-100 text-teal-800 dark:bg-teal-900/40 dark:text-teal-200', magnet: 'bg-gray-100 text-gray-800 dark:bg-gray-700/40 dark:text-gray-200', @@ -73,7 +83,13 @@ const CLOUD_TYPE_TO_CHECK_PLATFORM: Record = { '123': 'pan123', }; -type CheckItemStatus = 'pending' | 'checking' | 'valid' | 'invalid' | 'unknown' | 'rate_limited'; +type CheckItemStatus = + | 'pending' + | 'checking' + | 'valid' + | 'invalid' + | 'unknown' + | 'rate_limited'; interface NetdiskCheckTaskPayload { id: string; @@ -89,7 +105,10 @@ interface NetdiskCheckTaskPayload { currentBatch: number; totalBatches: number; }; - results: Record; + results: Record< + string, + { status: CheckItemStatus; reason?: string; fromCache?: boolean } + >; error?: string; } @@ -103,8 +122,10 @@ const CHECK_STATUS_STYLE: Record = { checking: 'bg-blue-100 text-blue-700 dark:bg-blue-900/40 dark:text-blue-200', valid: 'bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-200', invalid: 'bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-200', - unknown: 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900/40 dark:text-yellow-200', - rate_limited: 'bg-orange-100 text-orange-700 dark:bg-orange-900/40 dark:text-orange-200', + unknown: + 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900/40 dark:text-yellow-200', + rate_limited: + 'bg-orange-100 text-orange-700 dark:bg-orange-900/40 dark:text-orange-200', }; const CHECK_STATUS_TEXT: Record = { @@ -127,11 +148,17 @@ export default function PansouSearch({ const [error, setError] = useState(null); const [copiedUrl, setCopiedUrl] = useState(null); const [selectedType, setSelectedType] = useState('all'); // 'all' 表示显示全部 + const typeScrollContainerRef = useRef(null); + const isTypeDraggingRef = useRef(false); + const typeDragStartXRef = useRef(0); + const typeDragScrollLeftRef = useRef(0); const [transferingUrl, setTransferingUrl] = useState(null); const [playingUrl, setPlayingUrl] = useState(null); const [toast, setToast] = useState(null); const [cooldownRemainingMs, setCooldownRemainingMs] = useState(0); - const [checkStatesByType, setCheckStatesByType] = useState>({}); + const [checkStatesByType, setCheckStatesByType] = useState< + Record + >({}); useEffect(() => { setCooldownRemainingMs(0); @@ -149,7 +176,9 @@ export default function PansouSearch({ const updates = await Promise.all( runningEntries.map(async ([cloudType, state]) => { try { - const response = await fetch(`/api/netdisk/check/task?id=${encodeURIComponent(state.taskId)}`); + const response = await fetch( + `/api/netdisk/check/task?id=${encodeURIComponent(state.taskId)}` + ); const data = await response.json(); if (!response.ok) { throw new Error(data.error || '获取检测进度失败'); @@ -167,7 +196,8 @@ export default function PansouSearch({ task: { ...state.task, status: 'failed', - error: error instanceof Error ? error.message : '获取检测进度失败', + error: + error instanceof Error ? error.message : '获取检测进度失败', } as NetdiskCheckTaskPayload, cooldownRemainingMs: 0, }; @@ -187,11 +217,15 @@ export default function PansouSearch({ }); return next; }); - setCooldownRemainingMs(Math.max(0, ...updates.map((item) => item.cooldownRemainingMs))); + setCooldownRemainingMs( + Math.max(0, ...updates.map((item) => item.cooldownRemainingMs)) + ); updates.forEach((update) => { if (update.task.status === 'failed' && update.task.error) { setToast({ - message: `${CLOUD_TYPE_NAMES[update.cloudType] || update.cloudType}: ${update.task.error}`, + message: `${ + CLOUD_TYPE_NAMES[update.cloudType] || update.cloudType + }: ${update.task.error}`, type: 'error', onClose: () => setToast(null), }); @@ -301,23 +335,26 @@ export default function PansouSearch({ } }; - const handleNetdiskInstantPlay = async (cloudType: string, link: PansouLink) => { + const handleNetdiskInstantPlay = async ( + cloudType: string, + link: PansouLink + ) => { try { setPlayingUrl(link.url); const instantPlayApi = cloudType === 'mobile' ? '/api/netdisk/mobile/instant-play' : cloudType === 'baidu' - ? '/api/netdisk/baidu/instant-play' - : cloudType === 'tianyi' - ? '/api/netdisk/tianyi/instant-play' - : cloudType === '115' - ? '/api/netdisk/115/instant-play' - : cloudType === 'uc' - ? '/api/netdisk/uc/instant-play' - : cloudType === '123' - ? '/api/netdisk/123/instant-play' - : '/api/netdisk/quark/instant-play'; + ? '/api/netdisk/baidu/instant-play' + : cloudType === 'tianyi' + ? '/api/netdisk/tianyi/instant-play' + : cloudType === '115' + ? '/api/netdisk/115/instant-play' + : cloudType === 'uc' + ? '/api/netdisk/uc/instant-play' + : cloudType === '123' + ? '/api/netdisk/123/instant-play' + : '/api/netdisk/quark/instant-play'; const response = await fetch(instantPlayApi, { method: 'POST', headers: { @@ -336,7 +373,24 @@ export default function PansouSearch({ } router.push( - `/play?source=${encodeURIComponent(data.source || (cloudType === 'mobile' ? 'netdisk-mobile' : cloudType === 'baidu' ? 'netdisk-baidu' : cloudType === 'tianyi' ? 'netdisk-tianyi' : cloudType === '115' ? 'netdisk-115' : cloudType === 'uc' ? 'netdisk-uc' : cloudType === '123' ? 'netdisk-123' : 'netdisk-quark'))}&id=${encodeURIComponent(data.id)}&title=${encodeURIComponent(keyword)}` + `/play?source=${encodeURIComponent( + data.source || + (cloudType === 'mobile' + ? 'netdisk-mobile' + : cloudType === 'baidu' + ? 'netdisk-baidu' + : cloudType === 'tianyi' + ? 'netdisk-tianyi' + : cloudType === '115' + ? 'netdisk-115' + : cloudType === 'uc' + ? 'netdisk-uc' + : cloudType === '123' + ? 'netdisk-123' + : 'netdisk-quark') + )}&id=${encodeURIComponent(data.id)}&title=${encodeURIComponent( + keyword + )}` ); } catch (err: any) { setToast({ @@ -421,6 +475,40 @@ export default function PansouSearch({ return checkStatesByType[cloudType]?.task || null; }; + // 网盘类型选项卡横向拖动滚动 + const handleTypeMouseDown = (e: React.MouseEvent) => { + if (!typeScrollContainerRef.current) return; + isTypeDraggingRef.current = true; + typeDragStartXRef.current = + e.pageX - typeScrollContainerRef.current.offsetLeft; + typeDragScrollLeftRef.current = typeScrollContainerRef.current.scrollLeft; + typeScrollContainerRef.current.style.cursor = 'grabbing'; + typeScrollContainerRef.current.style.userSelect = 'none'; + }; + + const handleTypeMouseLeave = () => { + if (!typeScrollContainerRef.current) return; + isTypeDraggingRef.current = false; + typeScrollContainerRef.current.style.cursor = 'grab'; + typeScrollContainerRef.current.style.userSelect = 'auto'; + }; + + const handleTypeMouseUp = () => { + if (!typeScrollContainerRef.current) return; + isTypeDraggingRef.current = false; + typeScrollContainerRef.current.style.cursor = 'grab'; + typeScrollContainerRef.current.style.userSelect = 'auto'; + }; + + const handleTypeMouseMove = (e: React.MouseEvent) => { + if (!isTypeDraggingRef.current || !typeScrollContainerRef.current) return; + e.preventDefault(); + const x = e.pageX - typeScrollContainerRef.current.offsetLeft; + const walk = (x - typeDragStartXRef.current) * 2; + typeScrollContainerRef.current.scrollLeft = + typeDragScrollLeftRef.current - walk; + }; + const getCheckResultForUrl = (cloudType: string, url: string) => { const task = getCloudCheckState(cloudType); return task?.results?.[url] || null; @@ -463,7 +551,9 @@ export default function PansouSearch({
-

{error}

+

+ {error} +

- {typeStats.map(({ type, count }) => { - const typeName = CLOUD_TYPE_NAMES[type] || type; +
+

+ 网盘类型 +

+
+
+
+ + {typeStats.map(({ type, count }) => { + const typeName = CLOUD_TYPE_NAMES[type] || type; - return ( - - ); - })} + return ( + + ); + })} +
+
+
{/* 按网盘类型分类显示 */} @@ -547,7 +658,8 @@ export default function PansouSearch({ const sortedLinks = getSortedLinks(cloudType, links); const typeName = CLOUD_TYPE_NAMES[cloudType] || cloudType; - const typeColor = CLOUD_TYPE_COLORS[cloudType] || CLOUD_TYPE_COLORS.others; + const typeColor = + CLOUD_TYPE_COLORS[cloudType] || CLOUD_TYPE_COLORS.others; const checkable = CHECKABLE_CLOUD_TYPES.has(cloudType); const cloudCheckTask = getCloudCheckState(cloudType); const isCheckingThisType = cloudCheckTask?.status === 'running'; @@ -557,7 +669,9 @@ export default function PansouSearch({
{/* 网盘类型标题 */}
- + {typeName} @@ -565,7 +679,9 @@ export default function PansouSearch({ {groupProgress && ( - 进度 {groupProgress.done}/{groupProgress.total} · 有效 {groupProgress.valid} · 失效 {groupProgress.invalid} · 未知 {groupProgress.unknown + groupProgress.rateLimited} + 进度 {groupProgress.done}/{groupProgress.total} · 有效{' '} + {groupProgress.valid} · 失效 {groupProgress.invalid} · 未知{' '} + {groupProgress.unknown + groupProgress.rateLimited} )} {checkable && ( @@ -620,7 +736,10 @@ export default function PansouSearch({
{link.password && (
- 提取码: {link.password} + 提取码:{' '} + + {link.password} +
)}
@@ -628,21 +747,37 @@ export default function PansouSearch({ {/* 操作按钮 */}
{(() => { - const checkResult = getCheckResultForUrl(cloudType, link.url); + const checkResult = getCheckResultForUrl( + cloudType, + link.url + ); if (!checkResult) return null; return ( {CHECK_STATUS_TEXT[checkResult.status]} ); })()} - {(cloudType === 'quark' || cloudType === 'mobile' || cloudType === 'baidu' || cloudType === 'tianyi' || cloudType === '123' || cloudType === 'uc' || cloudType === '115') && ( + {(cloudType === 'quark' || + cloudType === 'mobile' || + cloudType === 'baidu' || + cloudType === 'tianyi' || + cloudType === '123' || + cloudType === 'uc' || + cloudType === '115') && ( <> )} )}