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.

862 lines
28 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, useEffect, useRef } from 'react';
import styles from './style/index.module.less';
import {
Button,
Divider,
Input,
Space,
Table,
Radio,
Pagination,
Modal,
Message,
Rate,
Tag
} from '@arco-design/web-react';
import { IconSearch } from '@arco-design/web-react/icon';
import {
getMyComponentList,
getCooperationComponentList,
remove,
exportComponent,
getImportComponentInfo,
importComponent
} from '@/api/componentBase';
import { deleteComponentMarket, getComponentHistoryReview } 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 CompAudit from '@/pages/componentDevelopment/componentList/compAudit';
import {
componentStatusConstant,
componentStatusDict,
publicStatusDict,
publishStatusDict
} from '@/const/isdp/componentBase';
import dayjs from 'dayjs';
import { updateComponentCodingPath } from '@/store/ideContainer';
import { useDispatch, useSelector } from 'react-redux';
import { componentOffSale } from '@/api/componentDeploy';
const Group = Radio.Group;
const GlobalVarContainer = () => {
const [selectedItem, setSelectedItem] = useState('我的组件');
const [selectComponent, setSelectComponent] = useState(null);
const [componentData, setComponentData] = useState<ComponentItem[]>([]);
const [pagination, setPagination] = useState({
total: 0,
size: 10,
current: 1
});
const [loading, setLoading] = useState(false);
const [visible, setVisible] = useState(false);
const [publishModalVisible, setPublishModalVisible] = useState(false);
const [selectedPublishComponent, setSelectedPublishComponent] = useState(null);
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 [showOffSaleModal, setShowOffSaleModal] = useState(false);
const [offSaleComponent, setOffSaleComponent] = useState(null);
// 审核历史弹窗状态
const [historyModalVisible, setHistoryModalVisible] = useState(false);
const [historyLoading, setHistoryLoading] = useState(false);
const [historyData, setHistoryData] = useState([]);
const [currentReviewComponent, setCurrentReviewComponent] = useState(null);
const dispatch = useDispatch();
const skipNextFetchRef = useRef(false); // 用于跳过下一次由分页变化触发的请求
// 获取用户信息,判断是否为超管
const userInfo = useSelector((state: any) => state.user?.userInfo);
// TODO: 等后端提供角色标识字段后,替换这里的判断逻辑
const isAdmin = userInfo?.role === 'admin' || userInfo?.isAdmin === true;
const menuItems = [
{
key: '1',
label: '我的组件',
icon: '/ideContainer/icon/myComp.png',
activeIcon: '/ideContainer/icon/myComp_active.png'
},
{
key: '2',
label: '协同组件',
icon: '/ideContainer/icon/teamComp.png',
activeIcon: '/ideContainer/icon/teamComp_active.png'
},
{
key: '3',
label: '组件审核',
icon: '/ideContainer/icon/compAudit.png',
activeIcon: '/ideContainer/icon/compAudit_active.png'
}
];
const columns = [
{
title: '组件名称',
dataIndex: 'name'
},
{
title: '组件标识',
dataIndex: 'projectId'
},
{
title: '分类',
dataIndex: 'componentClassify'
},
{
title: '创建人',
dataIndex: 'createUserName'
},
{
title: '代码语言',
dataIndex: 'codeLanguage'
},
{
title: '组件类型',
dataIndex: 'type',
render: (_, record) => {
return (
<span>{record.type === 'normal' ? '普通组件' : '监听组件'}</span>
);
}
},
{
title: '组件状态',
dataIndex: 'componentStatus',
render: (_, record) => {
const formattedData = componentStatusDict.find(item => item.value === componentStatusConstant[record.componentStatus]) || { label: '' };
return (
<span>{formattedData.label}</span>
);
}
},
{
title: '发布状态',
dataIndex: 'publicStatus',
render: (_, record) => {
const formattedData = publicStatusDict.find(item => item.value === record.publicStatus) || { label: '' };
return (
<span>{formattedData.label}</span>
);
}
},
{
title: '公开状态',
dataIndex: 'publishStatus',
render: (_, record) => {
const formattedData = publishStatusDict.find(item => item.value === record.publishStatus) || { label: '' };
return (
<span>{formattedData.label}</span>
);
}
},
{
title: '修改时间',
dataIndex: 'updateTime',
render: (_, record) => {
return (
<span>{dayjs(record.updateTime).format('YYYY-MM-DD HH:mm:ss')}</span>
);
}
},
{
title: '操作',
dataIndex: 'operations',
fixed: 'right' as const,
width: 400,
render: (_, record, index) => (
<HandleButtonGroup
row={record}
index={index}
onHandlePublishComponent={(row) => {
setSelectedPublishComponent(row);
setPublishModalVisible(true);
}}
onGoToReview={(row) => {
if (isAdmin) {
// 超管:切换到组件审核页面
setSelectedItem('组件审核');
}
else {
// 普通用户:弹出审核历史弹窗
handleShowReviewHistory(row);
}
}}
onPublishOrRevokeComponent={async (action, identifier, version) => {
try {
if (action === 'publish') {
await componentRelease({ identifier, version });
Message.success('组件公开成功');
}
else if (action === 'revoke') {
await componentRevoke({ identifier, version });
Message.success('组件撤销成功');
}
// 重新获取数据以更新状态
fetchComponentData();
} catch (error) {
console.error('操作失败:', error);
Message.error(`组件${action === 'publish' ? '公开' : '撤销'}失败: ${error.message || ''}`);
}
}}
onSourceCodeView={(row) => {
dispatch(updateComponentCodingPath({
localProjectPath: row.localProjectPath,
name: row.name,
projectId: row.projectId,
id: row.id
}));
const event = new CustomEvent('navigateToTab', {
detail: {
path: 'componentCoding'
}
});
document.dispatchEvent(event);
}}
onShowEdit={(row, index) => {
setSelectComponent(row);
setVisible(true);
setMode('edit'); // 设置模式为复制
}}
onCopyHandler={(row) => {
setSelectComponent(row);
setVisible(true);
setMode('copy'); // 设置模式为复制
}}
onShareCollaboration={(row) => {
// TODO: 实现分享协作逻辑
console.log('Share collaboration', row);
}}
onExportComponent={(row) => {
// 实现导出组件逻辑
onExportComponent(row);
}}
onStopComponentShow={(row) => {
setOffSaleComponent(row);
setShowOffSaleModal(true);
}}
onCancelPublish={handleCancelPublish}
onRowDel={(row) => {
// 显示确认框
Modal.confirm({
title: '确认删除',
content: `确定要删除组件 "${row.name}" 吗?此操作不可恢复。`,
okButtonProps: { status: 'danger' },
onOk: async () => {
try {
const res = await remove(row.id);
console.log('res:', res);
Message.success('组件删除成功');
// 重新获取数据
fetchComponentData();
} catch (error) {
console.error('删除组件失败:', error);
Message.error('组件删除失败');
}
}
});
}}
/>
)
}
];
useEffect(() => {
if (selectedItem === '我的组件' || selectedItem === '协同组件') {
if (pagination.current !== 1) {
setPagination(prev => ({
...prev,
current: 1
}));
}
else {
// 第一页j就直接请求数据
fetchComponentData({ current: 1, size: pagination.size });
}
}
}, [selectedItem]);
// 监听来自其他页面的搜索关键词
useEffect(() => {
const handleNavigateWithSearch = (event: any) => {
const { searchKeyword } = event.detail || {};
if (searchKeyword) {
setSearchValue(searchKeyword);
// 标记跳过下一次由分页变化触发的请求
skipNextFetchRef.current = true;
setPagination(prev => ({
...prev,
current: 1
}));
setTimeout(() => {
fetchComponentData({ current: 1, size: pagination.size, name: searchKeyword });
}, 500);
}
};
// 监听导航事件(如果有搜索关键词)
window.addEventListener('componentListSearch', handleNavigateWithSearch);
return () => {
window.removeEventListener('componentListSearch', handleNavigateWithSearch);
};
}, [pagination.size]);
// 下架组件
const componentOffSaleHandler = async (stopInstance: boolean) => {
const res: any = await componentOffSale({ identifier: offSaleComponent.identifier, stopInstance });
if (res.code === 200) {
setShowOffSaleModal(false);
setOffSaleComponent(null);
fetchComponentData();
}
else {
Modal.error({
title: '下架失败',
content: res.message
});
}
};
// 取消发布组件
const handleCancelPublish = async (row: ComponentItem) => {
Modal.confirm({
title: '确认取消发布',
content: `确定要取消发布组件 "${row.name}" 吗?取消后该组件将从组件市场下架。`,
okButtonProps: { status: 'warning' },
onOk: async () => {
try {
const res: any = await deleteComponentMarket(row.id);
if (res.code === 200) {
Message.success('取消发布成功');
// 重新获取数据以更新状态
fetchComponentData();
}
else {
Message.error(res.message || '取消发布失败');
}
} catch (error) {
console.error('取消发布失败:', error);
Message.error('取消发布失败');
}
}
});
};
// 处理导入组件 - 文件选择后获取组件信息
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) {
Message.error('解析组件信息失败');
setImportComponentInfo(null);
} finally {
setImportLoading(false);
}
};
// 处理导入组件 - 确认上传
const handleImportConfirm = async (files) => {
try {
setImportLoading(true);
const params = {
file: files.map(file => file.file)
};
const res: any = await importComponent(params);
console.log('res:', res);
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 handleShowReviewHistory = async (row) => {
setCurrentReviewComponent(row);
setHistoryModalVisible(true);
setHistoryLoading(true);
try {
const res: any = await getComponentHistoryReview({
identifier: row.identifier
});
if (res.code === 200 && res.data) {
setHistoryData(res.data.list || []);
}
else {
Message.error(res.msg || '获取审核历史失败');
}
} catch (error) {
console.error('获取审核历史失败:', error);
Message.error('获取审核历史失败');
} finally {
setHistoryLoading(false);
}
};
// 获取组件列表数据,支持传入额外参数
const fetchComponentData = async (extraParams: any = {}) => {
setLoading(true);
const apiMap = {
'我的组件': getMyComponentList,
'协同组件': getCooperationComponentList
};
try {
const params: any = {
current: extraParams.current ?? pagination.current,
size: extraParams.size ?? pagination.size
};
const searchName = extraParams.name !== undefined ? extraParams.name : searchValue.trim();
if (searchName) {
params.name = searchName;
}
const status = extraParams.componentStatus !== undefined ? extraParams.componentStatus : componentStatus;
if (status) {
params.componentStatus = status.toUpperCase();
}
const res: any = await apiMap[selectedItem](params);
setComponentData(res.data.records || res.data.list);
setPagination(prev => ({
...prev,
total: res.data.total || res.data.totalCount
}));
} catch (error) {
console.error('获取组件列表失败:', error);
Message.error('获取组件列表失败');
} finally {
setLoading(false);
}
};
const handlePageChange = (page: number, size?: number) => {
const newSize = size || pagination.size;
setPagination(prev => ({
...prev,
current: page,
size: newSize
}));
};
// 监听分页变化
useEffect(() => {
if (skipNextFetchRef.current) {
skipNextFetchRef.current = false;
return;
}
if (selectedItem === '我的组件' || selectedItem === '协同组件') {
fetchComponentData({ current: pagination.current, size: pagination.size });
}
}, [pagination.current, pagination.size, componentStatus]);
// 搜索处理函数
const searchHandle = () => {
if (pagination.current !== 1) {
setPagination(prev => ({
...prev,
current: 1
}));
}
else {
fetchComponentData({ current: 1, size: pagination.size });
}
};
// 重置搜索
const resetSearch = () => {
setSearchValue('');
setComponentStatus('');
setPagination(prev => ({
...prev,
current: 1
}));
fetchComponentData({ current: 1, size: pagination.size, name: '', componentStatus: '' });
};
// 修改导出组件的回调函数
const onExportComponent = async (row) => {
/*
* 特殊说明, next.js 使用rewrites 代理和静态导出会导致代理异常这个时候使用dev运行会出现二进制接口返回异常的问题
* 导致无法正确处理二进制数据并把二进制数据转换成字符串,导致导出功能异常。这里需要使用 pnpm run build 和pnpm run start 运行项目。
* */
try {
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);
Message.error('组件导出失败');
}
};
// 组件状态筛选处理函数
const handleStatusChange = (value) => {
setComponentStatus(value);
};
return (
<>
<div className={styles['comp-list-container']}>
{/*左侧菜单*/}
<div className={styles['comp-list-menu']}>
{menuItems.map((item, index) => (
<div
key={index}
className={selectedItem === item.label ? styles['selected'] : ''}
onClick={() => setSelectedItem(item.label)}
>
<img src={selectedItem === item.label ? item.activeIcon : item.icon} alt=""
style={{ width: 20, height: 20, marginRight: 12 }} />
<span>{item.label}</span>
</div>
))}
</div>
<div className={styles['comp-list-content']}>
{selectedItem !== '组件审核' && (
<>
{/*头部*/}
<div className={styles['comp-list-header']}>
<div className={styles['comp-list-search']}>
<Space>
<Input
prefix={<IconSearch />}
placeholder={'输入组件名称搜索'}
style={{ width: 236 }}
value={searchValue}
onChange={(value) => setSearchValue(value)}
onPressEnter={searchHandle}
/>
<Button
type="primary"
style={{ marginLeft: 5, borderRadius: 4 }}
onClick={searchHandle}
>
</Button>
<Button
type="outline"
style={{ marginLeft: 5, borderRadius: 4 }}
onClick={resetSearch}
>
</Button>
</Space>
</div>
<div className={styles['comp-list-handle']}>
<Radio.Group
defaultValue={''}
name="button-radio-group"
value={componentStatus}
onChange={handleStatusChange}
style={{ marginRight: 30 }}
>
{[{ label: '全部', value: '' }, ...componentStatusDict].map((item) => {
return (
<Radio key={item.value} value={item.value}>
{({ checked }) => {
return (
<Button
tabIndex={-1}
key={item.value}
shape="round"
type={checked ? 'primary' : 'default'}
>
{item.label}
</Button>
);
}}
</Radio>
);
})}
</Radio.Group>
{selectedItem === '我的组件' && <Space split={<Divider type="vertical" />}>
{/*<Button type="secondary" status="success" 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);
}}>+
</Button>
</Space>}
</div>
</div>
{/*数据列表*/}
<div className={styles['comp-list-list']}>
<Table
columns={columns}
data={componentData}
loading={loading}
pagination={false}
scroll={{ x: 1500, y: 'calc(100vh - 280px)' }}
border={{
wrapper: true,
cell: true
}}
/>
<div className={styles['pagination-container']}>
<Pagination
current={pagination.current}
pageSize={pagination.size}
total={pagination.total}
onChange={handlePageChange}
/>
</div>
</div>
</>
)}
{selectedItem === '组件审核' && (
<CompAudit />
)}
</div>
</div>
{/*新增弹窗*/}
<AddComponentModal
visible={visible}
baseInfo={selectComponent}
setVisible={(visible) => {
setVisible(visible);
// 当关闭模态框时重置模式为create
if (!visible) {
setMode('create');
}
}}
onReFresh={fetchComponentData}
mode={mode} //
/>
{/*发布组件弹窗*/}
<PublishComponentModal
visible={publishModalVisible}
componentInfo={selectedPublishComponent}
onCancel={() => {
setPublishModalVisible(false);
setSelectedPublishComponent(null);
}}
onSuccess={() => {
fetchComponentData();
}}
/>
{/*导入组件弹窗*/}
<ImportComponentModal
visible={importModalVisible}
onCancel={handleImportCancel}
onOk={handleImportConfirm}
onFileSelect={handleImportFileSelect}
componentInfo={importComponentInfo}
loading={importLoading}
/>
{/* 审核历史弹窗 */}
<Modal
title={`审核历史 - ${currentReviewComponent?.name || ''}`}
visible={historyModalVisible}
onCancel={() => setHistoryModalVisible(false)}
footer={null}
style={{ width: '90%', maxWidth: 1400, minWidth: 800 }}
>
<div style={{ minHeight: 400 }}>
<Table
columns={[
{
title: '组件名称',
dataIndex: 'name',
width: 150,
render: (_, record: any) => record.main?.name || '-'
},
{
title: '分类',
dataIndex: 'componentClassify',
width: 120,
render: (_, record: any) => record.main?.componentClassify || '-'
},
{
title: '组件标识',
dataIndex: 'identifier',
width: 180,
render: (_, record: any) => record.main?.identifier || '-'
},
{
title: '组件版本',
dataIndex: 'componentVersion',
width: 100,
render: (_, record: any) => record.main?.componentVersion ? `v${record.main.componentVersion}` : '-'
},
{
title: '发布状态',
dataIndex: 'publicStatus',
width: 120,
render: (_, record: any) => {
const status = record.main?.publicStatus;
const statusMap = {
'-1': { text: '未发布', color: 'orange' },
0: { text: '未发布', color: 'orange' },
1: { text: '已发布', color: 'green' },
2: { text: '审核中', color: 'arcoblue' },
3: { text: '审核被拒', color: 'red' }
};
const statusInfo = statusMap[status] || { text: '未知', color: 'gray' };
return <Tag color={statusInfo.color}>{statusInfo.text}</Tag>;
}
},
{
title: '组件评分',
dataIndex: 'stars',
width: 200,
render: (_, record: any) => {
const stars = record.main?.stars;
if (stars < 0) return <span style={{ color: '#999' }}></span>;
return <Rate disabled allowHalf value={stars} />;
}
},
{
title: '审核人',
dataIndex: 'reviewUserName',
width: 120,
render: (_, record: any) => record.main?.reviewUserName || '-'
},
{
title: '审核结果',
dataIndex: 'reviewOpinion',
render: (_, record: any) => record.main?.reviewOpinion || '-'
}
]}
data={historyData}
loading={historyLoading}
pagination={false}
scroll={{ x: 1600, y: 500 }}
border={{
wrapper: true,
cell: true
}}
/>
</div>
</Modal>
<Modal
title={'下架组件'}
visible={showOffSaleModal}
style={{ width: '45%' }}
onCancel={() => {
setSelectedItem(null);
setShowOffSaleModal(false);
}}
footer={[
<Button key="cancel" onClick={() => setShowOffSaleModal(false)}>
</Button>,
<Button key="offshelf" type="primary" onClick={() => {
componentOffSaleHandler(false);
}}>
</Button>,
<Button key="stopAndOffshelf" type="primary" status="danger" onClick={() => {
componentOffSaleHandler(true);
}}>
线
</Button>
]}
>
<p></p>
<p> 线使线</p>
</Modal>
</>
);
};
export default GlobalVarContainer;