forked from mx-space/docs
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtoast.tsx
More file actions
98 lines (86 loc) · 2.5 KB
/
toast.tsx
File metadata and controls
98 lines (86 loc) · 2.5 KB
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
97
98
'use client'
import { useEffect, useState } from 'react'
import { createPortal } from 'react-dom'
import { cn } from '@/utils/cn'
export interface ToastProps {
id: string
message: string
type?: 'success' | 'error' | 'warning' | 'info'
duration?: number
onClose: (id: string) => void
}
export function Toast({ id, message, type = 'success', duration = 3000, onClose }: ToastProps) {
const [isVisible, setIsVisible] = useState(false)
const [isLeaving, setIsLeaving] = useState(false)
const [mounted, setMounted] = useState(false)
useEffect(() => {
setMounted(true)
}, [])
useEffect(() => {
if (!mounted) return
// 进入动画
const timer = setTimeout(() => setIsVisible(true), 50)
// 自动关闭
const closeTimer = setTimeout(() => {
setIsLeaving(true)
setTimeout(() => onClose(id), 300)
}, duration)
return () => {
clearTimeout(timer)
clearTimeout(closeTimer)
}
}, [id, duration, onClose, mounted])
const getTypeStyles = () => {
switch (type) {
case 'success':
return 'bg-green-500 text-white border-green-600'
case 'error':
return 'bg-red-500 text-white border-red-600'
case 'warning':
return 'bg-yellow-500 text-white border-yellow-600'
case 'info':
return 'bg-blue-500 text-white border-blue-600'
default:
return 'bg-green-500 text-white border-green-600'
}
}
const getIcon = () => {
switch (type) {
case 'success':
return '✓'
case 'error':
return '✕'
case 'warning':
return '⚠'
case 'info':
return 'ℹ'
default:
return '✓'
}
}
if (!mounted) return null
return createPortal(
<div
className={cn(
'fixed top-4 right-4 z-50 flex items-center gap-3 px-4 py-3 rounded-lg shadow-lg border',
'transform transition-all duration-300 ease-in-out min-w-[280px] max-w-[400px]',
getTypeStyles(),
isVisible && !isLeaving ? 'translate-x-0 opacity-100' : 'translate-x-full opacity-0'
)}
>
<span className="text-lg font-semibold">{getIcon()}</span>
<span className="flex-1 font-medium text-sm">{message}</span>
<button
onClick={() => {
setIsLeaving(true)
setTimeout(() => onClose(id), 300)
}}
className="ml-2 text-white/80 hover:text-white transition-colors text-lg leading-none"
aria-label="关闭"
>
×
</button>
</div>,
document.body
)
}