新增 全局错误toast提示;

This commit is contained in:
2025-09-14 02:54:08 +08:00
parent 9f94e92eaf
commit 46e07cc5ac
7 changed files with 368 additions and 172 deletions

45
src/components/Toast.tsx Normal file
View File

@@ -0,0 +1,45 @@
import React from 'react';
import { cn } from '../utils/cn';
import { X } from 'lucide-react';
export interface ToastProps {
id: string;
message: string;
type: 'success' | 'error' | 'warning' | 'info';
onClose: (id: string) => void;
}
export const Toast: React.FC<ToastProps> = ({ id, message, type, onClose }) => {
const getTypeStyles = () => {
switch (type) {
case 'success':
return 'bg-green-500 text-white';
case 'error':
return 'bg-red-500 text-white';
case 'warning':
return 'bg-yellow-500 text-white';
case 'info':
return 'bg-blue-500 text-white';
default:
return 'bg-gray-500 text-white';
}
};
return (
<div
className={cn(
'flex items-center justify-between p-4 rounded-lg shadow-lg min-w-[300px] max-w-md transition-all duration-300 transform',
getTypeStyles(),
'animate-in slide-in-from-top-full duration-300'
)}
>
<span className="text-sm font-medium">{message}</span>
<button
onClick={() => onClose(id)}
className="ml-4 hover:bg-black/10 rounded-full p-1 transition-colors"
>
<X className="h-4 w-4" />
</button>
</div>
);
};

View File

@@ -0,0 +1,87 @@
import React, { createContext, useContext, useReducer, useState, useEffect } from 'react';
import { Toast } from './Toast';
type ToastType = 'success' | 'error' | 'warning' | 'info';
export interface ToastMessage {
id: string;
message: string;
type: ToastType;
duration?: number;
}
export interface ToastContextType {
addToast: (message: string, type: ToastType, duration?: number) => void;
removeToast: (id: string) => void;
}
const ToastContext = createContext<ToastContextType | undefined>(undefined);
type ToastAction =
| { type: 'ADD_TOAST'; payload: ToastMessage }
| { type: 'REMOVE_TOAST'; payload: string };
const toastReducer = (state: ToastMessage[], action: ToastAction): ToastMessage[] => {
switch (action.type) {
case 'ADD_TOAST':
return [...state, action.payload];
case 'REMOVE_TOAST':
return state.filter(toast => toast.id !== action.payload);
default:
return state;
}
};
export const ToastProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [toasts, dispatch] = useReducer(toastReducer, []);
const addToast = (message: string, type: ToastType, duration: number = 5000) => {
const id = Date.now().toString();
dispatch({ type: 'ADD_TOAST', payload: { id, message, type, duration } });
};
const removeToast = (id: string) => {
dispatch({ type: 'REMOVE_TOAST', payload: id });
};
// Auto remove toasts after duration
useEffect(() => {
const timers = toasts.map(toast => {
if (toast.duration === 0) return; // 0 means persistent
return setTimeout(() => {
removeToast(toast.id);
}, toast.duration);
});
return () => {
timers.forEach(timer => {
if (timer) clearTimeout(timer);
});
};
}, [toasts]);
return (
<ToastContext.Provider value={{ addToast, removeToast }}>
{children}
<div className="fixed top-4 right-4 z-50 space-y-2">
{toasts.map(toast => (
<Toast
key={toast.id}
id={toast.id}
message={toast.message}
type={toast.type}
onClose={removeToast}
/>
))}
</div>
</ToastContext.Provider>
);
};
export const useToast = () => {
const context = useContext(ToastContext);
if (!context) {
throw new Error('useToast must be used within a ToastProvider');
}
return context;
};