完成边的编辑窗口
This commit is contained in:
parent
a469133446
commit
098259177b
17
README.md
17
README.md
@ -1,16 +1 @@
|
|||||||
# React + Vite
|
# Wireguard 组网可视化编辑器
|
||||||
|
|
||||||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
|
||||||
|
|
||||||
Currently, two official plugins are available:
|
|
||||||
|
|
||||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
|
|
||||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
|
||||||
|
|
||||||
## React Compiler
|
|
||||||
|
|
||||||
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
|
|
||||||
|
|
||||||
## Expanding the ESLint configuration
|
|
||||||
|
|
||||||
If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.
|
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
## 产品基本功能实现待办事项
|
## 产品基本功能实现待办事项
|
||||||
|
|
||||||
- [x] 完成节点的编辑窗口
|
- [x] 完成节点的编辑窗口
|
||||||
- [ ] 完成边的编辑窗口
|
- [x] 完成边的编辑窗口
|
||||||
- [ ] 完成全局设置编辑窗口以及相关联动
|
- [ ] 完成全局设置编辑窗口以及相关联动
|
||||||
- [ ] 实现配置生成逻辑,并验证有效
|
- [ ] 实现配置生成逻辑,并验证有效
|
||||||
- [ ] 实现配置保存和加载功能
|
- [ ] 实现配置保存和加载功能
|
||||||
44
src/App.tsx
44
src/App.tsx
@ -14,12 +14,14 @@ import {
|
|||||||
NodeMouseHandler,
|
NodeMouseHandler,
|
||||||
OnConnect,
|
OnConnect,
|
||||||
MiniMap,
|
MiniMap,
|
||||||
IsValidConnection
|
IsValidConnection,
|
||||||
|
EdgeMouseHandler
|
||||||
} from '@xyflow/react';
|
} from '@xyflow/react';
|
||||||
import '@xyflow/react/dist/style.css';
|
import '@xyflow/react/dist/style.css';
|
||||||
import { AppNode, AppEdge, NodeData, Settings } from './types/graph';
|
import { AppNode, AppEdge, NodeData, EdgeData, Settings } from './types/graph';
|
||||||
import CustomNode from './components/CustomNode';
|
import CustomNode from './components/CustomNode';
|
||||||
import NodeEditor from './components/NodeEditor';
|
import NodeEditor from './components/NodeEditor';
|
||||||
|
import EdgeEditor from './components/EdgeEditor'
|
||||||
import Toggle from "./components/Toggle"
|
import Toggle from "./components/Toggle"
|
||||||
import { generateWireGuardPrivateKey } from './utils/wireguardConfig';
|
import { generateWireGuardPrivateKey } from './utils/wireguardConfig';
|
||||||
import './App.css';
|
import './App.css';
|
||||||
@ -50,7 +52,8 @@ function FlowContent(): ReactNode {
|
|||||||
const [edges, setEdges] = useState<AppEdge[]>(initialEdges);
|
const [edges, setEdges] = useState<AppEdge[]>(initialEdges);
|
||||||
const [settings, setSettings] = useState<Settings>(initialSettings);
|
const [settings, setSettings] = useState<Settings>(initialSettings);
|
||||||
|
|
||||||
const [editingNode, setEditingNode] = useState<NodeData | null>(null);
|
const [editingNode, setEditingNode] = useState<NodeData | undefined>(undefined);
|
||||||
|
const [editingEdge, setEditingEdge] = useState<EdgeData | undefined>(undefined);
|
||||||
const [enableTwoWay, setEnableTwoWay] = useState(false);
|
const [enableTwoWay, setEnableTwoWay] = useState(false);
|
||||||
|
|
||||||
const onNodesChange = useCallback(
|
const onNodesChange = useCallback(
|
||||||
@ -63,12 +66,14 @@ function FlowContent(): ReactNode {
|
|||||||
|
|
||||||
const onConnect = useCallback<OnConnect>(
|
const onConnect = useCallback<OnConnect>(
|
||||||
(params) => {
|
(params) => {
|
||||||
|
const id = `e-${crypto.randomUUID()}`
|
||||||
const newEdge : AppEdge = {
|
const newEdge : AppEdge = {
|
||||||
...params,
|
...params,
|
||||||
id: `e-${crypto.randomUUID()}`,
|
id: id,
|
||||||
animated: !enableTwoWay,
|
animated: !enableTwoWay,
|
||||||
markerEnd: enableTwoWay ? undefined : { type: MarkerType.ArrowClosed },
|
markerEnd: enableTwoWay ? undefined : { type: MarkerType.ArrowClosed },
|
||||||
data : {
|
data : {
|
||||||
|
id: id,
|
||||||
isTwoWayEdge: enableTwoWay
|
isTwoWayEdge: enableTwoWay
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -80,6 +85,11 @@ function FlowContent(): ReactNode {
|
|||||||
(_event, node) => setEditingNode(node.data),
|
(_event, node) => setEditingNode(node.data),
|
||||||
[]);
|
[]);
|
||||||
|
|
||||||
|
const onEdgeClick = useCallback<EdgeMouseHandler<AppEdge>>(
|
||||||
|
(_event, edge) => setEditingEdge(edge.data),
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
const validateConnection = useCallback<IsValidConnection>(
|
const validateConnection = useCallback<IsValidConnection>(
|
||||||
(connection) => {
|
(connection) => {
|
||||||
if (connection.source === connection.target) {
|
if (connection.source === connection.target) {
|
||||||
@ -117,7 +127,19 @@ function FlowContent(): ReactNode {
|
|||||||
return node;
|
return node;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
setEditingNode(null);
|
setEditingNode(undefined);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpdateEdge = (updatedData: EdgeData): void => {
|
||||||
|
setEdges((prev) =>
|
||||||
|
prev.map((edge) => {
|
||||||
|
if (edge.data && edge.data.id === editingEdge?.id) {
|
||||||
|
return { ...edge, data: updatedData };
|
||||||
|
}
|
||||||
|
return edge;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
setEditingNode(undefined);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -129,6 +151,7 @@ function FlowContent(): ReactNode {
|
|||||||
onEdgesChange={onEdgesChange}
|
onEdgesChange={onEdgesChange}
|
||||||
onConnect={onConnect}
|
onConnect={onConnect}
|
||||||
onNodeDoubleClick={onNodeClick}
|
onNodeDoubleClick={onNodeClick}
|
||||||
|
onEdgeDoubleClick={onEdgeClick}
|
||||||
nodeTypes={nodeTypes}
|
nodeTypes={nodeTypes}
|
||||||
deleteKeyCode={["Delete"]}
|
deleteKeyCode={["Delete"]}
|
||||||
fitView
|
fitView
|
||||||
@ -168,10 +191,19 @@ function FlowContent(): ReactNode {
|
|||||||
<NodeEditor
|
<NodeEditor
|
||||||
node={editingNode}
|
node={editingNode}
|
||||||
onUpdate={handleUpdateNode}
|
onUpdate={handleUpdateNode}
|
||||||
onClose={() => setEditingNode(null)}
|
onClose={() => setEditingNode(undefined)}
|
||||||
settings={settings}
|
settings={settings}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{editingEdge && (
|
||||||
|
<EdgeEditor
|
||||||
|
edge={editingEdge}
|
||||||
|
onUpdate={handleUpdateEdge}
|
||||||
|
onClose={() => setEditingEdge(undefined)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
68
src/components/EdgeEditor.tsx
Normal file
68
src/components/EdgeEditor.tsx
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import { useState, ReactNode } from 'react';
|
||||||
|
import { AppNode, AppEdge, EdgeData } from '../types/graph';
|
||||||
|
import { useReactFlow } from '@xyflow/react';
|
||||||
|
|
||||||
|
interface EdgeEditorProps {
|
||||||
|
edge: EdgeData;
|
||||||
|
onUpdate: (data: EdgeData) => void;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function NodeEditor({
|
||||||
|
edge,
|
||||||
|
onUpdate,
|
||||||
|
onClose
|
||||||
|
}: EdgeEditorProps): ReactNode {
|
||||||
|
const [formData, setFormData] = useState<EdgeData>(edge);
|
||||||
|
const { getNode, getEdge } = useReactFlow<AppNode, AppEdge>();
|
||||||
|
|
||||||
|
const handleInputChange = (field: keyof EdgeData, value: string): void => {
|
||||||
|
setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
[field]: value
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = (): void => {
|
||||||
|
onUpdate(formData);
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const e = getEdge(edge.id)
|
||||||
|
|
||||||
|
const source = e ? getNode(e.source) : undefined
|
||||||
|
const target = e ? getNode(e.target) : undefined
|
||||||
|
|
||||||
|
const sourceName = source ? source.data.label : "error"
|
||||||
|
const targetName = target ? target.data.label : "error"
|
||||||
|
|
||||||
|
const label = `[${sourceName}] ${edge.isTwoWayEdge ? "↔" : "→"} [${targetName}]`
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="node-editor-overlay">
|
||||||
|
<div className="node-editor">
|
||||||
|
<div className="editor-header">
|
||||||
|
<h2>设置边 {label} 参数</h2>
|
||||||
|
<button className="close-btn" onClick={onClose}>×</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-group">
|
||||||
|
<label>保活频率(单位:秒)</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
step="1"
|
||||||
|
value={formData.persistentKeepalive || ''}
|
||||||
|
onChange={(e) => handleInputChange('persistentKeepalive', e.target.value)}
|
||||||
|
placeholder={`留空或0代表不保活`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="editor-actions">
|
||||||
|
<button className="btn-save" onClick={handleSave}>保存</button>
|
||||||
|
<button className="btn-cancel" onClick={onClose}>取消</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -21,6 +21,7 @@ export type NodeData = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type EdgeData = {
|
export type EdgeData = {
|
||||||
|
readonly id: string;
|
||||||
isTwoWayEdge: boolean;
|
isTwoWayEdge: boolean;
|
||||||
persistentKeepalive?: string;
|
persistentKeepalive?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user