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:
copilot-swe-agent[bot] 2025-11-23 10:57:29 +00:00
parent b356069e6c
commit 54e2468c7a
8 changed files with 297 additions and 141 deletions

View File

@ -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"
} }

View File

@ -617,5 +617,11 @@
"放大": "放大", "放大": "放大",
"缩小": "缩小", "缩小": "缩小",
"适应画布": "适应画布", "适应画布": "适应画布",
"条件": "条件" "条件": "条件",
"scenario_add_node": "添加节点",
"scenario_http_node": "HTTP 请求",
"scenario_script_node": "脚本执行",
"scenario_condition_node": "条件判断",
"scenario_delay_node": "延迟",
"scenario_loop_node": "循环"
} }

View File

@ -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>

View File

@ -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>
);
};

View 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>
);
};

View 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);
}
`;

View 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>
);
};

View File

@ -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;
}
}
} }