|
|
import React, { useEffect, useState, useCallback, useMemo } from 'react';
|
|
|
import { Card, Grid, Input, Tag, Typography, Divider, Collapse, Button, Message } from '@arco-design/web-react';
|
|
|
import { IconSearch, IconSync } from '@arco-design/web-react/icon';
|
|
|
import styles from './style/market.module.less';
|
|
|
import { useSelector, useDispatch } from 'react-redux';
|
|
|
import { addProjectComp, addProjectBaseComp } from '@/api/scene';
|
|
|
import dayjs from 'dayjs';
|
|
|
import { getMyComponents, getPubComponents, getTeamComponents } from '@/api/components';
|
|
|
import { getMyFlowList, getPubFlowList } from '@/api/flow';
|
|
|
|
|
|
const { Row, Col } = Grid;
|
|
|
const { Title, Text } = Typography;
|
|
|
|
|
|
type componentItemType = {
|
|
|
label: string;
|
|
|
children?: any[];
|
|
|
};
|
|
|
|
|
|
interface MarketProps {
|
|
|
updateProjectComp: () => void;
|
|
|
}
|
|
|
|
|
|
const Market: React.FC<MarketProps> = ({ updateProjectComp }) => {
|
|
|
const [compList, setCompList] = useState<any>([]);
|
|
|
const [searchValue, setSearchValue] = useState(''); // 添加搜索状态
|
|
|
const [firstLevelCategory, setFirstLevelCategory] = useState('全部'); // 第一层分类:全部,基础,复合
|
|
|
const [secondLevelCategory, setSecondLevelCategory] = useState('全部'); // 第二层分类:全部,我的,公开,协同
|
|
|
const { projectComponentData, info } = useSelector((state: any) => state.ideContainer);
|
|
|
const dispatch = useDispatch();
|
|
|
|
|
|
// 第一层分类选项
|
|
|
const firstLevelCategories = [
|
|
|
{ label: '全部', value: '全部' },
|
|
|
{ label: '基础', value: '基础' },
|
|
|
{ label: '复合', value: '复合' }
|
|
|
];
|
|
|
|
|
|
// 第二层分类选项
|
|
|
const secondLevelCategories = [
|
|
|
{ label: '全部', value: '全部' },
|
|
|
{ label: '我的', value: '我的' },
|
|
|
{ label: '公开', value: '公开' },
|
|
|
{ label: '协同', value: '协同' }
|
|
|
];
|
|
|
|
|
|
// 计算各分类的数量
|
|
|
const getCategoryCounts = useCallback(() => {
|
|
|
if (!compList) return {
|
|
|
allCount: 0,
|
|
|
basicCount: 0,
|
|
|
compositeCount: 0,
|
|
|
myCount: 0,
|
|
|
publicCount: 0,
|
|
|
teamCount: 0,
|
|
|
myFlowCount: 0,
|
|
|
pubFlowCount: 0
|
|
|
};
|
|
|
|
|
|
// 基础组件数量 (myLibs, pubLibs, teamLibs)
|
|
|
// 需要统计children中的组件数量
|
|
|
const myCount = compList.myLibs ? compList.myLibs.reduce((acc, cur) => acc + (cur.children ? cur.children.length : 0), 0) : 0;
|
|
|
const publicCount = compList.pubLibs ? compList.pubLibs.reduce((acc, cur) => acc + (cur.children ? cur.children.length : 0), 0) : 0;
|
|
|
const teamCount = compList.teamLibs ? compList.teamLibs.reduce((acc, cur) => acc + (cur.children ? cur.children.length : 0), 0) : 0;
|
|
|
const basicCount = myCount + publicCount + teamCount;
|
|
|
|
|
|
// 复合组件数量 (myFlow, pubFlow) - 直接统计数组长度
|
|
|
const myFlowCount = compList.myFlow ? compList.myFlow.length : 0;
|
|
|
const pubFlowCount = compList.pubFlow ? compList.pubFlow.length : 0;
|
|
|
const compositeCount = myFlowCount + pubFlowCount;
|
|
|
|
|
|
return {
|
|
|
allCount: basicCount + compositeCount,
|
|
|
basicCount,
|
|
|
compositeCount,
|
|
|
myCount,
|
|
|
publicCount,
|
|
|
teamCount,
|
|
|
myFlowCount,
|
|
|
pubFlowCount
|
|
|
};
|
|
|
}, [compList]);
|
|
|
|
|
|
// 根据搜索值过滤组件列表
|
|
|
const filteredCompList = useMemo(() => {
|
|
|
if (!searchValue) return compList;
|
|
|
|
|
|
const filtered = { ...compList };
|
|
|
|
|
|
// 过滤基础组件 (myLibs, pubLibs, teamLibs)
|
|
|
['myLibs', 'pubLibs', 'teamLibs'].forEach(key => {
|
|
|
if (filtered[key]) {
|
|
|
filtered[key] = filtered[key].map(category => {
|
|
|
if (category && category.children) {
|
|
|
const filteredChildren = category.children.filter(child => {
|
|
|
const label = child.label || child.name || '';
|
|
|
return label.toLowerCase().includes(searchValue.toLowerCase());
|
|
|
});
|
|
|
return { ...category, children: filteredChildren };
|
|
|
}
|
|
|
return category;
|
|
|
}).filter(category => category.children && category.children.length > 0);
|
|
|
}
|
|
|
});
|
|
|
|
|
|
// 过滤复合组件 (myFlow, pubFlow)
|
|
|
['myFlow', 'pubFlow'].forEach(key => {
|
|
|
if (filtered[key]) {
|
|
|
filtered[key] = filtered[key].filter(item => {
|
|
|
const name = item.flowName || item.name || '';
|
|
|
return name.toLowerCase().includes(searchValue.toLowerCase());
|
|
|
});
|
|
|
}
|
|
|
});
|
|
|
|
|
|
return filtered;
|
|
|
}, [compList, searchValue]);
|
|
|
|
|
|
// 渲染Tag选择器
|
|
|
const renderCategoryTage = useCallback(() => {
|
|
|
/*
|
|
|
* 分类规则:先分类(全部,基础,复合) 再分类(全部,我的,公开,协同:复合类型中没有协同组件)
|
|
|
* 目前数据类型中存在的字段: 基础类型 [myLibs,pubLibs,teamLibs], 复合类型[pubFlow,myFlow]
|
|
|
* 需要渲染出两层Tag标签的选择,第一次的选择影响第二层的部分渲染
|
|
|
* */
|
|
|
|
|
|
const counts = getCategoryCounts();
|
|
|
|
|
|
// 根据第一层分类动态调整第二层分类选项
|
|
|
const getFilteredSecondLevelCategories = () => {
|
|
|
// 如果是复合类型,则不显示协同组件选项
|
|
|
if (firstLevelCategory === '复合') {
|
|
|
return secondLevelCategories.filter(item => item.value !== '协同');
|
|
|
}
|
|
|
return secondLevelCategories;
|
|
|
};
|
|
|
|
|
|
// 获取第二层分类的数量
|
|
|
const getSecondLevelCount = (category: string) => {
|
|
|
if (firstLevelCategory === '全部') {
|
|
|
switch (category) {
|
|
|
case '全部':
|
|
|
return counts.allCount;
|
|
|
case '我的':
|
|
|
return counts.myCount + counts.myFlowCount;
|
|
|
case '公开':
|
|
|
return counts.publicCount + counts.pubFlowCount;
|
|
|
case '协同':
|
|
|
return counts.teamCount;
|
|
|
default:
|
|
|
return 0;
|
|
|
}
|
|
|
}
|
|
|
else if (firstLevelCategory === '基础') {
|
|
|
switch (category) {
|
|
|
case '全部':
|
|
|
return counts.basicCount;
|
|
|
case '我的':
|
|
|
return counts.myCount;
|
|
|
case '公开':
|
|
|
return counts.publicCount;
|
|
|
case '协同':
|
|
|
return counts.teamCount;
|
|
|
default:
|
|
|
return 0;
|
|
|
}
|
|
|
}
|
|
|
else if (firstLevelCategory === '复合') {
|
|
|
switch (category) {
|
|
|
case '全部':
|
|
|
return counts.compositeCount;
|
|
|
case '我的':
|
|
|
return counts.myFlowCount;
|
|
|
case '公开':
|
|
|
return counts.pubFlowCount;
|
|
|
default:
|
|
|
return 0;
|
|
|
}
|
|
|
}
|
|
|
return 0;
|
|
|
};
|
|
|
|
|
|
return (
|
|
|
<div>
|
|
|
{/* 第一层分类标签 */}
|
|
|
<div style={{ marginBottom: '16px' }}>
|
|
|
{firstLevelCategories.map((category) => {
|
|
|
let count = 0;
|
|
|
switch (category.value) {
|
|
|
case '全部':
|
|
|
count = counts.allCount;
|
|
|
break;
|
|
|
case '基础':
|
|
|
count = counts.basicCount;
|
|
|
break;
|
|
|
case '复合':
|
|
|
count = counts.compositeCount;
|
|
|
break;
|
|
|
}
|
|
|
|
|
|
return (
|
|
|
<Tag
|
|
|
size="large"
|
|
|
key={category.value}
|
|
|
color={firstLevelCategory === category.value ? 'arcoblue' : 'gray'}
|
|
|
style={{ cursor: 'pointer', marginRight: '20px', padding: '0 12px' }}
|
|
|
onClick={() => {
|
|
|
setFirstLevelCategory(category.value);
|
|
|
// 当切换第一层分类时,默认选中"全部"作为第二层分类
|
|
|
setSecondLevelCategory('全部');
|
|
|
}}
|
|
|
>
|
|
|
{category.label}
|
|
|
({count})
|
|
|
</Tag>
|
|
|
);
|
|
|
})}
|
|
|
</div>
|
|
|
|
|
|
{/* 第二层分类标签 */}
|
|
|
<div>
|
|
|
{getFilteredSecondLevelCategories().map((category) => (
|
|
|
<Tag
|
|
|
size="large"
|
|
|
key={category.value}
|
|
|
color={secondLevelCategory === category.value ? 'arcoblue' : 'gray'}
|
|
|
style={{ cursor: 'pointer', marginRight: '20px', padding: '0 12px' }}
|
|
|
onClick={() => setSecondLevelCategory(category.value)}
|
|
|
>
|
|
|
{category.label}
|
|
|
({getSecondLevelCount(category.value)})
|
|
|
</Tag>
|
|
|
))}
|
|
|
</div>
|
|
|
</div>
|
|
|
);
|
|
|
}, [firstLevelCategory, secondLevelCategory, getCategoryCounts]);
|
|
|
|
|
|
// 渲染组件分类
|
|
|
const renderComponentCategory = useCallback(() => {
|
|
|
// 使用过滤后的组件列表
|
|
|
const currentCompList = searchValue ? filteredCompList : compList;
|
|
|
|
|
|
// 根据第一层和第二层分类筛选组件
|
|
|
let filteredComponents = [];
|
|
|
|
|
|
// 构建筛选条件
|
|
|
const conditions = {
|
|
|
includeMyLibs: (firstLevelCategory === '全部' || firstLevelCategory === '基础') &&
|
|
|
(secondLevelCategory === '全部' || secondLevelCategory === '我的'),
|
|
|
includePubLibs: (firstLevelCategory === '全部' || firstLevelCategory === '基础') &&
|
|
|
(secondLevelCategory === '全部' || secondLevelCategory === '公开'),
|
|
|
includeTeamLibs: (firstLevelCategory === '全部' || firstLevelCategory === '基础') &&
|
|
|
(secondLevelCategory === '全部' || secondLevelCategory === '协同'),
|
|
|
includeMyFlow: (firstLevelCategory === '全部' || firstLevelCategory === '复合') &&
|
|
|
(secondLevelCategory === '全部' || secondLevelCategory === '我的'),
|
|
|
includePubFlow: (firstLevelCategory === '全部' || firstLevelCategory === '复合') &&
|
|
|
(secondLevelCategory === '全部' || secondLevelCategory === '公开')
|
|
|
};
|
|
|
|
|
|
// 收集符合条件的基础组件
|
|
|
if (conditions.includeMyLibs && currentCompList.myLibs) {
|
|
|
filteredComponents = [...filteredComponents, ...currentCompList.myLibs];
|
|
|
}
|
|
|
if (conditions.includePubLibs && currentCompList.pubLibs) {
|
|
|
filteredComponents = [...filteredComponents, ...currentCompList.pubLibs];
|
|
|
}
|
|
|
if (conditions.includeTeamLibs && currentCompList.teamLibs) {
|
|
|
filteredComponents = [...filteredComponents, ...currentCompList.teamLibs];
|
|
|
}
|
|
|
|
|
|
// 收集符合条件的复合组件
|
|
|
const flowComponents = [];
|
|
|
if (conditions.includeMyFlow && currentCompList.myFlow) {
|
|
|
flowComponents.push(...currentCompList.myFlow.map(item => ({ ...item, isFlow: true })));
|
|
|
}
|
|
|
if (conditions.includePubFlow && currentCompList.pubFlow) {
|
|
|
flowComponents.push(...currentCompList.pubFlow.map(item => ({ ...item, isFlow: true })));
|
|
|
}
|
|
|
|
|
|
// 合并相同标签的组件
|
|
|
const mergedComponents = {};
|
|
|
|
|
|
// 处理基础组件(按label分组)
|
|
|
filteredComponents.forEach(category => {
|
|
|
if (category && category.children && category.children.length > 0) {
|
|
|
const label = category.label;
|
|
|
if (!mergedComponents[label]) {
|
|
|
mergedComponents[label] = {
|
|
|
label: label,
|
|
|
children: []
|
|
|
};
|
|
|
}
|
|
|
mergedComponents[label].children.push(...category.children);
|
|
|
}
|
|
|
});
|
|
|
|
|
|
// 处理复合组件
|
|
|
if (flowComponents.length > 0) {
|
|
|
const label = '复合组件';
|
|
|
if (!mergedComponents[label]) {
|
|
|
mergedComponents[label] = {
|
|
|
label: label,
|
|
|
children: []
|
|
|
};
|
|
|
}
|
|
|
mergedComponents[label].children.push(...flowComponents);
|
|
|
}
|
|
|
|
|
|
// 转换为数组格式
|
|
|
const resultComponents = Object.values(mergedComponents);
|
|
|
|
|
|
// 如果没有组件,显示提示信息
|
|
|
if (resultComponents.length === 0) {
|
|
|
return (
|
|
|
<div style={{ textAlign: 'center', padding: '40px 0', color: '#999' }}>
|
|
|
{searchValue ? '未找到匹配的组件' : '暂无组件数据'}
|
|
|
</div>
|
|
|
);
|
|
|
}
|
|
|
|
|
|
// 将组件添加到工程中
|
|
|
const addToProject = async (component) => {
|
|
|
const params = {
|
|
|
sceneId: info.id,
|
|
|
compIds: []
|
|
|
};
|
|
|
params['compIds'].push(component.id || component.comp.id);
|
|
|
params['type'] = component.fontCompType;
|
|
|
if (component.fontCompType === 'complex') {
|
|
|
const res: any = await addProjectComp(params);
|
|
|
if (res.code === 200) Message.success('添加成功');
|
|
|
else Message.error('添加失败');
|
|
|
}
|
|
|
else {
|
|
|
const res: any = await addProjectBaseComp(params);
|
|
|
if (res.code === 200) Message.success('添加成功');
|
|
|
else Message.error('添加失败');
|
|
|
}
|
|
|
// 通知父组件更新
|
|
|
updateProjectComp();
|
|
|
};
|
|
|
|
|
|
// 渲染组件
|
|
|
return (
|
|
|
<Collapse expandIconPosition={'right'} bordered={false}>
|
|
|
{resultComponents.map((category: componentItemType, index) => {
|
|
|
// 确保category有children属性
|
|
|
if (!category || !category.children || category.children.length === 0) {
|
|
|
return null;
|
|
|
}
|
|
|
|
|
|
return (
|
|
|
<Collapse.Item
|
|
|
key={index}
|
|
|
name={`${category.label}_${index}`}
|
|
|
header={
|
|
|
<div style={{ display: 'flex', alignItems: 'center' }}>
|
|
|
<span
|
|
|
style={{ marginLeft: 10, marginRight: 8, color: '#6D7278', fontSize: 14 }}>{category.label}</span>
|
|
|
</div>
|
|
|
}
|
|
|
>
|
|
|
{category.children.map((component, compIndex) => (
|
|
|
<div key={compIndex}>
|
|
|
<div className={styles['component-list']}>
|
|
|
<div className={styles['component-info']}>
|
|
|
<img src="/icons/compIcon.png" alt="" />
|
|
|
<span>{component.label || component.flowName}</span>
|
|
|
</div>
|
|
|
{/*两种状态 未添加的是primary 已添加的是secondary*/}
|
|
|
{
|
|
|
component.isAdd ? (
|
|
|
<Button type="secondary" style={{ borderRadius: 4 }}
|
|
|
>已添加</Button>
|
|
|
) : (
|
|
|
<Button type="primary" style={{ borderRadius: 4 }}
|
|
|
onClick={() => addToProject(component)}>添加</Button>
|
|
|
)
|
|
|
}
|
|
|
|
|
|
</div>
|
|
|
<Divider />
|
|
|
</div>
|
|
|
))}
|
|
|
</Collapse.Item>
|
|
|
);
|
|
|
}).filter(item => item !== null)}
|
|
|
</Collapse>
|
|
|
);
|
|
|
}, [compList, filteredCompList, firstLevelCategory, secondLevelCategory, searchValue]);
|
|
|
|
|
|
// 给账号下的组件列表(本地存储中的组件列表)增加一个是否添加至工程的初始状态
|
|
|
const addInitState = (componentData) => {
|
|
|
// 当前工程下已添加的组件ID列表
|
|
|
const projectComponent = projectComponentData[info.id];
|
|
|
const compListByProject = projectComponent.compIds.concat(projectComponent.flowIds);
|
|
|
const objectKeys = Object.keys(componentData);
|
|
|
/*
|
|
|
* 账号下的组件列表分两种结构:
|
|
|
* 1. myLibs,pubLibs,teamLibs 这三个是带有children数组的结构
|
|
|
* 2. pubFlow,myFlow 这两个是直接一个数组
|
|
|
* */
|
|
|
objectKeys.forEach(key => {
|
|
|
if (key === 'pubFlow' || key === 'myFlow') {
|
|
|
componentData[key].forEach(item => {
|
|
|
item.isAdd = compListByProject.includes(item.id);
|
|
|
});
|
|
|
}
|
|
|
else if (key === 'myLibs' || key === 'pubLibs' || key === 'teamLibs') {
|
|
|
componentData[key].length && componentData[key].forEach(item => {
|
|
|
item.children.forEach(v => {
|
|
|
v.isAdd = compListByProject.includes(v.comp.id);
|
|
|
});
|
|
|
});
|
|
|
}
|
|
|
});
|
|
|
|
|
|
setCompList(componentData);
|
|
|
};
|
|
|
|
|
|
// 处理搜索输入变化
|
|
|
const handleSearchChange = (value: string) => {
|
|
|
setSearchValue(value);
|
|
|
};
|
|
|
|
|
|
// 从缓存中获取组件列表的信息
|
|
|
const getCompList = async () => {
|
|
|
try {
|
|
|
const requests = [
|
|
|
{ promise: getMyComponents(), key: 'myLibs' },
|
|
|
{ promise: getPubComponents(), key: 'pubLibs' },
|
|
|
{ promise: getTeamComponents(), key: 'teamLibs' },
|
|
|
{
|
|
|
promise: getPubFlowList({
|
|
|
currPage: 1,
|
|
|
pageSize: 999
|
|
|
}),
|
|
|
key: 'pubFlow'
|
|
|
},
|
|
|
{
|
|
|
promise: getMyFlowList({
|
|
|
currPage: 1,
|
|
|
pageSize: 999
|
|
|
}),
|
|
|
key: 'myFlow'
|
|
|
}
|
|
|
];
|
|
|
|
|
|
const obj: any = {
|
|
|
myLibs: null,
|
|
|
pubLibs: null,
|
|
|
teamLibs: null,
|
|
|
pubFlow: null,
|
|
|
myFlow: null,
|
|
|
updateTime: dayjs().format('YYYY-MM-DD HH:mm:ss')
|
|
|
};
|
|
|
|
|
|
const userInfo = JSON.parse(sessionStorage.getItem('userInfo') || '{}');
|
|
|
|
|
|
// 分别处理每个请求
|
|
|
for (const { promise, key } of requests) {
|
|
|
try {
|
|
|
const res: any = await promise;
|
|
|
if (res?.code === 200) {
|
|
|
if (key === 'pubFlow' || key === 'myFlow') {
|
|
|
res?.data.list.forEach(item => {
|
|
|
item['fontCompType'] = 'complex';
|
|
|
});
|
|
|
// 更新本地存储数据
|
|
|
obj[key] = res?.data.list || null;
|
|
|
}
|
|
|
// 给协同组件添加一个后续处理数据时使用的类型
|
|
|
else if (key === 'teamLibs') {
|
|
|
res.data.forEach(item => {
|
|
|
item.children.forEach(v => {
|
|
|
v['fontCompType'] = 'team';
|
|
|
});
|
|
|
});
|
|
|
obj[key] = res?.data || null;
|
|
|
}
|
|
|
else {
|
|
|
// 更新本地存储数据
|
|
|
res.data.forEach(item => {
|
|
|
item.children.forEach(v => {
|
|
|
v['fontCompType'] = 'normal';
|
|
|
});
|
|
|
});
|
|
|
obj[key] = res?.data || null;
|
|
|
}
|
|
|
}
|
|
|
sessionStorage.setItem(`compLibs${userInfo.userId}`, JSON.stringify(obj));
|
|
|
const componentData = JSON.parse(sessionStorage.getItem(`compLibs${userInfo.userId}`));
|
|
|
addInitState(componentData);
|
|
|
} catch (error) {
|
|
|
console.error(`加载${key}失败:`, error);
|
|
|
}
|
|
|
}
|
|
|
} catch (error) {
|
|
|
console.error('更新组件库失败:', error);
|
|
|
}
|
|
|
};
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
getCompList();
|
|
|
}, []);
|
|
|
|
|
|
// 监听 Redux 中 projectComponentData 的变化,更新组件列表状态
|
|
|
useEffect(() => {
|
|
|
getCompList();
|
|
|
}, [projectComponentData]);
|
|
|
|
|
|
return (
|
|
|
<div className={styles['market-container']}>
|
|
|
{/* 头部搜索区域 */}
|
|
|
<div className={styles['market-header']}>
|
|
|
<Title heading={5}>组件仓库</Title>
|
|
|
|
|
|
<div className={styles['search-section']}>
|
|
|
<Input
|
|
|
size="large"
|
|
|
placeholder="搜索组件"
|
|
|
prefix={<IconSearch />}
|
|
|
style={{ marginRight: 16 }}
|
|
|
value={searchValue}
|
|
|
onChange={handleSearchChange}
|
|
|
/>
|
|
|
<div className={styles['filter-tags']}>
|
|
|
<Button
|
|
|
type="primary"
|
|
|
icon={<IconSync />}
|
|
|
onClick={() => getCompList()}
|
|
|
/>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
{/* tag样式选择器 */}
|
|
|
<div className={styles['category-section']}>
|
|
|
{renderCategoryTage()}
|
|
|
</div>
|
|
|
|
|
|
<Divider style={{ margin: '20px 0' }} />
|
|
|
|
|
|
<div className={styles['component-section']}>
|
|
|
{renderComponentCategory()}
|
|
|
</div>
|
|
|
</div>
|
|
|
);
|
|
|
};
|
|
|
|
|
|
export default Market; |