From b866b14e6463c42c084a23db5a8a523030226f2d Mon Sep 17 00:00:00 2001 From: ZLY Date: Wed, 26 Nov 2025 18:34:02 +0800 Subject: [PATCH] =?UTF-8?q?feat(component):=20=E6=94=AF=E6=8C=81=E6=89=B9?= =?UTF-8?q?=E9=87=8F=E5=AF=BC=E5=85=A5=E5=92=8C=E5=AF=BC=E5=87=BA=E7=BB=84?= =?UTF-8?q?=E4=BB=B6=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/componentBase.ts | 12 +- src/api/index.ts | 7 +- .../componentList/handleButtonGroup.tsx | 6 +- .../componentList/importComponentModal.tsx | 286 ++++++++++++------ .../componentList/index.tsx | 77 +++-- .../style/importComponentModal.module.less | 210 ++++++------- 6 files changed, 372 insertions(+), 226 deletions(-) diff --git a/src/api/componentBase.ts b/src/api/componentBase.ts index f02058d..12de7b8 100644 --- a/src/api/componentBase.ts +++ b/src/api/componentBase.ts @@ -47,7 +47,15 @@ export const getImportComponentInfo = (params) => { // 组件导入 export const importComponent = (params) => { const formData = new FormData(); - formData.append('files', params.file); + // 支持多个文件上传 + if (Array.isArray(params.file)) { + params.file.forEach(file => { + formData.append('files', file); + }); + } + else { + formData.append('files', params.file); + } return axios.post(`${urlPrefix}/componentBase/import`, formData, { headers: { 'Content-Type': 'multipart/form-data' @@ -57,7 +65,7 @@ export const importComponent = (params) => { // 组件导出 export const exportComponent = (id) => { - return axios.get(`${urlPrefix}/componentBase/export?id=${id}`); + return axios.get(`${urlPrefix}/componentBase/export?id=${id}`, { responseType: 'blob' }); }; // 复制代码和设计 diff --git a/src/api/index.ts b/src/api/index.ts index 1a0016d..fc51338 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -22,7 +22,7 @@ axios.interceptors.request.use( if (!config.headers) { config.headers = {}; } - // config.headers.Authorization = `Bearer ${token}` + config.headers.Authorization = `Bearer ${token}` } return config; }, @@ -33,6 +33,11 @@ axios.interceptors.request.use( // add response interceptors axios.interceptors.response.use( (response: AxiosResponse) => { + // 如果是 blob 类型响应(文件下载),直接返回 response.data + if (response.config.responseType === 'blob') { + return response.data; + } + const res = response.data; if (res.code && res.code !== 200) { Message.error({ diff --git a/src/pages/componentDevelopment/componentList/handleButtonGroup.tsx b/src/pages/componentDevelopment/componentList/handleButtonGroup.tsx index 079b3d8..850f5e6 100644 --- a/src/pages/componentDevelopment/componentList/handleButtonGroup.tsx +++ b/src/pages/componentDevelopment/componentList/handleButtonGroup.tsx @@ -13,12 +13,11 @@ interface HandleButtonGroupProps { onHandlePublishComponent: (row: ComponentItem) => void; onGoToReview: (row: ComponentItem) => void; onPublishOrRevokeComponent: (action: 'publish' | 'revoke', identifier: string, version: string) => void; - onNavTo: (id: number, type: string) => void; onSourceCodeView: (row: ComponentItem) => void; onShowEdit: (row: ComponentItem, index: number) => void; onCopyHandler: (row: ComponentItem) => void; onShareCollaboration: (row: ComponentItem) => void; - onExportComponent: (id: number) => void; + onExportComponent: (row: ComponentItem) => void; onStopComponentShow: (row: ComponentItem) => void; onRowDel: (row: ComponentItem) => void; } @@ -29,7 +28,6 @@ const HandleButtonGroup: React.FC = ({ onHandlePublishComponent, onGoToReview, onPublishOrRevokeComponent, - onNavTo, onSourceCodeView, onShowEdit, onCopyHandler, @@ -69,7 +67,7 @@ const HandleButtonGroup: React.FC = ({ // 导出组件 items.push( - + ); diff --git a/src/pages/componentDevelopment/componentList/importComponentModal.tsx b/src/pages/componentDevelopment/componentList/importComponentModal.tsx index 53b45fb..7ff82bc 100644 --- a/src/pages/componentDevelopment/componentList/importComponentModal.tsx +++ b/src/pages/componentDevelopment/componentList/importComponentModal.tsx @@ -1,17 +1,22 @@ -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 React, { useState, useRef, useEffect } from 'react'; +import { Modal, Button, Message, Divider, Tabs, Checkbox } from '@arco-design/web-react'; +import { IconUpload, IconDelete, IconFile } from '@arco-design/web-react/icon'; import styles from './style/importComponentModal.module.less'; interface ImportComponentModalProps { visible: boolean; onCancel: () => void; - onOk: (file: File) => void; + onOk: (files: FileItem[]) => void; onFileSelect: (file: File) => void; - componentInfo: any; + componentInfo: any; // 单次文件解析的结果 loading: boolean; } +interface FileItem { + file: File; + componentInfo: any[]; // 该文件包含的组件列表 +} + const ImportComponentModal: React.FC = ({ visible, onCancel, @@ -20,21 +25,56 @@ const ImportComponentModal: React.FC = ({ componentInfo, loading }) => { - const [selectedFile, setSelectedFile] = useState(null); + const [fileItems, setFileItems] = useState([]); + const [activeTabKey, setActiveTabKey] = useState('0'); + const [pendingFile, setPendingFile] = useState(null); // 等待解析的文件 const fileInputRef = useRef(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) => { - const file = event.target.files?.[0]; - if (!file) return; + const files = event.target.files; + if (!files || files.length === 0) return; // 验证文件类型 (.zip) + const file = files[0]; if (!file.name.match(/\.zip$/i)) { Message.error('请选择ZIP压缩包文件'); event.target.value = ''; return; } - setSelectedFile(file); + setPendingFile(file); onFileSelect(file); event.target.value = ''; }; @@ -43,14 +83,31 @@ const ImportComponentModal: React.FC = ({ 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 = () => { - if (selectedFile && componentInfo) { - onOk(selectedFile); + + if (fileItems.length === 0) { + Message.warning('请至少选择一个组件'); + return; } + + onOk(fileItems); }; const handleClose = () => { - setSelectedFile(null); + setFileItems([]); + setActiveTabKey('0'); + setPendingFile(null); onCancel(); }; @@ -59,12 +116,14 @@ const ImportComponentModal: React.FC = ({ title="导入组件" visible={visible} onCancel={handleClose} - style={{ width: 800 }} + style={{ width: 900 }} footer={ -
- +
+
+ +
@@ -90,92 +149,125 @@ const ImportComponentModal: React.FC = ({ />
- {!selectedFile && !componentInfo && ( + {fileItems.length === 0 && !pendingFile && (
-

请点击下方"选择文件"按钮选择组件压缩包

-

支持 .zip 格式文件

+

请点击下方"添加文件"按钮选择组件压缩包

+

支持 .zip 格式文件,可添加多个文件

)} - {selectedFile && !componentInfo && loading && ( -
-
-

正在解析组件信息...

-
- )} - - {selectedFile && componentInfo && ( -
-
-
- 文件名: - {selectedFile.name} -
-
- - - -
-
-

组件信息

-
- -
- 组件标识: - {componentInfo.identifier || '-'} -
- -
- 组件名称: - {componentInfo.name || '-'} -
- -
- 组件描述: - {componentInfo.description || '-'} -
- - {componentInfo.operates && componentInfo.operates.length > 0 && ( - <> - -
-

接口列表 ({componentInfo.operates.length})

-
- {componentInfo.operates.map((operate: any, index: number) => ( -
-
- {operate.ident} - {operate.type} -
- {operate.name && ( -
{operate.name}
- )} -
- {operate.parameters && operate.parameters.length > 0 && ( -
- 输入: - - {operate.parameters.length} 个参数 - -
- )} - {operate.responses && operate.responses.length > 0 && ( -
- 输出: - - {operate.responses.length} 个参数 + {fileItems.length > 0 && ( + + {fileItems.map((fileItem, fileIndex) => { + const current = fileItems[activeTabKey]; + const baseInfo = current.componentInfo[0].baseInfo; + const operates = current.componentInfo[0].operates; + return ( + + + {fileItem.file.name} +
+ } + > +
+
+
+
+ + {baseInfo.name || baseInfo.identifier || baseInfo.projectId || '组件信息'} -
- )} -
- ))} + {/* 组件基本信息 */} +
+ {baseInfo.projectId && ( +
+ 组件标识: + {baseInfo.projectId} +
+ )} + {baseInfo.codeLanguage && ( +
+ 代码语言: + {baseInfo.codeLanguage} +
+ )} + {baseInfo.componentClassify && ( +
+ 组件分类: + {baseInfo.componentClassify} +
+ )} + {baseInfo.desc && ( +
+ {baseInfo.desc} +
+ )} +
+ + {/* 接口列表 */} + {operates.length > 0 && ( +
+
+ 接口列表 ({operates.length}) +
+
+ {operates.map((operate: any, opIndex: number) => ( +
+
+ + {operate.ident} + + + {operate.type} + +
+ {operate.name && ( +
{operate.name}
+ )} +
+ {operate.parameters && operate.parameters.length > 0 && ( + + 输入: {operate.parameters.length} 个参数 + + )} + {operate.responses && operate.responses.length > 0 && ( + + 输出: {operate.responses.length} 个参数 + + )} +
+
+ ))} +
+
+ )} +
- - )} -
-
+ + ); + })} + )}
diff --git a/src/pages/componentDevelopment/componentList/index.tsx b/src/pages/componentDevelopment/componentList/index.tsx index eeedbd9..077b941 100644 --- a/src/pages/componentDevelopment/componentList/index.tsx +++ b/src/pages/componentDevelopment/componentList/index.tsx @@ -173,10 +173,6 @@ const GlobalVarContainer = () => { Message.error(`组件${action === 'publish' ? '公开' : '撤销'}失败: ${error.message || ''}`); } }} - onNavTo={(id, type) => { - // TODO: 实现导航逻辑 - console.log('Nav to', id, type); - }} onSourceCodeView={(row) => { dispatch(updateComponentCodingPath({ localProjectPath: row.localProjectPath, @@ -206,9 +202,9 @@ const GlobalVarContainer = () => { // TODO: 实现分享协作逻辑 console.log('Share collaboration', row); }} - onExportComponent={(id) => { + onExportComponent={(row) => { // 实现导出组件逻辑 - onExportComponent(id); + onExportComponent(row); }} onStopComponentShow={(row) => { // TODO: 实现下架组件逻辑 @@ -291,7 +287,6 @@ const GlobalVarContainer = () => { setImportComponentInfo(null); } } catch (error) { - console.error('解析组件信息失败:', error); Message.error('解析组件信息失败'); setImportComponentInfo(null); } finally { @@ -300,20 +295,21 @@ const GlobalVarContainer = () => { }; // 处理导入组件 - 确认上传 - const handleImportConfirm = async (file: File) => { + const handleImportConfirm = async (files) => { 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 || '导入组件失败'); - } + const params = { + file: files.map(file => file.file) + }; + + const res: any = await importComponent(params); + + console.log('res:', res); + + setImportModalVisible(false); + setImportComponentInfo(null); + // fetchComponentData(); // 刷新列表 } catch (error) { console.error('导入组件失败:', error); Message.error('导入组件失败'); @@ -417,9 +413,50 @@ const GlobalVarContainer = () => { }; // 修改导出组件的回调函数 - const onExportComponent = async (id) => { + const onExportComponent = async (row) => { try { - await exportComponent(id); + const res: any = await exportComponent(row.id); + + console.log('导出响应:', { + type: typeof res, + isBlob: res instanceof Blob, + size: res?.size, + blobType: res?.type + }); + + // 如果后端返回错误,可能会是 JSON 格式 + if (res instanceof Blob && res.type === 'application/json') { + const text = await res.text(); + const errorData = JSON.parse(text); + console.error('后端返回错误:', errorData); + Message.error(errorData.msg || '导出失败'); + return; + } + + // 确保 res 是 Blob 对象 + if (!(res instanceof Blob)) { + console.error('响应不是 Blob 对象:', res); + Message.error('导出失败:响应格式错误'); + return; + } + + // 直接使用返回的 blob + const url = window.URL.createObjectURL(res); + const link = document.createElement('a'); + link.href = url; + + // 设置文件名 + const fileName = `${row.name}.zip`; + link.setAttribute('download', fileName); + + // 触发下载 + document.body.appendChild(link); + link.click(); + + // 清理 + document.body.removeChild(link); + window.URL.revokeObjectURL(url); + Message.success('组件导出成功'); } catch (error) { console.error('导出组件失败:', error); diff --git a/src/pages/componentDevelopment/componentList/style/importComponentModal.module.less b/src/pages/componentDevelopment/componentList/style/importComponentModal.module.less index 2e5d370..3dcc0e2 100644 --- a/src/pages/componentDevelopment/componentList/style/importComponentModal.module.less +++ b/src/pages/componentDevelopment/componentList/style/importComponentModal.module.less @@ -1,19 +1,14 @@ .import-modal-content { - min-height: 300px; + min-height: 400px; .empty-state { display: flex; flex-direction: column; align-items: center; justify-content: center; - padding: 60px 20px; + padding: 80px 20px; text-align: center; - .empty-icon { - font-size: 64px; - margin-bottom: 16px; - } - .empty-text { font-size: 16px; color: #1d2129; @@ -31,7 +26,7 @@ flex-direction: column; align-items: center; justify-content: center; - padding: 60px 20px; + padding: 80px 20px; text-align: center; .loading-spinner { @@ -56,124 +51,135 @@ } } - .component-preview { - .file-info { - padding: 12px 16px; - background: #f7f8fa; - border-radius: 4px; - - .file-name { - display: flex; - align-items: center; - gap: 8px; + .tab-title { + display: flex; + align-items: center; + gap: 4px; + max-width: 200px; + + span { + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + } - .label { - font-weight: 500; - color: #4e5969; - } + .file-content { + padding: 16px 0; - .value { - color: #1d2129; - word-break: break-all; - } - } + .file-header { + padding: 0 4px; + margin-bottom: 12px; } - .component-info { - .info-header { - margin-bottom: 16px; + .component-list { + max-height: 500px; + overflow-y: auto; + padding-right: 4px; - h3 { - font-size: 16px; - font-weight: 600; - color: #1d2129; - margin: 0; + .component-card { + margin-bottom: 12px; + padding: 16px; + border: 1px solid #e5e6eb; + border-radius: 4px; + background: #fff; + transition: all 0.2s; + + &.selected { + border-color: #165dff; + background: #f2f8ff; } - } - .info-item { - display: flex; - margin-bottom: 12px; - line-height: 1.6; + .card-header { + margin-bottom: 8px; - .label { - font-weight: 500; - color: #4e5969; - min-width: 80px; - flex-shrink: 0; + .component-name { + font-size: 14px; + font-weight: 600; + color: #1d2129; + } } - .value { - color: #1d2129; - flex: 1; + .component-desc { + margin: 8px 0 8px 24px; + font-size: 13px; + color: #4e5969; + line-height: 1.6; } - } - .operates-section { - h4 { - font-size: 14px; - font-weight: 600; - color: #1d2129; - margin-bottom: 12px; - } + .component-info { + margin: 12px 0 12px 24px; - .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; - } + .info-item { + display: flex; + margin-bottom: 8px; + font-size: 13px; - .operate-type { - padding: 2px 8px; - background: #165dff; - color: #fff; - font-size: 12px; - border-radius: 2px; - } + .info-label { + font-weight: 500; + color: #4e5969; + min-width: 80px; + flex-shrink: 0; } - .operate-name { - font-size: 13px; - color: #4e5969; - margin-bottom: 8px; + .info-value { + color: #1d2129; + flex: 1; } + } + } + + .operates-section { + margin: 12px 0 0 24px; + + .section-title { + font-size: 13px; + font-weight: 500; + color: #1d2129; + margin-bottom: 8px; + } + + .operates-list { + display: flex; + flex-direction: column; + gap: 8px; - .operate-params { - display: flex; - gap: 16px; - font-size: 12px; + .operate-item { + padding: 10px 12px; + background: #fff; + border: 1px solid #e5e6eb; + border-radius: 4px; + border-left: 3px solid #165dff; - .param-group { + .operate-header { display: flex; align-items: center; - gap: 4px; + justify-content: space-between; + margin-bottom: 6px; - .param-label { - color: #86909c; + .operate-ident { + font-size: 13px; + font-weight: 500; + color: #1d2129; } - .param-count { - color: #4e5969; - font-weight: 500; + .operate-type { + padding: 2px 8px; + background: #165dff; + color: #fff; + font-size: 11px; + border-radius: 2px; + } + } + + .operate-params { + display: flex; + gap: 16px; + font-size: 12px; + + .param-info { + color: #86909c; } } }