feat: Docker部署与CI/CD集成, 搜索栏修复, 上传目录改为data
This commit is contained in:
87
components/ToastProvider.tsx
Normal file
87
components/ToastProvider.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
import React, { createContext, useContext, useState, useCallback } from 'react';
|
||||
import { X, CheckCircle, AlertCircle, Info } from 'lucide-react';
|
||||
|
||||
type ToastType = 'success' | 'error' | 'info';
|
||||
|
||||
interface Toast {
|
||||
id: string;
|
||||
message: string;
|
||||
type: ToastType;
|
||||
}
|
||||
|
||||
interface ToastContextType {
|
||||
showToast: (message: string, type: ToastType) => void;
|
||||
success: (message: string) => void;
|
||||
error: (message: string) => void;
|
||||
info: (message: string) => void;
|
||||
}
|
||||
|
||||
const ToastContext = createContext<ToastContextType | undefined>(undefined);
|
||||
|
||||
export const useToast = () => {
|
||||
const context = useContext(ToastContext);
|
||||
if (!context) {
|
||||
throw new Error('useToast must be used within ToastProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
export const ToastProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const [toasts, setToasts] = useState<Toast[]>([]);
|
||||
|
||||
const showToast = useCallback((message: string, type: ToastType) => {
|
||||
const id = Math.random().toString(36).substring(7);
|
||||
const newToast: Toast = { id, message, type };
|
||||
|
||||
setToasts((prev) => [...prev, newToast]);
|
||||
|
||||
// Auto remove after 5 seconds
|
||||
setTimeout(() => {
|
||||
setToasts((prev) => prev.filter((t) => t.id !== id));
|
||||
}, 5000);
|
||||
}, []);
|
||||
|
||||
const success = useCallback((message: string) => showToast(message, 'success'), [showToast]);
|
||||
const error = useCallback((message: string) => showToast(message, 'error'), [showToast]);
|
||||
const info = useCallback((message: string) => showToast(message, 'info'), [showToast]);
|
||||
|
||||
const removeToast = (id: string) => {
|
||||
setToasts((prev) => prev.filter((t) => t.id !== id));
|
||||
};
|
||||
|
||||
return (
|
||||
<ToastContext.Provider value={{ showToast, success, error, info }}>
|
||||
{children}
|
||||
|
||||
{/* Toast Container */}
|
||||
<div className="fixed top-4 right-4 z-[9999] space-y-2">
|
||||
{toasts.map((toast) => (
|
||||
<div
|
||||
key={toast.id}
|
||||
className={`
|
||||
flex items-center gap-3 min-w-[300px] max-w-md p-4 rounded-lg
|
||||
backdrop-blur-md border shadow-lg
|
||||
animate-in slide-in-from-right duration-300
|
||||
${toast.type === 'success' ? 'bg-green-900/90 border-green-500 text-green-100' : ''}
|
||||
${toast.type === 'error' ? 'bg-red-900/90 border-red-500 text-red-100' : ''}
|
||||
${toast.type === 'info' ? 'bg-blue-900/90 border-blue-500 text-blue-100' : ''}
|
||||
`}
|
||||
>
|
||||
{toast.type === 'success' && <CheckCircle className="w-5 h-5 flex-shrink-0" />}
|
||||
{toast.type === 'error' && <AlertCircle className="w-5 h-5 flex-shrink-0" />}
|
||||
{toast.type === 'info' && <Info className="w-5 h-5 flex-shrink-0" />}
|
||||
|
||||
<p className="flex-1 text-sm font-mono">{toast.message}</p>
|
||||
|
||||
<button
|
||||
onClick={() => removeToast(toast.id)}
|
||||
className="flex-shrink-0 hover:opacity-70 transition-opacity"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</ToastContext.Provider>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user