File size: 4,346 Bytes
3c76719 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 | 'use client';
import { useEffect, useState, useRef, useCallback } from 'react';
interface SearchLoadingAnimationProps {
currentSource?: string;
checkedSources?: number;
totalSources?: number;
isPaused?: boolean;
onComplete?: (checkedSources: number, totalSources: number) => void;
}
export function SearchLoadingAnimation({
currentSource,
checkedSources = 0,
totalSources = 16,
isPaused = false,
onComplete,
}: SearchLoadingAnimationProps) {
const [dots, setDots] = useState('');
const dotIntervalRef = useRef<NodeJS.Timeout | null>(null);
const hasCalledComplete = useRef(false);
// Calculate progress (0-100%)
const progress = totalSources > 0 ? (checkedSources / totalSources) * 100 : 0;
const isComplete = progress >= 100;
// Animation pause/resume logic - Optimized interval
useEffect(() => {
if (isPaused || isComplete) {
if (dotIntervalRef.current) {
clearInterval(dotIntervalRef.current);
dotIntervalRef.current = null;
}
return;
}
dotIntervalRef.current = setInterval(() => {
setDots((prev) => (prev.length >= 3 ? '' : prev + '.'));
}, 600); // Increased from 500ms to 600ms for better performance
return () => {
if (dotIntervalRef.current) {
clearInterval(dotIntervalRef.current);
dotIntervalRef.current = null;
}
};
}, [isPaused, isComplete]);
// Call onComplete callback when animation finishes
useEffect(() => {
if (isComplete && onComplete && !hasCalledComplete.current) {
hasCalledComplete.current = true;
// Small delay to allow animation to settle
const timeout = setTimeout(() => {
onComplete(checkedSources, totalSources);
}, 100);
return () => clearTimeout(timeout);
}
}, [isComplete, onComplete, checkedSources, totalSources]);
const statusText = `${checkedSources}/${totalSources} 个源`;
return (
<div className="w-full space-y-3 animate-fade-in">
{/* Loading Message with Icon */}
<div className="flex items-center justify-center gap-3">
{/* Spinning Icon */}
<svg className="w-5 h-5 animate-spin-slow" viewBox="0 0 24 24">
<circle
cx="12"
cy="12"
r="10"
fill="none"
stroke="var(--accent-color)"
strokeWidth="3"
strokeDasharray="60 40"
strokeLinecap="round"
/>
</svg>
<span className="text-sm font-medium text-[var(--text-color-secondary)]">
正在搜索视频源{dots}
</span>
</div>
{/* Progress Bar - Unified 0-100% */}
<div className="w-full">
<div
className="h-1 bg-[color-mix(in_srgb,var(--glass-bg)_50%,transparent)] overflow-hidden rounded-[var(--radius-full)]"
>
<div
className="h-full bg-[var(--accent-color)] transition-all duration-500 ease-out relative rounded-[var(--radius-full)]"
style={{
width: `${progress}%`
}}
>
{/* Shimmer Effect - Optimized for GPU with contain for better performance */}
<div
className="absolute inset-0 animate-shimmer"
style={{
background: 'linear-gradient(90deg, transparent 0%, rgba(255, 255, 255, 0.3) 50%, transparent 100%)',
willChange: 'transform',
transform: 'translateZ(0)',
contain: 'strict'
}}
></div>
</div>
</div>
{/* Progress Info - Real-time count with pause indicator */}
<div className="flex items-center justify-between mt-2 text-xs text-[var(--text-color-secondary)]">
<span className="flex items-center gap-2">
{statusText}
{isPaused && (
<span className="px-2 py-0.5 rounded-[var(--radius-full)] bg-[var(--glass-bg)] text-[10px]">
已暂停
</span>
)}
{isComplete && (
<span className="px-2 py-0.5 rounded-[var(--radius-full)] bg-[var(--accent-color)] text-white text-[10px]">
完成
</span>
)}
</span>
<span className="font-medium">{Math.round(progress)}%</span>
</div>
</div>
</div>
);
}
|