File size: 3,609 Bytes
ae4ceef
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
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>
  );
}