codex-ai-platform / src /components /NotificationToast.tsx
3v324v23's picture
chore: 彻底清理项目,符合 Hugging Face 部署规范
ae4ceef
import React, { useEffect, useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { Bell, CheckCircle, AlertTriangle, Info, X } from 'lucide-react';
import { useSocket } from '@/hooks/useSocket';
import { useTranslation } from 'react-i18next';
interface ToastMessage {
id: string;
title: string;
content: string;
type: 'success' | 'warning' | 'info' | 'error';
}
export default function NotificationToast() {
const { t } = useTranslation();
const [notifications, setNotifications] = useState<ToastMessage[]>([]);
const socket = useSocket();
const addToast = (title: string, content: string, type: ToastMessage['type'] = 'info') => {
const id = Math.random().toString(36).substring(7);
setNotifications(prev => [...prev, { id, title, content, type }]);
setTimeout(() => {
removeToast(id);
}, 5000);
};
const removeToast = (id: string) => {
setNotifications(prev => prev.filter(n => n.id !== id));
};
useEffect(() => {
if (!socket) return;
// 1. 监听支付成功
socket.on('payment_success', (data) => {
addToast(t('notifications.payment_success'), data.message, 'success');
});
// 2. 监听熔断告警 (仅管理员)
socket.on('admin:circuit_breaker_change', (data) => {
const type = data.state === 'OPEN' ? 'error' : 'success';
const title = data.state === 'OPEN' ? t('notifications.circuit_breaker_title') : t('notifications.service_recovered');
addToast(title, t('notifications.circuit_breaker_msg', { name: data.name, state: data.state }), type);
});
return () => {
socket.off('payment_success');
socket.off('admin:circuit_breaker_change');
};
}, [socket]);
return (
<div className="fixed top-6 right-6 z-[100] flex flex-col gap-3 pointer-events-none">
<AnimatePresence>
{notifications.map((n) => (
<motion.div
key={n.id}
initial={{ opacity: 0, x: 50, scale: 0.9 }}
animate={{ opacity: 1, x: 0, scale: 1 }}
exit={{ opacity: 0, scale: 0.9, transition: { duration: 0.2 } }}
className={`
pointer-events-auto w-80 p-4 rounded-xl shadow-2xl border flex gap-3 items-start
${n.type === 'success' ? 'bg-white border-green-100' :
n.type === 'error' ? 'bg-red-50 border-red-100' :
n.type === 'warning' ? 'bg-orange-50 border-orange-100' : 'bg-white border-zinc-100'}
`}
>
<div className={`
p-2 rounded-lg shrink-0
${n.type === 'success' ? 'bg-green-50 text-green-600' :
n.type === 'error' ? 'bg-red-100 text-red-600' :
n.type === 'warning' ? 'bg-orange-100 text-orange-600' : 'bg-blue-50 text-blue-600'}
`}>
{n.type === 'success' ? <CheckCircle size={18} /> :
n.type === 'error' ? <AlertTriangle size={18} /> :
n.type === 'warning' ? <AlertTriangle size={18} /> : <Info size={18} />}
</div>
<div className="flex-1 min-w-0">
<h4 className="text-sm font-bold text-zinc-900 mb-0.5">{n.title}</h4>
<p className="text-xs text-zinc-500 leading-relaxed">{n.content}</p>
</div>
<button
onClick={() => removeToast(n.id)}
className="text-zinc-300 hover:text-zinc-500 transition-colors"
>
<X size={14} />
</button>
</motion.div>
))}
</AnimatePresence>
</div>
);
}