forked from CarrotSkin/carrotskin
feat: 完成navbar隐藏优化和侧边栏冻结功能
- 优化navbar滚动隐藏逻辑,更敏感响应 - 添加返回顶部按钮,固定在右下角 - 实现profile页面侧边栏真正冻结效果 - 修复首页滑动指示器位置 - 优化整体布局确保首屏内容完整显示
This commit is contained in:
194
src/components/ErrorNotification.tsx
Normal file
194
src/components/ErrorNotification.tsx
Normal file
@@ -0,0 +1,194 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { XMarkIcon, ExclamationTriangleIcon, CheckCircleIcon, InformationCircleIcon } from '@heroicons/react/24/outline';
|
||||
|
||||
export type ErrorType = 'error' | 'warning' | 'success' | 'info';
|
||||
|
||||
interface ErrorNotificationProps {
|
||||
message: string;
|
||||
type?: ErrorType;
|
||||
duration?: number;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
export function ErrorNotification({ message, type = 'error', duration = 5000, onClose }: ErrorNotificationProps) {
|
||||
const [isVisible, setIsVisible] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (duration > 0) {
|
||||
const timer = setTimeout(() => {
|
||||
setIsVisible(false);
|
||||
onClose?.();
|
||||
}, duration);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [duration, onClose]);
|
||||
|
||||
const handleClose = () => {
|
||||
setIsVisible(false);
|
||||
onClose?.();
|
||||
};
|
||||
|
||||
const getIcon = () => {
|
||||
switch (type) {
|
||||
case 'error':
|
||||
return <ExclamationTriangleIcon className="w-5 h-5" />;
|
||||
case 'warning':
|
||||
return <ExclamationTriangleIcon className="w-5 h-5" />;
|
||||
case 'success':
|
||||
return <CheckCircleIcon className="w-5 h-5" />;
|
||||
case 'info':
|
||||
return <InformationCircleIcon className="w-5 h-5" />;
|
||||
}
|
||||
};
|
||||
|
||||
const getStyles = () => {
|
||||
switch (type) {
|
||||
case 'error':
|
||||
return {
|
||||
bg: 'bg-red-50 dark:bg-red-900/20',
|
||||
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'
|
||||
};
|
||||
case 'warning':
|
||||
return {
|
||||
bg: 'bg-yellow-50 dark:bg-yellow-900/20',
|
||||
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'
|
||||
};
|
||||
case 'success':
|
||||
return {
|
||||
bg: 'bg-green-50 dark:bg-green-900/20',
|
||||
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'
|
||||
};
|
||||
case 'info':
|
||||
return {
|
||||
bg: 'bg-blue-50 dark:bg-blue-900/20',
|
||||
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'
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const styles = getStyles();
|
||||
|
||||
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`}
|
||||
>
|
||||
<div className="flex items-start p-4">
|
||||
<div className={`flex-shrink-0 ${styles.icon} mr-3 mt-0.5`}>
|
||||
{getIcon()}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<p className={`text-sm font-medium ${styles.text}`}>
|
||||
{message}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleClose}
|
||||
className={`flex-shrink-0 ml-3 ${styles.close} transition-colors`}
|
||||
>
|
||||
<XMarkIcon className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
}
|
||||
|
||||
// 全局错误管理器
|
||||
class ErrorManager {
|
||||
private static instance: ErrorManager;
|
||||
private listeners: Array<(notification: ErrorNotificationProps & { id: string }) => void> = [];
|
||||
|
||||
static getInstance(): ErrorManager {
|
||||
if (!ErrorManager.instance) {
|
||||
ErrorManager.instance = new ErrorManager();
|
||||
}
|
||||
return ErrorManager.instance;
|
||||
}
|
||||
|
||||
showError(message: string, duration?: number) {
|
||||
this.showNotification(message, 'error', duration);
|
||||
}
|
||||
|
||||
showWarning(message: string, duration?: number) {
|
||||
this.showNotification(message, 'warning', duration);
|
||||
}
|
||||
|
||||
showSuccess(message: string, duration?: number) {
|
||||
this.showNotification(message, 'success', duration);
|
||||
}
|
||||
|
||||
showInfo(message: string, duration?: number) {
|
||||
this.showNotification(message, 'info', duration);
|
||||
}
|
||||
|
||||
private showNotification(message: string, type: ErrorType, duration?: number) {
|
||||
const notification = {
|
||||
id: Math.random().toString(36).substr(2, 9),
|
||||
message,
|
||||
type,
|
||||
duration: duration ?? (type === 'error' ? 5000 : 3000)
|
||||
};
|
||||
|
||||
this.listeners.forEach(listener => listener(notification));
|
||||
}
|
||||
|
||||
subscribe(listener: (notification: ErrorNotificationProps & { id: string }) => void) {
|
||||
this.listeners.push(listener);
|
||||
return () => {
|
||||
this.listeners = this.listeners.filter(l => l !== listener);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const errorManager = ErrorManager.getInstance();
|
||||
|
||||
// 错误提示容器组件
|
||||
export function ErrorNotificationContainer() {
|
||||
const [notifications, setNotifications] = useState<Array<ErrorNotificationProps & { id: string }>>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = errorManager.subscribe((notification) => {
|
||||
setNotifications(prev => [...prev, notification]);
|
||||
});
|
||||
|
||||
return unsubscribe;
|
||||
}, []);
|
||||
|
||||
const removeNotification = (id: string) => {
|
||||
setNotifications(prev => prev.filter(n => n.id !== id));
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{notifications.map((notification) => (
|
||||
<ErrorNotification
|
||||
key={notification.id}
|
||||
{...notification}
|
||||
onClose={() => removeNotification(notification.id)}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user