From c898da935c85374994f170e431da74bb0dafb8cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B0=A2=E6=96=B0=E6=A0=B9?= Date: Thu, 18 Jan 2024 14:59:27 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=85=A8=E5=B1=80=20?= =?UTF-8?q?namespace=20=E5=AE=9A=E4=B9=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/_assets/materials.ts | 589 +++++++++--------- src/Editor/index.tsx | 527 ++++++++-------- src/Preview/index.tsx | 301 ++++----- src/components/material-panel/index.tsx | 406 ++++++------ .../material-panel/material-node.tsx | 99 +-- .../props-panel/icon-select/icon-select.tsx | 91 +-- src/components/props-panel/index.tsx | 388 ++++++------ src/contexts/topology.ts | 9 +- src/core/snippet.ts | 171 ++--- src/index.ts | 6 +- src/types.d.ts | 196 ++++++ src/types/declare.d.ts | 21 - src/types/global.d.ts | 167 ----- tsconfig.json | 36 +- 14 files changed, 1521 insertions(+), 1486 deletions(-) delete mode 100644 src/types/declare.d.ts delete mode 100644 src/types/global.d.ts diff --git a/docs/_assets/materials.ts b/docs/_assets/materials.ts index 9b4d729..cfbd342 100644 --- a/docs/_assets/materials.ts +++ b/docs/_assets/materials.ts @@ -1,300 +1,299 @@ -import type { Topology } from '@/types/global'; import { ICON_NAMES } from './icon-map'; const BACKGROUND_NORMAL = 'var(--topology-editor-background-normal)'; -export const dashboard: Topology.Materials = [ - { - name: 'device', - label: '设备', - items: [ - { - id: 'database-audit', - component: 'topology-device', - size: { - height: 74, - width: 74, - }, - componentProps: { - borderStyle: 'solid', - label: '数据库审计', - icon: ICON_NAMES.databaseAudit, - background: 'white', - }, - }, - { - id: 'switch', - component: 'topology-device', - size: { - height: 74, - width: 74, - }, - componentProps: { - borderStyle: 'solid', - label: '交换机', - icon: ICON_NAMES.switch, - background: 'white', - }, - }, - { - id: 'firewall', - component: 'topology-device', - size: { - height: 74, - width: 74, - }, - componentProps: { - borderStyle: 'solid', - label: '防火墙', - icon: ICON_NAMES.firewall, - background: 'white', - }, - }, - { - id: 'firewall-ip', - component: 'topology-device', - size: { - height: 74, - width: 74, - }, - componentProps: { - borderStyle: 'solid', - label: 'IP封禁', - icon: ICON_NAMES.firewallIp, - background: 'white', - }, - }, - { - id: 'china-unicom', - component: 'topology-device', - size: { - height: 74, - width: 74, - }, - componentProps: { - borderStyle: 'solid', - label: '联通专线', - icon: ICON_NAMES.chinaUnicom, - background: 'white', - }, - }, - { - id: 'china-mobile', - component: 'topology-device', - size: { - height: 74, - width: 74, - }, - componentProps: { - borderStyle: 'solid', - label: '移动专线', - icon: ICON_NAMES.chinaMobile, - background: 'white', - }, - }, - { - id: 'internet', - component: 'topology-device', - size: { - height: 74, - width: 74, - }, - componentProps: { - borderStyle: 'solid', - label: '互联网', - icon: ICON_NAMES.internet, - background: 'white', - }, - }, - { - id: 'ddos', - component: 'topology-device', - size: { - height: 74, - width: 74, - }, - componentProps: { - borderStyle: 'solid', - label: '抗DDOs攻击', - icon: ICON_NAMES.ddos, - background: 'white', - }, - }, - { - id: 'web', - component: 'topology-device', - size: { - height: 74, - width: 74, - }, - componentProps: { - borderStyle: 'solid', - label: 'Web应用', - icon: ICON_NAMES.web, - background: 'white', - }, - }, - { - id: 'bank', - component: 'topology-device', - size: { - height: 74, - width: 74, - }, - componentProps: { - borderStyle: 'none', - label: '银行', - icon: ICON_NAMES.bank, - background: 'transparent', - }, - }, - { - id: 'government', - component: 'topology-device', - size: { - height: 74, - width: 74, - }, - componentProps: { - borderStyle: 'none', - label: '政府', - icon: ICON_NAMES.government, - background: 'transparent', - }, - }, - { - id: 'police', - component: 'topology-device', - size: { - height: 74, - width: 74, - }, - componentProps: { - borderStyle: 'none', - label: '公安', - icon: ICON_NAMES.police, - background: 'transparent', - }, - }, - { - id: 'cloud', - component: 'topology-device', - size: { - height: 74, - width: 74, - }, - componentProps: { - borderStyle: 'dashed', - label: '云平台', - icon: ICON_NAMES.cloud, - background: BACKGROUND_NORMAL, - }, - }, - ], - }, - { - name: 'other', - label: '其他', - items: [ - { - id: 'circle', - label: '圆形', - component: 'topology-circle', - embeddable: true, - componentProps: { - borderStyle: 'solid', - background: 'var(--topology-editor-background-normal)', - }, - size: { - height: 74, - width: 74, - }, - }, - { - id: 'rect-dashed-normal', - label: '正方形', - component: 'topology-rect', - embeddable: true, - componentProps: { - borderStyle: 'dashed', - background: 'var(--topology-editor-background-normal)', - }, - size: { - height: 74, - width: 74, - }, - }, - { - id: 'rect-solid-white', - label: '正方形', - component: 'topology-rect', - embeddable: true, - componentProps: { - borderStyle: 'solid', - background: 'white', - }, - size: { - height: 74, - width: 74, - }, - }, - { - id: 'lightning', - label: '闪电', - icon: 'lightning', - component: 'topology-lightning', - size: { - height: 48, - width: 48, - }, - }, - { - id: 'line', - label: '线条', - component: 'topology-line', - componentProps: { - stroke: 'var(--topology-editor-normal-color)', - strokeWidth: 1, - }, - size: { - height: 300, - width: 20, - }, - rowSpan: 2, - }, - { - id: 'larger-label', - label: '大文本', - component: 'topology-label', - componentProps: { - label: '大文本', - type: 'title', - layout: 'vertical', - textAlign: 'center', - color: 'var(--topology-editor-normal-color)', - }, - size: { - height: 240, - width: 60, - }, - columnSpan: 2, - }, - { - id: 'label', - label: '普通文本', - component: 'topology-label', - componentProps: { - label: '普通文本', - type: 'normal', - layout: 'horizontal', - textAlign: 'center', - color: 'var(--topology-editor-normal-color)', - }, - size: { - height: 40, - width: 100, - }, - }, - ], - }, +export const dashboard = [ + { + name: 'device', + label: '设备', + items: [ + { + id: 'database-audit', + component: 'topology-device', + size: { + height: 74, + width: 74, + }, + componentProps: { + borderStyle: 'solid', + label: '数据库审计', + icon: ICON_NAMES.databaseAudit, + background: 'white', + }, + }, + { + id: 'switch', + component: 'topology-device', + size: { + height: 74, + width: 74, + }, + componentProps: { + borderStyle: 'solid', + label: '交换机', + icon: ICON_NAMES.switch, + background: 'white', + }, + }, + { + id: 'firewall', + component: 'topology-device', + size: { + height: 74, + width: 74, + }, + componentProps: { + borderStyle: 'solid', + label: '防火墙', + icon: ICON_NAMES.firewall, + background: 'white', + }, + }, + { + id: 'firewall-ip', + component: 'topology-device', + size: { + height: 74, + width: 74, + }, + componentProps: { + borderStyle: 'solid', + label: 'IP封禁', + icon: ICON_NAMES.firewallIp, + background: 'white', + }, + }, + { + id: 'china-unicom', + component: 'topology-device', + size: { + height: 74, + width: 74, + }, + componentProps: { + borderStyle: 'solid', + label: '联通专线', + icon: ICON_NAMES.chinaUnicom, + background: 'white', + }, + }, + { + id: 'china-mobile', + component: 'topology-device', + size: { + height: 74, + width: 74, + }, + componentProps: { + borderStyle: 'solid', + label: '移动专线', + icon: ICON_NAMES.chinaMobile, + background: 'white', + }, + }, + { + id: 'internet', + component: 'topology-device', + size: { + height: 74, + width: 74, + }, + componentProps: { + borderStyle: 'solid', + label: '互联网', + icon: ICON_NAMES.internet, + background: 'white', + }, + }, + { + id: 'ddos', + component: 'topology-device', + size: { + height: 74, + width: 74, + }, + componentProps: { + borderStyle: 'solid', + label: '抗DDOs攻击', + icon: ICON_NAMES.ddos, + background: 'white', + }, + }, + { + id: 'web', + component: 'topology-device', + size: { + height: 74, + width: 74, + }, + componentProps: { + borderStyle: 'solid', + label: 'Web应用', + icon: ICON_NAMES.web, + background: 'white', + }, + }, + { + id: 'bank', + component: 'topology-device', + size: { + height: 74, + width: 74, + }, + componentProps: { + borderStyle: 'none', + label: '银行', + icon: ICON_NAMES.bank, + background: 'transparent', + }, + }, + { + id: 'government', + component: 'topology-device', + size: { + height: 74, + width: 74, + }, + componentProps: { + borderStyle: 'none', + label: '政府', + icon: ICON_NAMES.government, + background: 'transparent', + }, + }, + { + id: 'police', + component: 'topology-device', + size: { + height: 74, + width: 74, + }, + componentProps: { + borderStyle: 'none', + label: '公安', + icon: ICON_NAMES.police, + background: 'transparent', + }, + }, + { + id: 'cloud', + component: 'topology-device', + size: { + height: 74, + width: 74, + }, + componentProps: { + borderStyle: 'dashed', + label: '云平台', + icon: ICON_NAMES.cloud, + background: BACKGROUND_NORMAL, + }, + }, + ], + }, + { + name: 'other', + label: '其他', + items: [ + { + id: 'circle', + label: '圆形', + component: 'topology-circle', + embeddable: true, + componentProps: { + borderStyle: 'solid', + background: 'var(--topology-editor-background-normal)', + }, + size: { + height: 74, + width: 74, + }, + }, + { + id: 'rect-dashed-normal', + label: '正方形', + component: 'topology-rect', + embeddable: true, + componentProps: { + borderStyle: 'dashed', + background: 'var(--topology-editor-background-normal)', + }, + size: { + height: 74, + width: 74, + }, + }, + { + id: 'rect-solid-white', + label: '正方形', + component: 'topology-rect', + embeddable: true, + componentProps: { + borderStyle: 'solid', + background: 'white', + }, + size: { + height: 74, + width: 74, + }, + }, + { + id: 'lightning', + label: '闪电', + icon: 'lightning', + component: 'topology-lightning', + size: { + height: 48, + width: 48, + }, + }, + { + id: 'line', + label: '线条', + component: 'topology-line', + componentProps: { + stroke: 'var(--topology-editor-normal-color)', + strokeWidth: 1, + }, + size: { + height: 300, + width: 20, + }, + rowSpan: 2, + }, + { + id: 'larger-label', + label: '大文本', + component: 'topology-label', + componentProps: { + label: '大文本', + type: 'title', + layout: 'vertical', + textAlign: 'center', + color: 'var(--topology-editor-normal-color)', + }, + size: { + height: 240, + width: 60, + }, + columnSpan: 2, + }, + { + id: 'label', + label: '普通文本', + component: 'topology-label', + componentProps: { + label: '普通文本', + type: 'normal', + layout: 'horizontal', + textAlign: 'center', + color: 'var(--topology-editor-normal-color)', + }, + size: { + height: 40, + width: 100, + }, + }, + ], + }, ]; diff --git a/src/Editor/index.tsx b/src/Editor/index.tsx index a5e59b0..7dc3825 100644 --- a/src/Editor/index.tsx +++ b/src/Editor/index.tsx @@ -1,3 +1,15 @@ +import { Cell, Graph, Model } from '@antv/x6'; +import React, { + forwardRef, + useEffect, + useImperativeHandle, + useRef, + useState, +} from 'react'; +import throttle from 'lodash/throttle'; +import classNames from 'classnames'; +import { toPng } from 'html-to-image'; +import { Drawer, Layout } from 'antd'; import MaterialPanel from '@/components/material-panel'; import type { PropsPanelProps } from '@/components/props-panel'; import PropsPanel from '@/components/props-panel'; @@ -7,296 +19,291 @@ import { EVENT_MAP } from '@/constants'; import TopologyContext from '@/contexts/topology'; import X6ReactPortalProvider from '@/contexts/x6-react-portal'; import useGraph from '@/hooks/useGraph'; -import type { Topology } from '@/types/global.d'; -import { Cell, Graph, Model } from '@antv/x6'; -import { Drawer, Layout } from 'antd'; -import classNames from 'classnames'; -import { toPng } from 'html-to-image'; -import throttle from 'lodash/throttle'; -import React, { - forwardRef, - useEffect, - useImperativeHandle, - useRef, - useState, -} from 'react'; - import '@/index.less'; import EventBus from '@/utils/event-bus'; export type EditorProps = { - iconMap: Record; - materials: Topology.Materials; - materialFilterable?: boolean; - style?: React.CSSProperties; - className?: string; - value?: Model.FromJSONData; - propsPanelSchemaMap?: PropsPanelProps['schemaMap']; - propsPanelComponents?: PropsPanelProps['components']; - propsPanelScope?: PropsPanelProps['scope']; - size?: Topology.Size; - toolbar?: ToolbarProps['toolbar']; - onImport?: (data: Topology.Graph) => Promise; - onExport?: (type: 'json' | 'image') => Promise; - onChange?: (change: any) => void; + iconMap: Record; + materials: Topology.Materials; + materialFilterable?: boolean; + style?: React.CSSProperties; + className?: string; + value?: Model.FromJSONData; + propsPanelSchemaMap?: PropsPanelProps['schemaMap']; + propsPanelComponents?: PropsPanelProps['components']; + propsPanelScope?: PropsPanelProps['scope']; + size?: Topology.Size; + toolbar?: ToolbarProps['toolbar']; + onImport?: (data: Topology.Graph) => Promise; + onExport?: (type: 'json' | 'image') => Promise; + onChange?: (change: any) => void; }; export type EditorRef = { - getJsonData(): Promise<{ - cells: Cell.Properties[]; - }>; - getImageData(option?: Record): Promise; - getThumbData(width?: number, height?: number): Promise; - getInstance: () => Graph; + getJsonData(): Promise<{ + cells: Cell.Properties[]; + }>; + getImageData(option?: Record): Promise; + getThumbData(width?: number, height?: number): Promise; + getInstance: () => Graph; }; const layoutWidth = { - materialPanel: 268, - propsPanel: 260, + materialPanel: 268, + propsPanel: 260, }; const Editor: React.ForwardRefRenderFunction = ( - componentProp, - ref, -) => { - const graphContainerRef = useRef(); - const eventBusRef = useRef(new EventBus()); - const [currentNode, setCurrentNode] = useState(null); - const [topologyBase64Image, setTopologyBase64Image] = useState(''); - // merge 默认属性 - const props = Object.assign( - { - materialFilterable: false, - materials: [], - propsPanelScope: {}, - propsPanelComponents: {}, - size: { - height: 666, - width: 1888, - }, - toolbar: true, - }, componentProp, - ); - - const [graphInstance] = useGraph(graphContainerRef, eventBusRef.current, { - graphOption: { - width: props.size.width, - height: props.size.height, - }, - }); + ref, +) => { + const graphContainerRef = useRef(); + const eventBusRef = useRef(new EventBus()); + const [currentNode, setCurrentNode] = useState(null); + const [topologyBase64Image, setTopologyBase64Image] = useState(''); + // merge 默认属性 + const props = Object.assign( + { + materialFilterable: false, + materials: [], + propsPanelScope: {}, + propsPanelComponents: {}, + size: { + height: 666, + width: 1888, + }, + toolbar: true, + }, + componentProp, + ); - const handleCellRemoved = () => { - setCurrentNode(null); - }; - const handleChangeCurrentNode = (cell: Cell) => { - setCurrentNode(cell); - }; - const handleExport = async (type: 'json' | 'image') => { - await props.onExport?.(type); - }; + const [graphInstance] = useGraph(graphContainerRef, eventBusRef.current, { + graphOption: { + width: props.size.width, + height: props.size.height, + }, + }); - const handleImport = async (data: Topology.Graph) => { - await props.onImport?.(data); - }; + const handleCellRemoved = () => { + setCurrentNode(null); + }; + const handleChangeCurrentNode = (cell: Cell) => { + setCurrentNode(cell); + }; + const handleExport = async (type: 'json' | 'image') => { + await props.onExport?.(type); + }; - // 获取编辑器的预览图 - const getImageData = async ( - htmlToImageOption: Record = {}, - ): Promise => { - if (!graphInstance) { - throw Error('[graph] graph 尚未实例化'); - } - const graphSvg = graphInstance.container.querySelector( - '.x6-graph-svg', - ) as HTMLElement; - if (!graphSvg) { - throw Error('[graph] 未找到画板'); - } - const clonedGraphSvg = graphSvg.cloneNode(true) as HTMLElement; - try { - clonedGraphSvg - .querySelector('g.x6-graph-svg-viewport') - ?.setAttribute('transform', 'matrix(1,0,0,1,0,0)'); - graphInstance.container.appendChild(clonedGraphSvg); - clonedGraphSvg.style.zIndex = '-1'; - clonedGraphSvg.style.width = `${props.size.width}px`; - clonedGraphSvg.style.height = `${props.size.height}px`; - const imageBase64 = await toPng(clonedGraphSvg, { - backgroundColor: 'white', - ...htmlToImageOption, - }); - return imageBase64; - } finally { - graphInstance.container.removeChild(clonedGraphSvg); - } - }; + const handleImport = async (data: Topology.Graph) => { + await props.onImport?.(data); + }; - // 获取编辑器的缩略图 - async function getThumbData(width = 356, height = 140): Promise { - // 创建一个 Image 对象 - const image = new Image(); - image.src = await getImageData({ quality: 0.2 }); - return await new Promise((resolve, reject) => { - // 当图像加载完成后执行回调函数 - image.onload = () => { - // 创建一个 canvas 元素 - const canvas = document.createElement('canvas'); - canvas.width = width; - canvas.height = height; - const ctx = canvas.getContext('2d'); - if (!ctx) { - throw Error('[graph] 获取Canvas异常'); + // 获取编辑器的预览图 + const getImageData = async ( + htmlToImageOption: Record = {}, + ): Promise => { + if (!graphInstance) { + throw Error('[graph] graph 尚未实例化'); } - ctx.drawImage(image, 0, 0, width, height); - resolve(canvas.toDataURL()); - }; - image.onerror = (error) => { - reject(error); - }; - }); - } + const graphSvg = graphInstance.container.querySelector( + '.x6-graph-svg', + ) as HTMLElement; + if (!graphSvg) { + throw Error('[graph] 未找到画板'); + } + const clonedGraphSvg = graphSvg.cloneNode(true) as HTMLElement; + try { + clonedGraphSvg + .querySelector('g.x6-graph-svg-viewport') + ?.setAttribute('transform', 'matrix(1,0,0,1,0,0)'); + graphInstance.container.appendChild(clonedGraphSvg); + clonedGraphSvg.style.zIndex = '-1'; + clonedGraphSvg.style.width = `${props.size.width}px`; + clonedGraphSvg.style.height = `${props.size.height}px`; + const imageBase64 = await toPng(clonedGraphSvg, { + backgroundColor: 'white', + ...htmlToImageOption, + }); + return imageBase64; + } finally { + graphInstance.container.removeChild(clonedGraphSvg); + } + }; - const getJsonData = async (): Promise<{ - cells: Cell.Properties[]; - }> => { - if (!graphInstance) { - throw Error('[graph] graph 尚未实例化'); + // 获取编辑器的缩略图 + async function getThumbData(width = 356, height = 140): Promise { + // 创建一个 Image 对象 + const image = new Image(); + image.src = await getImageData({ quality: 0.2 }); + return await new Promise((resolve, reject) => { + // 当图像加载完成后执行回调函数 + image.onload = () => { + // 创建一个 canvas 元素 + const canvas = document.createElement('canvas'); + canvas.width = width; + canvas.height = height; + const ctx = canvas.getContext('2d'); + if (!ctx) { + throw Error('[graph] 获取Canvas异常'); + } + ctx.drawImage(image, 0, 0, width, height); + resolve(canvas.toDataURL()); + }; + image.onerror = (error) => { + reject(error); + }; + }); } - return new Promise((resolve) => { - resolve(graphInstance.toJSON()); - }); - }; - const handlePreview = async () => { - const topoBase64Image = await getImageData(); - setTopologyBase64Image(topoBase64Image); - }; + const getJsonData = async (): Promise<{ + cells: Cell.Properties[]; + }> => { + if (!graphInstance) { + throw Error('[graph] graph 尚未实例化'); + } + return new Promise((resolve) => { + resolve(graphInstance.toJSON()); + }); + }; - const handleChange = throttle((arg: any) => { - props.onChange?.(arg); - }, 300); + const handlePreview = async () => { + const topoBase64Image = await getImageData(); + setTopologyBase64Image(topoBase64Image); + }; - useEffect(() => { - eventBusRef.current?.on(EVENT_MAP.CELL_REMOVE, handleCellRemoved); - eventBusRef.current?.on(EVENT_MAP.NODE_SELECTED, handleChangeCurrentNode); - eventBusRef.current?.on(EVENT_MAP.EDGE_SELECTED, handleChangeCurrentNode); - eventBusRef.current?.on(EVENT_MAP.ON_EXPORT, handleExport); - eventBusRef.current?.on(EVENT_MAP.ON_IMPORT, handleImport); - eventBusRef.current?.on(EVENT_MAP.PREVIEW, handlePreview); - eventBusRef.current?.on(EVENT_MAP.ON_CHANGE, handleChange); + const handleChange = throttle((arg: any) => { + props.onChange?.(arg); + }, 300); - return () => { - eventBusRef.current?.off(EVENT_MAP.CELL_REMOVE, handleCellRemoved); - eventBusRef.current?.off( - EVENT_MAP.NODE_SELECTED, - handleChangeCurrentNode, - ); - eventBusRef.current?.off( - EVENT_MAP.EDGE_SELECTED, + useEffect(() => { + eventBusRef.current?.on(EVENT_MAP.CELL_REMOVE, handleCellRemoved); + eventBusRef.current?.on( + EVENT_MAP.NODE_SELECTED, + handleChangeCurrentNode, + ); + eventBusRef.current?.on( + EVENT_MAP.EDGE_SELECTED, + handleChangeCurrentNode, + ); + eventBusRef.current?.on(EVENT_MAP.ON_EXPORT, handleExport); + eventBusRef.current?.on(EVENT_MAP.ON_IMPORT, handleImport); + eventBusRef.current?.on(EVENT_MAP.PREVIEW, handlePreview); + eventBusRef.current?.on(EVENT_MAP.ON_CHANGE, handleChange); + + return () => { + eventBusRef.current?.off(EVENT_MAP.CELL_REMOVE, handleCellRemoved); + eventBusRef.current?.off( + EVENT_MAP.NODE_SELECTED, + handleChangeCurrentNode, + ); + eventBusRef.current?.off( + EVENT_MAP.EDGE_SELECTED, + handleChangeCurrentNode, + ); + eventBusRef.current?.off(EVENT_MAP.ON_EXPORT, handleExport); + eventBusRef.current?.off(EVENT_MAP.ON_IMPORT, handleImport); + eventBusRef.current?.off(EVENT_MAP.PREVIEW, handlePreview); + eventBusRef.current?.off(EVENT_MAP.ON_CHANGE, handleChange); + }; + }, [ + handleCellRemoved, handleChangeCurrentNode, - ); - eventBusRef.current?.off(EVENT_MAP.ON_EXPORT, handleExport); - eventBusRef.current?.off(EVENT_MAP.ON_IMPORT, handleImport); - eventBusRef.current?.off(EVENT_MAP.PREVIEW, handlePreview); - eventBusRef.current?.off(EVENT_MAP.ON_CHANGE, handleChange); - }; - }, [ - handleCellRemoved, - handleChangeCurrentNode, - handleExport, - handleImport, - handlePreview, - handleChange, - ]); + handleExport, + handleImport, + handlePreview, + handleChange, + ]); - useEffect(() => { - if (graphInstance) { - if (props.value) { - graphInstance.fromJSON(props.value); - } else { - graphInstance.clearCells(); - } - } - }, [graphInstance, props.value]); + useEffect(() => { + if (graphInstance) { + if (props.value) { + graphInstance.fromJSON(props.value); + } else { + graphInstance.clearCells(); + } + } + }, [graphInstance, props.value]); - useImperativeHandle(ref, () => ({ - getJsonData, - getImageData, - getThumbData, - getInstance: () => graphInstance as Graph, - })); + useImperativeHandle(ref, () => ({ + getJsonData, + getImageData, + getThumbData, + getInstance: () => graphInstance as Graph, + })); - return ( - - {/* 为 自定义 rect 节点提供数据 */} - - - - - - - - -
-
-
-
-
- - - -
- { - setTopologyBase64Image(''); - }} - open={!!topologyBase64Image} - > -
- 预览 -
-
-
- ); + {/* 为 自定义 rect 节点提供数据 */} + + + + + + + + +
+
+
+
+
+ + + +
+ { + setTopologyBase64Image(''); + }} + open={!!topologyBase64Image} + > +
+ 预览 +
+
+ + ); }; export default forwardRef(Editor); diff --git a/src/Preview/index.tsx b/src/Preview/index.tsx index 0a0159d..caebef0 100644 --- a/src/Preview/index.tsx +++ b/src/Preview/index.tsx @@ -1,180 +1,183 @@ -import TopologyContext from '@/contexts/topology'; -import X6ReactPortalProvider from '@/contexts/x6-react-portal'; -import useGraph from '@/hooks/useGraph'; -import type { Topology } from '@/types/global.d'; -import { MinusOutlined, PlusOutlined } from '@ant-design/icons'; import { Graph, Model } from '@antv/x6'; -import { useSize } from 'ahooks'; -import { Button, Layout, Space } from 'antd'; -import classNames from 'classnames'; import React, { - forwardRef, - useEffect, - useImperativeHandle, - useRef, - useState, + forwardRef, + useEffect, + useImperativeHandle, + useRef, + useState, } from 'react'; - +import classNames from 'classnames'; +import { Button, Layout, Space } from 'antd'; +import { MinusOutlined, PlusOutlined } from '@ant-design/icons'; +import { useSize } from 'ahooks'; import { GRAPH_ZOOM } from '@/constants'; +import TopologyContext from '@/contexts/topology'; +import X6ReactPortalProvider from '@/contexts/x6-react-portal'; +import useGraph from '@/hooks/useGraph'; import '@/index.less'; import EventBus from '@/utils/event-bus'; export type EditorProps = { - iconMap: Record; - value?: Model.FromJSONData; - style?: React.CSSProperties; - className?: string; + iconMap: Record; + value?: Model.FromJSONData; + style?: React.CSSProperties; + className?: string; }; export type EditorRef = { - getInstance: () => Graph; + getInstance: () => Graph; }; const zoomFitPadding = 14; const Editor: React.ForwardRefRenderFunction = ( - props, - ref, + props, + ref, ) => { - const graphContainerRef = useRef(); - const eventBusRef = useRef(new EventBus()); - const [zoom, setZoom] = useState(1); - const graphContainerSize = useSize(graphContainerRef); + const graphContainerRef = useRef(); + const eventBusRef = useRef(new EventBus()); + const [zoom, setZoom] = useState(1); + const graphContainerSize = useSize(graphContainerRef); - const [graphInstance] = useGraph(graphContainerRef, eventBusRef.current, { - graphOption: { - width: graphContainerSize?.width, - height: graphContainerSize?.height, - // autoResize: true, - grid: false, - embedding: false, - interacting: false, - panning: true, - }, - pluginOption: { - transform: false, - selection: false, - snapline: false, - keyboard: false, - clipboard: false, - history: false, - scroller: false, - }, - keyBoardEvent: false, - graphEvent: false, - }); - - const handleZoomToFit = () => { - if (!graphInstance) { - return; - } - graphInstance.zoomToFit({ - padding: zoomFitPadding, + const [graphInstance] = useGraph(graphContainerRef, eventBusRef.current, { + graphOption: { + width: graphContainerSize?.width, + height: graphContainerSize?.height, + // autoResize: true, + grid: false, + embedding: false, + interacting: false, + panning: true, + }, + pluginOption: { + transform: false, + selection: false, + snapline: false, + keyboard: false, + clipboard: false, + history: false, + scroller: false, + }, + keyBoardEvent: false, + graphEvent: false, }); - }; - useEffect(() => { - if (graphInstance) { - graphInstance.resize( - graphContainerSize?.width, - graphContainerSize?.height, - ); - graphInstance.zoomToFit({ - padding: zoomFitPadding, - }); - } - }, [graphInstance, graphContainerSize]); + const handleZoomToFit = () => { + if (!graphInstance) { + return; + } + graphInstance.zoomToFit({ + padding: zoomFitPadding, + }); + }; - useEffect(() => { - if (graphInstance) { - if (props.value) { - graphInstance.fromJSON(props.value); - } else { - graphInstance.clearCells(); - } - } - }, [graphInstance, props.value]); + useEffect(() => { + if (graphInstance) { + graphInstance.resize( + graphContainerSize?.width, + graphContainerSize?.height, + ); + graphInstance.zoomToFit({ + padding: zoomFitPadding, + }); + } + }, [graphInstance, graphContainerSize]); - useEffect(() => { - if (!graphInstance) { - return; - } - graphInstance.on('scale', () => { - const zoom = graphInstance.zoom(); - let zoomValue = Math.min(zoom, GRAPH_ZOOM.max); - zoomValue = Math.max(zoomValue, GRAPH_ZOOM.min); - setZoom(Math.max(zoomValue)); - }); - }, [graphInstance]); + useEffect(() => { + if (graphInstance) { + if (props.value) { + graphInstance.fromJSON(props.value); + } else { + graphInstance.clearCells(); + } + } + }, [graphInstance, props.value]); - useImperativeHandle(ref, () => ({ - getInstance: () => graphInstance as Graph, - })); + useEffect(() => { + if (!graphInstance) { + return; + } + graphInstance.on('scale', () => { + const zoom = graphInstance.zoom(); + let zoomValue = Math.min(zoom, GRAPH_ZOOM.max); + zoomValue = Math.max(zoomValue, GRAPH_ZOOM.min); + setZoom(Math.max(zoomValue)); + }); + }, [graphInstance]); - const handleChangeZoom = (zoom: number, absolute = false) => { - if (!graphInstance) { - return; - } - graphInstance.zoom(zoom, { - minScale: GRAPH_ZOOM.min, - maxScale: GRAPH_ZOOM.max, - absolute, - }); - }; + useImperativeHandle(ref, () => ({ + getInstance: () => graphInstance as Graph, + })); + + const handleChangeZoom = (zoom: number, absolute = false) => { + if (!graphInstance) { + return; + } + graphInstance.zoom(zoom, { + minScale: GRAPH_ZOOM.min, + maxScale: GRAPH_ZOOM.max, + absolute, + }); + }; - const zoomText = Math.floor(zoom * 100) + '%'; + const zoomText = Math.floor(zoom * 100) + '%'; - return ( - - - - -
-
- - -
-
-
-
-
-
- ); +
+
+ + +
+
+
+ + + + ); }; export default forwardRef(Editor); diff --git a/src/components/material-panel/index.tsx b/src/components/material-panel/index.tsx index f147459..0b0b7db 100644 --- a/src/components/material-panel/index.tsx +++ b/src/components/material-panel/index.tsx @@ -1,19 +1,18 @@ -import defaultPorts from '@/constants/default-ports'; -import { EVENT_MAP, TOPOLOGY_SNIPPET } from '@/constants/index'; -import TopologyContext from '@/contexts/topology'; -import createSnippet from '@/core/snippet'; -import type { Topology } from '@/types/global.d'; import type { Node } from '@antv/x6'; import { Dnd } from '@antv/x6-plugin-dnd'; +import React, { useContext, useEffect, useRef, useState } from 'react'; import type { CollapseProps } from 'antd'; import { Collapse, Input } from 'antd'; -import React, { useContext, useEffect, useRef, useState } from 'react'; +import defaultPorts from '@/constants/default-ports'; +import { EVENT_MAP, TOPOLOGY_SNIPPET } from '@/constants/index'; +import TopologyContext from '@/contexts/topology'; +import createSnippet from '@/core/snippet'; import MaterialNode from './material-node'; interface IMaterialPanelProps { - filterable?: boolean; - iconMap: { [key: string]: Topology.TopologyIconProp }; - materials: Topology.Materials; + filterable?: boolean; + iconMap: { [key: string]: Topology.TopologyIconProp }; + materials: Topology.Materials; } /** @@ -22,216 +21,217 @@ interface IMaterialPanelProps { * @returns */ function getMaterialMap(materials: Topology.Materials) { - const result: Record = {}; - materials?.forEach((group) => { - const prefix = group.name ? `${group.name}-` : ''; - group.items.forEach((material) => { - const key = `${prefix}${material.id}`; - result[key] = material; + const result: Record = {}; + materials?.forEach((group) => { + const prefix = group.name ? `${group.name}-` : ''; + group.items.forEach((material) => { + const key = `${prefix}${material.id}`; + result[key] = material; + }); }); - }); - return result; + return result; } const MaterialPanel: React.FC = (props) => { - // 物料面板元素 ref - const sideBarRef = useRef(null); - // dnd 实例 ref - const dndRef = useRef(); - // 存储是否有组 - const [hasGroup, setHasGroup] = useState(true); - // 默认打开的面板 - const [activeKeys, setActiveKeys] = useState([]); - // 展示的物料 - const [materials, setMaterials] = useState(props.materials); - // material id - 配置映射 - const [materialMap, setMaterialMap] = useState>( - {}, - ); - - const { graph, eventBus } = useContext(TopologyContext); + // 物料面板元素 ref + const sideBarRef = useRef(null); + // dnd 实例 ref + const dndRef = useRef(); + // 存储是否有组 + const [hasGroup, setHasGroup] = useState(true); + // 默认打开的面板 + const [activeKeys, setActiveKeys] = useState([]); + // 展示的物料 + const [materials, setMaterials] = useState(props.materials); + // material id - 配置映射 + const [materialMap, setMaterialMap] = useState< + Record + >({}); - // 初始化物料侧边栏 - const initSideBar = () => { - if (!sideBarRef.current) { - throw Error('[graph] 物料面板不存在'); - } - if (!graph) { - throw Error('[graph] graph尚未初始化完成'); - } - dndRef.current = new Dnd({ - target: graph, - scaled: false, - dndContainer: sideBarRef.current, - // getDragNode(node) { - // return graph.value.createNode(node); - // }, - // getDropNode: (node) => { - // return node.clone(); - // }, - }); - }; - - /** - * 针对代码片段的处理逻辑 - */ - const onNodeAdded = async (arg: any) => { - if (!graph) { - return; - } - if (arg.cell.shape !== TOPOLOGY_SNIPPET) { - return; - } - // 解决往容器节点中拖入片段节点时重复创建节点的问题 - if (arg.options.ui) { - graph.removeNode(arg.cell, { disconnectEdges: true, dryrun: true }); - return; - } - // 解决往容器中拖拽获取不到 parent 的情况 - await new Promise((resolve) => { - resolve(1); - }); - // 创建拓扑图片段 - createSnippet({ - graph: graph, - snippetNode: arg.cell, - }); - }; - - useEffect(() => { - const hasGroupName = props.materials.some((item) => item.name); - // 是否有组 - setHasGroup(hasGroupName); - // 默认打开的物料面板 - setActiveKeys(props.materials.map((item) => item.name)); - // id - 配置映射 - const tempMaterialMap = getMaterialMap(props.materials); - setMaterialMap(tempMaterialMap); - }, [props.materials]); + const { graph, eventBus } = useContext(TopologyContext); - useEffect(() => { - if (!graph) { - return; - } - initSideBar(); - eventBus?.on(EVENT_MAP.NODE_ADDED, onNodeAdded); - return () => { - eventBus?.off(EVENT_MAP.NODE_ADDED, onNodeAdded); + // 初始化物料侧边栏 + const initSideBar = () => { + if (!sideBarRef.current) { + throw Error('[graph] 物料面板不存在'); + } + if (!graph) { + throw Error('[graph] graph尚未初始化完成'); + } + dndRef.current = new Dnd({ + target: graph, + scaled: false, + dndContainer: sideBarRef.current, + // getDragNode(node) { + // return graph.value.createNode(node); + // }, + // getDropNode: (node) => { + // return node.clone(); + // }, + }); }; - }, [graph]); - const handleStartDrag = (e: any) => { - if (!graph) { - throw Error('[graph] graph 尚未初始化'); - } - if (!dndRef.current) { - throw Error('[graph] dnd实例尚未初始化'); - } - const target = e.currentTarget; - const key = target.getAttribute('data-type'); - if (!key) { - throw Error('[graph] 拖拽节点尚未配置 data-key 属性'); - } - const componentConfig = materialMap[key]; - const nodeConfig: Node.Metadata = { - shape: componentConfig.component, - size: componentConfig.size, - ports: defaultPorts, - embeddable: componentConfig.embeddable, - data: { - componentProps: componentConfig.componentProps, - }, + /** + * 针对代码片段的处理逻辑 + */ + const onNodeAdded = async (arg: any) => { + if (!graph) { + return; + } + if (arg.cell.shape !== TOPOLOGY_SNIPPET) { + return; + } + // 解决往容器节点中拖入片段节点时重复创建节点的问题 + if (arg.options.ui) { + graph.removeNode(arg.cell, { disconnectEdges: true, dryrun: true }); + return; + } + // 解决往容器中拖拽获取不到 parent 的情况 + await new Promise((resolve) => { + resolve(1); + }); + // 创建拓扑图片段 + createSnippet({ + graph: graph, + snippetNode: arg.cell, + }); }; - const node = graph.createNode(nodeConfig); - dndRef.current.start(node, e); - }; - const items: CollapseProps['items'] = materials.map((collapseGroup) => { - return { - key: collapseGroup.name, - label: collapseGroup.label, - children: collapseGroup.items?.map((item) => { - return ( - - ); - }), + useEffect(() => { + const hasGroupName = props.materials.some((item) => item.name); + // 是否有组 + setHasGroup(hasGroupName); + // 默认打开的物料面板 + setActiveKeys(props.materials.map((item) => item.name)); + // id - 配置映射 + const tempMaterialMap = getMaterialMap(props.materials); + setMaterialMap(tempMaterialMap); + }, [props.materials]); + + useEffect(() => { + if (!graph) { + return; + } + initSideBar(); + eventBus?.on(EVENT_MAP.NODE_ADDED, onNodeAdded); + return () => { + eventBus?.off(EVENT_MAP.NODE_ADDED, onNodeAdded); + }; + }, [graph]); + + const handleStartDrag = (e: any) => { + if (!graph) { + throw Error('[graph] graph 尚未初始化'); + } + if (!dndRef.current) { + throw Error('[graph] dnd实例尚未初始化'); + } + const target = e.currentTarget; + const key = target.getAttribute('data-type'); + if (!key) { + throw Error('[graph] 拖拽节点尚未配置 data-key 属性'); + } + const componentConfig = materialMap[key]; + const nodeConfig: Node.Metadata = { + shape: componentConfig.component, + size: componentConfig.size, + ports: defaultPorts, + embeddable: componentConfig.embeddable, + data: { + componentProps: componentConfig.componentProps, + }, + }; + const node = graph.createNode(nodeConfig); + dndRef.current.start(node, e); }; - }); - const handleSearch = (value: string) => { - const filterText = value || ''; - const filteredMaterials = props.materials.map((material) => { - return { - ...material, - items: material.items?.filter((item) => { - const label = item.label || item.componentProps?.label || ''; - return ( - label - .toLocaleLowerCase() - .indexOf(filterText.toLocaleLowerCase()) !== -1 - ); - }), - }; + const items: CollapseProps['items'] = materials.map((collapseGroup) => { + return { + key: collapseGroup.name, + label: collapseGroup.label, + children: collapseGroup.items?.map((item) => { + return ( + + ); + }), + }; }); - setMaterials(filteredMaterials); - }; - const handleChangeActiveKeys = (keys: string | string[]) => { - if (typeof keys === 'string') { - setActiveKeys([keys]); - return; - } - setActiveKeys(keys); - }; + const handleSearch = (value: string) => { + const filterText = value || ''; + const filteredMaterials = props.materials.map((material) => { + return { + ...material, + items: material.items?.filter((item) => { + const label = + item.label || item.componentProps?.label || ''; + return ( + label + .toLocaleLowerCase() + .indexOf(filterText.toLocaleLowerCase()) !== -1 + ); + }), + }; + }); + setMaterials(filteredMaterials); + }; - return ( -
- {props.filterable && ( -
- -
- )} - {hasGroup ? ( - - ) : ( -
- {props.materials[0].items.map((item) => { - return ( - - ); - })} + const handleChangeActiveKeys = (keys: string | string[]) => { + if (typeof keys === 'string') { + setActiveKeys([keys]); + return; + } + setActiveKeys(keys); + }; + + return ( +
+ {props.filterable && ( +
+ +
+ )} + {hasGroup ? ( + + ) : ( +
+ {props.materials[0].items.map((item) => { + return ( + + ); + })} +
+ )}
- )} -
- ); + ); }; export default MaterialPanel; diff --git a/src/components/material-panel/material-node.tsx b/src/components/material-panel/material-node.tsx index 4e7b72e..39ffbc3 100644 --- a/src/components/material-panel/material-node.tsx +++ b/src/components/material-panel/material-node.tsx @@ -1,63 +1,70 @@ -import type { Topology } from '@/types/global.d'; import React from 'react'; import classNames from 'classnames'; -import Default from './default'; import BaseNode from '../shapes/base-node'; +import Circle from '../shapes/circle'; import Label from '../shapes/label'; +import Lightning from '../shapes/lightning'; import Line from '../shapes/line'; import Rect from '../shapes/rect'; -import Lightning from '../shapes/lightning'; -import Circle from '../shapes/circle'; - +import Default from './default'; interface IMaterialNodeProps { - material: Topology.Node; - prefix?: string; - icon?: string; - onMouseDown?: React.MouseEventHandler; + material: Topology.Node; + prefix?: string; + icon?: string; + onMouseDown?: React.MouseEventHandler; } -const componentMap:{[key:string]:any} = { - 'topology-downlink': BaseNode, - 'topology-device': BaseNode, - 'topology-label': Label, - 'topology-line': Line, - 'topology-rect': Rect, - 'topology-lightning': Lightning, - 'topology-circle': Circle, +const componentMap: { [key: string]: any } = { + 'topology-downlink': BaseNode, + 'topology-device': BaseNode, + 'topology-label': Label, + 'topology-line': Line, + 'topology-rect': Rect, + 'topology-lightning': Lightning, + 'topology-circle': Circle, }; const MaterialNode: React.FC = (props) => { - const prefixText = props.prefix ? `${props.prefix}-` : ''; - - const computedStyle: React.CSSProperties = { - gridRow: props.material.rowSpan ? `span ${props.material.rowSpan}` : undefined, - gridColumn: props.material.columnSpan ? `span ${props.material.columnSpan}` : undefined, - } - - // 匹配上的节点 - const MaterialNode= componentMap[props.material.component] ?? null; - - return ( -
- { - MaterialNode?( - - ) :( - - ) - } -
- ); + const prefixText = props.prefix ? `${props.prefix}-` : ''; + + const computedStyle: React.CSSProperties = { + gridRow: props.material.rowSpan + ? `span ${props.material.rowSpan}` + : undefined, + gridColumn: props.material.columnSpan + ? `span ${props.material.columnSpan}` + : undefined, + }; + + // 匹配上的节点 + const MaterialNode = componentMap[props.material.component] ?? null; + + return ( +
+ {MaterialNode ? ( + + ) : ( + + )} +
+ ); }; MaterialNode.defaultProps = { - prefix: '', -} + prefix: '', +}; -export default MaterialNode +export default MaterialNode; diff --git a/src/components/props-panel/icon-select/icon-select.tsx b/src/components/props-panel/icon-select/icon-select.tsx index a8cd84b..dd4d2d9 100644 --- a/src/components/props-panel/icon-select/icon-select.tsx +++ b/src/components/props-panel/icon-select/icon-select.tsx @@ -1,52 +1,59 @@ -import type { Topology } from '@/types/global'; import React from 'react'; -import { Select, Flex } from 'antd'; +import { Flex, Select } from 'antd'; type IconSelectProps = { - value?: string; - dataSource?: Array; - allowClear?: boolean; - showSearch?: boolean; - disabled?: boolean; - optionValueProp?: string; - optionLabelProp?: string; - onChange?: (value: string, option: any) => void; -} + value?: string; + dataSource?: Array; + allowClear?: boolean; + showSearch?: boolean; + disabled?: boolean; + optionValueProp?: string; + optionLabelProp?: string; + onChange?: (value: string, option: any) => void; +}; const IconSelect: React.FC = (props) => { - const filterOption = (input: string, option) => { - return (option?.[props.optionLabelProp || 'label'] ?? '').toLowerCase().includes(input.toLowerCase()) - }; + const filterOption = (input: string, option) => { + return (option?.[props.optionLabelProp || 'label'] ?? '') + .toLowerCase() + .includes(input.toLowerCase()); + }; - return ( - { + const item = option.data; + return ( + + {item[props.optionLabelProp as string]} + {item[props.optionLabelProp + + ); + }} + /> + ); }; IconSelect.defaultProps = { - dataSource: [], - optionLabelProp: 'label', - optionValueProp: 'value', - disabled: false, - showSearch: true, - allowClear: true, -} + dataSource: [], + optionLabelProp: 'label', + optionValueProp: 'value', + disabled: false, + showSearch: true, + allowClear: true, +}; -export default IconSelect +export default IconSelect; diff --git a/src/components/props-panel/index.tsx b/src/components/props-panel/index.tsx index 857297b..5aa3f84 100644 --- a/src/components/props-panel/index.tsx +++ b/src/components/props-panel/index.tsx @@ -1,235 +1,237 @@ -import IconSelect from '@/components/props-panel/icon-select'; -import TopologyContext from '@/contexts/topology'; -import type { Topology } from '@/types/global'; import type { Cell } from '@antv/x6'; -import { - Cascader, - Checkbox, - DatePicker, - Form, - FormItem, - Input, - NumberPicker, - Password, - Radio, - Select, - Switch, - TimePicker, - Transfer, - TreeSelect, -} from '@formily/antd-v5'; -import { createForm, onFieldInputValueChange } from '@formily/core'; -import type { ISchema, SchemaReactComponents } from '@formily/react'; -import { createSchemaField } from '@formily/react'; -import { Card, Empty } from 'antd'; +import React, { useContext, useEffect, useState } from 'react'; import cloneDeep from 'lodash/cloneDeep'; import get from 'lodash/get'; import isEmpty from 'lodash/isEmpty'; import pick from 'lodash/pick'; import reduce from 'lodash/reduce'; import set from 'lodash/set'; -import React, { useContext, useEffect, useState } from 'react'; +import { Card, Empty } from 'antd'; +import { + Cascader, + Checkbox, + DatePicker, + Form, + FormItem, + Input, + NumberPicker, + Password, + Radio, + Select, + Switch, + TimePicker, + Transfer, + TreeSelect, +} from '@formily/antd-v5'; +import { createForm, onFieldInputValueChange } from '@formily/core'; +import type { ISchema, SchemaReactComponents } from '@formily/react'; +import { createSchemaField } from '@formily/react'; +import IconSelect from '@/components/props-panel/icon-select'; +import TopologyContext from '@/contexts/topology'; import ColorPicker from './color-picker'; export type PropsPanelProps = { - node: Cell | null; - schemaMap?: Record; - components?: SchemaReactComponents; - scope?: any; + node: Cell | null; + schemaMap?: Record; + components?: SchemaReactComponents; + scope?: any; }; function getTitle(node: PropsPanelProps['node']) { - if (node === null) { - return '编辑画布信息'; - } - if (node.isNode()) { - return '编辑节点信息'; - } - if (node.isEdge()) { - return '编辑连线信息'; - } - return '-'; + if (node === null) { + return '编辑画布信息'; + } + if (node.isNode()) { + return '编辑节点信息'; + } + if (node.isEdge()) { + return '编辑连线信息'; + } + return '-'; } const SchemaField = createSchemaField(); const defaultComponents = { - FormItem, - Input, - InputNumber: NumberPicker, - Select, - NumberPicker, - Checkbox, - Radio, - Cascader, - DatePicker, - Password, - Switch, - TimePicker, - Transfer, - TreeSelect, - IconSelect, - ColorPicker, + FormItem, + Input, + InputNumber: NumberPicker, + Select, + NumberPicker, + Checkbox, + Radio, + Cascader, + DatePicker, + Password, + Switch, + TimePicker, + Transfer, + TreeSelect, + IconSelect, + ColorPicker, }; const EDGE_PROP_KEYS = ['attrs', 'router', 'connector']; function getNodeComponentProps(node: Cell) { - // 查询出节点上配置的属性数据 - let nodeComponentProps = {}; - if (node.isEdge()) { - nodeComponentProps = cloneDeep(pick(node.prop(), EDGE_PROP_KEYS)); - } else if (node.isNode()) { - const nodeData = node.getData(); - nodeComponentProps = nodeData.componentProps; - } else { - throw Error(`[graph] 未实现的节点类型:${JSON.stringify(node)}`); - } - return nodeComponentProps; + // 查询出节点上配置的属性数据 + let nodeComponentProps = {}; + if (node.isEdge()) { + nodeComponentProps = cloneDeep(pick(node.prop(), EDGE_PROP_KEYS)); + } else if (node.isNode()) { + const nodeData = node.getData(); + nodeComponentProps = nodeData.componentProps; + } else { + throw Error(`[graph] 未实现的节点类型:${JSON.stringify(node)}`); + } + return nodeComponentProps; } const PropsPanel: React.FC = (props) => { - const [title, setTitle] = useState('-'); - const [schema, setSchema] = useState({}); - const [deviceIcons, setDeviceIcons] = useState( - [], - ); - const hasSchema = isEmpty(schema); + const [title, setTitle] = useState('-'); + const [schema, setSchema] = useState({}); + const [deviceIcons, setDeviceIcons] = useState( + [], + ); + const hasSchema = isEmpty(schema); - const { iconMap = {} } = useContext(TopologyContext); + const { iconMap = {} } = useContext(TopologyContext); - const form = createForm({ - effects() { - // 当用户改变表单项的时候更新到节点 - onFieldInputValueChange('*', async (field) => { - if (!props.node) { - return; - } - await field.validate(); - const inputValue = field.inputValue; - // 查询出节点上配置的属性数据 - const nodeComponentProps: Record = getNodeComponentProps( - props.node, - ); - const pathName = field.path.toString(); - const preValue = get(nodeComponentProps, pathName); - if (inputValue === preValue) { - return; + const form = createForm({ + effects() { + // 当用户改变表单项的时候更新到节点 + onFieldInputValueChange('*', async (field) => { + if (!props.node) { + return; + } + await field.validate(); + const inputValue = field.inputValue; + // 查询出节点上配置的属性数据 + const nodeComponentProps: Record = + getNodeComponentProps(props.node); + const pathName = field.path.toString(); + const preValue = get(nodeComponentProps, pathName); + if (inputValue === preValue) { + return; + } + if (props.node.isEdge()) { + const newProps = set( + nodeComponentProps, + pathName, + inputValue, + ); + props.node.prop(newProps); + } else if (props.node.isNode()) { + props.node.setData( + { + componentProps: { + ...nodeComponentProps, + [pathName]: inputValue, + }, + }, + { deep: false }, + ); + } + }); + }, + }); + + const handleNodeChange = (node: Cell | null) => { + // 将表单的值清空 + form.setValues({}, 'overwrite'); + if (!node) { + return; } - if (props.node.isEdge()) { - const newProps = set(nodeComponentProps, pathName, inputValue); - props.node.prop(newProps); - } else if (props.node.isNode()) { - props.node.setData( + const nodeComponentProps = getNodeComponentProps(node); + // 必须使用展开运算符,否则会丢失引用类型的值 + form.setValues( { - componentProps: { ...nodeComponentProps, - [pathName]: inputValue, - }, }, - { deep: false }, - ); - } - }); - }, - }); - - const handleNodeChange = (node: Cell | null) => { - // 将表单的值清空 - form.setValues({}, 'overwrite'); - if (!node) { - return; - } - const nodeComponentProps = getNodeComponentProps(node); - // 必须使用展开运算符,否则会丢失引用类型的值 - form.setValues( - { - ...nodeComponentProps, - }, - 'overwrite', - ); - }; + 'overwrite', + ); + }; - useEffect(() => { - const title = getTitle(props.node); - const tempSchema = props.node?.shape - ? props.schemaMap?.[props.node.shape] - : {}; - setTitle(title); - setSchema(tempSchema || {}); - }, [props.node]); + useEffect(() => { + const title = getTitle(props.node); + const tempSchema = props.node?.shape + ? props.schemaMap?.[props.node.shape] + : {}; + setTitle(title); + setSchema(tempSchema || {}); + }, [props.node]); - useEffect(() => { - handleNodeChange(props.node); - }, [schema, form, props.node]); + useEffect(() => { + handleNodeChange(props.node); + }, [schema, form, props.node]); - useEffect(() => { - const result = reduce( - iconMap, - (result, cur) => { - result.push(cur); - return result; - }, - [] as Topology.TopologyIconProp[], - ); - setDeviceIcons(result); - }, [iconMap]); + useEffect(() => { + const result = reduce( + iconMap, + (result, cur) => { + result.push(cur); + return result; + }, + [] as Topology.TopologyIconProp[], + ); + setDeviceIcons(result); + }, [iconMap]); - // const loadDeviceIcons = async () => { - // return new Promise((resolve) => { - // const result = reduce(iconMap, (result, cur) => { - // result.push(cur) - // return result; - // }, [] as Topology.TopologyIconProp[]) - // resolve(result) - // }) - // } + // const loadDeviceIcons = async () => { + // return new Promise((resolve) => { + // const result = reduce(iconMap, (result, cur) => { + // result.push(cur) + // return result; + // }, [] as Topology.TopologyIconProp[]) + // resolve(result) + // }) + // } - // const useAsyncDataSource = (service) => (field) => { - // field.loading = true - // service(field).then( - // action?.bound?.((data) => { - // field.dataSource = data - // field.loading = false - // }) - // ) - // } + // const useAsyncDataSource = (service) => (field) => { + // field.loading = true + // service(field).then( + // action?.bound?.((data) => { + // field.dataSource = data + // field.loading = false + // }) + // ) + // } - return ( - - {hasSchema ? ( - - ) : ( -
- - - )} -
- ); + {hasSchema ? ( + + ) : ( +
+ + + )} + + ); }; PropsPanel.defaultProps = { - node: null, - schemaMap: {}, - components: {}, + node: null, + schemaMap: {}, + components: {}, }; export default PropsPanel; diff --git a/src/contexts/topology.ts b/src/contexts/topology.ts index 5bc6ab7..6ef99a0 100644 --- a/src/contexts/topology.ts +++ b/src/contexts/topology.ts @@ -1,12 +1,11 @@ -import type { Topology } from '@/types/global.d'; -import EventBus from '@/utils/event-bus'; import { Graph } from '@antv/x6'; import { createContext } from 'react'; +import EventBus from '@/utils/event-bus'; interface ITopologyContext { - graph: Graph; - iconMap: Record; - eventBus: EventBus; + graph: Graph; + iconMap: Record; + eventBus: EventBus; } export default createContext>({}); diff --git a/src/core/snippet.ts b/src/core/snippet.ts index d27b950..367e688 100644 --- a/src/core/snippet.ts +++ b/src/core/snippet.ts @@ -1,10 +1,9 @@ -import type { Topology } from '@/types/global.d'; import type { Edge, Graph, Node } from '@antv/x6'; import defaultPorts from '../constants/default-ports'; export type TSnippetOption = { - graph: Graph; - snippetNode: Node; + graph: Graph; + snippetNode: Node; }; /** @@ -12,89 +11,95 @@ export type TSnippetOption = { * @param position */ function createSnippet(option: TSnippetOption) { - const { graph, snippetNode } = option; - const nodeData = snippetNode.getData(); - const { children: snippetProps } = nodeData.componentProps; - const position = snippetNode.getPosition(); - const size = snippetNode.getSize(); - const containerParentNode = snippetNode.getParent(); - graph.removeNode(snippetNode, { disconnectEdges: true, dryrun: true }); - // 查找配置的父层容器 - const containerOption: Topology.SnippetNode = snippetProps.nodes.find( - (item: any) => !item.parent, - ); - const containerNode: Node = graph.addNode({ - shape: containerOption.component, - embeddable: containerOption.embeddable, - data: { - componentProps: containerOption.componentProps, - }, - position, - size: size, - ports: defaultPorts, - }); - // 存储配置的id跟添加到画板中的节点Id的映射(配置连接线的时候需要使用) - const idMap: Record = {}; - // 添加子节点 - const childNodes: Topology.SnippetNode[] = snippetProps.nodes.filter( - (item: any) => item.parent, - ); - const containerZindex = containerNode.getZIndex() ?? 1; - childNodes.forEach((item) => { - const itemPosition = item.position; - // 如果是相对位置则加上父节点的位置 - if (item.relative !== false && itemPosition) { - itemPosition.x = itemPosition.x + position.x; - itemPosition.y = itemPosition.y + position.y; - } - const newNode = graph.createNode({ - position: itemPosition, - shape: item.component, - data: { - componentProps: item.componentProps, - }, - size: item.size, - zIndex: containerZindex + 1, - ports: defaultPorts, + const { graph, snippetNode } = option; + const nodeData = snippetNode.getData(); + const { children: snippetProps } = nodeData.componentProps; + const position = snippetNode.getPosition(); + const size = snippetNode.getSize(); + const containerParentNode = snippetNode.getParent(); + graph.removeNode(snippetNode, { disconnectEdges: true, dryrun: true }); + // 查找配置的父层容器 + const containerOption: Topology.SnippetNode = snippetProps.nodes.find( + (item: any) => !item.parent, + ); + const containerNode: Node = graph.addNode({ + shape: containerOption.component, + embeddable: containerOption.embeddable, + data: { + componentProps: containerOption.componentProps, + }, + position, + size: size, + ports: defaultPorts, + }); + // 存储配置的id跟添加到画板中的节点Id的映射(配置连接线的时候需要使用) + const idMap: Record = {}; + // 添加子节点 + const childNodes: Topology.SnippetNode[] = snippetProps.nodes.filter( + (item: any) => item.parent, + ); + const containerZindex = containerNode.getZIndex() ?? 1; + childNodes.forEach((item) => { + const itemPosition = item.position; + // 如果是相对位置则加上父节点的位置 + if (item.relative !== false && itemPosition) { + itemPosition.x = itemPosition.x + position.x; + itemPosition.y = itemPosition.y + position.y; + } + const newNode = graph.createNode({ + position: itemPosition, + shape: item.component, + data: { + componentProps: item.componentProps, + }, + size: item.size, + zIndex: containerZindex + 1, + ports: defaultPorts, + }); + containerNode.addChild(newNode); + idMap[item.id] = newNode; }); - containerNode.addChild(newNode); - idMap[item.id] = newNode; - }); - if (containerParentNode) { - containerParentNode.addChild(containerNode); - } - // 添加连线 - const edges: Edge.Metadata[] = snippetProps.edges?.map((edgeConfig: any) => { - const sourceCell = idMap[edgeConfig.source.cell]; - const targetCell = idMap[edgeConfig.target.cell]; - const [sourceCellPort] = sourceCell.getPortsByGroup(edgeConfig.source.port); - const [targetCellPort] = targetCell.getPortsByGroup(edgeConfig.target.port); - return { - attrs: { - line: { - stroke: 'var(--topology-editor-border-color)', - strokeWidth: 1, - targetMarker: null, - sourceMarker: null, + if (containerParentNode) { + containerParentNode.addChild(containerNode); + } + // 添加连线 + const edges: Edge.Metadata[] = snippetProps.edges?.map( + (edgeConfig: any) => { + const sourceCell = idMap[edgeConfig.source.cell]; + const targetCell = idMap[edgeConfig.target.cell]; + const [sourceCellPort] = sourceCell.getPortsByGroup( + edgeConfig.source.port, + ); + const [targetCellPort] = targetCell.getPortsByGroup( + edgeConfig.target.port, + ); + return { + attrs: { + line: { + stroke: 'var(--topology-editor-border-color)', + strokeWidth: 1, + targetMarker: null, + sourceMarker: null, + }, + }, + router: 'normal', + connector: 'rounded', + ...edgeConfig, + source: { + cell: sourceCell, + port: sourceCellPort?.id, + }, + target: { + cell: targetCell, + port: targetCellPort?.id, + }, + }; }, - }, - router: 'normal', - connector: 'rounded', - ...edgeConfig, - source: { - cell: sourceCell, - port: sourceCellPort?.id, - }, - target: { - cell: targetCell, - port: targetCellPort?.id, - }, - }; - }); - if (Array.isArray(edges) && edges.length > 0) { - graph.addEdges(edges); - } + ); + if (Array.isArray(edges) && edges.length > 0) { + graph.addEdges(edges); + } } export default createSnippet; diff --git a/src/index.ts b/src/index.ts index 27907b1..4f18f26 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,5 @@ -export type { Topology } from './types/global' +export { default as Editor } from './Editor/index'; -export { default as Editor } from './Editor/index' - -export { default as Preview } from './Preview/index' +export { default as Preview } from './Preview/index'; export { default as defaultPropsSchema } from './constants/default-props-schema'; diff --git a/src/types.d.ts b/src/types.d.ts index e69de29..113536e 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -0,0 +1,196 @@ +// 扩展现有模块 +// declare module 'xxg'{ +// interface Test{ +// test:string; +// } +// } + +declare module '*.svg' { + const content: React.FunctionComponent>; + export default content; +} + +declare module '*.png' { + const src: string; + export default src; +} + +declare module '*.jpeg' { + const src: string; + export default src; +} + +declare module '*.less' { + const content: { + [className: string]: string; + }; + export default content; +} + +declare namespace Topology { + /** + * 连接线 + */ + export type Line = { + router: string; + connector?: string; + source?: { + nodeId: string; + port: string; + }; + target?: { + nodeId: string; + port: string; + }; + }; + /** + * 节点 + */ + export type Node = { + /** + * 唯一标识Id + */ + id: string; + /** + * 业务名称(一般对应业务,如: PC、WebApplication) + */ + label?: string; + /** + * 使用的基础组件 + */ + component: string; + /** + * 资产展示态 + */ + icon?: string; + /** + * 是否允许作为父容器 + */ + embeddable?: boolean; + size: { + width: number; + height: number; + }; + /** + * 透传给基础组件的属性定义 + */ + componentProps?: any; + /** + * 跨行 + */ + rowSpan?: number; + /** + * 跨列 + */ + columnSpan?: number; + }; + export type SnippetNode = { + /** + * 唯一标识Id + */ + id: string; + /** + * 所属父节点Id + */ + parent?: string; + /** + * 使用的基础组件 + */ + component: string; + /** + * 是否允许作为父容器 + */ + embeddable?: boolean; + size: { + width: number; + height: number; + }; + position?: { + x: number; + y: number; + }; + relative?: boolean; + /** + * 透传给基础组件的属性定义 + */ + componentProps: any; + }; + export type SnippetEdge = { + source: { + cell: string; + port?: string; + }; + target: { + cell: string; + port?: string; + }; + }; + /** + * 资产定义 + */ + type Material = { + name: string; + /** + * 展示的分组名称(为空则不以组的形式展示) + */ + label: string; + items: Array; + }; + export type Materials = Array; + /** + * 拓扑图结构 + */ + export type Graph = { + /** + * 拓扑图的唯一标识 + */ + id: string; + /** + * 用来区别是第几张图(1:技防图、2:资产分布图、3:资产拓扑图) + */ + type: number; + /** + * 存储画板尺寸 + */ + size: { + /** + * 画板实际宽度 + */ + width: string; + /** + * 画板实际高度 + */ + height: string; + }; + /** + * 版本号 + */ + version?: string; + /** + * 描述 + */ + description?: string; + /** + * 缩略图 + */ + thumb: string; + /** + * 存储 x6 的数据结构 + */ + graph: any; + [key: string]: any; + }; + + export type PropSchema = { + [key: string]: any; + }; + export type Size = { + height: number; + width: number; + }; + export type TopologyIconProp = { + label: string; + value: string; + icon: React.FunctionComponent>; + }; +} diff --git a/src/types/declare.d.ts b/src/types/declare.d.ts deleted file mode 100644 index 8b8ac32..0000000 --- a/src/types/declare.d.ts +++ /dev/null @@ -1,21 +0,0 @@ -declare module '*.svg' { - const content: React.FunctionComponent>; - export default content; -} - -declare module '*.png' { - const value: string; - export default value; -} - -declare module '*.jpeg' { - const value: string; - export default value; -} - -declare module '*.less' { - const content: { - [className: string]: string - }; - export default content; -} diff --git a/src/types/global.d.ts b/src/types/global.d.ts deleted file mode 100644 index 5c5436f..0000000 --- a/src/types/global.d.ts +++ /dev/null @@ -1,167 +0,0 @@ -export namespace Topology { - /** - * 连接线 - */ - export type Line = { - router: string; - connector?: string; - source?: { - nodeId: string; - port: string; - }; - target?: { - nodeId: string; - port: string; - }; - }; - /** - * 节点 - */ - export type Node = { - /** - * 唯一标识Id - */ - id: string; - /** - * 业务名称(一般对应业务,如: PC、WebApplication) - */ - label?: string; - /** - * 使用的基础组件 - */ - component: string; - /** - * 资产展示态 - */ - icon?: string; - /** - * 是否允许作为父容器 - */ - embeddable?: boolean; - size: { - width: number; - height: number; - }; - /** - * 透传给基础组件的属性定义 - */ - componentProps?: any; - /** - * 跨行 - */ - rowSpan?: number; - /** - * 跨列 - */ - columnSpan?: number; - }; - export type SnippetNode = { - /** - * 唯一标识Id - */ - id: string; - /** - * 所属父节点Id - */ - parent?: string; - /** - * 使用的基础组件 - */ - component: string; - /** - * 是否允许作为父容器 - */ - embeddable?: boolean; - size: { - width: number; - height: number; - }; - position?: { - x: number; - y: number; - }; - relative?: boolean; - /** - * 透传给基础组件的属性定义 - */ - componentProps: any; - }; - export type SnippetEdge = { - source: { - cell: string; - port?: string; - }; - target: { - cell: string; - port?: string; - }; - }; - /** - * 资产定义 - */ - type Material = { - name: string; - /** - * 展示的分组名称(为空则不以组的形式展示) - */ - label: string; - items: Array; - }; - export type Materials = Array; - /** - * 拓扑图结构 - */ - export type Graph = { - /** - * 拓扑图的唯一标识 - */ - id: string; - /** - * 用来区别是第几张图(1:技防图、2:资产分布图、3:资产拓扑图) - */ - type: number; - /** - * 存储画板尺寸 - */ - size: { - /** - * 画板实际宽度 - */ - width: string; - /** - * 画板实际高度 - */ - height: string; - }; - /** - * 版本号 - */ - version?: string; - /** - * 描述 - */ - description?: string; - /** - * 缩略图 - */ - thumb: string; - /** - * 存储 x6 的数据结构 - */ - graph: any; - [key: string]: any; - }; - - export type PropSchema = { - [key: string]: any; - }; - export type Size= { - height:number; - width:number; - } - export type TopologyIconProp={ - label: string, - value: string, - icon: React.FunctionComponent> - } -} diff --git a/tsconfig.json b/tsconfig.json index d626d1d..381c00f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,20 +1,20 @@ { - "compilerOptions": { - "typeRoots": ["./typings", "./node_modules/@types"], - "strict": true, - "declaration": true, - "skipLibCheck": true, - "esModuleInterop": true, - "noImplicitAny": false, - "resolveJsonModule": true, - "jsx": "react", - "baseUrl": "./", - "paths": { - "@/*": ["src/*"], - "@@/*": [".dumi/tmp/*"], - "topology-designable": ["src"], - "topology-designable/*": ["src/*", "*"] - } - }, - "include": [".dumirc.ts", "src/**/*"] + "compilerOptions": { + // "typeRoots": ["./typings", "./node_modules/@types"], + "strict": true, + "declaration": true, + "skipLibCheck": true, + "esModuleInterop": true, + "noImplicitAny": false, + "resolveJsonModule": true, + "jsx": "react", + "baseUrl": "./", + "paths": { + "@/*": ["src/*"], + "@@/*": [".dumi/tmp/*"], + "topology-designable": ["src"], + "topology-designable/*": ["src/*", "*"] + } + }, + "include": [".dumirc.ts", "src/**/*"] }