Files
opsxshell/src/components/common/Toast.tsx
T
Logan.LiandCursor 08696896f9 feat: harden local shell, SFTP, and desktop session reliability
Finder-launched terminals now inherit a login PATH so tools like ollama resolve, destructive SFTP/editor actions require confirmation, and terminal search plus window dragging work again. SSH forwards and RDP sessions keep listeners and survive reconnect without FastPath crashes.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-16 16:08:11 +08:00

153 lines
4.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Toast 通知组件
*
* 包含单个 Toast 条目渲染 + ToastProvider 全局容器
* 右上角堆叠显示,支持入场/退场动画
*/
import { useEffect, useState, useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { CheckCircle, XCircle, AlertTriangle, Info, X } from 'lucide-react'
import { useToastStore, type ToastItem, type ToastType } from '../../stores/toastStore'
/** 类型 → 图标映射 */
const iconMap: Record<ToastType, typeof CheckCircle> = {
success: CheckCircle,
error: XCircle,
warning: AlertTriangle,
info: Info,
}
/** 类型 → 颜色样式映射(theme tokens,浅色/深色都可读) */
const colorMap: Record<ToastType, { bg: string; border: string; icon: string; text: string }> = {
success: {
bg: 'bg-feedback-success-bg',
border: 'border-feedback-success/35',
icon: 'text-feedback-success',
text: 'text-content-primary',
},
error: {
bg: 'bg-feedback-error-bg',
border: 'border-feedback-error/35',
icon: 'text-feedback-error',
text: 'text-content-primary',
},
warning: {
bg: 'bg-feedback-warning-bg',
border: 'border-feedback-warning/35',
icon: 'text-feedback-warning',
text: 'text-content-primary',
},
info: {
bg: 'bg-feedback-info-bg',
border: 'border-feedback-info/35',
icon: 'text-feedback-info',
text: 'text-content-primary',
},
}
interface ToastItemProps {
toast: ToastItem
onClose: (id: string) => void
}
/**
* 单个 Toast 条目
*
* 支持自动消失和手动关闭,带入场/退场动画
*/
function ToastItemView({ toast, onClose }: ToastItemProps) {
const { t } = useTranslation('common')
const [visible, setVisible] = useState(false)
const Icon = iconMap[toast.type]
const colors = colorMap[toast.type]
// 入场动画:挂载后下一帧设为可见
useEffect(() => {
const raf = requestAnimationFrame(() => setVisible(true))
return () => cancelAnimationFrame(raf)
}, [])
// 自动关闭
const handleClose = useCallback(() => {
setVisible(false)
// 等待退场动画结束后再移除
setTimeout(() => onClose(toast.id), 200)
}, [onClose, toast.id])
useEffect(() => {
if (toast.duration <= 0) return
const timer = setTimeout(() => {
handleClose()
}, toast.duration)
return () => clearTimeout(timer)
}, [toast.duration, handleClose])
return (
<div
className={`
flex items-start gap-3 w-80 px-4 py-3 rounded-lg border
shadow-lg backdrop-blur-sm
transition-all duration-200 ease-out
${colors.bg} ${colors.border}
${visible
? 'translate-x-0 opacity-100'
: 'translate-x-4 opacity-0'
}
`}
role="alert"
>
{/* 图标 */}
<Icon className={`w-5 h-5 mt-0.5 shrink-0 ${colors.icon}`} />
{/* 消息 */}
<p className={`flex-1 text-sm font-medium ${colors.text}`}>
{toast.message}
</p>
{/* 关闭按钮 */}
<button
onClick={handleClose}
className="p-0.5 rounded hover:bg-white/10 transition-colors shrink-0"
aria-label={t('toast.closeAria')}
>
<X className="w-4 h-4 text-txt-muted hover:text-content-primary" />
</button>
</div>
)
}
/**
* Toast 通知容器(Provider)
*
* 在应用根节点中使用,渲染右上角通知堆叠
*
* @example
* // 在 App.tsx 中
* <ToastProvider>
* <Router />
* </ToastProvider>
*/
export function ToastProvider({ children }: { children: React.ReactNode }) {
const toasts = useToastStore((s) => s.toasts)
const removeToast = useToastStore((s) => s.removeToast)
return (
<>
{children}
<div
className="fixed top-4 right-4 z-50 flex flex-col gap-2 pointer-events-none"
aria-live="polite"
>
{toasts.map((toast) => (
<div key={toast.id} className="pointer-events-auto">
<ToastItemView toast={toast} onClose={removeToast} />
</div>
))}
</div>
</>
)
}
export default ToastProvider