225 lines
9.0 KiB
TypeScript
225 lines
9.0 KiB
TypeScript
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>
|
||
);
|
||
};
|