You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

293 lines
11 KiB
TypeScript

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import React, { useState, useRef, useEffect } from 'react';
import { Modal, Button, Message, Tabs } from '@arco-design/web-react';
import { IconUpload, IconDelete, IconFile } from '@arco-design/web-react/icon';
import styles from './style/importComponentModal.module.less';
interface ParsedComponentInfo {
baseInfo?: any;
operates?: any[];
}
interface ImportComponentModalProps {
visible: boolean;
onCancel: () => void;
onOk: (files: FileItem[]) => void;
onFileSelect: (file: File) => Promise<ParsedComponentInfo[] | ParsedComponentInfo | null>;
loading: boolean;
}
interface FileItem {
file: File;
componentInfo: ParsedComponentInfo[]; // 该文件包含的组件列表
}
const ImportComponentModal: React.FC<ImportComponentModalProps> = ({
visible,
onCancel,
onOk,
onFileSelect,
loading
}) => {
const [fileItems, setFileItems] = useState<FileItem[]>([]);
const [activeTabKey, setActiveTabKey] = useState<string>('0');
const fileInputRef = useRef<HTMLInputElement>(null);
// 当弹窗关闭时清空所有状态
useEffect(() => {
if (!visible) {
setFileItems([]);
setActiveTabKey('0');
}
}, [visible]);
const updateFileItem = (file: File, componentInfo: ParsedComponentInfo[] | ParsedComponentInfo) => {
const componentList = Array.isArray(componentInfo) ? componentInfo : [componentInfo];
let nextActiveKey = '0';
setFileItems((prev) => {
const existingIndex = prev.findIndex((item) => item.file.name === file.name);
if (existingIndex >= 0) {
nextActiveKey = String(existingIndex);
const nextItems = [...prev];
nextItems[existingIndex] = {
file,
componentInfo: componentList
};
return nextItems;
}
nextActiveKey = String(prev.length);
return [
...prev,
{
file,
componentInfo: componentList
}
];
});
setActiveTabKey(nextActiveKey);
};
const handleFileChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
const selectedFiles = Array.from(event.target.files ?? []);
if (selectedFiles.length === 0) return;
const validFiles = selectedFiles.filter((file) => file.name.match(/\.zip$/i));
const invalidFileCount = selectedFiles.length - validFiles.length;
if (invalidFileCount > 0) {
Message.error('请选择 ZIP 压缩包文件');
}
for (const file of validFiles) {
const parsedComponentInfo = await onFileSelect(file);
if (!parsedComponentInfo) {
continue;
}
updateFileItem(file, parsedComponentInfo);
}
event.target.value = '';
};
const handleSelectFile = () => {
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 (fileItems.length === 0) {
Message.warning('请至少选择一个组件');
return;
}
onOk(fileItems);
};
const handleClose = () => {
setFileItems([]);
setActiveTabKey('0');
onCancel();
};
return (
<Modal
title="导入组件"
visible={visible}
onCancel={handleClose}
style={{ width: 900 }}
footer={
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div>
<Button onClick={handleSelectFile} type="outline" icon={<IconUpload />}>
</Button>
</div>
<div>
<Button onClick={handleClose} style={{ marginRight: 12 }}>
</Button>
<Button
type="primary"
onClick={handleUpload}
disabled={fileItems.length === 0}
loading={loading}
>
</Button>
</div>
</div>
}
>
<input
ref={fileInputRef}
multiple
type="file"
accept=".zip"
style={{ display: 'none' }}
onChange={handleFileChange}
/>
<div className={styles['import-modal-content']}>
{fileItems.length === 0 && (
<div className={styles['empty-state']}>
<p className={styles['empty-text']}>&#34;&#34;</p>
<p className={styles['empty-hint']}> .zip </p>
</div>
)}
{fileItems.length > 0 && (
<Tabs
activeTab={activeTabKey}
onChange={setActiveTabKey}
type="card"
>
{fileItems.map((fileItem, fileIndex) => {
const currentComponent = fileItem.componentInfo[0];
const baseInfo = currentComponent?.baseInfo || {};
const operates = currentComponent?.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 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 className={styles['component-info']}>
{baseInfo.projectId && (
<div className={styles['info-item']}>
<span className={styles['info-label']}>:</span>
<span className={styles['info-value']}>{baseInfo.projectId}</span>
</div>
)}
{baseInfo.codeLanguage && (
<div className={styles['info-item']}>
<span className={styles['info-label']}>:</span>
<span className={styles['info-value']}>{baseInfo.codeLanguage}</span>
</div>
)}
{baseInfo.componentClassify && (
<div className={styles['info-item']}>
<span className={styles['info-label']}>:</span>
<span className={styles['info-value']}>{baseInfo.componentClassify}</span>
</div>
)}
{baseInfo.desc && (
<div className={styles['component-desc']}>
{baseInfo.desc}
</div>
)}
</div>
{/* 接口列表 */}
{operates.length > 0 && (
<div className={styles['operates-section']}>
<div className={styles['section-title']}>
({operates.length})
</div>
<div className={styles['operates-list']}>
{operates.map((operate: any, opIndex: number) => (
<div key={opIndex} 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 && (
<span className={styles['param-info']}>
: {operate.parameters.length}
</span>
)}
{operate.responses && operate.responses.length > 0 && (
<span className={styles['param-info']}>
: {operate.responses.length}
</span>
)}
</div>
</div>
))}
</div>
</div>
)}
</div>
</div>
</div>
</Tabs.TabPane>
);
})}
</Tabs>
)}
</div>
</Modal>
);
};
export default ImportComponentModal;