feat: 实现AI画布工具并隐藏,设置项目为首页
- 新增完整的AI画布工具系统 - 可视化节点编辑器,支持拖拽连线 - 多种节点类型:文本输入、图片生成、视频生成等 - 智能连接验证和数据流转换 - 异步处理引擎,支持进度追踪和取消 - 批量处理系统,支持并发处理 - AI服务集成框架,支持多种AI API - 用户体验优化 - 智能弹框定位,防止被遮挡 - 节点删除功能(悬停删除按钮 + 键盘快捷键) - 通知系统和错误处理 - 快速开始模板 - 键盘快捷键支持 - 界面调整 - 暂时隐藏AI画布,保留代码 - 设置项目列表为首页 - 简化导航栏结构
This commit is contained in:
248
apps/desktop/src/components/canvas/NotificationSystem.tsx
Normal file
248
apps/desktop/src/components/canvas/NotificationSystem.tsx
Normal file
@@ -0,0 +1,248 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { CheckCircle, XCircle, AlertCircle, Info, X } from 'lucide-react';
|
||||
|
||||
export type NotificationType = 'success' | 'error' | 'warning' | 'info';
|
||||
|
||||
export interface Notification {
|
||||
id: string;
|
||||
type: NotificationType;
|
||||
title: string;
|
||||
message?: string;
|
||||
duration?: number;
|
||||
action?: {
|
||||
label: string;
|
||||
onClick: () => void;
|
||||
};
|
||||
}
|
||||
|
||||
interface NotificationSystemProps {
|
||||
notifications: Notification[];
|
||||
onRemove: (id: string) => void;
|
||||
}
|
||||
|
||||
const NotificationItem: React.FC<{
|
||||
notification: Notification;
|
||||
onRemove: (id: string) => void;
|
||||
}> = ({ notification, onRemove }) => {
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
const [isLeaving, setIsLeaving] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// 入场动画
|
||||
setTimeout(() => setIsVisible(true), 50);
|
||||
|
||||
// 自动移除
|
||||
if (notification.duration !== 0) {
|
||||
const timer = setTimeout(() => {
|
||||
handleRemove();
|
||||
}, notification.duration || 5000);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [notification.duration]);
|
||||
|
||||
const handleRemove = () => {
|
||||
setIsLeaving(true);
|
||||
setTimeout(() => {
|
||||
onRemove(notification.id);
|
||||
}, 300);
|
||||
};
|
||||
|
||||
const getIcon = () => {
|
||||
switch (notification.type) {
|
||||
case 'success':
|
||||
return <CheckCircle className="w-5 h-5 text-green-500" />;
|
||||
case 'error':
|
||||
return <XCircle className="w-5 h-5 text-red-500" />;
|
||||
case 'warning':
|
||||
return <AlertCircle className="w-5 h-5 text-yellow-500" />;
|
||||
case 'info':
|
||||
return <Info className="w-5 h-5 text-blue-500" />;
|
||||
}
|
||||
};
|
||||
|
||||
const getBackgroundColor = () => {
|
||||
switch (notification.type) {
|
||||
case 'success':
|
||||
return 'bg-green-50 border-green-200';
|
||||
case 'error':
|
||||
return 'bg-red-50 border-red-200';
|
||||
case 'warning':
|
||||
return 'bg-yellow-50 border-yellow-200';
|
||||
case 'info':
|
||||
return 'bg-blue-50 border-blue-200';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
transform transition-all duration-300 ease-in-out
|
||||
${isVisible && !isLeaving ? 'translate-x-0 opacity-100' : 'translate-x-full opacity-0'}
|
||||
${getBackgroundColor()}
|
||||
border rounded-lg p-4 shadow-lg max-w-sm w-full
|
||||
`}
|
||||
>
|
||||
<div className="flex items-start space-x-3">
|
||||
<div className="flex-shrink-0">
|
||||
{getIcon()}
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<h4 className="text-sm font-medium text-gray-900">
|
||||
{notification.title}
|
||||
</h4>
|
||||
{notification.message && (
|
||||
<p className="text-sm text-gray-600 mt-1">
|
||||
{notification.message}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{notification.action && (
|
||||
<button
|
||||
onClick={notification.action.onClick}
|
||||
className="text-sm font-medium text-blue-600 hover:text-blue-800 mt-2 transition-colors"
|
||||
>
|
||||
{notification.action.label}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleRemove}
|
||||
className="flex-shrink-0 text-gray-400 hover:text-gray-600 transition-colors"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const NotificationSystem: React.FC<NotificationSystemProps> = ({
|
||||
notifications,
|
||||
onRemove
|
||||
}) => {
|
||||
return (
|
||||
<div className="fixed top-4 right-4 z-50 space-y-2">
|
||||
{notifications.map(notification => (
|
||||
<NotificationItem
|
||||
key={notification.id}
|
||||
notification={notification}
|
||||
onRemove={onRemove}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Hook for managing notifications
|
||||
export const useNotifications = () => {
|
||||
const [notifications, setNotifications] = useState<Notification[]>([]);
|
||||
|
||||
const addNotification = (notification: Omit<Notification, 'id'>) => {
|
||||
const id = `notification-${Date.now()}-${Math.random()}`;
|
||||
setNotifications(prev => [...prev, { ...notification, id }]);
|
||||
};
|
||||
|
||||
const removeNotification = (id: string) => {
|
||||
setNotifications(prev => prev.filter(n => n.id !== id));
|
||||
};
|
||||
|
||||
const clearAll = () => {
|
||||
setNotifications([]);
|
||||
};
|
||||
|
||||
// 便捷方法
|
||||
const success = (title: string, message?: string, options?: Partial<Notification>) => {
|
||||
addNotification({ type: 'success', title, message, ...options });
|
||||
};
|
||||
|
||||
const error = (title: string, message?: string, options?: Partial<Notification>) => {
|
||||
addNotification({ type: 'error', title, message, duration: 0, ...options });
|
||||
};
|
||||
|
||||
const warning = (title: string, message?: string, options?: Partial<Notification>) => {
|
||||
addNotification({ type: 'warning', title, message, ...options });
|
||||
};
|
||||
|
||||
const info = (title: string, message?: string, options?: Partial<Notification>) => {
|
||||
addNotification({ type: 'info', title, message, ...options });
|
||||
};
|
||||
|
||||
return {
|
||||
notifications,
|
||||
addNotification,
|
||||
removeNotification,
|
||||
clearAll,
|
||||
success,
|
||||
error,
|
||||
warning,
|
||||
info
|
||||
};
|
||||
};
|
||||
|
||||
// 全局通知管理器
|
||||
class GlobalNotificationManager {
|
||||
private listeners: Array<(notifications: Notification[]) => void> = [];
|
||||
private notifications: Notification[] = [];
|
||||
|
||||
subscribe(listener: (notifications: Notification[]) => void) {
|
||||
this.listeners.push(listener);
|
||||
return () => {
|
||||
this.listeners = this.listeners.filter(l => l !== listener);
|
||||
};
|
||||
}
|
||||
|
||||
private notify() {
|
||||
this.listeners.forEach(listener => listener([...this.notifications]));
|
||||
}
|
||||
|
||||
add(notification: Omit<Notification, 'id'>) {
|
||||
const id = `global-notification-${Date.now()}-${Math.random()}`;
|
||||
this.notifications.push({ ...notification, id });
|
||||
this.notify();
|
||||
}
|
||||
|
||||
remove(id: string) {
|
||||
this.notifications = this.notifications.filter(n => n.id !== id);
|
||||
this.notify();
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.notifications = [];
|
||||
this.notify();
|
||||
}
|
||||
|
||||
success(title: string, message?: string, options?: Partial<Notification>) {
|
||||
this.add({ type: 'success', title, message, ...options });
|
||||
}
|
||||
|
||||
error(title: string, message?: string, options?: Partial<Notification>) {
|
||||
this.add({ type: 'error', title, message, duration: 0, ...options });
|
||||
}
|
||||
|
||||
warning(title: string, message?: string, options?: Partial<Notification>) {
|
||||
this.add({ type: 'warning', title, message, ...options });
|
||||
}
|
||||
|
||||
info(title: string, message?: string, options?: Partial<Notification>) {
|
||||
this.add({ type: 'info', title, message, ...options });
|
||||
}
|
||||
}
|
||||
|
||||
export const globalNotifications = new GlobalNotificationManager();
|
||||
|
||||
// Hook for global notifications
|
||||
export const useGlobalNotifications = () => {
|
||||
const [notifications, setNotifications] = useState<Notification[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
return globalNotifications.subscribe(setNotifications);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
notifications,
|
||||
removeNotification: globalNotifications.remove.bind(globalNotifications)
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user