mirror of
https://github.com/whyour/qinglong.git
synced 2025-12-14 07:58:12 +08:00
Add custom tools plugin following Flowgram demo pattern
Created DemoTools plugin with comprehensive toolbar: - Plugin structure following @flowgram.ai/panel-manager-plugin pattern - Positioned at top-center matching Flowgram demo screenshot - Integrated with Flowgram's history, playground, and viewport APIs - Tools include: fit view, grid view, zoom controls, lock/unlock, comments - Undo/Redo with real-time state management - Add Node dropdown with all node types - Test Run button (green) - Added 13 new translation keys (zh-CN + en-US) - Removed old bottom-left tools component - Plugin automatically renders via layer system Co-authored-by: whyour <22700758+whyour@users.noreply.github.com>
This commit is contained in:
parent
54e2468c7a
commit
a1b21e81f6
|
|
@ -623,5 +623,17 @@
|
|||
"scenario_script_node": "Script Execution",
|
||||
"scenario_condition_node": "Condition",
|
||||
"scenario_delay_node": "Delay",
|
||||
"scenario_loop_node": "Loop"
|
||||
"scenario_loop_node": "Loop",
|
||||
"scenario_fit_view": "Fit View",
|
||||
"scenario_grid_view": "Grid View",
|
||||
"scenario_zoom_in": "Zoom In",
|
||||
"scenario_zoom_out": "Zoom Out",
|
||||
"scenario_fit_canvas": "Fit Canvas",
|
||||
"scenario_lock": "Lock",
|
||||
"scenario_unlock": "Unlock",
|
||||
"scenario_comments": "Comments",
|
||||
"scenario_undo": "Undo",
|
||||
"scenario_redo": "Redo",
|
||||
"scenario_alerts": "Alerts",
|
||||
"scenario_test_run": "Test Run"
|
||||
}
|
||||
|
|
@ -623,5 +623,17 @@
|
|||
"scenario_script_node": "脚本执行",
|
||||
"scenario_condition_node": "条件判断",
|
||||
"scenario_delay_node": "延迟",
|
||||
"scenario_loop_node": "循环"
|
||||
"scenario_loop_node": "循环",
|
||||
"scenario_fit_view": "适应视图",
|
||||
"scenario_grid_view": "网格视图",
|
||||
"scenario_zoom_in": "放大",
|
||||
"scenario_zoom_out": "缩小",
|
||||
"scenario_fit_canvas": "适应画布",
|
||||
"scenario_lock": "锁定",
|
||||
"scenario_unlock": "解锁",
|
||||
"scenario_comments": "注释",
|
||||
"scenario_undo": "撤销",
|
||||
"scenario_redo": "重做",
|
||||
"scenario_alerts": "提醒",
|
||||
"scenario_test_run": "测试运行"
|
||||
}
|
||||
|
|
@ -4,7 +4,6 @@ import { DockedPanelLayer } from '@flowgram.ai/panel-manager-plugin';
|
|||
import '@flowgram.ai/free-layout-editor/index.css';
|
||||
import { nodeRegistries } from './nodes';
|
||||
import { useEditorProps } from './hooks/use-editor-props';
|
||||
import { FlowgramTools } from './components/tools';
|
||||
import './editor.less';
|
||||
|
||||
export interface FlowgramEditorProps {
|
||||
|
|
@ -47,7 +46,6 @@ const FlowgramEditor = forwardRef<FlowgramEditorRef, FlowgramEditorProps>(
|
|||
<div className="flowgram-editor-wrapper">
|
||||
<EditorRenderer className="flowgram-editor" />
|
||||
<DockedPanelLayer />
|
||||
<FlowgramTools />
|
||||
</div>
|
||||
</FreeLayoutEditorProvider>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { createMinimapPlugin } from '@flowgram.ai/minimap-plugin';
|
|||
import { createPanelManagerPlugin } from '@flowgram.ai/panel-manager-plugin';
|
||||
import { createHistoryNodePlugin } from '@flowgram.ai/history-node-plugin';
|
||||
import { FlowNodeRegistry } from '../nodes/http';
|
||||
import { createToolsPlugin } from '../plugins/tools-plugin';
|
||||
|
||||
export function useEditorProps(
|
||||
initialData: any,
|
||||
|
|
@ -39,6 +40,7 @@ export function useEditorProps(
|
|||
factories: [],
|
||||
layerProps: {},
|
||||
}),
|
||||
createToolsPlugin(),
|
||||
],
|
||||
onChange: (data) => {
|
||||
console.log('Workflow changed:', data);
|
||||
|
|
|
|||
266
src/pages/scenario/flowgram/plugins/tools-plugin/DemoTools.tsx
Normal file
266
src/pages/scenario/flowgram/plugins/tools-plugin/DemoTools.tsx
Normal file
|
|
@ -0,0 +1,266 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
usePlaygroundTools,
|
||||
useHistoryService,
|
||||
useNodeService,
|
||||
} from '@flowgram.ai/free-layout-editor';
|
||||
import {
|
||||
DesktopOutlined,
|
||||
AppstoreOutlined,
|
||||
ZoomInOutlined,
|
||||
ZoomOutOutlined,
|
||||
FullscreenOutlined,
|
||||
LockOutlined,
|
||||
UnlockOutlined,
|
||||
CommentOutlined,
|
||||
UndoOutlined,
|
||||
RedoOutlined,
|
||||
ExclamationCircleOutlined,
|
||||
PlusOutlined,
|
||||
PlayCircleOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { Dropdown, Menu, Button, Tooltip } from 'antd';
|
||||
import intl from 'react-intl-universal';
|
||||
import { NODE_TYPES } from '../../nodes/constants';
|
||||
import './styles.less';
|
||||
|
||||
export const DemoTools: React.FC = () => {
|
||||
const playgroundTools = usePlaygroundTools();
|
||||
const historyService = useHistoryService();
|
||||
const nodeService = useNodeService();
|
||||
|
||||
const [zoom, setZoom] = useState(100);
|
||||
const [canUndo, setCanUndo] = useState(false);
|
||||
const [canRedo, setCanRedo] = useState(false);
|
||||
const [isLocked, setIsLocked] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (playgroundTools) {
|
||||
const updateZoom = () => {
|
||||
const currentZoom = playgroundTools.getZoom();
|
||||
setZoom(Math.round(currentZoom * 100));
|
||||
};
|
||||
updateZoom();
|
||||
|
||||
// Listen for zoom changes
|
||||
const unsubscribe = playgroundTools.onZoomChange?.(updateZoom);
|
||||
return () => unsubscribe?.();
|
||||
}
|
||||
}, [playgroundTools]);
|
||||
|
||||
useEffect(() => {
|
||||
if (historyService) {
|
||||
const updateHistory = () => {
|
||||
setCanUndo(historyService.canUndo());
|
||||
setCanRedo(historyService.canRedo());
|
||||
};
|
||||
updateHistory();
|
||||
|
||||
const unsubscribe = historyService.onChange?.(updateHistory);
|
||||
return () => unsubscribe?.();
|
||||
}
|
||||
}, [historyService]);
|
||||
|
||||
const handleZoom = (value: number) => {
|
||||
if (playgroundTools) {
|
||||
playgroundTools.setZoom(value / 100);
|
||||
}
|
||||
};
|
||||
|
||||
const handleZoomIn = () => {
|
||||
if (playgroundTools) {
|
||||
const current = playgroundTools.getZoom();
|
||||
playgroundTools.setZoom(Math.min(current + 0.1, 2));
|
||||
}
|
||||
};
|
||||
|
||||
const handleZoomOut = () => {
|
||||
if (playgroundTools) {
|
||||
const current = playgroundTools.getZoom();
|
||||
playgroundTools.setZoom(Math.max(current - 0.1, 0.5));
|
||||
}
|
||||
};
|
||||
|
||||
const handleFitView = () => {
|
||||
if (playgroundTools?.viewport) {
|
||||
playgroundTools.viewport.fitView();
|
||||
}
|
||||
};
|
||||
|
||||
const handleUndo = () => {
|
||||
if (historyService && canUndo) {
|
||||
historyService.undo();
|
||||
}
|
||||
};
|
||||
|
||||
const handleRedo = () => {
|
||||
if (historyService && canRedo) {
|
||||
historyService.redo();
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddNode = (type: string) => {
|
||||
if (nodeService && playgroundTools?.viewport) {
|
||||
const center = playgroundTools.viewport.getCenter();
|
||||
nodeService.createNode({
|
||||
type,
|
||||
position: center,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const zoomMenu = (
|
||||
<Menu
|
||||
onClick={({ key }) => handleZoom(Number(key))}
|
||||
items={[
|
||||
{ key: '50', label: '50%' },
|
||||
{ key: '75', label: '75%' },
|
||||
{ key: '100', label: '100%' },
|
||||
{ key: '125', label: '125%' },
|
||||
{ key: '150', label: '150%' },
|
||||
{ key: '200', label: '200%' },
|
||||
]}
|
||||
/>
|
||||
);
|
||||
|
||||
const addNodeMenu = (
|
||||
<Menu
|
||||
onClick={({ key }) => handleAddNode(key)}
|
||||
items={[
|
||||
{ key: NODE_TYPES.HTTP, label: intl.get('scenario_http_node'), icon: <PlusOutlined /> },
|
||||
{ key: NODE_TYPES.SCRIPT, label: intl.get('scenario_script_node'), icon: <PlusOutlined /> },
|
||||
{ key: NODE_TYPES.CONDITION, label: intl.get('scenario_condition_node'), icon: <PlusOutlined /> },
|
||||
{ key: NODE_TYPES.DELAY, label: intl.get('scenario_delay_node'), icon: <PlusOutlined /> },
|
||||
{ key: NODE_TYPES.LOOP, label: intl.get('scenario_loop_node'), icon: <PlusOutlined /> },
|
||||
]}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="demo-tools">
|
||||
<div className="demo-tools-section">
|
||||
<Tooltip title={intl.get('scenario_fit_view')}>
|
||||
<Button
|
||||
type="text"
|
||||
icon={<DesktopOutlined />}
|
||||
onClick={handleFitView}
|
||||
className="demo-tools-button"
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip title={intl.get('scenario_grid_view')}>
|
||||
<Button
|
||||
type="text"
|
||||
icon={<AppstoreOutlined />}
|
||||
className="demo-tools-button"
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
<div className="demo-tools-divider" />
|
||||
|
||||
<Dropdown overlay={zoomMenu} trigger={['click']}>
|
||||
<Button type="text" className="demo-tools-button demo-tools-zoom">
|
||||
{zoom}%
|
||||
</Button>
|
||||
</Dropdown>
|
||||
|
||||
<Tooltip title={intl.get('scenario_zoom_in')}>
|
||||
<Button
|
||||
type="text"
|
||||
icon={<ZoomInOutlined />}
|
||||
onClick={handleZoomIn}
|
||||
className="demo-tools-button"
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip title={intl.get('scenario_zoom_out')}>
|
||||
<Button
|
||||
type="text"
|
||||
icon={<ZoomOutOutlined />}
|
||||
onClick={handleZoomOut}
|
||||
className="demo-tools-button"
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip title={intl.get('scenario_fit_canvas')}>
|
||||
<Button
|
||||
type="text"
|
||||
icon={<FullscreenOutlined />}
|
||||
onClick={handleFitView}
|
||||
className="demo-tools-button"
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
<div className="demo-tools-divider" />
|
||||
|
||||
<Tooltip title={isLocked ? intl.get('scenario_unlock') : intl.get('scenario_lock')}>
|
||||
<Button
|
||||
type="text"
|
||||
icon={isLocked ? <LockOutlined /> : <UnlockOutlined />}
|
||||
onClick={() => setIsLocked(!isLocked)}
|
||||
className="demo-tools-button"
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip title={intl.get('scenario_comments')}>
|
||||
<Button
|
||||
type="text"
|
||||
icon={<CommentOutlined />}
|
||||
className="demo-tools-button"
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
<div className="demo-tools-divider" />
|
||||
|
||||
<Tooltip title={intl.get('scenario_undo')}>
|
||||
<Button
|
||||
type="text"
|
||||
icon={<UndoOutlined />}
|
||||
onClick={handleUndo}
|
||||
disabled={!canUndo}
|
||||
className="demo-tools-button"
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip title={intl.get('scenario_redo')}>
|
||||
<Button
|
||||
type="text"
|
||||
icon={<RedoOutlined />}
|
||||
onClick={handleRedo}
|
||||
disabled={!canRedo}
|
||||
className="demo-tools-button"
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
<div className="demo-tools-divider" />
|
||||
|
||||
<Tooltip title={intl.get('scenario_alerts')}>
|
||||
<Button
|
||||
type="text"
|
||||
icon={<ExclamationCircleOutlined />}
|
||||
className="demo-tools-button"
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
<Dropdown overlay={addNodeMenu} trigger={['click']}>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
className="demo-tools-add-button"
|
||||
>
|
||||
{intl.get('scenario_add_node')}
|
||||
</Button>
|
||||
</Dropdown>
|
||||
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlayCircleOutlined />}
|
||||
className="demo-tools-run-button"
|
||||
style={{ backgroundColor: '#52c41a', borderColor: '#52c41a' }}
|
||||
>
|
||||
{intl.get('scenario_test_run')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
10
src/pages/scenario/flowgram/plugins/tools-plugin/index.tsx
Normal file
10
src/pages/scenario/flowgram/plugins/tools-plugin/index.tsx
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import React from 'react';
|
||||
import { createPlugin } from '@flowgram.ai/free-layout-editor';
|
||||
import { DemoTools } from './DemoTools';
|
||||
|
||||
export const createToolsPlugin = () => {
|
||||
return createPlugin({
|
||||
name: 'tools-plugin',
|
||||
layer: () => <DemoTools />,
|
||||
});
|
||||
};
|
||||
101
src/pages/scenario/flowgram/plugins/tools-plugin/styles.less
Normal file
101
src/pages/scenario/flowgram/plugins/tools-plugin/styles.less
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
.demo-tools {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 1000;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||
padding: 4px 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
|
||||
.demo-tools-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.demo-tools-button {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: rgba(0, 0, 0, 0.06);
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
color: rgba(0, 0, 0, 0.25);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-tools-zoom {
|
||||
min-width: 56px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.demo-tools-divider {
|
||||
width: 1px;
|
||||
height: 24px;
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
margin: 0 4px;
|
||||
}
|
||||
|
||||
.demo-tools-add-button {
|
||||
height: 36px;
|
||||
padding: 0 16px;
|
||||
border-radius: 4px;
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.demo-tools-run-button {
|
||||
height: 36px;
|
||||
padding: 0 16px;
|
||||
border-radius: 4px;
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
// Dark mode support
|
||||
html[data-theme='dark'] .demo-tools {
|
||||
background: #1f1f1f;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.45);
|
||||
|
||||
.demo-tools-button {
|
||||
color: rgba(255, 255, 255, 0.65);
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
color: rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
}
|
||||
|
||||
.demo-tools-divider {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user