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:
OpsXShell Dev committed 2026-04-18 16:21:10 +08:00
1 parent 53274e2291
commit d344900118
14 files changed
+1224 -247

No files matched your search

+150
View File
@@ -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