mirror of
https://github.com/whyour/qinglong.git
synced 2025-12-14 16:05:39 +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_script_node": "Script Execution",
|
||||||
"scenario_condition_node": "Condition",
|
"scenario_condition_node": "Condition",
|
||||||
"scenario_delay_node": "Delay",
|
"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_script_node": "脚本执行",
|
||||||
"scenario_condition_node": "条件判断",
|
"scenario_condition_node": "条件判断",
|
||||||
"scenario_delay_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 '@flowgram.ai/free-layout-editor/index.css';
|
||||||
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 {
|
||||||
|
|
@ -47,7 +46,6 @@ const FlowgramEditor = forwardRef<FlowgramEditorRef, FlowgramEditorProps>(
|
||||||
<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>
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import { createMinimapPlugin } from '@flowgram.ai/minimap-plugin';
|
||||||
import { createPanelManagerPlugin } from '@flowgram.ai/panel-manager-plugin';
|
import { createPanelManagerPlugin } from '@flowgram.ai/panel-manager-plugin';
|
||||||
import { createHistoryNodePlugin } from '@flowgram.ai/history-node-plugin';
|
import { createHistoryNodePlugin } from '@flowgram.ai/history-node-plugin';
|
||||||
import { FlowNodeRegistry } from '../nodes/http';
|
import { FlowNodeRegistry } from '../nodes/http';
|
||||||
|
import { createToolsPlugin } from '../plugins/tools-plugin';
|
||||||
|
|
||||||
export function useEditorProps(
|
export function useEditorProps(
|
||||||
initialData: any,
|
initialData: any,
|
||||||
|
|
@ -39,6 +40,7 @@ export function useEditorProps(
|
||||||
factories: [],
|
factories: [],
|
||||||
layerProps: {},
|
layerProps: {},
|
||||||
}),
|
}),
|
||||||
|
createToolsPlugin(),
|
||||||
],
|
],
|
||||||
onChange: (data) => {
|
onChange: (data) => {
|
||||||
console.log('Workflow changed:', 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