头图

上一篇文章主要对可视化的页面进行展示及改造,本篇主要对自定义节点进行说明。

流程图节点组件

上一篇页面左侧的流程节点组件为FlowchartNodePanel,这个组件是基于NodeCollapsePanel 封装的,提供了一些内置的常用节点,还可以添加一些自定义节点,但是缺点在于内置的节点无法添加我们自定义的属性,所以只能作于画图展示,并无流程节点的实际意义。
所以我们这里去除掉FlowchartNodePanel,使用扩展性更强的(原始的)NodeCollapsePanel进行替换,但是NodeCollapsePanel很多方法需要我们自行添加,本文将一步一步进行说明。

流程节点要注意的有两点,一个是左侧的节点列表,这个列表提供了可拖拽的节点在主视图进行添加,二是主视图展示的节点(反显及拖拽展示),这两个节点实际上都是使用流程节点组件提供的。

自定义节点

首先我们把上一章的组件FlowchartNodePanel删除,新建一个文件夹CustomNodeCollapsePanel,新建index.tsxconfig-dnd-panel.tsx,前者是我们的自定义左侧组件,后者是自定义节点的各个方法

// index.tsx
import { NodeCollapsePanel } from '@antv/xflow';
import { FC } from 'react';
import * as panelConfig from './config-dnd-panel';

const CustomNodeCollapsePanel: FC = () => {
  return (
    <NodeCollapsePanel
      footer={<div> Foorter </div>}
      onNodeDrop={panelConfig.onNodeDrop}
      searchService={panelConfig.searchService}
      nodeDataService={panelConfig.nodeDataService}
      position={{ top: 40, bottom: 0, left: 0, width: 290 }}
    />
  );
};

export default CustomNodeCollapsePanel;

searchService为搜索节点,onNodeDrop为左侧节点列表拖拽在主视图上的回调,我们用它在主视图上创建节点,nodeDataService为返回我们节点列表数组。

我们接下来对几个方法进行一一添加说明:

渲染节点列表

// config-dnd-panel.tsx
import {
  NsNodeCollapsePanel,
  NsNodeCmd,
  uuidv4,
  XFlowNodeCommands,
  IFlowchartGraphProps,
} from '@antv/xflow';
import Nodes from './Nodes';

const renderNode = (
  props: {
    data: NsNodeCollapsePanel.IPanelNode;
    isNodePanel: boolean;
  },
  style: { width: number; height: number },
) => {
  const Node = Nodes[props.data.renderKey!];
  return Node ? <Node {...props} style={style} /> : null;
};

const nodeList = (arr: any[]) => {
  const newArr = arr.map((s) => {
    const attrs = s.attrs;
    return {
      popoverContent: () => <div>{s.label}</div>,
      renderKey: s.renderKey || 'CustomNode',
      renderComponent: (props: any) => renderNode(props, attrs.style),
      label: s.label,
      id: s.id,
      attrs: attrs,
      ...attrs.canvansStyle,
    };
  });
  return newArr;
};

export const nodeDataService: NsNodeCollapsePanel.INodeDataService =
  async () => {
    // 这里可以通过接口获取节点列表
    const resData = [
      {
        id: 1,
        renderKey: 'CustomNode',
        label: '开始',
        attrs: {
          style: {
            width: 280,
            height: 40,
          },
          canvansStyle: {
            width: 120,
            height: 40,
          },
        },
      },
      {
        id: 2,
        renderKey: 'CustomConnecto',
        label: '审核节点',
        attrs: {
          style: {
            width: 80,
            height: 80,
          },
          canvansStyle: {
            width: 80,
            height: 80,
          },
        },
      },
    ];
    return [
      {
        id: 'NODE',
        header: '节点',
        children: nodeList(resData),
      },
    ];
  };

我们把接口返回的节点属性转换为面板Dnd节点所需要的属性结构,我这里声明了两个宽高,主要是一个用于canvas主视图展示的节点宽高,一个为节点列表中组件的宽高。
我们的节点渲染实际上是通过renderComponent返回的组件进行渲染的,renderKey的作用为我们视图上已有节点进行反显时,可以根据renderKey来渲染节点列表中对应的节点。

自定义节点

新建CustomNodeCollapsePanel/Nodes/index.ts,我们这里使用批量导出,Nodes里面建立每个节点的文件夹,使用index.ts进行批量导出,先安装解析require.context的包yarn add @types/webpack-env -D

// index.ts
const files = require.context('.', true, /index\.tsx$/);
const modules: { [key: string]: any } = {};

function isPromise(obj: Promise<any> | null | undefined) {
  return (
    obj !== null &&
    obj !== undefined &&
    typeof obj.then === 'function' &&
    typeof obj.catch === 'function'
  );
}

files.keys().forEach(async (key) => {
  const pathArr = key.replace(/(\.\/|\.tsx)/g, '').split('/');
  pathArr.pop();
  const moduleName = pathArr.join('/').replace(/\/\w{1}/g, function (val) {
    return val.substring(1, 2).toUpperCase();
  });
  const module = isPromise(files(key)) ? await files(key) : files(key);
  modules[moduleName] = module.default;
});

export default modules;

require.context是webpack提供的检索api,这里多了个Promise的判断,如果umi开启了mfsu,拿到的module会是一个按需的异步导出,故判断处理,这个index.ts的作用是遍历Nodes中的所有目录中的index.tsx,然后将其组装成一个对象返回,列如:

aaa/bbb/index.tsx
ccc/index.tsx

{
    aaaBbb: module
    ccc: module
}

接下来我们创建一下我们的自定义节点CustomNodeCustomConnecto同理就略过了,新建Nodes/CustomNode/index.tsx

import type { FC } from 'react';
import type { NsNodeCollapsePanel } from '@antv/xflow';
import styles from './index.less';

interface CustomNodeProps {
  data: NsNodeCollapsePanel.IPanelNode;
  isNodePanel: boolean;
  style: React.CSSProperties;
}

const CustomNode: FC<CustomNodeProps> = ({ data, style, isNodePanel }) => {
  return (
    <div
      style={style}
      className={`${styles.customNode} ${isNodePanel ? 'isNodePanel' : ''}`}
    >
      {data.label}
    </div>
  );
};

export default CustomNode;

index.less:

.customNode {
  width: 100%;
  height: 100%;
  border: 1px solid rgb(162, 177, 195);
  display: flex;
  justify-content: center;
  align-items: center;
  border-radius: 4px;
  &:global(.isNodePanel) {
    border-color: red;
  }
}

我们这里建立一个简单的节点,接受传过来的节点属性以及样式,可以用isNodePanel这个值来区分是节点列表渲染还是主视图反显渲染,为true时为前者,否则为后者,所以可以用这个值进行不同的返回,比如组件列表是一个圆形,拖拽到主视图可以变成一个方形,当然我们这里只是简单的把边框颜色修改一下,可自行修改样式。需要注意的是节点的样式宽高不能写死,因为我们主画布上的图形是可以拖动改变宽高的,所以我们接受了传入style的宽高为初始宽高。

反显节点

在进行canvans视图中的节点渲染时,我们使用setNodeRender设置当renderKeyCustomNode时,使用我们的CustomNode组件进行渲染。

// config-dnd-panel.tsx
export const useGraphConfig: IFlowchartGraphProps['useConfig'] = (config) => {
  Object.keys(Nodes).map((key) => {
    config.setNodeRender(key, (props) => {
      return renderNode({ data: props.data, isNodePanel: false }, props.size);
    });
  });
};

// Xflow/index.tsx
import { useGraphConfig } from './CustomNodeCollapsePanel/config-dnd-panel';
<FlowchartCanvas
  useConfig={useGraphConfig}
  position={{ top: 40, left: 0, right: 0, bottom: 0 }}
>
...
</FlowchartCanvas>

拖拽生成节点

我们进行左侧组件列表拖拽到主视图时会触发onNodeDrop,我们在这个事件里进行节点的添加:

// CustomNodeCollapsePanel/index.tsx
import {
  NsNodeCollapsePanel,
  NsNodeCmd,
  uuidv4,
  XFlowNodeCommands,
  IFlowchartGraphProps,
} from '@antv/xflow';
import getPorts from './ports';
export const onNodeDrop: NsNodeCollapsePanel.IOnNodeDrop = async (
  nodeConfig,
  commandService,
) => {
  const args: NsNodeCmd.AddNode.IArgs = {
    nodeConfig: { ...nodeConfig, id: uuidv4(), ports: getPorts() },
  };
  commandService.executeCommand<NsNodeCmd.AddNode.IArgs>(
    XFlowNodeCommands.ADD_NODE.id,
    args,
  );
};

当然,添加了节点还不够,节点需要连线的,所以我们新建一个ports.ts,用来生成连线所需的连接桩:

// CustomNodeCollapsePanel/ports.ts
import { uuidv4 } from '@antv/xflow';

const getAnchorStyle = (position: string) => {
  return {
    position: { name: position },
    attrs: {
      circle: {
        r: 4,
        magnet: true,
        stroke: '#31d0c6',
        strokeWidth: 2,
        fill: '#fff',
        style: {
          visibility: 'hidden',
        },
      },
    },
    zIndex: 10,
  };
};

const getPorts = (position = ['top', 'right', 'bottom', 'left']) => {
  return {
    items: position.map((name) => {
      return { group: name, id: uuidv4() };
    }),
    groups: {
      top: getAnchorStyle('top'),
      right: getAnchorStyle('right'),
      bottom: getAnchorStyle('bottom'),
      left: getAnchorStyle('left'),
    },
  };
};

export default getPorts;

搜索节点

最后我们添加一下搜索节点功能:

// CustomNodeCollapsePanel/index.tsx
export const searchService: NsNodeCollapsePanel.ISearchService = async (
  nodes: NsNodeCollapsePanel.IPanelNode[] = [],
  keyword: string,
) => {
  const list = nodes.filter((node) => node?.label?.includes(keyword));
  return list;
};

自定义节点反显测试

我们添加几个自定义节点,连接起来,在点击保存时把数据存在localStorage,然后在onLoad时赋值,看看能否正常反显我们的自定义节点:

// Xflow/config-toolbar.ts
找到saveGraphDataService:
saveGraphDataService: (meta, graphData) => {
  console.log(graphData);
  localStorage.setItem('graphData', JSON.stringify(graphData));
  return null;
},

// Xflow/index.tsx
const onLoad: IAppLoad = async (app) => {
    graphRef.current = await app.getGraphInstance();
    const graphData: NsGraph.IGraphData = JSON.parse(
      localStorage.getItem('graphData')!,
    ) || { nodes: [], edges: [] };
    await app.executeCommand<NsGraphCmd.GraphRender.IArgs>(
      XFlowGraphCommands.GRAPH_RENDER.id,
      {
        graphData: graphData,
      },
    );
    // 居中
    await app.executeCommand<NsGraphCmd.GraphZoom.IArgs>(
      XFlowGraphCommands.GRAPH_ZOOM.id,
      {
        factor: 'real',
      },
    );
    graphBind();
  };

自定义节点

好了,这样我们的自定义节点就基本完成了,实现了左侧列表节点及样式,主视图节点及样式,添加连接桩,连线,主视图反显等功能,但是这时原先右侧的节点编辑功能有些失效了,且现在的编辑功能不能满足我们的需要,接下来下一篇将介绍自定义节点的编辑功能,编辑自定义节点上的各种属性,尽情期待。

本文地址:链接
本文github地址:链接
github demo地址:链接


陌路凡歌
7.9k 声望258 粉丝

人笨,多听,多写,多看书