feat(component): 实现组件导入功能

master
钟良源 2 months ago
parent 399d3676c0
commit 069c45b80e

@ -33,6 +33,28 @@ export const remove = (ids) => {
return axios.post(`${urlPrefix}/componentBase/remove?ids=${ids}`);
};
// 获取导入组件的信息
export const getImportComponentInfo = (params) => {
const formData = new FormData();
formData.append('file', params.file);
return axios.post(`${urlPrefix}/componentBase/import/check`, formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
});
};
// 组件导入
export const importComponent = (params) => {
const formData = new FormData();
formData.append('file', params.file);
return axios.post(`${urlPrefix}/componentBase/import`, formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
});
};
// 组件导出
export const exportComponent = (id) => {
return axios.get(`${urlPrefix}/componentBase/export?id=${id}`);

@ -0,0 +1,185 @@
import React, { useState, useRef } from 'react';
import { Modal, Button, Message, Divider, Upload } from '@arco-design/web-react';
import { IconUpload } from '@arco-design/web-react/icon';
import styles from './style/importComponentModal.module.less';
interface ImportComponentModalProps {
visible: boolean;
onCancel: () => void;
onOk: (file: File) => void;
onFileSelect: (file: File) => void;
componentInfo: any;
loading: boolean;
}
const ImportComponentModal: React.FC<ImportComponentModalProps> = ({
visible,
onCancel,
onOk,
onFileSelect,
componentInfo,
loading
}) => {
const [selectedFile, setSelectedFile] = useState<File | null>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (!file) return;
// 验证文件类型 (.zip)
if (!file.name.match(/\.zip$/i)) {
Message.error('请选择ZIP压缩包文件');
event.target.value = '';
return;
}
setSelectedFile(file);
onFileSelect(file);
event.target.value = '';
};
const handleSelectFile = () => {
fileInputRef.current?.click();
};
const handleUpload = () => {
if (selectedFile && componentInfo) {
onOk(selectedFile);
}
};
const handleClose = () => {
setSelectedFile(null);
onCancel();
};
return (
<Modal
title="导入组件"
visible={visible}
onCancel={handleClose}
style={{ width: 800 }}
footer={
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<Button onClick={handleSelectFile} type="outline" icon={<IconUpload />}>
{selectedFile ? '重新选择文件' : '选择文件'}
</Button>
<div>
<Button onClick={handleClose} style={{ marginRight: 12 }}>
</Button>
<Button
type="primary"
onClick={handleUpload}
disabled={!selectedFile || !componentInfo}
loading={loading}
>
</Button>
</div>
</div>
}
>
<input
ref={fileInputRef}
type="file"
accept=".zip"
style={{ display: 'none' }}
onChange={handleFileChange}
/>
<div className={styles['import-modal-content']}>
{!selectedFile && !componentInfo && (
<div className={styles['empty-state']}>
<p className={styles['empty-text']}>&#34;&#34;</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>
)}
{selectedFile && componentInfo && (
<div className={styles['component-preview']}>
<div className={styles['file-info']}>
<div className={styles['file-name']}>
<span className={styles['label']}></span>
<span className={styles['value']}>{selectedFile.name}</span>
</div>
</div>
<Divider style={{ margin: '16px 0' }} />
<div className={styles['component-info']}>
<div className={styles['info-header']}>
<h3></h3>
</div>
<div className={styles['info-item']}>
<span className={styles['label']}></span>
<span className={styles['value']}>{componentInfo.identifier || '-'}</span>
</div>
<div className={styles['info-item']}>
<span className={styles['label']}></span>
<span className={styles['value']}>{componentInfo.name || '-'}</span>
</div>
<div className={styles['info-item']}>
<span className={styles['label']}></span>
<span className={styles['value']}>{componentInfo.description || '-'}</span>
</div>
{componentInfo.operates && componentInfo.operates.length > 0 && (
<>
<Divider style={{ margin: '16px 0' }} />
<div className={styles['operates-section']}>
<h4> ({componentInfo.operates.length})</h4>
<div className={styles['operates-list']}>
{componentInfo.operates.map((operate: any, index: number) => (
<div key={index} className={styles['operate-item']}>
<div className={styles['operate-header']}>
<span className={styles['operate-ident']}>{operate.ident}</span>
<span className={styles['operate-type']}>{operate.type}</span>
</div>
{operate.name && (
<div className={styles['operate-name']}>{operate.name}</div>
)}
<div className={styles['operate-params']}>
{operate.parameters && operate.parameters.length > 0 && (
<div className={styles['param-group']}>
<span className={styles['param-label']}></span>
<span className={styles['param-count']}>
{operate.parameters.length}
</span>
</div>
)}
{operate.responses && operate.responses.length > 0 && (
<div className={styles['param-group']}>
<span className={styles['param-label']}></span>
<span className={styles['param-count']}>
{operate.responses.length}
</span>
</div>
)}
</div>
</div>
))}
</div>
</div>
</>
)}
</div>
</div>
)}
</div>
</Modal>
);
};
export default ImportComponentModal;

@ -2,12 +2,13 @@ import React, { useState, useEffect } from 'react';
import styles from './style/index.module.less';
import { Button, Divider, Input, Space, Table, Radio, Pagination, Modal, Message } from '@arco-design/web-react';
import { IconSearch } from '@arco-design/web-react/icon';
import { getMyComponentList, getCooperationComponentList, remove, exportComponent } from '@/api/componentBase';
import { getMyComponentList, getCooperationComponentList, remove, exportComponent, getImportComponentInfo, importComponent } from '@/api/componentBase';
import { getReviewGroupByNew } from '@/api/componentMarket';
import { componentRelease, componentRevoke } from '@/api/componentRelease';
import { ComponentItem } from '@/api/interface';
import AddComponentModal from '@/pages/componentDevelopment/componentList/addComponentModal';
import PublishComponentModal from '@/pages/componentDevelopment/componentList/publishComponentModal';
import ImportComponentModal from '@/pages/componentDevelopment/componentList/importComponentModal';
import HandleButtonGroup from '@/pages/componentDevelopment/componentList/handleButtonGroup';
import {
componentStatusConstant,
@ -38,6 +39,9 @@ const GlobalVarContainer = () => {
const [mode, setMode] = useState<'create' | 'edit' | 'copy'>('create'); // 添加模式状态
const [searchValue, setSearchValue] = useState(''); // 添加搜索状态
const [componentStatus, setComponentStatus] = useState(''); // 添加组件状态筛选
const [importModalVisible, setImportModalVisible] = useState(false); // 导入弹窗
const [importComponentInfo, setImportComponentInfo] = useState(null); // 导入组件信息
const [importLoading, setImportLoading] = useState(false); // 导入加载状态
const dispatch = useDispatch();
const menuItems = [
@ -245,6 +249,56 @@ const GlobalVarContainer = () => {
}
}, [selectedItem, searchValue]);
// 处理导入组件 - 文件选择后获取组件信息
const handleImportFileSelect = async (file: File) => {
try {
setImportLoading(true);
const res: any = await getImportComponentInfo({ file });
if (res.code === 200) {
setImportComponentInfo(res.data);
Message.success('组件信息解析成功');
} else {
Message.error(res.msg || '解析组件信息失败');
setImportComponentInfo(null);
}
} catch (error) {
console.error('解析组件信息失败:', error);
Message.error('解析组件信息失败');
setImportComponentInfo(null);
} finally {
setImportLoading(false);
}
};
// 处理导入组件 - 确认上传
const handleImportConfirm = async (file: File) => {
try {
setImportLoading(true);
const res: any = await importComponent({ file });
if (res.code === 200) {
Message.success('组件导入成功');
setImportModalVisible(false);
setImportComponentInfo(null);
fetchComponentData(); // 刷新列表
} else {
Message.error(res.msg || '导入组件失败');
}
} catch (error) {
console.error('导入组件失败:', error);
Message.error('导入组件失败');
} finally {
setImportLoading(false);
}
};
// 关闭导入弹窗
const handleImportCancel = () => {
setImportModalVisible(false);
setImportComponentInfo(null);
};
// 获取组件列表数据,支持传入额外参数
const fetchComponentData = async (extraParams: any = {}) => {
setLoading(true);
@ -424,7 +478,13 @@ const GlobalVarContainer = () => {
</Radio.Group>
{selectedItem === '我的组件' && <Space split={<Divider type="vertical" />}>
{/*<Button type="secondary" status="success" style={{ borderRadius: 4 }}>生成组件</Button>*/}
<Button type="outline" style={{ borderRadius: 4 }}></Button>
<Button
type="outline"
style={{ borderRadius: 4 }}
onClick={() => setImportModalVisible(true)}
>
</Button>
<Button type="primary" style={{ borderRadius: 4 }} onClick={() => {
setSelectComponent(null);
setVisible(true);
@ -481,6 +541,16 @@ const GlobalVarContainer = () => {
fetchComponentData();
}}
/>
{/*导入组件弹窗*/}
<ImportComponentModal
visible={importModalVisible}
onCancel={handleImportCancel}
onOk={handleImportConfirm}
onFileSelect={handleImportFileSelect}
componentInfo={importComponentInfo}
loading={importLoading}
/>
</>
);
};

@ -0,0 +1,185 @@
.import-modal-content {
min-height: 300px;
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
text-align: center;
.empty-icon {
font-size: 64px;
margin-bottom: 16px;
}
.empty-text {
font-size: 16px;
color: #1d2129;
margin-bottom: 8px;
}
.empty-hint {
font-size: 14px;
color: #86909c;
}
}
.loading-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
text-align: center;
.loading-spinner {
width: 40px;
height: 40px;
border: 4px solid #f0f2f5;
border-top-color: #165dff;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 16px;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
p {
font-size: 14px;
color: #4e5969;
}
}
.component-preview {
.file-info {
padding: 12px 16px;
background: #f7f8fa;
border-radius: 4px;
.file-name {
display: flex;
align-items: center;
gap: 8px;
.label {
font-weight: 500;
color: #4e5969;
}
.value {
color: #1d2129;
word-break: break-all;
}
}
}
.component-info {
.info-header {
margin-bottom: 16px;
h3 {
font-size: 16px;
font-weight: 600;
color: #1d2129;
margin: 0;
}
}
.info-item {
display: flex;
margin-bottom: 12px;
line-height: 1.6;
.label {
font-weight: 500;
color: #4e5969;
min-width: 80px;
flex-shrink: 0;
}
.value {
color: #1d2129;
flex: 1;
}
}
.operates-section {
h4 {
font-size: 14px;
font-weight: 600;
color: #1d2129;
margin-bottom: 12px;
}
.operates-list {
display: flex;
flex-direction: column;
gap: 12px;
max-height: 300px;
overflow-y: auto;
.operate-item {
padding: 12px;
background: #f7f8fa;
border-radius: 4px;
border-left: 3px solid #165dff;
.operate-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 6px;
.operate-ident {
font-size: 14px;
font-weight: 600;
color: #1d2129;
}
.operate-type {
padding: 2px 8px;
background: #165dff;
color: #fff;
font-size: 12px;
border-radius: 2px;
}
}
.operate-name {
font-size: 13px;
color: #4e5969;
margin-bottom: 8px;
}
.operate-params {
display: flex;
gap: 16px;
font-size: 12px;
.param-group {
display: flex;
align-items: center;
gap: 4px;
.param-label {
color: #86909c;
}
.param-count {
color: #4e5969;
font-weight: 500;
}
}
}
}
}
}
}
}
}
Loading…
Cancel
Save