feat(v1.1): Toast通知系统 + 自定义对话框 + 终端搜索 + SFTP真实实现 + 标签持久化
任务7: Toast通知系统 — ToastProvider+4类型(success/error/warning/info)+右上角堆叠动画 任务8: 替换alert/confirm/prompt — ConfirmDialog+PromptDialog+useToast替换7处原生对话框 任务9: 终端搜索 — xterm SearchAddon Ctrl+F搜索栏(上一个/下一个/关闭) 任务10: SFTP上传/删除 — useSFTP.ts upload/delete方法实现 任务11: 会话恢复 — tabStore持久化到localStorage,启动恢复tabs 14 files changed, 1218 insertions(+), 241 deletions(-)
This commit is contained in:
1 parent
53274e2291
commit
d344900118
14 files changed
+1224
-247
No files matched your search
@@ -0,0 +1,150 @@
|
||||
/**
|
||||
* Toast 通知组件
|
||||
*
|
||||
* 包含单个 Toast 条目渲染 + ToastProvider 全局容器
|
||||
* 右上角堆叠显示,支持入场/退场动画
|
||||
*/
|
||||
|
||||
import { useEffect, useState, useCallback } from 'react'
|
||||
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,
|
||||
}
|
||||
|
||||
/** 类型 → 颜色样式映射 */
|
||||
const colorMap: Record<ToastType, { bg: string; border: string; icon: string; text: string }> = {
|
||||
success: {
|
||||
bg: 'bg-green-900/40',
|
||||
border: 'border-green-700/60',
|
||||
icon: 'text-green-400',
|
||||
text: 'text-green-200',
|
||||
},
|
||||
error: {
|
||||
bg: 'bg-red-900/40',
|
||||
border: 'border-red-700/60',
|
||||
icon: 'text-red-400',
|
||||
text: 'text-red-200',
|
||||
},
|
||||
warning: {
|
||||
bg: 'bg-amber-900/40',
|
||||
border: 'border-amber-700/60',
|
||||
icon: 'text-amber-400',
|
||||
text: 'text-amber-200',
|
||||
},
|
||||
info: {
|
||||
bg: 'bg-blue-900/40',
|
||||
border: 'border-blue-700/60',
|
||||
icon: 'text-blue-400',
|
||||
text: 'text-blue-200',
|
||||
},
|
||||
}
|
||||
|
||||
interface ToastItemProps {
|
||||
toast: ToastItem
|
||||
onClose: (id: string) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* 单个 Toast 条目
|
||||
*
|
||||
* 支持自动消失和手动关闭,带入场/退场动画
|
||||
*/
|
||||
function ToastItemView({ toast, onClose }: ToastItemProps) {
|
||||
const [visible, setVisible] = useState(false)
|
||||
const Icon = iconMap[toast.type]
|
||||
const colors = colorMap[toast.type]
|
||||
|
||||
// 入场动画:挂载后下一帧设为可见
|
||||
useEffect(() => {
|
||||
const raf = requestAnimationFrame(() => setVisible(true))
|
||||
return () => cancelAnimationFrame(raf)
|
||||
}, [])
|
||||
|
||||
// 自动关闭
|
||||
useEffect(() => {
|
||||
if (toast.duration <= 0) return
|
||||
const timer = setTimeout(() => {
|
||||
handleClose()
|
||||
}, toast.duration)
|
||||
return () => clearTimeout(timer)
|
||||
}, [toast.duration])
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
setVisible(false)
|
||||
// 等待退场动画结束后再移除
|
||||
setTimeout(() => onClose(toast.id), 200)
|
||||
}, [onClose, toast.id])
|
||||
|
||||
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="关闭通知"
|
||||
>
|
||||
<X className="w-4 h-4 text-gray-400 hover:text-gray-200" />
|
||||
</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
|
||||
Reference in new issue
Block a user