Initial commit: CarrotSkin project setup

This commit is contained in:
2025-12-05 20:07:50 +08:00
parent a9ff72a9bf
commit f5e4c2a04b
24 changed files with 5389 additions and 2145 deletions

View File

@@ -15,16 +15,26 @@ interface ErrorNotificationProps {
export function ErrorNotification({ message, type = 'error', duration = 5000, onClose }: ErrorNotificationProps) {
const [isVisible, setIsVisible] = useState(true);
const [isHovered, setIsHovered] = useState(false);
const [progress, setProgress] = useState(100);
useEffect(() => {
if (duration > 0) {
const timer = setTimeout(() => {
setIsVisible(false);
onClose?.();
}, duration);
return () => clearTimeout(timer);
if (duration > 0 && !isHovered) {
const startTime = Date.now();
const timer = setInterval(() => {
const elapsed = Date.now() - startTime;
const remaining = Math.max(0, duration - elapsed);
setProgress((remaining / duration) * 100);
if (remaining === 0) {
setIsVisible(false);
onClose?.();
}
}, 50);
return () => clearInterval(timer);
}
}, [duration, onClose]);
}, [duration, onClose, isHovered]);
const handleClose = () => {
setIsVisible(false);
@@ -52,7 +62,8 @@ export function ErrorNotification({ message, type = 'error', duration = 5000, on
border: 'border-red-200 dark:border-red-800',
text: 'text-red-800 dark:text-red-200',
icon: 'text-red-500',
close: 'text-red-400 hover:text-red-600 dark:text-red-300 dark:hover:text-red-100'
close: 'text-red-400 hover:text-red-600 dark:text-red-300 dark:hover:text-red-100',
progress: 'bg-red-500'
};
case 'warning':
return {
@@ -60,7 +71,8 @@ export function ErrorNotification({ message, type = 'error', duration = 5000, on
border: 'border-yellow-200 dark:border-yellow-800',
text: 'text-yellow-800 dark:text-yellow-200',
icon: 'text-yellow-500',
close: 'text-yellow-400 hover:text-yellow-600 dark:text-yellow-300 dark:hover:text-yellow-100'
close: 'text-yellow-400 hover:text-yellow-600 dark:text-yellow-300 dark:hover:text-yellow-100',
progress: 'bg-yellow-500'
};
case 'success':
return {
@@ -68,7 +80,8 @@ export function ErrorNotification({ message, type = 'error', duration = 5000, on
border: 'border-green-200 dark:border-green-800',
text: 'text-green-800 dark:text-green-200',
icon: 'text-green-500',
close: 'text-green-400 hover:text-green-600 dark:text-green-300 dark:hover:text-green-100'
close: 'text-green-400 hover:text-green-600 dark:text-green-300 dark:hover:text-green-100',
progress: 'bg-green-500'
};
case 'info':
return {
@@ -76,38 +89,98 @@ export function ErrorNotification({ message, type = 'error', duration = 5000, on
border: 'border-blue-200 dark:border-blue-800',
text: 'text-blue-800 dark:text-blue-200',
icon: 'text-blue-500',
close: 'text-blue-400 hover:text-blue-600 dark:text-blue-300 dark:hover:text-blue-100'
close: 'text-blue-400 hover:text-blue-600 dark:text-blue-300 dark:hover:text-blue-100',
progress: 'bg-blue-500'
};
}
};
const styles = getStyles();
const getAnimationVariants = () => {
switch (type) {
case 'error':
return {
initial: { opacity: 0, x: 100, scale: 0.8, rotate: -5 },
animate: { opacity: 1, x: 0, scale: 1, rotate: 0 },
exit: { opacity: 0, x: 100, scale: 0.8, rotate: 5 },
};
case 'warning':
return {
initial: { opacity: 0, y: -20, scale: 0.9 },
animate: { opacity: 1, y: 0, scale: 1 },
exit: { opacity: 0, y: -20, scale: 0.9 },
};
case 'success':
return {
initial: { opacity: 0, scale: 0.5 },
animate: { opacity: 1, scale: 1 },
exit: { opacity: 0, scale: 0.5 },
};
case 'info':
return {
initial: { opacity: 0, x: -20, scale: 0.9 },
animate: { opacity: 1, x: 0, scale: 1 },
exit: { opacity: 0, x: -20, scale: 0.9 },
};
}
};
return (
<AnimatePresence>
{isVisible && (
<motion.div
initial={{ opacity: 0, y: -20, scale: 0.9 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: -20, scale: 0.9 }}
transition={{ duration: 0.3, ease: 'easeOut' }}
className={`fixed top-4 right-4 z-50 max-w-sm w-full ${styles.bg} ${styles.border} border rounded-xl shadow-lg backdrop-blur-sm`}
{...getAnimationVariants()}
transition={{
duration: 0.4,
ease: 'easeOut',
type: 'spring',
stiffness: 300,
damping: 20
}}
className={`fixed top-4 right-4 z-[9999] max-w-lg w-full ${styles.bg} ${styles.border} border rounded-xl shadow-2xl backdrop-blur-lg overflow-hidden`}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
whileHover={{ scale: 1.02, y: -2 }}
>
{/* Progress bar */}
<div className="absolute bottom-0 left-0 h-1 bg-gray-200/50 dark:bg-gray-700/50 w-full">
<motion.div
className={`h-full ${styles.progress}`}
initial={{ width: '100%' }}
animate={{ width: `${progress}%` }}
transition={{ duration: 0.1, ease: 'linear' }}
/>
</div>
<div className="flex items-start p-4">
<div className={`flex-shrink-0 ${styles.icon} mr-3 mt-0.5`}>
<motion.div
className={`flex-shrink-0 ${styles.icon} mr-3 mt-0.5`}
animate={{
scale: type === 'error' ? [1, 1.1, 1] : 1,
rotate: type === 'warning' ? [0, -5, 5, 0] : 0
}}
transition={{
duration: type === 'error' ? 0.5 : 0.3,
repeat: type === 'error' ? Infinity : 0,
repeatDelay: 2
}}
>
{getIcon()}
</div>
</motion.div>
<div className="flex-1">
<p className={`text-sm font-medium ${styles.text}`}>
<p className={`text-sm font-medium ${styles.text} leading-relaxed`}>
{message}
</p>
</div>
<button
<motion.button
onClick={handleClose}
className={`flex-shrink-0 ml-3 ${styles.close} transition-colors`}
className={`flex-shrink-0 ml-3 ${styles.close} transition-all duration-200 p-1 rounded-full hover:bg-white/20 dark:hover:bg-black/20`}
whileHover={{ scale: 1.1, rotate: 90 }}
whileTap={{ scale: 0.9 }}
>
<XMarkIcon className="w-5 h-5" />
</button>
</motion.button>
</div>
</motion.div>
)}
@@ -115,10 +188,11 @@ export function ErrorNotification({ message, type = 'error', duration = 5000, on
);
}
// 全局错误管理器
// 增强的全局错误管理器
class ErrorManager {
private static instance: ErrorManager;
private listeners: Array<(notification: ErrorNotificationProps & { id: string }) => void> = [];
private soundEnabled: boolean = true;
static getInstance(): ErrorManager {
if (!ErrorManager.instance) {
@@ -127,19 +201,51 @@ class ErrorManager {
return ErrorManager.instance;
}
private playSound(type: ErrorType) {
if (!this.soundEnabled) return;
// 创建音频反馈
const audioContext = new (window.AudioContext || (window as any).webkitAudioContext)();
const oscillator = audioContext.createOscillator();
const gainNode = audioContext.createGain();
oscillator.connect(gainNode);
gainNode.connect(audioContext.destination);
const frequencies = {
error: 300,
warning: 400,
success: 600,
info: 500
};
oscillator.frequency.setValueAtTime(frequencies[type], audioContext.currentTime);
oscillator.type = type === 'error' ? 'sawtooth' : 'sine';
gainNode.gain.setValueAtTime(0.1, audioContext.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.3);
oscillator.start(audioContext.currentTime);
oscillator.stop(audioContext.currentTime + 0.3);
}
showError(message: string, duration?: number) {
this.playSound('error');
this.showNotification(message, 'error', duration);
}
showWarning(message: string, duration?: number) {
this.playSound('warning');
this.showNotification(message, 'warning', duration);
}
showSuccess(message: string, duration?: number) {
this.playSound('success');
this.showNotification(message, 'success', duration);
}
showInfo(message: string, duration?: number) {
this.playSound('info');
this.showNotification(message, 'info', duration);
}
@@ -160,13 +266,18 @@ class ErrorManager {
this.listeners = this.listeners.filter(l => l !== listener);
};
}
setSoundEnabled(enabled: boolean) {
this.soundEnabled = enabled;
}
}
export const errorManager = ErrorManager.getInstance();
// 错误提示容器组件
// 增强的错误提示容器组件
export function ErrorNotificationContainer() {
const [notifications, setNotifications] = useState<Array<ErrorNotificationProps & { id: string }>>([]);
const [soundEnabled, setSoundEnabled] = useState(true);
useEffect(() => {
const unsubscribe = errorManager.subscribe((notification) => {
@@ -176,19 +287,30 @@ export function ErrorNotificationContainer() {
return unsubscribe;
}, []);
useEffect(() => {
errorManager.setSoundEnabled(soundEnabled);
}, [soundEnabled]);
const removeNotification = (id: string) => {
setNotifications(prev => prev.filter(n => n.id !== id));
};
return (
<>
{notifications.map((notification) => (
<ErrorNotification
{notifications.map((notification, index) => (
<motion.div
key={notification.id}
{...notification}
onClose={() => removeNotification(notification.id)}
/>
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.1 }}
>
<ErrorNotification
{...notification}
onClose={() => removeNotification(notification.id)}
/>
</motion.div>
))}
</>
);
}