mirror of
https://github.com/whyour/qinglong.git
synced 2025-12-14 16:05:39 +08:00
Refactor: Replace custom toolbar with Flowgram-style tools component
Following Flowgram demo structure: - Created tools component directory with index, styles, zoom-select, add-node-dropdown - Implemented FloatingTools positioned at bottom-left (like Flowgram demo) - Added undo/redo buttons with history integration - Added zoom selector dropdown (50%, 100%, 150%, 200%) - Added fit-view button for canvas fitting - Added Add Node dropdown with all node types - Updated FlowgramEditor to use new tools component - Removed old toolbar from editor - Added 6 new translation keys (zh-CN + en-US) - Following exact Flowgram UI patterns with styled-components Co-authored-by: whyour <22700758+whyour@users.noreply.github.com>
This commit is contained in:
parent
b356069e6c
commit
54e2468c7a
|
|
@ -617,5 +617,11 @@
|
||||||
"放大": "Zoom In",
|
"放大": "Zoom In",
|
||||||
"缩小": "Zoom Out",
|
"缩小": "Zoom Out",
|
||||||
"适应画布": "Fit to Canvas",
|
"适应画布": "Fit to Canvas",
|
||||||
"条件": "Condition"
|
"条件": "Condition",
|
||||||
|
"scenario_add_node": "Add Node",
|
||||||
|
"scenario_http_node": "HTTP Request",
|
||||||
|
"scenario_script_node": "Script Execution",
|
||||||
|
"scenario_condition_node": "Condition",
|
||||||
|
"scenario_delay_node": "Delay",
|
||||||
|
"scenario_loop_node": "Loop"
|
||||||
}
|
}
|
||||||
|
|
@ -617,5 +617,11 @@
|
||||||
"放大": "放大",
|
"放大": "放大",
|
||||||
"缩小": "缩小",
|
"缩小": "缩小",
|
||||||
"适应画布": "适应画布",
|
"适应画布": "适应画布",
|
||||||
"条件": "条件"
|
"条件": "条件",
|
||||||
|
"scenario_add_node": "添加节点",
|
||||||
|
"scenario_http_node": "HTTP 请求",
|
||||||
|
"scenario_script_node": "脚本执行",
|
||||||
|
"scenario_condition_node": "条件判断",
|
||||||
|
"scenario_delay_node": "延迟",
|
||||||
|
"scenario_loop_node": "循环"
|
||||||
}
|
}
|
||||||
|
|
@ -1,22 +1,10 @@
|
||||||
import React, { useEffect, useImperativeHandle, forwardRef } from 'react';
|
import React, { useEffect, useImperativeHandle, forwardRef } from 'react';
|
||||||
import { Button, Tooltip } from 'antd';
|
|
||||||
import {
|
|
||||||
PlusOutlined,
|
|
||||||
ZoomInOutlined,
|
|
||||||
ZoomOutOutlined,
|
|
||||||
FullscreenOutlined,
|
|
||||||
ApiOutlined,
|
|
||||||
CodeOutlined,
|
|
||||||
BranchesOutlined,
|
|
||||||
ClockCircleOutlined,
|
|
||||||
SyncOutlined,
|
|
||||||
} from '@ant-design/icons';
|
|
||||||
import { EditorRenderer, FreeLayoutEditorProvider } from '@flowgram.ai/free-layout-editor';
|
import { EditorRenderer, FreeLayoutEditorProvider } from '@flowgram.ai/free-layout-editor';
|
||||||
import { DockedPanelLayer } from '@flowgram.ai/panel-manager-plugin';
|
import { DockedPanelLayer } from '@flowgram.ai/panel-manager-plugin';
|
||||||
import '@flowgram.ai/free-layout-editor/index.css';
|
import '@flowgram.ai/free-layout-editor/index.css';
|
||||||
import intl from 'react-intl-universal';
|
|
||||||
import { nodeRegistries } from './nodes';
|
import { nodeRegistries } from './nodes';
|
||||||
import { useEditorProps } from './hooks/use-editor-props';
|
import { useEditorProps } from './hooks/use-editor-props';
|
||||||
|
import { FlowgramTools } from './components/tools';
|
||||||
import './editor.less';
|
import './editor.less';
|
||||||
|
|
||||||
export interface FlowgramEditorProps {
|
export interface FlowgramEditorProps {
|
||||||
|
|
@ -53,96 +41,13 @@ const FlowgramEditor = forwardRef<FlowgramEditorRef, FlowgramEditorProps>(
|
||||||
}
|
}
|
||||||
}, [onChange]);
|
}, [onChange]);
|
||||||
|
|
||||||
const handleAddNode = (nodeType: string) => {
|
|
||||||
// This will be implemented to add nodes via Flowgram API
|
|
||||||
console.log('Add node:', nodeType);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleZoom = (direction: 'in' | 'out' | 'fit') => {
|
|
||||||
// This will be implemented to control zoom via Flowgram API
|
|
||||||
console.log('Zoom:', direction);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flowgram-editor-container">
|
<div className="flowgram-editor-container">
|
||||||
<FreeLayoutEditorProvider {...editorProps}>
|
<FreeLayoutEditorProvider {...editorProps}>
|
||||||
<div className="flowgram-editor-toolbar">
|
|
||||||
<div className="toolbar-group">
|
|
||||||
<span className="toolbar-label">{intl.get('新建节点')}:</span>
|
|
||||||
<Tooltip title={intl.get('HTTP请求')}>
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
icon={<ApiOutlined />}
|
|
||||||
onClick={() => handleAddNode('http')}
|
|
||||||
>
|
|
||||||
HTTP
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip title={intl.get('脚本执行')}>
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
icon={<CodeOutlined />}
|
|
||||||
onClick={() => handleAddNode('script')}
|
|
||||||
>
|
|
||||||
{intl.get('脚本')}
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip title={intl.get('条件判断')}>
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
icon={<BranchesOutlined />}
|
|
||||||
onClick={() => handleAddNode('condition')}
|
|
||||||
>
|
|
||||||
{intl.get('条件')}
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip title={intl.get('延迟')}>
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
icon={<ClockCircleOutlined />}
|
|
||||||
onClick={() => handleAddNode('delay')}
|
|
||||||
>
|
|
||||||
{intl.get('延迟')}
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip title={intl.get('循环')}>
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
icon={<SyncOutlined />}
|
|
||||||
onClick={() => handleAddNode('loop')}
|
|
||||||
>
|
|
||||||
{intl.get('循环')}
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
<div className="toolbar-group">
|
|
||||||
<span className="toolbar-label">{intl.get('视图')}:</span>
|
|
||||||
<Tooltip title={intl.get('放大')}>
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
icon={<ZoomInOutlined />}
|
|
||||||
onClick={() => handleZoom('in')}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip title={intl.get('缩小')}>
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
icon={<ZoomOutOutlined />}
|
|
||||||
onClick={() => handleZoom('out')}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip title={intl.get('适应画布')}>
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
icon={<FullscreenOutlined />}
|
|
||||||
onClick={() => handleZoom('fit')}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flowgram-editor-wrapper">
|
<div className="flowgram-editor-wrapper">
|
||||||
<EditorRenderer className="flowgram-editor" />
|
<EditorRenderer className="flowgram-editor" />
|
||||||
<DockedPanelLayer />
|
<DockedPanelLayer />
|
||||||
|
<FlowgramTools />
|
||||||
</div>
|
</div>
|
||||||
</FreeLayoutEditorProvider>
|
</FreeLayoutEditorProvider>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,93 @@
|
||||||
|
/**
|
||||||
|
* Add Node dropdown component
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { Button, Dropdown, Menu } from 'antd';
|
||||||
|
import {
|
||||||
|
PlusOutlined,
|
||||||
|
ApiOutlined,
|
||||||
|
CodeOutlined,
|
||||||
|
BranchesOutlined,
|
||||||
|
ClockCircleOutlined,
|
||||||
|
SyncOutlined,
|
||||||
|
} from '@ant-design/icons';
|
||||||
|
import { usePlayground } from '@flowgram.ai/free-layout-editor';
|
||||||
|
import { nanoid } from 'nanoid';
|
||||||
|
import intl from 'react-intl-universal';
|
||||||
|
|
||||||
|
export const AddNodeDropdown: React.FC = () => {
|
||||||
|
const playground = usePlayground();
|
||||||
|
|
||||||
|
const handleAddNode = (type: string) => {
|
||||||
|
// Get center of viewport
|
||||||
|
const viewport = playground.viewport.getViewport();
|
||||||
|
const centerX = viewport.x + viewport.width / 2;
|
||||||
|
const centerY = viewport.y + viewport.height / 2;
|
||||||
|
|
||||||
|
// Add node at center
|
||||||
|
const nodeId = nanoid();
|
||||||
|
playground.nodeService.createNode({
|
||||||
|
id: nodeId,
|
||||||
|
type,
|
||||||
|
x: centerX - 140, // Offset to center the node (280px width / 2)
|
||||||
|
y: centerY - 60, // Offset to center the node (120px height / 2)
|
||||||
|
width: type === 'start' || type === 'end' ? 120 : 280,
|
||||||
|
height: type === 'delay' || type === 'loop' ? 100 : 120,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const menu = (
|
||||||
|
<Menu
|
||||||
|
items={[
|
||||||
|
{
|
||||||
|
key: 'http',
|
||||||
|
label: intl.get('scenario_http_node'),
|
||||||
|
icon: <ApiOutlined />,
|
||||||
|
onClick: () => handleAddNode('http'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'script',
|
||||||
|
label: intl.get('scenario_script_node'),
|
||||||
|
icon: <CodeOutlined />,
|
||||||
|
onClick: () => handleAddNode('script'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'condition',
|
||||||
|
label: intl.get('scenario_condition_node'),
|
||||||
|
icon: <BranchesOutlined />,
|
||||||
|
onClick: () => handleAddNode('condition'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'delay',
|
||||||
|
label: intl.get('scenario_delay_node'),
|
||||||
|
icon: <ClockCircleOutlined />,
|
||||||
|
onClick: () => handleAddNode('delay'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'loop',
|
||||||
|
label: intl.get('scenario_loop_node'),
|
||||||
|
icon: <SyncOutlined />,
|
||||||
|
onClick: () => handleAddNode('loop'),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dropdown overlay={menu} trigger={['click']} placement="topLeft">
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
icon={<PlusOutlined />}
|
||||||
|
style={{
|
||||||
|
backgroundColor: 'rgba(24, 144, 255, 0.1)',
|
||||||
|
color: '#1890ff',
|
||||||
|
border: '1px solid rgba(24, 144, 255, 0.3)',
|
||||||
|
borderRadius: '8px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{intl.get('scenario_add_node')}
|
||||||
|
</Button>
|
||||||
|
</Dropdown>
|
||||||
|
);
|
||||||
|
};
|
||||||
67
src/pages/scenario/flowgram/components/tools/index.tsx
Normal file
67
src/pages/scenario/flowgram/components/tools/index.tsx
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
/**
|
||||||
|
* Flowgram Tools Component
|
||||||
|
* Based on: https://github.com/bytedance/flowgram.ai/tree/main/apps/demo-free-layout/src/components/tools
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { Button, Tooltip, Divider } from 'antd';
|
||||||
|
import {
|
||||||
|
UndoOutlined,
|
||||||
|
RedoOutlined,
|
||||||
|
PlusOutlined,
|
||||||
|
ZoomInOutlined,
|
||||||
|
ZoomOutOutlined,
|
||||||
|
FullscreenOutlined,
|
||||||
|
} from '@ant-design/icons';
|
||||||
|
import { useClientContext } from '@flowgram.ai/free-layout-editor';
|
||||||
|
import { ZoomSelect } from './zoom-select';
|
||||||
|
import { AddNodeDropdown } from './add-node-dropdown';
|
||||||
|
import { ToolContainer, ToolSection } from './styles';
|
||||||
|
|
||||||
|
export const FlowgramTools: React.FC = () => {
|
||||||
|
const { history, playground } = useClientContext();
|
||||||
|
const [canUndo, setCanUndo] = useState(false);
|
||||||
|
const [canRedo, setCanRedo] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const disposable = history.undoRedoService.onChange(() => {
|
||||||
|
setCanUndo(history.canUndo());
|
||||||
|
setCanRedo(history.canRedo());
|
||||||
|
});
|
||||||
|
return () => disposable.dispose();
|
||||||
|
}, [history]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ToolContainer className="flowgram-tools">
|
||||||
|
<ToolSection>
|
||||||
|
<ZoomSelect />
|
||||||
|
<Tooltip title="Fit View">
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
icon={<FullscreenOutlined />}
|
||||||
|
onClick={() => playground.viewport.fitView()}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Divider type="vertical" style={{ height: '20px', margin: '0 4px' }} />
|
||||||
|
<Tooltip title="Undo">
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
icon={<UndoOutlined />}
|
||||||
|
disabled={!canUndo}
|
||||||
|
onClick={() => history.undo()}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip title="Redo">
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
icon={<RedoOutlined />}
|
||||||
|
disabled={!canRedo}
|
||||||
|
onClick={() => history.redo()}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Divider type="vertical" style={{ height: '20px', margin: '0 4px' }} />
|
||||||
|
<AddNodeDropdown />
|
||||||
|
</ToolSection>
|
||||||
|
</ToolContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
54
src/pages/scenario/flowgram/components/tools/styles.tsx
Normal file
54
src/pages/scenario/flowgram/components/tools/styles.tsx
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
/**
|
||||||
|
* Styled components for Flowgram tools
|
||||||
|
*/
|
||||||
|
|
||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
export const ToolContainer = styled.div`
|
||||||
|
position: absolute;
|
||||||
|
bottom: 16px;
|
||||||
|
left: 16px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: left;
|
||||||
|
min-width: 360px;
|
||||||
|
pointer-events: none;
|
||||||
|
gap: 8px;
|
||||||
|
z-index: 20;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const ToolSection = styled.div`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background-color: #fff;
|
||||||
|
border: 1px solid rgba(68, 83, 130, 0.25);
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: rgba(0, 0, 0, 0.04) 0px 2px 6px 0px, rgba(0, 0, 0, 0.02) 0px 4px 12px 0px;
|
||||||
|
column-gap: 2px;
|
||||||
|
height: 40px;
|
||||||
|
padding: 0 4px;
|
||||||
|
pointer-events: auto;
|
||||||
|
|
||||||
|
.ant-btn {
|
||||||
|
border: none;
|
||||||
|
box-shadow: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(0, 0, 0, 0.04);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const SelectZoom = styled.span`
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid rgba(68, 83, 130, 0.25);
|
||||||
|
font-size: 12px;
|
||||||
|
min-width: 60px;
|
||||||
|
text-align: center;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(0, 0, 0, 0.04);
|
||||||
|
}
|
||||||
|
`;
|
||||||
65
src/pages/scenario/flowgram/components/tools/zoom-select.tsx
Normal file
65
src/pages/scenario/flowgram/components/tools/zoom-select.tsx
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
/**
|
||||||
|
* Zoom selector component
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Dropdown, Menu } from 'antd';
|
||||||
|
import { usePlayground, usePlaygroundTools } from '@flowgram.ai/free-layout-editor';
|
||||||
|
import { SelectZoom } from './styles';
|
||||||
|
|
||||||
|
export const ZoomSelect: React.FC = () => {
|
||||||
|
const tools = usePlaygroundTools({ maxZoom: 2, minZoom: 0.25 });
|
||||||
|
const playground = usePlayground();
|
||||||
|
const [visible, setVisible] = useState(false);
|
||||||
|
|
||||||
|
const menu = (
|
||||||
|
<Menu
|
||||||
|
onClick={() => setVisible(false)}
|
||||||
|
items={[
|
||||||
|
{
|
||||||
|
key: 'zoomin',
|
||||||
|
label: 'Zoom In',
|
||||||
|
onClick: () => tools.zoomin(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'zoomout',
|
||||||
|
label: 'Zoom Out',
|
||||||
|
onClick: () => tools.zoomout(),
|
||||||
|
},
|
||||||
|
{ type: 'divider' },
|
||||||
|
{
|
||||||
|
key: '50',
|
||||||
|
label: 'Zoom to 50%',
|
||||||
|
onClick: () => playground.config.updateZoom(0.5),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: '100',
|
||||||
|
label: 'Zoom to 100%',
|
||||||
|
onClick: () => playground.config.updateZoom(1),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: '150',
|
||||||
|
label: 'Zoom to 150%',
|
||||||
|
onClick: () => playground.config.updateZoom(1.5),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: '200',
|
||||||
|
label: 'Zoom to 200%',
|
||||||
|
onClick: () => playground.config.updateZoom(2.0),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dropdown
|
||||||
|
overlay={menu}
|
||||||
|
trigger={['click']}
|
||||||
|
visible={visible}
|
||||||
|
onVisibleChange={setVisible}
|
||||||
|
placement="topLeft"
|
||||||
|
>
|
||||||
|
<SelectZoom>{Math.floor(tools.zoom * 100)}%</SelectZoom>
|
||||||
|
</Dropdown>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -6,33 +6,6 @@
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.flowgram-editor-toolbar {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
padding: 12px 16px;
|
|
||||||
border-bottom: 1px solid #d9d9d9;
|
|
||||||
background: #fff;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
.toolbar-group {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
padding-right: 12px;
|
|
||||||
border-right: 1px solid #d9d9d9;
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
border-right: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.toolbar-label {
|
|
||||||
font-size: 12px;
|
|
||||||
color: #666;
|
|
||||||
align-self: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.flowgram-editor-wrapper {
|
.flowgram-editor-wrapper {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
@ -58,17 +31,4 @@
|
||||||
.flowgram-editor-container {
|
.flowgram-editor-container {
|
||||||
background: #141414;
|
background: #141414;
|
||||||
}
|
}
|
||||||
|
|
||||||
.flowgram-editor-toolbar {
|
|
||||||
background: #1f1f1f;
|
|
||||||
border-bottom-color: #434343;
|
|
||||||
|
|
||||||
.toolbar-group {
|
|
||||||
border-right-color: #434343;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toolbar-label {
|
|
||||||
color: #999;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user