|
|
|
@ -1,17 +1,22 @@
|
|
|
|
import React, { useState, useRef } from 'react';
|
|
|
|
import React, { useState, useRef, useEffect } from 'react';
|
|
|
|
import { Modal, Button, Message, Divider, Upload } from '@arco-design/web-react';
|
|
|
|
import { Modal, Button, Message, Divider, Tabs, Checkbox } from '@arco-design/web-react';
|
|
|
|
import { IconUpload } from '@arco-design/web-react/icon';
|
|
|
|
import { IconUpload, IconDelete, IconFile } from '@arco-design/web-react/icon';
|
|
|
|
import styles from './style/importComponentModal.module.less';
|
|
|
|
import styles from './style/importComponentModal.module.less';
|
|
|
|
|
|
|
|
|
|
|
|
interface ImportComponentModalProps {
|
|
|
|
interface ImportComponentModalProps {
|
|
|
|
visible: boolean;
|
|
|
|
visible: boolean;
|
|
|
|
onCancel: () => void;
|
|
|
|
onCancel: () => void;
|
|
|
|
onOk: (file: File) => void;
|
|
|
|
onOk: (files: FileItem[]) => void;
|
|
|
|
onFileSelect: (file: File) => void;
|
|
|
|
onFileSelect: (file: File) => void;
|
|
|
|
componentInfo: any;
|
|
|
|
componentInfo: any; // 单次文件解析的结果
|
|
|
|
loading: boolean;
|
|
|
|
loading: boolean;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
interface FileItem {
|
|
|
|
|
|
|
|
file: File;
|
|
|
|
|
|
|
|
componentInfo: any[]; // 该文件包含的组件列表
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const ImportComponentModal: React.FC<ImportComponentModalProps> = ({
|
|
|
|
const ImportComponentModal: React.FC<ImportComponentModalProps> = ({
|
|
|
|
visible,
|
|
|
|
visible,
|
|
|
|
onCancel,
|
|
|
|
onCancel,
|
|
|
|
@ -20,21 +25,56 @@ const ImportComponentModal: React.FC<ImportComponentModalProps> = ({
|
|
|
|
componentInfo,
|
|
|
|
componentInfo,
|
|
|
|
loading
|
|
|
|
loading
|
|
|
|
}) => {
|
|
|
|
}) => {
|
|
|
|
const [selectedFile, setSelectedFile] = useState<File | null>(null);
|
|
|
|
const [fileItems, setFileItems] = useState<FileItem[]>([]);
|
|
|
|
|
|
|
|
const [activeTabKey, setActiveTabKey] = useState<string>('0');
|
|
|
|
|
|
|
|
const [pendingFile, setPendingFile] = useState<File | null>(null); // 等待解析的文件
|
|
|
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
|
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 当 componentInfo 更新时,添加到 fileItems
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
|
|
if (pendingFile && componentInfo) {
|
|
|
|
|
|
|
|
const componentList = Array.isArray(componentInfo) ? componentInfo : [componentInfo];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 检查文件是否已存在
|
|
|
|
|
|
|
|
const existingIndex = fileItems.findIndex(item => item.file.name === pendingFile.name);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (existingIndex >= 0) {
|
|
|
|
|
|
|
|
// 更新现有文件
|
|
|
|
|
|
|
|
const newFileItems = [...fileItems];
|
|
|
|
|
|
|
|
newFileItems[existingIndex] = {
|
|
|
|
|
|
|
|
file: pendingFile,
|
|
|
|
|
|
|
|
componentInfo: componentList
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
setFileItems(newFileItems);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
else {
|
|
|
|
|
|
|
|
// 添加新文件
|
|
|
|
|
|
|
|
const newFileItem: FileItem = {
|
|
|
|
|
|
|
|
file: pendingFile,
|
|
|
|
|
|
|
|
componentInfo: componentList
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
const updatedFileItems = [...fileItems, newFileItem];
|
|
|
|
|
|
|
|
setFileItems(updatedFileItems);
|
|
|
|
|
|
|
|
setActiveTabKey(String(fileItems.length)); // 切换到新添加的标签页
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
setPendingFile(null);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}, [componentInfo]);
|
|
|
|
|
|
|
|
|
|
|
|
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
|
|
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
|
|
const file = event.target.files?.[0];
|
|
|
|
const files = event.target.files;
|
|
|
|
if (!file) return;
|
|
|
|
if (!files || files.length === 0) return;
|
|
|
|
|
|
|
|
|
|
|
|
// 验证文件类型 (.zip)
|
|
|
|
// 验证文件类型 (.zip)
|
|
|
|
|
|
|
|
const file = files[0];
|
|
|
|
if (!file.name.match(/\.zip$/i)) {
|
|
|
|
if (!file.name.match(/\.zip$/i)) {
|
|
|
|
Message.error('请选择ZIP压缩包文件');
|
|
|
|
Message.error('请选择ZIP压缩包文件');
|
|
|
|
event.target.value = '';
|
|
|
|
event.target.value = '';
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
setSelectedFile(file);
|
|
|
|
setPendingFile(file);
|
|
|
|
onFileSelect(file);
|
|
|
|
onFileSelect(file);
|
|
|
|
event.target.value = '';
|
|
|
|
event.target.value = '';
|
|
|
|
};
|
|
|
|
};
|
|
|
|
@ -43,14 +83,31 @@ const ImportComponentModal: React.FC<ImportComponentModalProps> = ({
|
|
|
|
fileInputRef.current?.click();
|
|
|
|
fileInputRef.current?.click();
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 删除文件
|
|
|
|
|
|
|
|
const handleDeleteFile = (index: number) => {
|
|
|
|
|
|
|
|
const newFileItems = fileItems.filter((_, i) => i !== index);
|
|
|
|
|
|
|
|
setFileItems(newFileItems);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 如果删除的是当前激活的标签页,切换到第一个
|
|
|
|
|
|
|
|
if (String(index) === activeTabKey && newFileItems.length > 0) {
|
|
|
|
|
|
|
|
setActiveTabKey('0');
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleUpload = () => {
|
|
|
|
const handleUpload = () => {
|
|
|
|
if (selectedFile && componentInfo) {
|
|
|
|
|
|
|
|
onOk(selectedFile);
|
|
|
|
if (fileItems.length === 0) {
|
|
|
|
|
|
|
|
Message.warning('请至少选择一个组件');
|
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
onOk(fileItems);
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleClose = () => {
|
|
|
|
const handleClose = () => {
|
|
|
|
setSelectedFile(null);
|
|
|
|
setFileItems([]);
|
|
|
|
|
|
|
|
setActiveTabKey('0');
|
|
|
|
|
|
|
|
setPendingFile(null);
|
|
|
|
onCancel();
|
|
|
|
onCancel();
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
@ -59,12 +116,14 @@ const ImportComponentModal: React.FC<ImportComponentModalProps> = ({
|
|
|
|
title="导入组件"
|
|
|
|
title="导入组件"
|
|
|
|
visible={visible}
|
|
|
|
visible={visible}
|
|
|
|
onCancel={handleClose}
|
|
|
|
onCancel={handleClose}
|
|
|
|
style={{ width: 800 }}
|
|
|
|
style={{ width: 900 }}
|
|
|
|
footer={
|
|
|
|
footer={
|
|
|
|
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
|
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
|
|
|
|
|
|
<div>
|
|
|
|
<Button onClick={handleSelectFile} type="outline" icon={<IconUpload />}>
|
|
|
|
<Button onClick={handleSelectFile} type="outline" icon={<IconUpload />}>
|
|
|
|
{selectedFile ? '重新选择文件' : '选择文件'}
|
|
|
|
添加文件
|
|
|
|
</Button>
|
|
|
|
</Button>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
<div>
|
|
|
|
<div>
|
|
|
|
<Button onClick={handleClose} style={{ marginRight: 12 }}>
|
|
|
|
<Button onClick={handleClose} style={{ marginRight: 12 }}>
|
|
|
|
取消
|
|
|
|
取消
|
|
|
|
@ -72,10 +131,10 @@ const ImportComponentModal: React.FC<ImportComponentModalProps> = ({
|
|
|
|
<Button
|
|
|
|
<Button
|
|
|
|
type="primary"
|
|
|
|
type="primary"
|
|
|
|
onClick={handleUpload}
|
|
|
|
onClick={handleUpload}
|
|
|
|
disabled={!selectedFile || !componentInfo}
|
|
|
|
disabled={fileItems.length === 0}
|
|
|
|
loading={loading}
|
|
|
|
loading={loading}
|
|
|
|
>
|
|
|
|
>
|
|
|
|
确认上传
|
|
|
|
确认导入
|
|
|
|
</Button>
|
|
|
|
</Button>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
@ -90,92 +149,125 @@ const ImportComponentModal: React.FC<ImportComponentModalProps> = ({
|
|
|
|
/>
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
<div className={styles['import-modal-content']}>
|
|
|
|
<div className={styles['import-modal-content']}>
|
|
|
|
{!selectedFile && !componentInfo && (
|
|
|
|
{fileItems.length === 0 && !pendingFile && (
|
|
|
|
<div className={styles['empty-state']}>
|
|
|
|
<div className={styles['empty-state']}>
|
|
|
|
<p className={styles['empty-text']}>请点击下方"选择文件"按钮选择组件压缩包</p>
|
|
|
|
<p className={styles['empty-text']}>请点击下方"添加文件"按钮选择组件压缩包</p>
|
|
|
|
<p className={styles['empty-hint']}>支持 .zip 格式文件</p>
|
|
|
|
<p className={styles['empty-hint']}>支持 .zip 格式文件,可添加多个文件</p>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
{selectedFile && !componentInfo && loading && (
|
|
|
|
|
|
|
|
<div className={styles['loading-state']}>
|
|
|
|
|
|
|
|
<div className={styles['loading-spinner']}></div>
|
|
|
|
|
|
|
|
<p>正在解析组件信息...</p>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{selectedFile && componentInfo && (
|
|
|
|
{fileItems.length > 0 && (
|
|
|
|
<div className={styles['component-preview']}>
|
|
|
|
<Tabs
|
|
|
|
<div className={styles['file-info']}>
|
|
|
|
activeTab={activeTabKey}
|
|
|
|
<div className={styles['file-name']}>
|
|
|
|
onChange={setActiveTabKey}
|
|
|
|
<span className={styles['label']}>文件名:</span>
|
|
|
|
type="card"
|
|
|
|
<span className={styles['value']}>{selectedFile.name}</span>
|
|
|
|
>
|
|
|
|
|
|
|
|
{fileItems.map((fileItem, fileIndex) => {
|
|
|
|
|
|
|
|
const current = fileItems[activeTabKey];
|
|
|
|
|
|
|
|
const baseInfo = current.componentInfo[0].baseInfo;
|
|
|
|
|
|
|
|
const operates = current.componentInfo[0].operates;
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
|
|
<Tabs.TabPane
|
|
|
|
|
|
|
|
key={String(fileIndex)}
|
|
|
|
|
|
|
|
title={
|
|
|
|
|
|
|
|
<div className={styles['tab-title']}>
|
|
|
|
|
|
|
|
<IconFile style={{ marginRight: 4 }} />
|
|
|
|
|
|
|
|
<span>{fileItem.file.name}</span>
|
|
|
|
|
|
|
|
<Button
|
|
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
|
|
size="mini"
|
|
|
|
|
|
|
|
icon={<IconDelete />}
|
|
|
|
|
|
|
|
status="danger"
|
|
|
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
|
|
|
handleDeleteFile(fileIndex);
|
|
|
|
|
|
|
|
}}
|
|
|
|
|
|
|
|
style={{ marginLeft: 8 }}
|
|
|
|
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
>
|
|
|
|
|
|
|
|
<div className={styles['file-content']}>
|
|
|
|
|
|
|
|
<div className={styles['component-list']}>
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
|
|
|
className={styles['component-card']}
|
|
|
|
|
|
|
|
>
|
|
|
|
|
|
|
|
<div className={styles['card-header']}>
|
|
|
|
|
|
|
|
<span className={styles['component-name']} style={{ fontWeight: 600, fontSize: 16 }}>
|
|
|
|
|
|
|
|
{baseInfo.name || baseInfo.identifier || baseInfo.projectId || '组件信息'}
|
|
|
|
|
|
|
|
</span>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
{/* 组件基本信息 */}
|
|
|
|
<Divider style={{ margin: '16px 0' }} />
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<div className={styles['component-info']}>
|
|
|
|
<div className={styles['component-info']}>
|
|
|
|
<div className={styles['info-header']}>
|
|
|
|
{baseInfo.projectId && (
|
|
|
|
<h3>组件信息</h3>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<div className={styles['info-item']}>
|
|
|
|
<div className={styles['info-item']}>
|
|
|
|
<span className={styles['label']}>组件标识:</span>
|
|
|
|
<span className={styles['info-label']}>组件标识:</span>
|
|
|
|
<span className={styles['value']}>{componentInfo.identifier || '-'}</span>
|
|
|
|
<span className={styles['info-value']}>{baseInfo.projectId}</span>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
{baseInfo.codeLanguage && (
|
|
|
|
<div className={styles['info-item']}>
|
|
|
|
<div className={styles['info-item']}>
|
|
|
|
<span className={styles['label']}>组件名称:</span>
|
|
|
|
<span className={styles['info-label']}>代码语言:</span>
|
|
|
|
<span className={styles['value']}>{componentInfo.name || '-'}</span>
|
|
|
|
<span className={styles['info-value']}>{baseInfo.codeLanguage}</span>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
{baseInfo.componentClassify && (
|
|
|
|
<div className={styles['info-item']}>
|
|
|
|
<div className={styles['info-item']}>
|
|
|
|
<span className={styles['label']}>组件描述:</span>
|
|
|
|
<span className={styles['info-label']}>组件分类:</span>
|
|
|
|
<span className={styles['value']}>{componentInfo.description || '-'}</span>
|
|
|
|
<span className={styles['info-value']}>{baseInfo.componentClassify}</span>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
{baseInfo.desc && (
|
|
|
|
|
|
|
|
<div className={styles['component-desc']}>
|
|
|
|
|
|
|
|
{baseInfo.desc}
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{componentInfo.operates && componentInfo.operates.length > 0 && (
|
|
|
|
{/* 接口列表 */}
|
|
|
|
<>
|
|
|
|
{operates.length > 0 && (
|
|
|
|
<Divider style={{ margin: '16px 0' }} />
|
|
|
|
|
|
|
|
<div className={styles['operates-section']}>
|
|
|
|
<div className={styles['operates-section']}>
|
|
|
|
<h4>接口列表 ({componentInfo.operates.length})</h4>
|
|
|
|
<div className={styles['section-title']}>
|
|
|
|
|
|
|
|
接口列表 ({operates.length})
|
|
|
|
|
|
|
|
</div>
|
|
|
|
<div className={styles['operates-list']}>
|
|
|
|
<div className={styles['operates-list']}>
|
|
|
|
{componentInfo.operates.map((operate: any, index: number) => (
|
|
|
|
{operates.map((operate: any, opIndex: number) => (
|
|
|
|
<div key={index} className={styles['operate-item']}>
|
|
|
|
<div key={opIndex} className={styles['operate-item']}>
|
|
|
|
<div className={styles['operate-header']}>
|
|
|
|
<div className={styles['operate-header']}>
|
|
|
|
<span className={styles['operate-ident']}>{operate.ident}</span>
|
|
|
|
<span className={styles['operate-ident']}>
|
|
|
|
<span className={styles['operate-type']}>{operate.type}</span>
|
|
|
|
{operate.ident}
|
|
|
|
|
|
|
|
</span>
|
|
|
|
|
|
|
|
<span className={styles['operate-type']}>
|
|
|
|
|
|
|
|
{operate.type}
|
|
|
|
|
|
|
|
</span>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
{operate.name && (
|
|
|
|
{operate.name && (
|
|
|
|
<div className={styles['operate-name']}>{operate.name}</div>
|
|
|
|
<div className={styles['operate-name']}>{operate.name}</div>
|
|
|
|
)}
|
|
|
|
)}
|
|
|
|
<div className={styles['operate-params']}>
|
|
|
|
<div className={styles['operate-params']}>
|
|
|
|
{operate.parameters && operate.parameters.length > 0 && (
|
|
|
|
{operate.parameters && operate.parameters.length > 0 && (
|
|
|
|
<div className={styles['param-group']}>
|
|
|
|
<span className={styles['param-info']}>
|
|
|
|
<span className={styles['param-label']}>输入:</span>
|
|
|
|
输入: {operate.parameters.length} 个参数
|
|
|
|
<span className={styles['param-count']}>
|
|
|
|
|
|
|
|
{operate.parameters.length} 个参数
|
|
|
|
|
|
|
|
</span>
|
|
|
|
</span>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
)}
|
|
|
|
)}
|
|
|
|
{operate.responses && operate.responses.length > 0 && (
|
|
|
|
{operate.responses && operate.responses.length > 0 && (
|
|
|
|
<div className={styles['param-group']}>
|
|
|
|
<span className={styles['param-info']}>
|
|
|
|
<span className={styles['param-label']}>输出:</span>
|
|
|
|
输出: {operate.responses.length} 个参数
|
|
|
|
<span className={styles['param-count']}>
|
|
|
|
|
|
|
|
{operate.responses.length} 个参数
|
|
|
|
|
|
|
|
</span>
|
|
|
|
</span>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
)}
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
))}
|
|
|
|
))}
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</>
|
|
|
|
|
|
|
|
)}
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
</Tabs.TabPane>
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
})}
|
|
|
|
|
|
|
|
</Tabs>
|
|
|
|
)}
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</Modal>
|
|
|
|
</Modal>
|
|
|
|
|