mirror of
https://github.com/black-ant/Ant-Browser.git
synced 2026-07-14 18:48:55 +08:00
111 lines
3.3 KiB
TypeScript
111 lines
3.3 KiB
TypeScript
import { CheckCircle, XCircle, AlertCircle, Info, X } from 'lucide-react'
|
|
import { create } from 'zustand'
|
|
import { useNotificationStore } from '../../store/notificationStore'
|
|
|
|
type ToastType = 'success' | 'error' | 'warning' | 'info'
|
|
|
|
interface Toast {
|
|
id: string
|
|
type: ToastType
|
|
message: string
|
|
duration?: number
|
|
}
|
|
|
|
interface ToastStore {
|
|
toasts: Toast[]
|
|
addToast: (toast: Omit<Toast, 'id'>) => void
|
|
removeToast: (id: string) => void
|
|
}
|
|
|
|
export const useToastStore = create<ToastStore>((set) => ({
|
|
toasts: [],
|
|
addToast: (toast) => {
|
|
const id = Math.random().toString(36).substring(7)
|
|
set((state) => ({
|
|
toasts: [...state.toasts, { ...toast, id }],
|
|
}))
|
|
|
|
// 自动移除
|
|
const duration = toast.duration ?? 3000
|
|
if (duration > 0) {
|
|
setTimeout(() => {
|
|
set((state) => ({
|
|
toasts: state.toasts.filter((t) => t.id !== id),
|
|
}))
|
|
}, duration)
|
|
}
|
|
},
|
|
removeToast: (id) =>
|
|
set((state) => ({
|
|
toasts: state.toasts.filter((t) => t.id !== id),
|
|
})),
|
|
}))
|
|
|
|
// Toast 工具函数
|
|
function recordErrorNotification(message: string) {
|
|
useNotificationStore.getState().addNotification({
|
|
type: 'error',
|
|
title: '操作异常',
|
|
message,
|
|
})
|
|
}
|
|
|
|
export const toast = {
|
|
success: (message: string, duration?: number) =>
|
|
useToastStore.getState().addToast({ type: 'success', message, duration }),
|
|
error: (message: string, duration?: number) => {
|
|
useToastStore.getState().addToast({ type: 'error', message, duration })
|
|
recordErrorNotification(message)
|
|
},
|
|
warning: (message: string, duration?: number) =>
|
|
useToastStore.getState().addToast({ type: 'warning', message, duration }),
|
|
info: (message: string, duration?: number) =>
|
|
useToastStore.getState().addToast({ type: 'info', message, duration }),
|
|
}
|
|
|
|
const icons = {
|
|
success: CheckCircle,
|
|
error: XCircle,
|
|
warning: AlertCircle,
|
|
info: Info,
|
|
}
|
|
|
|
const styles = {
|
|
success: 'bg-[var(--color-bg-surface)] text-[var(--color-success)] border-[var(--color-success)]/30 shadow-lg shadow-[var(--color-success)]/5',
|
|
error: 'bg-[var(--color-bg-surface)] text-[var(--color-error)] border-[var(--color-error)]/30 shadow-lg shadow-[var(--color-error)]/5',
|
|
warning: 'bg-[var(--color-bg-surface)] text-[var(--color-warning)] border-[var(--color-warning)]/30 shadow-lg shadow-[var(--color-warning)]/5',
|
|
info: 'bg-[var(--color-bg-surface)] text-[var(--color-accent)] border-[var(--color-accent)]/30 shadow-lg shadow-[var(--color-accent)]/5',
|
|
}
|
|
|
|
function ToastItem({ toast: t }: { toast: Toast }) {
|
|
const removeToast = useToastStore((state) => state.removeToast)
|
|
const Icon = icons[t.type]
|
|
|
|
return (
|
|
<div
|
|
className={`flex items-start gap-3 px-4 py-3 rounded-lg border shadow-lg animate-slide-in-right ${styles[t.type]}`}
|
|
>
|
|
<Icon className="w-5 h-5 flex-shrink-0 mt-0.5" />
|
|
<p className="flex-1 text-sm font-medium">{t.message}</p>
|
|
<button
|
|
onClick={() => removeToast(t.id)}
|
|
className="p-0.5 rounded hover:bg-black/10 transition-colors"
|
|
>
|
|
<X className="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export function ToastContainer() {
|
|
const toasts = useToastStore((state) => state.toasts)
|
|
|
|
return (
|
|
<div className="fixed top-4 right-4 z-50 flex flex-col gap-2 max-w-md">
|
|
{toasts.map((t) => (
|
|
<ToastItem key={t.id} toast={t} />
|
|
))}
|
|
</div>
|
|
)
|
|
}
|