Merge branch 'refs/heads/master' into production

master
钟良源 4 months ago
commit 715c4c7ba3

@ -58,10 +58,6 @@ module.exports = withLess(
source: '/api/:path*',
destination: `${target}/api/:path*` // api代理
}
// {
// source: '/ws/:path*',
// destination: `${target.replace('http', 'ws')}/WS/:path*` // WebSocket 代理
// }
];
},
pageExtensions: ['tsx']

Binary file not shown.

After

Width:  |  Height:  |  Size: 988 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 945 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 594 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 515 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 837 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 939 B

@ -61,4 +61,14 @@ export function getSceneAndId() {
// 场景管理-获取工程下的所有组件
export function getProjectComp(projectId: string) {
return axios.get(`${urlPrefix}/scenes/projectComp/${projectId}`);
}
// 增加工程复合组件
export function addProjectComp(data: any) {
return axios.post(`${urlPrefix}/scenes/addProjectSubComp`, data);
}
// 增加工程基础组件
export function addProjectBaseComp(data: any) {
return axios.post(`${urlPrefix}/scenes/addProjectComp`, data);
}

@ -0,0 +1,131 @@
import { useState, useEffect, useRef, useCallback } from 'react';
interface WebSocketOptions {
reconnectInterval?: number;
maxReconnectAttempts?: number;
onOpen?: (event: Event) => void;
onClose?: (event: CloseEvent) => void;
onError?: (event: Event) => void;
onMessage?: (event: MessageEvent) => void;
}
interface WebSocketHook {
connect: (url: string) => void;
disconnect: () => void;
sendMessage: (message: string | object) => void;
readyState: number;
isConnected: boolean;
}
const useWebSocket = (options: WebSocketOptions = {}): WebSocketHook => {
const {
reconnectInterval = 3000,
maxReconnectAttempts = 0,
onOpen,
onClose,
onError,
onMessage
} = options;
const [readyState, setReadyState] = useState<number>(WebSocket.CLOSED);
const [isConnected, setIsConnected] = useState<boolean>(false);
const wsRef = useRef<WebSocket | null>(null);
const reconnectAttemptsRef = useRef<number>(0);
const reconnectTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const urlRef = useRef<string>('');
// 清理重连定时器
const clearReconnectTimeout = useCallback(() => {
if (reconnectTimeoutRef.current) {
clearTimeout(reconnectTimeoutRef.current);
reconnectTimeoutRef.current = null;
}
}, []);
// 断开连接
const disconnect = useCallback(() => {
clearReconnectTimeout();
if (wsRef.current) {
wsRef.current.close();
wsRef.current = null;
}
setReadyState(WebSocket.CLOSED);
setIsConnected(false);
}, [clearReconnectTimeout]);
// 发送消息
const sendMessage = useCallback((message: string | object) => {
if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
const messageStr = typeof message === 'string' ? message : JSON.stringify(message);
wsRef.current.send(messageStr);
} else {
console.warn('WebSocket is not connected. Cannot send message.');
}
}, []);
// 连接WebSocket
const connect = useCallback((url: string) => {
// 先断开现有连接
disconnect();
urlRef.current = url;
try {
const ws = new WebSocket(url);
wsRef.current = ws;
ws.onopen = (event) => {
setReadyState(WebSocket.OPEN);
setIsConnected(true);
reconnectAttemptsRef.current = 0;
onOpen?.(event);
};
ws.onclose = (event) => {
setReadyState(WebSocket.CLOSED);
setIsConnected(false);
onClose?.(event);
// 处理重连
if (reconnectAttemptsRef.current < maxReconnectAttempts) {
reconnectAttemptsRef.current++;
reconnectTimeoutRef.current = setTimeout(() => {
connect(url);
}, reconnectInterval);
}
};
ws.onerror = (event) => {
setIsConnected(false);
onError?.(event);
};
ws.onmessage = (event) => {
onMessage?.(event);
};
setReadyState(WebSocket.CONNECTING);
} catch (error) {
console.error('Failed to create WebSocket connection:', error);
setIsConnected(false);
setReadyState(WebSocket.CLOSED);
}
}, [disconnect, maxReconnectAttempts, onOpen, onClose, onError, onMessage, reconnectInterval]);
// 组件卸载时清理
useEffect(() => {
return () => {
disconnect();
};
}, [disconnect]);
return {
connect,
disconnect,
sendMessage,
readyState,
isConnected
};
};
export default useWebSocket;

@ -21,7 +21,7 @@ import { setSessionUserInfo } from '@/utils/auth';
import store from '@/store';
import { updateUserInfo } from '@/store/user';
import { getMyComponents, getPubComponents, getTeamComponents } from '@/api/components';
import { getPublishPage } from '@/api/flow';
import { getPubFlowList, getMyFlowList } from '@/api/flow';
import dayjs from 'dayjs';
@ -65,8 +65,20 @@ export default function MyApp({
{ promise: getMyComponents(), key: 'myLibs' },
{ promise: getPubComponents(), key: 'pubLibs' },
{ promise: getTeamComponents(), key: 'teamLibs' },
{ promise: getPublishPage(), key: 'pubFlow' }
// {promise: appId ? getMineSubs({id: appId}) : Promise.resolve(null), key: 'myFlow'},
{
promise: getPubFlowList({
currPage: 1,
pageSize: 999
}),
key: 'pubFlow'
},
{
promise: getMyFlowList({
currPage: 1,
pageSize: 999
}),
key: 'myFlow'
}
// {promise: getEventList(), key: 'eventList'}
];
@ -75,7 +87,7 @@ export default function MyApp({
pubLibs: null,
teamLibs: null,
pubFlow: null,
// myFlow: null,
myFlow: null,
updateTime: dayjs().format('YYYY-MM-DD HH:mm:ss')
};
@ -85,32 +97,33 @@ export default function MyApp({
for (const { promise, key } of requests) {
try {
const res: any = await promise;
if (res?.code === 200 && res.data?.length > 0) {
// if (key === 'myLibs') {
// addCompInfo(res.data);
// libsStore.setMyLibs(res.data);
// } else if (key === 'pubLibs') {
// addCompInfo(res.data);
// libsStore.setPubLibs(res.data);
// } else if (key === 'teamLibs') {
// addCompInfo(res.data);
// libsStore.setTeamLibs(res.data);
// } else if (key === 'pubFlow') {
// addCompInfo(res.data, true);
// libsStore.setPubFlow(res.data);
// }
// else if (key === 'myFlow') {
// let newData = formatFlowMy(res.data);
// addCompInfo(newData, true);
// libsStore.setMyFlow(newData);
// }
// else if (key === 'eventList') {
// eventStore.setEventList(res.data);
// }
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;
}
}
// 更新本地存储数据
obj[key] = res?.data || null;
sessionStorage.setItem(`compLibs${userInfo.userId}`, JSON.stringify(obj));
} catch (error) {
console.error(`加载${key}失败:`, error);

@ -0,0 +1,56 @@
import React from 'react';
import styles from './style/index.module.less';
import { Button, Input, Radio, Space, Tag, Divider } from '@arco-design/web-react';
import { IconSearch } from '@arco-design/web-react/icon';
import SideBar from '@/pages/componentDevelopment/componentTest/sideBar';
import InstanceList from '@/pages/componentDevelopment/componentTest/instanceList';
const Group = Radio.Group;
const ComponentTest = () => {
return (
<div className={styles['component-test']}>
<div className={styles['header']}>
<Group defaultValue={'Beijing'} name="button-radio-group">
{['总数126', '已测试45', '未测试81'].map((item, index) => {
return (
<Radio key={item} value={item}>
{({ checked }) => {
return (
<Tag size="large" tabIndex={-1} key={item} color={['purple', 'arcoblue', 'orangered'][index]}>
{item}
</Tag>
);
}}
</Radio>
);
})}
</Group>
<Space split={<Divider type="vertical" />}>
<Input
prefix={<IconSearch />}
placeholder={'搜索'}
style={{ width: 236 }}
/>
<Button
type="primary"
style={{ marginLeft: 5, borderRadius: 4 }}
>
</Button>
</Space>
</div>
<div className={styles['content']}>
<div className={styles['left']}>
<SideBar />
</div>
<div className={styles['right']}>
<InstanceList />
</div>
</div>
</div>
);
};
export default ComponentTest;

@ -0,0 +1,11 @@
import React from 'react';
const InstanceList = () => {
return (
<div>
</div>
);
};
export default InstanceList;

@ -0,0 +1,11 @@
import React from 'react';
const SideBar = () => {
return (
<div>
</div>
);
};
export default SideBar;

@ -0,0 +1,25 @@
.component-test {
height: 98%;
background-color: #ffffff;
padding: 17px 19px 0 24px;
.header {
display: flex;
justify-content: space-between;
padding-bottom: 15px;
border-bottom: 1px solid #ebebeb;
margin-bottom: 25px;
}
.content {
display: flex;
.left {
width: 20%;
}
.right {
width: 80%;
}
}
}

@ -1,16 +1,30 @@
import React from 'react';
import { Button } from '@arco-design/web-react';
import { IconSave, IconPlayArrow, IconCodeSquare } from '@arco-design/web-react/icon';
import { Button, Message } from '@arco-design/web-react';
import { IconSave, IconPlayArrow, IconCodeSquare, IconUndo, IconRedo } from '@arco-design/web-react/icon';
import { updateLogBarStatus } from '@/store/ideContainer';
import { useSelector, useDispatch } from 'react-redux';
const ButtonGroup = Button.Group;
interface ActionBarProps {
onSave: () => void;
onSave?: () => void;
onUndo?: () => void;
onRedo?: () => void;
canUndo?: boolean;
canRedo?: boolean;
onRun?: (isRunning: boolean) => void;
isRunning?: boolean;
}
const ActionBar: React.FC<ActionBarProps> = ({ onSave }) => {
const ActionBar: React.FC<ActionBarProps> = ({
onSave,
onUndo,
onRedo,
canUndo = false,
canRedo = false,
onRun,
isRunning = false
}) => {
const { logBarStatus } = useSelector((state) => state.ideContainer);
const dispatch = useDispatch();
@ -18,6 +32,10 @@ const ActionBar: React.FC<ActionBarProps> = ({ onSave }) => {
dispatch(updateLogBarStatus(!logBarStatus));
};
const handleRun = () => {
onRun?.(!isRunning);
};
return (
<div className="action-bar">
<Button onClick={onSave} type="primary" shape="round" icon={<IconSave />}></Button>
@ -26,9 +44,11 @@ const ActionBar: React.FC<ActionBarProps> = ({ onSave }) => {
type="outline"
shape="round"
icon={<IconPlayArrow />}
onClick={() => handleRun()}
style={{ padding: '0 8px', backgroundColor: '#fff' }}
status={isRunning ? 'danger' : undefined}
>
{isRunning ? '停止' : '运行'}
</Button>
<Button
type="outline"
@ -40,6 +60,28 @@ const ActionBar: React.FC<ActionBarProps> = ({ onSave }) => {
</Button>
</ButtonGroup>
<ButtonGroup style={{ marginLeft: 15 }}>
<Button
type="outline"
shape="round"
icon={<IconUndo />}
onClick={onUndo}
disabled={!canUndo}
style={{ padding: '0 8px', backgroundColor: '#fff' }}
>
</Button>
<Button
type="outline"
shape="round"
icon={<IconRedo />}
onClick={onRedo}
disabled={!canRedo}
style={{ padding: '0 8px', backgroundColor: '#fff' }}
>
</Button>
</ButtonGroup>
</div>
);
};

@ -1,11 +1,12 @@
import React from 'react';
import React, { useCallback, useEffect, useState } from 'react';
import { Menu, Tabs } from '@arco-design/web-react';
import { localNodeData } from '@/pages/flowEditor/sideBar/config/localNodeData';
import { useSelector } from 'react-redux';
const TabPane = Tabs.TabPane;
interface AddNodeMenuProps {
onAddNode: (nodeType: string) => void;
onAddNode: (nodeType: string, node: any) => void;
position?: { x: number; y: number }; // 用于画布上下文菜单
edgeId?: string; // 用于边上下文菜单
}
@ -13,24 +14,86 @@ interface AddNodeMenuProps {
const AddNodeMenu: React.FC<AddNodeMenuProps> = ({
onAddNode
}) => {
const { projectComponentData, info } = useSelector((state: any) => state.ideContainer);
const [groupedNodes, setGroupedNodes] = useState<Record<string, any[]>>({});
const [activeTab, setActiveTab] = useState('common');
// 按分组组织节点数据
const groupedNodes = localNodeData.reduce((acc, node) => {
if (!acc[node.nodeGroup]) {
acc[node.nodeGroup] = [];
const formattedNodes = useCallback(() => {
// 先复制本地节点数据
const initialGroupedNodes = localNodeData.reduce((acc, node) => {
if (!acc[node.nodeGroup]) {
acc[node.nodeGroup] = [];
}
acc[node.nodeGroup].push(node);
return acc;
}, {} as Record<string, typeof localNodeData>);
const projectComponent = projectComponentData[info.id];
if (projectComponent) {
const projectComp = projectComponent.projectCompDto;
const projectFlow = projectComponent.projectFlowDto;
const projectCompList = projectComp && Object.values(projectComp).flat(2) || [];
const projectFlowList = projectFlow && Object.values(projectFlow).flat(2) || [];
initialGroupedNodes['application'] = projectCompList.map((v: any) => {
return {
...v,
nodeName: v.name,
nodeType: 'BASIC',
nodeGroup: 'application',
data: {
parameters: {
apiIns: v.def?.apis || [],
apiOuts: Array.isArray(v.def?.apiOut) ? v.def?.apiOut : (v.def?.apiOut ? [v.def?.apiOut] : []),
dataIns: v.def?.dataIns || [],
dataOuts: v.def?.dataOuts || []
}
}
};
});
// groupedNodes['composite'] = projectFlowList.map((v: any) => {
// return {
// ...v,
// nodeName: v.name,
// nodeType: 'BASE',
// nodeGroup: 'application',
// data: {
// parameters: {
// apiIns: v.def.apis,
// apiOuts: v.def.apiOut,
// dataIns: v.def.dataIns,
// dataOuts: v.def.dataOuts
// }
// }
// };
// });
console.log(projectCompList, projectFlowList, initialGroupedNodes);
}
acc[node.nodeGroup].push(node);
return acc;
}, {} as Record<string, typeof localNodeData>);
const handleAddNode = (nodeType: string) => {
onAddNode(nodeType);
// 更新状态以触发重新渲染
setGroupedNodes(initialGroupedNodes);
}, [projectComponentData, info.id]);
const handleAddNode = (nodeType: string, node: any) => {
onAddNode(nodeType, node);
};
// 分组名称映射
const groupNames: Record<string, string> = {
'common': '系统组件',
'application': '应用组件',
'composite': '复合组件'
'application': '基础组件',
'composite': '复合组件',
'common': '系统组件'
};
useEffect(() => {
formattedNodes();
}, [formattedNodes]);
const handleTabChange = (key: string) => {
setActiveTab(key);
};
return (
@ -44,35 +107,38 @@ const AddNodeMenu: React.FC<AddNodeMenuProps> = ({
flexDirection: 'column'
}}
>
<Tabs defaultActiveTab="common" style={{ flex: '0 0 auto' }}>
<Tabs defaultActiveTab="common" style={{ flex: '0 0 auto' }} onChange={handleTabChange}>
{Object.entries(groupedNodes).map(([group, nodes]) => (
<TabPane key={group} title={groupNames[group] || group}>
<div style={{ maxHeight: '300px', overflowY: 'auto' }}>
<Menu
style={{
width: 200,
border: '1px solid #e4e7ed',
borderRadius: 4,
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)'
}}
mode="vertical"
hasCollapseButton={false}
>
{nodes.map((node) => (
<Menu.Item
key={node.nodeType}
onClick={() => handleAddNode(node.nodeType)}
style={{
padding: '0 16px',
height: 36,
lineHeight: '36px'
}}
>
{node.nodeName}
</Menu.Item>
))}
</Menu>
</div>
{/* 只有在当前 tab 激活时才渲染内容 */}
{activeTab === group && (
<div style={{ maxHeight: '300px', overflowY: 'auto' }}>
<Menu
style={{
width: 200,
border: '1px solid #e4e7ed',
borderRadius: 4,
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)'
}}
mode="vertical"
hasCollapseButton={false}
>
{nodes && nodes.map((node) => (
<Menu.Item
key={node.nodeType + (node.id || '')}
onClick={() => handleAddNode(node.nodeType, node)}
style={{
padding: '0 16px',
height: 36,
lineHeight: '36px'
}}
>
{node.nodeName}
</Menu.Item>
))}
</Menu>
</div>
)}
</TabPane>
))}
</Tabs>

@ -0,0 +1,172 @@
import React, { createContext, useContext, useState, useCallback, useRef, useEffect } from 'react';
import { Node, Edge } from '@xyflow/react';
interface HistoryContextType {
undo: () => void;
redo: () => void;
canUndo: boolean;
canRedo: boolean;
takeSnapshot: () => void;
}
const HistoryContext = createContext<HistoryContextType | undefined>(undefined);
export const useHistory = () => {
const context = useContext(HistoryContext);
if (!context) {
throw new Error('useHistory must be used within a HistoryProvider');
}
return context;
};
interface HistoryProviderProps {
children: React.ReactNode;
initialNodes: Node[];
initialEdges: Edge[];
onHistoryChange: (nodes: Node[], edges: Edge[]) => void;
}
export const HistoryProvider: React.FC<HistoryProviderProps> = ({
children,
initialNodes,
initialEdges,
onHistoryChange
}) => {
// 历史记录状态
const [history, setHistory] = useState<{ nodes: Node[]; edges: Edge[] }[]>([
{ nodes: initialNodes, edges: initialEdges }
]);
const [step, setStep] = useState(0);
// 当前状态的引用,避免重复添加相同状态
const currentState = useRef({
nodes: initialNodes,
edges: initialEdges
});
// 检查两个状态是否相等
const isSameState = useCallback((state1: { nodes: Node[]; edges: Edge[] }, state2: { nodes: Node[]; edges: Edge[] }) => {
// 只比较节点和边的关键属性,忽略拖动过程中的临时状态
if (state1.nodes.length !== state2.nodes.length || state1.edges.length !== state2.edges.length) {
return false;
}
// 比较节点
for (let i = 0; i < state1.nodes.length; i++) {
const node1 = state1.nodes[i];
const node2 = state2.nodes[i];
if (node1.id !== node2.id ||
node1.type !== node2.type ||
node1.position.x !== node2.position.x ||
node1.position.y !== node2.position.y ||
JSON.stringify(node1.data) !== JSON.stringify(node2.data)) {
return false;
}
}
// 比较边
for (let i = 0; i < state1.edges.length; i++) {
const edge1 = state1.edges[i];
const edge2 = state2.edges[i];
if (edge1.id !== edge2.id ||
edge1.source !== edge2.source ||
edge1.target !== edge2.target ||
edge1.sourceHandle !== edge2.sourceHandle ||
edge1.targetHandle !== edge2.targetHandle) {
return false;
}
}
return true;
}, []);
// 拍摄快照
const takeSnapshot = useCallback(() => {
// 获取当前状态
const { nodes, edges } = currentState.current;
// 如果当前状态与历史记录中的当前步骤相同,则不添加新快照
const currentHistoryState = history[step];
if (isSameState({ nodes, edges }, currentHistoryState)) {
return;
}
// 删除当前步骤之后的所有历史记录
const newHistory = history.slice(0, step + 1);
// 添加新快照
newHistory.push({
nodes: nodes.map(node => ({...node})),
edges: edges.map(edge => ({...edge}))
});
// 限制历史记录长度,防止内存泄漏
const maxLength = 100;
if (newHistory.length > maxLength) {
newHistory.shift();
setStep(prev => prev - 1);
}
setHistory(newHistory);
setStep(newHistory.length - 1);
}, [history, step, isSameState]);
// 撤销操作
const undo = useCallback(() => {
if (step <= 0) return;
const prevStep = step - 1;
const { nodes, edges } = history[prevStep];
currentState.current = { nodes, edges };
setStep(prevStep);
onHistoryChange([...nodes], [...edges]);
}, [step, history, onHistoryChange]);
// 重做操作
const redo = useCallback(() => {
if (step >= history.length - 1) return;
const nextStep = step + 1;
const { nodes, edges } = history[nextStep];
currentState.current = { nodes, edges };
setStep(nextStep);
onHistoryChange([...nodes], [...edges]);
}, [step, history, onHistoryChange]);
// 更新当前状态的引用
const updateCurrentState = useCallback((nodes: Node[], edges: Edge[]) => {
currentState.current = { nodes, edges };
}, []);
// 监听 takeSnapshot 事件
useEffect(() => {
const handleTakeSnapshot = ((event: CustomEvent) => {
const { nodes, edges } = event.detail;
updateCurrentState(nodes, edges);
takeSnapshot();
}) as EventListener;
document.addEventListener('takeSnapshot', handleTakeSnapshot);
return () => {
document.removeEventListener('takeSnapshot', handleTakeSnapshot);
};
}, [takeSnapshot, updateCurrentState]);
const value = {
undo,
redo,
canUndo: step > 0,
canRedo: step < history.length - 1,
takeSnapshot
};
return (
<HistoryContext.Provider value={value}>
{children}
</HistoryContext.Provider>
);
};

@ -127,7 +127,7 @@ const renderRegularNodeHandles = (dataIns: any[], dataOuts: any[], apiIns: any[]
key={`api-output-handle-${index}`}
type="source"
position={Position.Right}
id={apiOuts[index].name || `output-${index}`}
id={apiOuts[index].name || apiOuts[index].id || `output-${index}`}
style={{
...handleStyles.mainSource,
top: `${35 + index * 20}px`
@ -139,7 +139,7 @@ const renderRegularNodeHandles = (dataIns: any[], dataOuts: any[], apiIns: any[]
key={`api-input-handle-${index}`}
type="target"
position={Position.Left}
id={apiIns[index].name || `input-${index}`}
id={apiIns[index].name || apiIns[index].id || `input-${index}`}
style={{
...handleStyles.mainTarget,
top: `${35 + index * 20}px`
@ -153,7 +153,7 @@ const renderRegularNodeHandles = (dataIns: any[], dataOuts: any[], apiIns: any[]
key={`data-input-handle-${index}`}
type="target"
position={Position.Left}
id={dataIns[index].name || `input-${index}`}
id={dataIns[index].name || dataIns[index].id || `input-${index}`}
style={{
...handleStyles.data,
top: `${70 + (apiIns.length + index) * 20}px`
@ -167,7 +167,7 @@ const renderRegularNodeHandles = (dataIns: any[], dataOuts: any[], apiIns: any[]
key={`data-output-handle-${index}`}
type="source"
position={Position.Right}
id={dataOuts[index].name || `output-${index}`}
id={dataOuts[index].name || dataOuts[index].id || `output-${index}`}
style={{
...handleStyles.data,
top: `${70 + (apiOuts.length + index) * 20}px`
@ -209,6 +209,7 @@ const NodeContent = ({ data }: { data: NodeContentData }) => {
const dataOuts = data.parameters?.dataOuts || [];
const showFooter = data?.component?.customDef || false;
const footerData = (showFooter && data.component) || {};
// console.log(apiIns, apiOuts, dataIns, dataOuts);
// 判断节点类型
const isStartNode = data.type === 'start';
@ -223,7 +224,7 @@ const NodeContent = ({ data }: { data: NodeContentData }) => {
{apiIns.length > 0 && (
<div className={styles['node-inputs']}>
{apiIns.map((input, index) => (
<div key={`input-${index}`} className={styles['node-input-label']}>
<div key={input.id || `input-${index}`} className={styles['node-input-label']}>
{input.desc}
</div>
))}
@ -233,7 +234,7 @@ const NodeContent = ({ data }: { data: NodeContentData }) => {
{apiOuts.length > 0 && (
<div className={styles['node-outputs-api']}>
{apiOuts.map((output, index) => (
<div key={`output-${index}`} className={styles['node-input-label']}>
<div key={output.id || `output-${index}`} className={styles['node-input-label']}>
{output.desc}
</div>
))}
@ -256,7 +257,7 @@ const NodeContent = ({ data }: { data: NodeContentData }) => {
{dataIns.length > 0 && !isStartNode && (
<div className={styles['node-inputs']}>
{dataIns.map((input, index) => (
<div key={`input-${index}`} className={styles['node-input-label']}>
<div key={input.id || `input-${index}`} className={styles['node-input-label']}>
{input.id || `输入${index + 1}`}
</div>
))}
@ -266,7 +267,7 @@ const NodeContent = ({ data }: { data: NodeContentData }) => {
{dataOuts.length > 0 && !isEndNode && (
<div className={styles['node-outputs']}>
{dataOuts.map((output, index) => (
<div key={`output-${index}`} className={styles['node-input-label']}>
<div key={output.id || `output-${index}`} className={styles['node-input-label']}>
{output.id || `输出${index + 1}`}
</div>
))}

@ -5,7 +5,7 @@ import AddNodeMenu from './addNodeMenu';
const SubMenu = Menu.SubMenu;
interface PaneContextMenuProps {
onAddNode: (nodeType: string, position: { x: number; y: number }) => void;
onAddNode: (nodeType: string, position: { x: number; y: number }, node: any) => void;
position: { x: number; y: number };
}
@ -14,8 +14,8 @@ const PaneContextMenu: React.FC<PaneContextMenuProps> = ({
position
}) => {
// 包装onAddNode函数以适配AddNodeMenu组件的接口
const handleAddNode = (nodeType: string) => {
onAddNode(nodeType, position);
const handleAddNode = (nodeType: string, node: any) => {
onAddNode(nodeType, position, node);
};
return (
@ -35,7 +35,9 @@ const PaneContextMenu: React.FC<PaneContextMenuProps> = ({
popupStyle: { height: 'auto', maxHeight: 'none' }
}}
>
<AddNodeMenu onAddNode={handleAddNode} />
<AddNodeMenu onAddNode={(nodeType, node) => {
handleAddNode(nodeType, node);
}} />
</SubMenu>
</Menu>
);

@ -38,7 +38,11 @@ import { defaultNodeTypes } from '@/components/FlowEditor/node/types/defaultType
import { localNodeData } from '@/pages/flowEditor/sideBar/config/localNodeData';
import { useAlignmentGuidelines } from '@/hooks/useAlignmentGuidelines';
import { setMainFlow } from '@/api/appRes';
import { getUserToken } from '@/api/user';
import { Message } from '@arco-design/web-react';
import BasicNode from '@/components/FlowEditor/node/basicNode/BasicNode';
import { HistoryProvider, useHistory } from './components/historyContext';
import useWebSocket from '@/hooks/useWebSocket';
const edgeTypes: EdgeTypes = {
custom: CustomEdge
@ -97,8 +101,8 @@ const FlowEditor: React.FC<{ initialData?: any, useDefault?: boolean }> = ({ ini
const apiOuts = nodeParams.apiOuts || [];
const apiIns = nodeParams.apiIns || [];
if (apiOuts.some((api: any) => api.name === handleId) ||
apiIns.some((api: any) => api.name === handleId)) {
if (apiOuts.some((api: any) => (api.name || api.id) === handleId) ||
apiIns.some((api: any) => (api.name || api.id) === handleId)) {
return 'api';
}
@ -106,8 +110,8 @@ const FlowEditor: React.FC<{ initialData?: any, useDefault?: boolean }> = ({ ini
const dataOuts = nodeParams.dataOuts || [];
const dataIns = nodeParams.dataIns || [];
if (dataOuts.some((data: any) => data.name === handleId) ||
dataIns.some((data: any) => data.name === handleId)) {
if (dataOuts.some((data: any) => (data.name || data.id) === handleId) ||
dataIns.some((data: any) => (data.name || data.id) === handleId)) {
return 'data';
}
@ -161,24 +165,57 @@ const FlowEditor: React.FC<{ initialData?: any, useDefault?: boolean }> = ({ ini
return sourceDataType === targetDataType;
};
// 在组件顶部添加历史记录相关状态
const [historyInitialized, setHistoryInitialized] = useState(false);
const historyTimeoutRef = useRef<NodeJS.Timeout | null>(null);
// 修改 onNodesChange 函数,添加防抖机制
const onNodesChange = useCallback(
(changes: any) => {
const newNodes = applyNodeChanges(changes, nodes);
setNodes(newNodes);
// 如果需要在节点变化时执行某些操作,可以在这里添加
onPaneClick();
// 只有当变化是节点位置变化时才不立即记录历史
const isPositionChange = changes.some((change: any) =>
change.type === 'position' && change.dragging === false
);
// 如果是位置变化结束或者不是位置变化,则记录历史
if (isPositionChange || !changes.some((change: any) => change.type === 'position')) {
// 清除之前的定时器
if (historyTimeoutRef.current) {
clearTimeout(historyTimeoutRef.current);
}
// 设置新的定时器,延迟记录历史记录
historyTimeoutRef.current = setTimeout(() => {
const event = new CustomEvent('takeSnapshot', {
detail: { nodes: [...newNodes], edges: [...edges] }
});
document.dispatchEvent(event);
}, 100);
}
},
[nodes]
[nodes, edges]
);
// 修改 onEdgesChange 函数
const onEdgesChange = useCallback(
(changes: any) => {
const newEdges = applyEdgeChanges(changes, edges);
setEdges(newEdges);
// 如果需要在边变化时执行某些操作,可以在这里添加
onPaneClick();
// 边的变化立即记录历史
const event = new CustomEvent('takeSnapshot', {
detail: { nodes: [...nodes], edges: [...newEdges] }
});
document.dispatchEvent(event);
},
[edges]
[edges, nodes]
);
const onNodesDelete = useCallback((deletedNodes) => {
@ -186,6 +223,7 @@ const FlowEditor: React.FC<{ initialData?: any, useDefault?: boolean }> = ({ ini
closeEditModal();
}, []);
// 修改 onConnect 函数
const onConnect = useCallback(
(params: any) => {
// 获取源节点和目标节点
@ -200,6 +238,7 @@ const FlowEditor: React.FC<{ initialData?: any, useDefault?: boolean }> = ({ ini
// 获取源节点和目标节点的参数信息
const sourceParams = sourceNode.data?.parameters || {};
const targetParams = targetNode.data?.parameters || {};
console.log(sourceParams, targetParams, params);
// 获取源handle和目标handle的类型 (api或data)
const sourceHandleType = getHandleType(params.sourceHandle, sourceParams);
@ -226,7 +265,19 @@ const FlowEditor: React.FC<{ initialData?: any, useDefault?: boolean }> = ({ ini
// };
// 如果验证通过,创建连接
setEdges((edgesSnapshot) => addEdge({ ...params, type: 'custom' }, edgesSnapshot));
setEdges((edgesSnapshot) => {
const newEdges = addEdge({ ...params, type: 'custom' }, edgesSnapshot);
// 连接建立后记录历史
setTimeout(() => {
const event = new CustomEvent('takeSnapshot', {
detail: { nodes: [...nodes], edges: [...newEdges] }
});
document.dispatchEvent(event);
}, 0);
return newEdges;
});
},
[nodes]
);
@ -275,6 +326,7 @@ const FlowEditor: React.FC<{ initialData?: any, useDefault?: boolean }> = ({ ini
}, []);
// 侧边栏节点实例
// 修改 onDrop 函数
const onDrop = useCallback(
(event: React.DragEvent) => {
event.preventDefault();
@ -304,9 +356,21 @@ const FlowEditor: React.FC<{ initialData?: any, useDefault?: boolean }> = ({ ini
// 目前默认添加的都是系统组件/本地组件
if (!nodeMap.includes(nodeData.nodeType)) registerNodeType(nodeData.nodeType, LocalNode, nodeData.nodeName);
setNodes((nds) => nds.concat(newNode));
setNodes((nds) => {
const newNodes = nds.concat(newNode);
// 添加节点后记录历史
setTimeout(() => {
const event = new CustomEvent('takeSnapshot', {
detail: { nodes: [...newNodes], edges: [...edges] }
});
document.dispatchEvent(event);
}, 0);
return newNodes;
});
},
[reactFlowInstance]
[reactFlowInstance, edges]
);
const onNodeDrag = useCallback(
@ -348,6 +412,9 @@ const FlowEditor: React.FC<{ initialData?: any, useDefault?: boolean }> = ({ ini
}));
}
}
// 标记历史记录已初始化
setHistoryInitialized(true);
}, [initialData]);
// 实时更新 canvasDataMap
@ -491,18 +558,40 @@ const FlowEditor: React.FC<{ initialData?: any, useDefault?: boolean }> = ({ ini
// saveFlowDataToServer();
}, [nodes, editingNode, closeEditModal]);
// 删除节点
// 修改删除节点函数
const deleteNode = useCallback((node: Node) => {
setNodes((nds) => nds.filter((n) => n.id !== node.id));
setEdges((eds) => eds.filter((e) => e.source !== node.id && e.target !== node.id));
setMenu(null);
}, []);
// 删除边
// 删除节点后记录历史
setTimeout(() => {
const event = new CustomEvent('takeSnapshot', {
detail: {
nodes: [...nodes.filter((n) => n.id !== node.id)],
edges: [...edges.filter((e) => e.source !== node.id && e.target !== node.id)]
}
});
document.dispatchEvent(event);
}, 0);
}, [nodes, edges]);
// 修改删除边函数
const deleteEdge = useCallback((edge: Edge) => {
setEdges((eds) => eds.filter((e) => e.id !== edge.id));
setMenu(null);
}, []);
// 删除边后记录历史
setTimeout(() => {
const event = new CustomEvent('takeSnapshot', {
detail: {
nodes: [...nodes],
edges: [...edges.filter((e) => e.id !== edge.id)]
}
});
document.dispatchEvent(event);
}, 0);
}, [nodes, edges]);
// 编辑节点
const editNode = useCallback((node: Node) => {
@ -526,11 +615,12 @@ const FlowEditor: React.FC<{ initialData?: any, useDefault?: boolean }> = ({ ini
}, []);
// 在边上添加节点的具体实现
const addNodeOnEdge = useCallback((nodeType: string) => {
// 修改 addNodeOnEdge 函数
const addNodeOnEdge = useCallback((nodeType: string, node: any) => {
if (!edgeForNodeAdd || !reactFlowInstance) return;
// 查找节点定义
const nodeDefinition = localNodeData.find(n => n.nodeType === nodeType);
const nodeDefinition = localNodeData.find(n => n.nodeType === nodeType) || node;
if (!nodeDefinition) return;
// 获取源节点和目标节点
@ -559,7 +649,7 @@ const FlowEditor: React.FC<{ initialData?: any, useDefault?: boolean }> = ({ ini
// 将未定义的节点动态追加进nodeTypes
const nodeMap = Array.from(Object.values(nodeTypeMap).map(key => key));
if (!nodeMap.includes(nodeType)) registerNodeType(nodeType, LocalNode, nodeDefinition.nodeName);
if (!nodeMap.includes(nodeType)) registerNodeType(nodeType, nodeType === 'BASIC' ? BasicNode : LocalNode, nodeDefinition.nodeName);
// 添加新节点
setNodes((nds) => [...nds, newNode]);
@ -568,8 +658,8 @@ const FlowEditor: React.FC<{ initialData?: any, useDefault?: boolean }> = ({ ini
setEdges((eds) => eds.filter(e => e.id !== edgeForNodeAdd.id));
// 创建新边: source -> new node, new node -> target
setEdges((eds) => [
...eds,
const newEdges = [
...edges.filter(e => e.id !== edgeForNodeAdd.id),
{
id: `e${edgeForNodeAdd.source}-${newNode.id}`,
source: edgeForNodeAdd.source,
@ -582,21 +672,35 @@ const FlowEditor: React.FC<{ initialData?: any, useDefault?: boolean }> = ({ ini
target: edgeForNodeAdd.target,
type: 'custom'
}
]);
];
setEdges(newEdges);
// 关闭菜单
setEdgeForNodeAdd(null);
setPositionForNodeAdd(null);
}, [edgeForNodeAdd, nodes, reactFlowInstance]);
// 添加节点后记录历史
setTimeout(() => {
const event = new CustomEvent('takeSnapshot', {
detail: {
nodes: [...nodes, newNode],
edges: [...newEdges]
}
});
document.dispatchEvent(event);
}, 0);
}, [edgeForNodeAdd, nodes, reactFlowInstance, edges]);
// 在画布上添加节点
const addNodeOnPane = useCallback((nodeType: string, position: { x: number; y: number }) => {
// 修改 addNodeOnPane 函数
const addNodeOnPane = useCallback((nodeType: string, position: { x: number; y: number }, node?: any) => {
setMenu(null);
if (!reactFlowInstance) return;
// 查找节点定义
const nodeDefinition = localNodeData.find(n => n.nodeType === nodeType);
const nodeDefinition = localNodeData.find(n => n.nodeType === nodeType) || node;
if (!nodeDefinition) return;
// 创建新节点
@ -614,20 +718,32 @@ const FlowEditor: React.FC<{ initialData?: any, useDefault?: boolean }> = ({ ini
// 将未定义的节点动态追加进nodeTypes
const nodeMap = Array.from(Object.values(nodeTypeMap).map(key => key));
// 目前默认添加的都是系统组件/本地组件
if (!nodeMap.includes(nodeType)) registerNodeType(nodeType, LocalNode, nodeDefinition.nodeName);
if (!nodeMap.includes(nodeType)) registerNodeType(nodeType, nodeType === 'BASIC' ? BasicNode : LocalNode, nodeDefinition.nodeName);
setNodes((nds) => [...nds, newNode]);
}, [reactFlowInstance]);
setNodes((nds) => {
const newNodes = [...nds, newNode];
// 添加节点后记录历史
setTimeout(() => {
const event = new CustomEvent('takeSnapshot', {
detail: { nodes: [...newNodes], edges: [...edges] }
});
document.dispatchEvent(event);
}, 0);
return newNodes;
});
}, [reactFlowInstance, edges]);
// 处理添加节点的统一方法
const handleAddNode = useCallback((nodeType: string) => {
const handleAddNode = useCallback((nodeType: string, node) => {
// 如果是通过边添加节点
if (edgeForNodeAdd) {
addNodeOnEdge(nodeType);
addNodeOnEdge(nodeType, node);
}
// 如果是通过画布添加节点
else if (positionForNodeAdd) {
addNodeOnPane(nodeType, positionForNodeAdd);
addNodeOnPane(nodeType, positionForNodeAdd, node);
}
// 清除状态
@ -655,6 +771,215 @@ const FlowEditor: React.FC<{ initialData?: any, useDefault?: boolean }> = ({ ini
}
}, [nodes, edges]);
// 添加运行状态
const [isRunning, setIsRunning] = useState(false);
// 初始化WebSocket hook
const ws = useWebSocket({
onOpen: () => {
console.log('WebSocket连接已建立');
Message.success('运行已启动');
},
onClose: () => {
console.log('WebSocket连接已关闭');
setIsRunning(false);
Message.info('运行已停止');
},
onError: (event) => {
console.error('WebSocket错误:', event);
setIsRunning(false);
Message.error('运行连接出错');
},
onMessage: (event) => {
console.log('收到WebSocket消息:', event.data);
// 这里可以处理从后端收到的消息,例如日志更新等
}
});
// 修改运行处理函数
const handleRun = useCallback(async (running: boolean) => {
if (running) {
// 启动运行
const res = await getUserToken();
const token = res.data;
const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws';
let wsApi = `${protocol}://${window.location.host}/ws/v1/bpms-runtime`;
if (window.location.host.includes('localhost')) {
// WS_API = `wss://${host}/ws/v1/bpms-runtime`;
wsApi = `ws://api.myserver.com:4121/ws/v1/bpms-runtime`;
}
const uri = `${wsApi}?x-auth0-token=${token}`;
ws.connect(uri);
setIsRunning(true);
}
else {
// 停止运行
ws.disconnect();
setIsRunning(false);
}
}, [initialData?.id, ws]);
if (!historyInitialized) {
return <div>Loading...</div>;
}
return (
<HistoryProvider
initialNodes={nodes}
initialEdges={edges}
onHistoryChange={(newNodes, newEdges) => {
setNodes(newNodes);
setEdges(newEdges);
}}
>
<FlowEditorContent
nodes={nodes}
edges={edges}
setNodes={setNodes}
setEdges={setEdges}
reactFlowInstance={reactFlowInstance}
reactFlowWrapper={reactFlowWrapper}
menu={menu}
setMenu={setMenu}
store={store}
dispatch={dispatch}
editingNode={editingNode}
setEditingNode={setEditingNode}
isEditModalOpen={isEditModalOpen}
setIsEditModalOpen={setIsEditModalOpen}
isDelete={isDelete}
setIsDelete={setIsDelete}
edgeForNodeAdd={edgeForNodeAdd}
setEdgeForNodeAdd={setEdgeForNodeAdd}
positionForNodeAdd={positionForNodeAdd}
setPositionForNodeAdd={setPositionForNodeAdd}
getGuidelines={getGuidelines}
clearGuidelines={clearGuidelines}
AlignmentGuides={AlignmentGuides}
updateCanvasDataMapDebounced={updateCanvasDataMapDebounced}
canvasDataMap={canvasDataMap}
initialData={initialData}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
onReconnect={onReconnect}
onDragOver={onDragOver}
onDrop={onDrop}
onNodeDrag={onNodeDrag}
onNodeDragStop={onNodeDragStop}
onNodeContextMenu={onNodeContextMenu}
onNodeDoubleClick={onNodeDoubleClick}
onEdgeContextMenu={onEdgeContextMenu}
onPaneContextMenu={onPaneContextMenu}
onPaneClick={onPaneClick}
closeEditModal={closeEditModal}
saveNodeEdit={saveNodeEdit}
deleteNode={deleteNode}
deleteEdge={deleteEdge}
editNode={editNode}
editEdge={editEdge}
copyNode={copyNode}
addNodeOnEdge={addNodeOnEdge}
addNodeOnPane={addNodeOnPane}
handleAddNode={handleAddNode}
saveFlowDataToServer={saveFlowDataToServer}
handleRun={handleRun}
isRunning={isRunning}
/>
</HistoryProvider>
);
};
// 创建一个新的组件来包含 ReactFlow 和其他 UI 元素
const FlowEditorContent: React.FC<any> = (props) => {
const {
nodes,
edges,
setNodes,
setEdges,
reactFlowInstance,
reactFlowWrapper,
menu,
setMenu,
editingNode,
setEditingNode,
isEditModalOpen,
setIsEditModalOpen,
isDelete,
setIsDelete,
edgeForNodeAdd,
setEdgeForNodeAdd,
positionForNodeAdd,
setPositionForNodeAdd,
getGuidelines,
clearGuidelines,
AlignmentGuides,
initialData,
onNodesChange,
onEdgesChange,
onConnect,
onReconnect,
onDragOver,
onDrop,
onNodeDrag,
onNodeDragStop,
onNodeContextMenu,
onNodeDoubleClick,
onEdgeContextMenu,
onPaneContextMenu,
onPaneClick,
closeEditModal,
saveNodeEdit,
deleteNode,
deleteEdge,
editNode,
editEdge,
copyNode,
addNodeOnEdge,
addNodeOnPane,
handleAddNode,
saveFlowDataToServer,
handleRun,
isRunning
} = props;
const { undo, redo, canUndo, canRedo } = useHistory();
// 监听键盘事件实现快捷键
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
// Ctrl+Z 撤销
if (e.ctrlKey && e.key === 'z' && !e.shiftKey && canUndo) {
e.preventDefault();
undo();
}
// Ctrl+Shift+Z 重做
if (e.ctrlKey && e.shiftKey && e.key === 'Z' && canRedo) {
e.preventDefault();
redo();
}
// Ctrl+Y 重做
if (e.ctrlKey && e.key === 'y' && canRedo) {
e.preventDefault();
redo();
}
};
document.addEventListener('keydown', handleKeyDown);
return () => {
document.removeEventListener('keydown', handleKeyDown);
};
}, [undo, redo, canUndo, canRedo]);
// 监听节点和边的变化以拍摄快照
useEffect(() => {
// 获取 HistoryProvider 中的 takeSnapshot 方法
const event = new CustomEvent('takeSnapshot', {
detail: { nodes: [...nodes], edges: [...edges] }
});
document.dispatchEvent(event);
}, [nodes, edges]);
return (
<div ref={reactFlowWrapper} style={{ width: '100%', height: '100%', position: 'relative' }}
onContextMenu={(e) => e.preventDefault()}>
@ -666,7 +991,9 @@ const FlowEditor: React.FC<{ initialData?: any, useDefault?: boolean }> = ({ ini
edgeTypes={edgeTypes}
snapToGrid={true}
snapGrid={[2, 2]}
onNodesDelete={onNodesDelete}
onNodesDelete={(deleted) => {
setNodes((nds) => nds.filter((n) => !deleted.find(d => d.id === n.id)));
}}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
@ -709,7 +1036,15 @@ const FlowEditor: React.FC<{ initialData?: any, useDefault?: boolean }> = ({ ini
<Background />
{/*<Controls />*/}
<Panel position="top-left">
<ActionBar onSave={saveFlowDataToServer}></ActionBar>
<ActionBar
onSave={saveFlowDataToServer}
onUndo={undo}
onRedo={redo}
canUndo={canUndo}
canRedo={canRedo}
onRun={handleRun}
isRunning={isRunning}
></ActionBar>
</Panel>
<AlignmentGuides />
</ReactFlow>
@ -767,8 +1102,8 @@ const FlowEditor: React.FC<{ initialData?: any, useDefault?: boolean }> = ({ ini
>
<PaneContextMenu
position={menu.position!}
onAddNode={(nodeType: string, position: { x: number, y: number }) => {
addNodeOnPane(nodeType, position);
onAddNode={(nodeType: string, position: { x: number, y: number }, node) => {
addNodeOnPane(nodeType, position, node);
setMenu(null); // 关闭上下文菜单
}}
/>
@ -797,8 +1132,8 @@ const FlowEditor: React.FC<{ initialData?: any, useDefault?: boolean }> = ({ ini
}}
>
<AddNodeMenu
onAddNode={(nodeType) => {
handleAddNode(nodeType);
onAddNode={(nodeType, node) => {
handleAddNode(nodeType, node);
// 关闭菜单
setEdgeForNodeAdd(null);
setPositionForNodeAdd(null);
@ -808,10 +1143,8 @@ const FlowEditor: React.FC<{ initialData?: any, useDefault?: boolean }> = ({ ini
/>
</div>
)}
</div>
);
};
export default FlowEditorWithProvider;

@ -6,9 +6,10 @@ import LogBar from './logBar';
import RightSideBar from './rightSideBar';
import NavBar, { NavBarRef } from './navBar';
import { Selected } from '@/pages/ideContainer/types';
import { updateInfo } from '@/store/ideContainer';
import { updateInfo, updateProjectComponentData } from '@/store/ideContainer';
import { useDispatch, useSelector } from 'react-redux';
import { getAppListBySceneId } from '@/api/apps';
import { getProjectComp } from '@/api/scene';
import ProjectContainer from '@/pages/orchestration/project';
import ApplicationContainer from '@/pages/orchestration/application';
@ -18,6 +19,8 @@ import AppCompComponent from '@/pages/orchestration/appCompComponent';
import ComponentList from '@/pages/componentDevelopment/componentList';
import ComponentCoding from '@/pages/componentDevelopment/componentCoding';
import ComponentDeployment from '@/pages/componentDevelopment/componentDeployment';
import ComponentTest from '@/pages/componentDevelopment/componentTest';
type UrlParamsOptions = {
identity?: string;
@ -29,7 +32,6 @@ const AppInstanceComponent = () => <div style={{ height: '100%', width: '100%' }
const MyComponents = () => <div style={{ height: '100%', width: '100%' }}></div>;
const MatchingComponents = () => <div style={{ height: '100%', width: '100%' }}></div>;
const ComponentReview = () => <div style={{ height: '100%', width: '100%' }}></div>;
const ComponentTest = () => <div style={{ height: '100%', width: '100%' }}></div>;
const EnvConfig = () => <div style={{ height: '100%', width: '100%' }}></div>;
// 所有可显示的组件路径列表
@ -59,6 +61,13 @@ function IDEContainer() {
if (res.code === 200) setSubMenuData({ 'appList': res.data.list.reverse() });
};
const getProjectCompData = async () => {
const res: any = await getProjectComp(urlParams.id);
if (res.code === 200) {
await dispatch(updateProjectComponentData({ [urlParams.id]: res.data }));
}
};
useEffect(() => {
setUrlParams(getUrlParams(window.location.href) as UrlParamsOptions);
dispatch(updateInfo(getUrlParams(window.location.href) as UrlParamsOptions));
@ -66,7 +75,10 @@ function IDEContainer() {
}, []);
useEffect(() => {
if (urlParams.id) getAppList();
if (urlParams.id) {
getProjectCompData();
getAppList();
}
}, [urlParams.id]);
// 当selected.path变化时添加到已打开的tab集合中
@ -79,15 +91,15 @@ function IDEContainer() {
// 根据选中的菜单项渲染对应组件
const renderContent = () => {
return (
<div style={{ height: '100%', position: 'relative' }}>
<div style={{ height: '100%', width: '100%', position: 'relative', boxSizing: 'border-box' }}>
{ALL_PATHS.map((path, i) => {
// 检查该路径的组件是否已打开
const isOpened = openedTabs.has(path);
const isOpened = openedTabs.has(selected.key);
// 检查是否是当前选中的路径
const isSelected = selected.path === path;
// 只有已打开的组件才渲染通过CSS控制显示/隐藏
return isOpened ? (
// 只有已打开且已经选中的组件才渲染通过CSS控制显示/隐藏
return isOpened && isSelected ? (
<div
key={`${selected.key}-${i}`}
style={{
@ -260,7 +272,8 @@ function IDEContainer() {
</div>
</div>
{['appList', 'appFlow'].includes(selected.parentKey) && <RightSideBar></RightSideBar>}
{['appList', 'appFlow'].includes(selected.parentKey) &&
<RightSideBar updateProjectComp={() => getProjectCompData()}></RightSideBar>}
</div>
</>
);

@ -1,198 +1,433 @@
import React from 'react';
import { Card, Grid, Input, Tag, Typography, Divider, Avatar } from '@arco-design/web-react';
import {
IconSearch,
IconStar,
IconDownload,
IconUser,
IconCalendar,
IconFilter
} from '@arco-design/web-react/icon';
import React, { useEffect, useState, useCallback } 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';
const { Row, Col } = Grid;
const { Title, Text } = Typography;
// 模拟组件数据
const components = [
{
id: 1,
name: '数据表格',
description: '功能强大的数据展示表格,支持排序、筛选、分页等功能',
author: '张三',
avatar: '',
downloads: 12560,
rating: 4.8,
tags: ['数据展示', '表格', '交互'],
category: '数据展示',
updateTime: '2023-10-15'
},
{
id: 2,
name: '表单生成器',
description: '可视化表单构建工具,支持多种表单元素和校验规则',
author: '李四',
avatar: '',
downloads: 8920,
rating: 4.6,
tags: ['表单', '生成器', '可视化'],
category: '表单',
updateTime: '2023-10-18'
},
{
id: 3,
name: '图表组件',
description: '基于ECharts的图表组件库支持多种图表类型',
author: '王五',
avatar: '',
downloads: 15630,
rating: 4.9,
tags: ['图表', '数据可视化', 'ECharts'],
category: '数据可视化',
updateTime: '2023-10-10'
},
{
id: 4,
name: '流程设计器',
description: '可视化流程设计工具,支持拖拽式流程编排',
author: '赵六',
avatar: '',
downloads: 6750,
rating: 4.5,
tags: ['流程', '设计器', '可视化'],
category: '流程',
updateTime: '2023-10-20'
},
{
id: 5,
name: '文件上传',
description: '支持多种文件格式上传,包含进度显示和校验功能',
author: '孙七',
avatar: '',
downloads: 9800,
rating: 4.7,
tags: ['文件', '上传', '工具'],
category: '工具',
updateTime: '2023-10-12'
},
{
id: 6,
name: '通知中心',
description: '系统通知管理组件,支持多种通知类型和样式',
author: '周八',
avatar: '',
downloads: 5420,
rating: 4.3,
tags: ['通知', '消息', '系统'],
category: '反馈',
updateTime: '2023-10-05'
},
];
// 分类数据
const categories = [
{ id: 1, name: '全部组件', count: 24 },
{ id: 2, name: '数据展示', count: 8 },
{ id: 3, name: '表单', count: 6 },
{ id: 4, name: '数据可视化', count: 5 },
{ id: 5, name: '流程', count: 3 },
{ id: 6, name: '工具', count: 7 },
{ id: 7, name: '反馈', count: 4 },
];
const Market: React.FC = () => {
type componentItemType = {
label: string;
children?: any[];
};
interface MarketProps {
updateProjectComp: () => void;
}
const Market: React.FC<MarketProps> = ({ updateProjectComp }) => {
const [compList, setCompList] = useState<any>([]);
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]);
// 渲染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(() => {
// 根据第一层和第二层分类筛选组件
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 && compList.myLibs) {
filteredComponents = [...filteredComponents, ...compList.myLibs];
}
if (conditions.includePubLibs && compList.pubLibs) {
filteredComponents = [...filteredComponents, ...compList.pubLibs];
}
if (conditions.includeTeamLibs && compList.teamLibs) {
filteredComponents = [...filteredComponents, ...compList.teamLibs];
}
// 收集符合条件的复合组件
const flowComponents = [];
if (conditions.includeMyFlow && compList.myFlow) {
flowComponents.push(...compList.myFlow.map(item => ({ ...item, isFlow: true })));
}
if (conditions.includePubFlow && compList.pubFlow) {
flowComponents.push(...compList.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' }}>
</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, firstLevelCategory, secondLevelCategory]);
// 给账号下的组件列表(本地存储中的组件列表)增加一个是否添加至工程的初始状态
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 getCompList = () => {
const userInfo = JSON.parse(sessionStorage.getItem('userInfo') || '{}');
const componentData = JSON.parse(sessionStorage.getItem(`compLibs${userInfo.userId}`));
addInitState(componentData);
};
useEffect(() => {
getCompList();
}, []);
// 监听 Redux 中 projectComponentData 的变化,更新组件列表状态
useEffect(() => {
getCompList();
}, [projectComponentData]);
return (
<div className={styles['market-container']}>
{/* 头部搜索区域 */}
<div className={styles['market-header']}>
<Title heading={4}></Title>
<Text type="secondary">使</Text>
<Title heading={5}></Title>
<div className={styles['search-section']}>
<Input
size="large"
placeholder="搜索组件名称、功能或标签..."
placeholder="搜索组件"
prefix={<IconSearch />}
style={{ width: 400, marginRight: 16 }}
style={{ marginRight: 16 }}
/>
<div className={styles['filter-tags']}>
<Tag icon={<IconFilter />} color="arcoblue"></Tag>
<Button
type="primary"
icon={<IconSync />}
onClick={() => getCompList()}
/>
</div>
</div>
</div>
<Divider style={{ margin: '20px 0' }} />
{/* 分类导航 */}
{/* tag样式选择器 */}
<div className={styles['category-section']}>
<div className={styles['category-list']}>
{categories.map(category => (
<Tag
key={category.id}
className={styles['category-tag']}
size="large"
>
{category.name} <span className={styles['category-count']}>({category.count})</span>
</Tag>
))}
</div>
{renderCategoryTage()}
</div>
{/* 组件列表 */}
<div className={styles['components-section']}>
<Row gutter={20}>
{components.map(component => (
<Col span={12} key={component.id} className={styles['component-col']}>
<Card className={styles['component-card']} hoverable>
<div className={styles['card-header']}>
<div className={styles['component-title']}>
<Title heading={6}>{component.name}</Title>
<Tag color="arcoblue">{component.category}</Tag>
</div>
<Text type="secondary" ellipsis>{component.description}</Text>
</div>
<div className={styles['card-content']}>
<div className={styles['component-meta']}>
<div className={styles['meta-item']}>
<IconUser />
<span>{component.author}</span>
</div>
<div className={styles['meta-item']}>
<IconCalendar />
<span>{component.updateTime}</span>
</div>
</div>
<div className={styles['component-tags']}>
{component.tags.map((tag, index) => (
<Tag key={index} color="gray">{tag}</Tag>
))}
</div>
</div>
<Divider style={{ margin: '20px 0' }} />
<div className={styles['card-footer']}>
<div className={styles['component-stats']}>
<div className={styles['stat-item']}>
<IconDownload />
<span>{component.downloads.toLocaleString()}</span>
</div>
<div className={styles['stat-item']}>
<IconStar />
<span>{component.rating}</span>
</div>
</div>
<div className={styles['component-actions']}>
<Text className={styles['install-btn']}></Text>
</div>
</div>
</Card>
</Col>
))}
</Row>
<div className={styles['component-section']}>
{renderComponentCategory()}
</div>
</div>
);
};
export default Market;
export default Market;

@ -7,7 +7,11 @@ import Market from './market';
const TabPane = Tabs.TabPane;
const RightSideBar: React.FC = () => {
interface RightSideBarProps {
updateProjectComp: () => void;
}
const RightSideBar: React.FC<RightSideBarProps> = ({ updateProjectComp }) => {
const [activeTab, setActiveTab] = useState('1');
const [isExpanded, setIsExpanded] = useState(false);
const resizeBoxRef1 = useRef<HTMLDivElement>(null); // 引用第一个 ResizeBox 容器
@ -25,7 +29,7 @@ const RightSideBar: React.FC = () => {
// 当 isExpanded 或 activeTab 状态改变时,直接更新对应元素的样式
useEffect(() => {
const width = isExpanded ? 350 : 0;
const width = isExpanded ? 550 : 0;
if (activeTab === '1' && resizeBoxRef1.current) {
resizeBoxRef1.current.style.width = `${width}px`;
@ -69,7 +73,7 @@ const RightSideBar: React.FC = () => {
className={styles['right-resize-box']}
directions={['left']}
style={{
width: isExpanded ? 350 : 0,
width: isExpanded ? 550 : 0,
maxWidth: '100%',
minWidth: 0
}}
@ -92,13 +96,13 @@ const RightSideBar: React.FC = () => {
className={styles['right-resize-box']}
directions={['left']}
style={{
width: isExpanded ? 350 : 0,
width: isExpanded ? 550 : 0,
maxWidth: '100%',
minWidth: 0
}}
onMoving={handleResize}
>
<Market></Market>
<Market updateProjectComp={updateProjectComp}></Market>
</ResizeBox>}
</TabPane>
</Tabs>

@ -19,107 +19,44 @@
}
.category-section {
margin: 20px 0;
.category-list {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin-top: 20px;
}
.category-tag {
cursor: pointer;
transition: all 0.2s;
.component-section {
:global(.arco-collapse-item ) {
border-bottom: none;
}
&:hover {
transform: translateY(-2px);
}
:global(.arco-collapse-item-content-box) {
background-color: #fff;
}
.category-count {
color: #86909c;
}
}
:global(.arco-collapse-item-header) {
border-bottom: none;
color: #6D7278;
}
}
.components-section {
.component-col {
margin-bottom: 20px;
:global(.arco-collapse-item-header-right) {
padding-left: 0;
}
.component-card {
height: 100%;
.component-list {
display: flex;
flex-direction: column;
.card-header {
flex: 1;
margin-bottom: 16px;
.component-title {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
}
.card-content {
flex: 1;
margin-bottom: 16px;
.component-meta {
display: flex;
gap: 16px;
margin-bottom: 12px;
.meta-item {
display: flex;
align-items: center;
gap: 4px;
font-size: 12px;
color: #86909c;
}
}
.component-tags {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
}
justify-content: space-between;
align-items: center;
padding-top: 10px;
.card-footer {
.component-info {
display: flex;
justify-content: space-between;
align-items: center;
border-top: 1px solid #f2f3f5;
padding-top: 16px;
.component-stats {
display: flex;
gap: 16px;
.stat-item {
display: flex;
align-items: center;
gap: 4px;
font-size: 12px;
color: #86909c;
}
}
.component-actions {
.install-btn {
color: #626aea;
cursor: pointer;
font-weight: 500;
&:hover {
color: #4169e1;
}
}
img {
width: 40px;
height: 40px;
margin-right: 10px;
}
}
}
}
}

@ -84,7 +84,6 @@ export default function LoginForm() {
className={styles['login-form']}
layout="vertical"
ref={formRef}
initialValues={{ username: 'admin', password: 'admin' }}
>
<Form.Item
field="username"

@ -0,0 +1,78 @@
import React from 'react';
import styles from './style/compInfo.module.less';
import { Space, Divider, Table, TableColumnProps } from '@arco-design/web-react';
const CompInfo = ({ currentCompInfo }) => {
const columns: TableColumnProps[] = [
{
title: '参数名',
dataIndex: 'id'
},
{
title: '参数类型',
dataIndex: 'dataType'
},
{
title: '描述',
dataIndex: 'desc'
}
];
// 渲染节点参数列表
const renderParamsTable = () => {
return (
<div className={styles['params']}>
<h3></h3>
<Table
columns={columns}
data={currentCompInfo.def?.dataIns}
pagination={false}
scroll={{
y: 150
}} />
<h3></h3>
<Table
columns={columns}
data={currentCompInfo.def?.dataOuts}
pagination={false}
scroll={{
y: 150
}} />
</div>
);
};
return (
currentCompInfo ? (<div className={styles['comp-container']}>
<div className={styles['comp-preview']}>
<h3></h3>
<div className={styles['comp-housing']}></div>
</div>
<div className={styles['comp-info']}>
<div className={styles['header']}>
<Space size={40}>
<div className={styles['title']}>{currentCompInfo.name}</div>
</Space>
</div>
<Divider style={{ borderColor: '#5484ff', marginTop: 0, marginBottom: 30 }} />
<div className={styles['extra']}>
<Space size={30}>
<div className={styles['extra-font']}>{currentCompInfo.identifier}</div>
<div className={styles['extra-font']}>{currentCompInfo.componentClassify}</div>
<div className={styles['extra-font']}>{currentCompInfo.codeLanguage}</div>
</Space>
</div>
<Divider style={{ marginTop: 15, borderBottomStyle: 'dashed' }} />
<div className={styles['description']}>
{currentCompInfo.description ? currentCompInfo.description : ' - '}
</div>
<Divider style={{ borderBottomStyle: 'dashed' }} />
{renderParamsTable()}
<Divider style={{ borderBottomStyle: 'dashed' }} />
</div>
</div>) : <div></div>
);
};
export default CompInfo;

@ -1,24 +1,26 @@
import React, { useEffect, useState } from 'react';
import { useSelector } from 'react-redux';
import { getProjectComp } from '@/api/scene';
import styles from './style/index.module.less';
import SideBar from './sideBar';
import CompInfo from './compInfo';
const AppCompComponent = () => {
const [compList, setCompList] = useState([]);
const { info } = useSelector((state: any) => state.ideContainer);
const fetchData = async () => {
const res: any = await getProjectComp(info.id);
console.log('res:', res);
if (res.code === 200) setCompList(res.data);
};
const [currentCompInfo, setCurrentCompInfo] = useState(null);
const { info, projectComponentData } = useSelector((state: any) => state.ideContainer);
useEffect(() => {
fetchData();
setCompList(projectComponentData[info.id]);
}, []);
return (
<div>
<div className={styles['app-comp-component']}>
<div className={styles['left']}>
<SideBar compList={compList} onSelect={(e) => setCurrentCompInfo(e)} />
</div>
<div className={styles['right']}>
<CompInfo currentCompInfo={currentCompInfo} />
</div>
</div>
);
};

@ -0,0 +1,114 @@
import React from 'react';
import styles from './style/sideBar.module.less';
import { Button, Input, Tree } from '@arco-design/web-react';
import { IconSearch } from '@arco-design/web-react/icon';
const TreeNode = Tree.Node;
const SideBar = ({ compList, onSelect }) => {
const renderTreeNode = (menuItems, parentKey = '0') => {
// 标题枚举值
const titleMap = new Map([
['projectCompDto', {
title: '基础组件',
icon: <img src={'/ideContainer/icon/projectComp.png'} style={{ width: 17, height: 17 }} />
}],
['projectFlowDto', {
title: '复合组件',
icon: <img src={'/ideContainer/icon/projectComp.png'} style={{ width: 17, height: 17 }} />
}],
['mineComp', {
title: '我的组件',
icon: <img src={'/ideContainer/icon/mineComp.png'} style={{ width: 17, height: 17 }} />
}],
['pubComp', {
title: '公共组件',
icon: <img src={'/ideContainer/icon/pubComp.png'} style={{ width: 17, height: 17 }} />
}],
['teamWorkComp', {
title: '协助组件',
icon: <img src={'/ideContainer/icon/teamWorkComp.png'} style={{ width: 17, height: 17 }} />
}],
['mineFlow', {
title: '我的组件',
icon: <img src={'/ideContainer/icon/mineComp.png'} style={{ width: 17, height: 17 }} />
}],
['pubFlow', {
title: '公共组件',
icon: <img src={'/ideContainer/icon/pubComp.png'} style={{ width: 17, height: 17 }} />
}]
]);
if (!menuItems) return null;
// 如果是数组,表示是最底层的子节点,直接渲染
if (Array.isArray(menuItems)) {
return menuItems.map((item, index) => {
const treeNodeProps = {
dataRef: item // 传递原始数据
};
return (<TreeNode
{...treeNodeProps}
key={`${parentKey}-${index}`}
title={item.name}
icon={<img src={'/ideContainer/icon/compItem.png'} style={{ width: 17, height: 17 }} />}
/>);
});
}
// 如果是对象,递归渲染子节点
return Object.keys(menuItems).map((key, index) => {
const child = menuItems[key];
const currentKey = `${parentKey}-${index}`;
const title = titleMap.get(key)?.title || key;
const icon = titleMap.get(key)?.icon || null;
// 如果子节点是数组或对象,继续递归
if (Array.isArray(child) || typeof child === 'object') {
return (
<TreeNode key={currentKey} title={title} icon={icon}>
{renderTreeNode(child, currentKey)}
</TreeNode>
);
}
// 否则直接渲染叶子节点
return <TreeNode key={currentKey} title={title} />;
});
};
return (
<div className={styles['side-bar']}>
<div className={styles['handle-box']}>
<Input
prefix={<IconSearch />}
placeholder={'搜索组件'}
style={{ width: '90%' }}
/>
<Button
type="primary"
style={{ marginLeft: 5 }}
>
</Button>
</div>
<div className={styles['comp-list']}>
<Tree
defaultExpandedKeys={['0-0-0']}
defaultSelectedKeys={['0-0-0', '0-0-1']}
selectedKeys={[]} // 移除选中样式
style={{ background: 'transparent' }} // 移除背景色
onSelect={(value, info) => {
if (info.node.props.dataRef.hasOwnProperty('children')) return;
onSelect(info.node?.props?.dataRef || null);
}}
>
{renderTreeNode({ projectCompDto: compList.projectCompDto, projectFlowDto: compList.projectFlowDto })}
</Tree>
</div>
</div>
);
};
export default SideBar;

@ -0,0 +1,59 @@
.comp-container {
box-sizing: border-box;
display: flex;
width: 100%;
height: 100%;
background-color: #fff;
padding: 32px 27px 27px 29px;
.comp-preview {
box-sizing: border-box;
width: 345px;
height: 100%;
margin-right: 68px;
padding: 20px 25px;
border-radius: 8px;
box-shadow: 2px 2px 20px 0 rgba(0, 0, 0, .25);
.comp-housing {
width: 95%;
height: 250px;
margin: 0 auto;
border: 1px solid #CCCCCC;
border-top-left-radius: 6px;
border-top-right-radius: 6px;
}
}
.comp-info {
flex: 1;
.header {
.title {
font-size: 22px;
font-weight: 700;
}
.update-time {
color: #888888;
}
}
.extra {
.extra-font {
font-size: 18px;
font-weight: 700;
}
}
.description {
max-height: 15%;
overflow-y: auto;
}
.params {
box-sizing: border-box;
padding: 10px 0 25px 20px;
background-color: #fbfbfb;
}
}
}

@ -0,0 +1,17 @@
.app-comp-component {
height: 100%;
display: flex;
.left {
width: 314px;
padding: 17px;
background-color: #ffffff;
border-right: 4px solid #E5E6EB;
}
.right {
flex: 1;
background-color: #f7f8fa;
padding: 20px;
}
}

@ -0,0 +1,11 @@
.side-bar {
box-sizing: border-box;
.handle-box {
display: flex;
}
.comp-list {
}
}

@ -5,14 +5,16 @@ interface IDEContainerState {
menuData: any;
flowData: any;
canvasDataMap: any;
projectComponentData: any;
logBarStatus?: boolean;
}
const initialState: IDEContainerState = {
info: {},
menuData: {},
flowData: {},
canvasDataMap: {},
info: {}, // 项目信息
menuData: {}, // 菜单数据
flowData: {}, // 编排数据,即流程图的渲染数据
canvasDataMap: {}, // 每个画布的缓存信息
projectComponentData: {}, // 工程下的组件列表
logBarStatus: false
};
@ -32,6 +34,9 @@ const ideContainerSlice = createSlice({
updateCanvasDataMap(state, action) {
state.canvasDataMap = { ...state.canvasDataMap, ...action.payload };
},
updateProjectComponentData(state, action) {
state.projectComponentData = { ...state.projectComponentData, ...action.payload };
},
updateLogBarStatus(state, action) {
state.logBarStatus = action.payload;
}
@ -43,6 +48,7 @@ export const {
updateMenuData,
updateFlowData,
updateCanvasDataMap,
updateProjectComponentData,
updateLogBarStatus
} = ideContainerSlice.actions;

Loading…
Cancel
Save