FE/src/components/editor/ImportModal.tsx

225 lines
9.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.

import React, { useState, useRef } from 'react';
import { UploadCloud, X, Image as ImageIcon, Loader2, Sparkles, FileText } from 'lucide-react';
import { useStore } from '../../store/useStore';
import { useToastStore } from '../layout/Toast';
const parsedData = {
id: `node-ai-${Date.now()}`,
text: '推理服务',
children: [
{
id: `node-ai-${Date.now()}-1`,
text: '创建推理服务',
children: [
{
id: `node-ai-${Date.now()}-1-1`,
text: '点击创建推理服务按钮',
steps: [{ action: '点击创建推理服务按钮', expected: '进入创建推理服务页面' }]
},
{
id: `node-ai-${Date.now()}-1-2`,
text: '查看页面显示',
children: [
{ id: `node-ai-${Date.now()}-1-2-1`, text: '预期: 左上角显示返回按钮,显示创建推理服务标题' },
{ id: `node-ai-${Date.now()}-1-2-2`, text: '预期: 下方显示基本信息、推理类型、规格信息等' },
{ id: `node-ai-${Date.now()}-1-2-3`, text: '预期: 右下角显示取消、确认按钮' }
]
},
{
id: `node-ai-${Date.now()}-1-3`,
text: '基本信息',
children: [
{
id: `node-ai-${Date.now()}-1-3-1`,
text: '名称',
children: [
{ id: `node-ai-${Date.now()}-1-3-1-1`, text: '备注: 为必填项不允许重复不允许为空支持1-64字符' },
{ id: `node-ai-${Date.now()}-1-3-1-2`, text: '不输入名称', steps: [{ action: '不输入名称,其他配置合法点击确认', expected: '提示名称不能为空' }] },
{ id: `node-ai-${Date.now()}-1-3-1-3`, text: '名称重复', steps: [{ action: '输入重复名称', expected: '提示名称已存在' }] },
{ id: `node-ai-${Date.now()}-1-3-1-4`, text: '名称合法', steps: [{ action: '输入合法名称', expected: '推理服务创建成功' }] },
{ id: `node-ai-${Date.now()}-1-3-1-5`, text: '输入65位字符', steps: [{ action: '输入65位字符', expected: '第65位字符不允许输入' }] }
]
},
{
id: `node-ai-${Date.now()}-1-3-2`,
text: '描述',
children: [
{ id: `node-ai-${Date.now()}-1-3-2-1`, text: '备注: 非必填项200字符以内' },
{ id: `node-ai-${Date.now()}-1-3-2-2`, text: '输入为空', steps: [{ action: '输入为空', expected: '创建成功,描述为空' }] },
{ id: `node-ai-${Date.now()}-1-3-2-3`, text: '输入201字符', steps: [{ action: '输入201字符', expected: '第201位字符不允许输入' }] }
]
},
{
id: `node-ai-${Date.now()}-1-3-3`,
text: '标签',
children: [
{ id: `node-ai-${Date.now()}-1-3-3-1`, text: '备注: 最多支持创建20个标签' },
{ id: `node-ai-${Date.now()}-1-3-3-2`, text: '不输入标签键,输入标签值', steps: [{ action: '点击确认', expected: '给出提示,标签键不能为空' }] }
]
}
]
},
{
id: `node-ai-${Date.now()}-1-4`,
text: '推理类型',
children: [
{ id: `node-ai-${Date.now()}-1-4-1`, text: '查看类型显示', steps: [{ action: '查看显示', expected: '显示推理服务、分布式推理服务' }] },
{ id: `node-ai-${Date.now()}-1-4-2`, text: '多次切换选项', steps: [{ action: '多次切换', expected: '仅支持单选,下方规格随之切换' }] }
]
},
{ id: `node-ai-${Date.now()}-1-5`, text: '规格信息' },
{ id: `node-ai-${Date.now()}-1-6`, text: '基础配置' },
{ id: `node-ai-${Date.now()}-1-7`, text: '存储配置' }
]
},
{ id: `node-ai-${Date.now()}-2`, text: '搜索推理服务' },
{ id: `node-ai-${Date.now()}-3`, text: '推理服务列表' }
]
};
interface ImportModalProps {
onClose: () => void;
}
export const ImportModal: React.FC<ImportModalProps> = ({ onClose }) => {
const [isDragging, setIsDragging] = useState(false);
const [isAnalyzing, setIsAnalyzing] = useState(false);
const [progress, setProgress] = useState(0);
const fileInputRef = useRef<HTMLInputElement>(null);
const { currentSpaceId, spaceData, importNodes } = useStore();
const { addToast } = useToastStore();
const handleSimulateAI = () => {
setIsAnalyzing(true);
let current = 0;
const interval = setInterval(() => {
current += 15;
if (current >= 100) {
clearInterval(interval);
setProgress(100);
setTimeout(async () => {
// Add parsed data to the tree and persist to backend
await importNodes([parsedData]);
addToast('AI 识别完毕!已成功导入推理服务用例脑图', 'success');
onClose();
}, 500);
} else {
setProgress(current);
}
}, 400);
};
const onFileDrop = (e: React.DragEvent) => {
e.preventDefault();
setIsDragging(false);
if (e.dataTransfer.files && e.dataTransfer.files[0]) {
handleSimulateAI();
}
};
return (
<div className="modal-overlay" onClick={onClose} style={{ zIndex: 9999 }}>
<div className="import-modal glass" onClick={e => e.stopPropagation()}>
<div className="dialog-header">
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<Sparkles size={18} style={{ color: '#165DFF' }} />
<h3>AI </h3>
</div>
<button className="close-btn" onClick={onClose}><X size={18} /></button>
</div>
<div className="dialog-body" style={{ padding: '24px' }}>
{!isAnalyzing ? (
<div
className={`drop-zone ${isDragging ? 'dragging' : ''}`}
onDragOver={e => { e.preventDefault(); setIsDragging(true); }}
onDragLeave={() => setIsDragging(false)}
onDrop={onFileDrop}
onClick={() => fileInputRef.current?.click()}
>
<div className="drop-icon-wrapper">
<ImageIcon size={32} color={isDragging ? '#165DFF' : '#86909C'} />
</div>
<h4 style={{ margin: '12px 0 4px 0', color: '#1D2129' }}></h4>
<p style={{ color: '#86909C', fontSize: '13px', margin: 0 }}>
XMind Excel
</p>
<input
type="file"
ref={fileInputRef}
style={{ display: 'none' }}
accept="image/*"
onChange={(e) => {
if (e.target.files && e.target.files.length > 0) {
handleSimulateAI();
}
}}
/>
</div>
) : (
<div className="analyzing-state">
<div className="loader-ring">
<Loader2 size={36} className="spin-anim" color="#165DFF" />
</div>
<h4 style={{ margin: '16px 0 8px 0', color: '#1D2129' }}>AI ...</h4>
<p style={{ color: '#86909C', fontSize: '13px', marginBottom: '20px' }}>
28
</p>
<div className="progress-bar-bg" style={{ width: '100%', height: '6px', background: '#F2F3F5', borderRadius: '3px', overflow: 'hidden' }}>
<div className="progress-bar-fill" style={{ width: `${progress}%`, height: '100%', background: 'linear-gradient(90deg, #165DFF, #36ABFF)', transition: 'width 0.3s ease' }}></div>
</div>
</div>
)}
</div>
</div>
<style>{`
.import-modal {
width: 500px;
border-radius: 12px;
background: white;
box-shadow: 0 12px 32px rgba(0,0,0,0.1);
}
.drop-zone {
border: 2px dashed #E5E6EB;
border-radius: 12px;
padding: 40px 20px;
text-align: center;
cursor: pointer;
background: #FAFAFB;
transition: all 0.2s;
}
.drop-zone:hover, .drop-zone.dragging {
border-color: #165DFF;
background: #F0F7FF;
}
.drop-icon-wrapper {
width: 64px;
height: 64px;
background: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto;
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
}
.analyzing-state {
display: flex;
flex-direction: column;
align-items: center;
padding: 30px 20px;
}
.spin-anim {
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
`}</style>
</div>
);
};