Merge branch 'refs/heads/master' into production
commit
715c4c7ba3
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 |
@ -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;
|
||||
@ -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%;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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 {
|
||||
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue