npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

vue-x6-editor

v0.0.5

Published

[toc]

Downloads

14

Readme

[toc]

简介

  • 是一个基于@antv/x6以及plain-ui-composition封装的Vue3.0可视化流程编辑组件;
  • 旨在于封装开箱即用的常用功能,包括快速定义画布组件、快速定义画布React组件、撤销重做、放大缩小、数据导入导出、冻结画布、拦截新增(删除)节点(边)等功能;

安装

安装依赖

npm i vue-x6-editor plain-ui-compositoin @antv/x6 @antv/x6-vue-shape -S

示例代码

  • plain-ui-composition是一个基于Vue3.0 CompositionAPI封装的插件库,旨在于提供更好的组件与TS类型提示开发体验。
  • 目前仅支持组合式API的方式使用。

JSX使用示例

import {createApp} from "vue";
import {designPage} from 'plain-ui-composition';
import {createBranchNode, createEndNode, createFlowEdge, createFlowNode, createRenderNode, createStartNode, ReactX6Editor, useFlowEditor} from "vue-x6-editor";
import 'vue-x6-editor/dist/vue-x6-editor.css'
import './main.scss';

const App = designPage(() => {

  const editor = useFlowEditor({
    onlyOneStartNode: true,
    operators: [
      {
        label: '选项一',
        icon: () => (<i class="iconfont icon-pay_wechat" style={{ height: '27px', display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }}/>),
        handler: () => console.log('option 1...')
      }
    ]
  });
  editor.components.registry(createStartNode());
  editor.components.registry(createFlowNode());
  editor.components.registry(createBranchNode());
  editor.components.registry(createEndNode());
  editor.components.registry(createFlowEdge());
  editor.components.registry(createRenderNode());

  import('./flow.data.json').then(data => {editor.methods.update(data.default);});

  editor.hooks.onCreateContextmenu.use(({ options, edge, node, type }) => {
    switch (type) {
      case 'node':
        options.push({
          label: '自定义node选项2',
          icon: () => <i class="iconfont icon-pay_zhifubao" style={{ padding: '0 4px' }}/>,
          handler: () => alert('自定义node选项2')
        });
        return;
      case 'edge':
        options.push({
          label: '自定义edge选项',
          icon: 'data',
          handler: () => alert('自定义edge选项')
        });
        return;
      case 'vertex':
        options.push({
          label: '自定义拐点选项',
          icon: 'data',
          handler: () => alert('自定义拐点选项')
        });
        return;
      case 'blank':
        options.push({
          label: '自定义白板选项',
          icon: 'data',
          handler: () => alert('自定义白板选项')
        });
        return;
    }
  });

  return () => (
    <div style={{ height: '100%', width: '100%' }}>
      <ReactX6Editor editor={editor}/>
      {/*<div style={{ position: 'absolute', bottom: 0, right: 0 }}>
        {JSON.stringify(editor.state.data)}
      </div>*/}
    </div>
  );
});

createApp(<App/>).mount('#app');

Template使用示例

  • App.vue
<template>
  <div :style="{ height: '100%', width: '100%' }">
    <ReactX6Editor :editor="editor"/>
  </div>
</template>

<script setup lang="tsx">

import {createBranchNode, createEndNode, createFlowEdge, createFlowNode, createRenderNode, createStartNode, ReactX6Editor, useFlowEditor} from "vue-x6-editor";
import 'vue-x6-editor/dist/vue-x6-editor.css'
import './main.scss';

const editor = useFlowEditor({
  onlyOneStartNode: true,
  operators: [
    {
      label: '选项一',
      icon: () => (<i class="iconfont icon-pay_wechat" style={{ height: '27px', display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }}/>),
      handler: () => console.log('option 1...')
    }
  ]
});
editor.components.registry(createStartNode());
editor.components.registry(createFlowNode());
editor.components.registry(createBranchNode());
editor.components.registry(createEndNode());
editor.components.registry(createFlowEdge());
editor.components.registry(createRenderNode());

import('./flow.data.json').then(data => {editor.methods.update(data.default);});

editor.hooks.onCreateContextmenu.use(({ options, edge, node, type }) => {
  switch (type) {
    case 'node':
      options.push({
        label: '自定义node选项2',
        icon: () => <i class="iconfont icon-pay_zhifubao" style={{ padding: '0 4px' }}/>,
        handler: () => alert('自定义node选项2')
      });
      return;
    case 'edge':
      options.push({
        label: '自定义edge选项',
        icon: 'data',
        handler: () => alert('自定义edge选项')
      });
      return;
    case 'vertex':
      options.push({
        label: '自定义拐点选项',
        icon: 'data',
        handler: () => alert('自定义拐点选项')
      });
      return;
    case 'blank':
      options.push({
        label: '自定义白板选项',
        icon: 'data',
        handler: () => alert('自定义白板选项')
      });
      return;
  }
});
</script>
  • main.tsx
import {createApp} from "vue";
import App from './App.vue';

createApp(<App/>).mount('#app');

API说明

useFlowEditor对象参数类型

  • useFlowEditor(options)

|属性名称|类型|说明| |---|---|---| |height|string,number|画布高度| |width|string,number|画布宽度| |processGraphConfig|(data: { graphConfig: Partial, module: iGraphModule, config: iFlowEditorConfig }) => Partial|在创建graph对象之前,处理grapgConfig对象; |nodeConfig|Object|节点默认配置,请见下文nodeConfig说明| |onlyOneStartNode|boolean|只允许有一个开始节点| |cannotDeleteStartNode|boolean|不可以删除所有开始节点| |onlyOneEndNode|boolean|只允许有一个结束节点| |cannotDeleteEndNode|boolean|不可以删除所有结束节点|

  • nodeConfig

|属性名称|类型|说明| |---|---|---| |deepColor|string|节点深色(主色调)| |lightColor|string|节点浅色| |edgeLightColor|string|连接线浅色| |edgeDeepColor|string|连接线深色| |fontSize|string|节点文字大小| |height|nuimber|节点默认高度| |width|number|节点默认宽度|

x6画布参数设置

  • 通过属性graphConfig设置一些功能的启用状态,比如默认关闭网格
  • 关于画布的属性设置请见文档: API:Graph
const editor = useFlowEditor({
    graphConfig: {
      grid: false,
    }
});

自定义(继承)组件

  • editor.components.registry(createFlowNode())
  • createFlowNode如下示例所示:
export function createFlowNode(custom: { name?: string, icon?: string, defaultLabel?: string } = {}): iFlowEditorComponent {
  const code = 'flow-node';
  return {
    name: custom.name || '流程节点',
    icon: custom.icon || 'node',
    code,
    defaultNodeData: ({ config }) => ({
      width: config.nodeConfig.width,
      height: config.nodeConfig.height,
    }),
    process: ({ Graph, config }) => {
      Graph.registerNode(code, {
        inherit: 'rect',
        attrs: {
          body: {
            stroke: config.nodeConfig.lightColor,
            fill: config.nodeConfig.lightColor,
          },
          label: {
            text: custom.defaultLabel || '流程节点',
            fill: config.nodeConfig.deepColor,
            fontsize: config.nodeConfig.fontSize,
          }
        },
        ports: {
          ...createFlowConnectPorts(config),
        }
      }, true);
    }
  };
}

自定义(继承)连接线

  • editor.components.registry(createFlowEdge())
  • createFlowEdge如下示例所示,具体定义连接线请参考x6文档;
export function createFlowEdge(): iFlowEditorRegister {
  const code = 'flow-edge';
  return {
    code,
    process: ({ Graph, config }) => {
      Graph.registerEdge(code, {
        ...createDefaultEdge(config),
      }, true);
    },
  };
}

定义Vue渲染的节点

  • editor.components.registry(createRenderNode());
  • createRenderNode如下示例所示,示例中的DemoRenderNodeContent就是要实现的Vue组件;
import {iFlowEditorComponent} from "../utils/flow.type.base";
import {BaseVueComponent} from "../utils/BaseReactComponent";
import {DemoRenderNodeContent} from "./DemoRenderNodeContent";
import {createFlowConnectPorts} from "../utils/FlowConnectPorts";

export function createRenderNode(custom: { name?: string, icon?: string, defaultLabel?: string } = {}): iFlowEditorComponent {
  const code = 'render-node';
  return {
    name: custom.name || 'Vue节点',
    icon: custom.icon || 'data',
    code,
    defaultNodeData: ({ config }) => ({
      width: 300,
      height: 100,
    }),
    process: async ({ Graph, config }) => {
      Graph.registerNode(code, {
        inherit: 'vue-shape',
        component: (
          <BaseVueComponent config={config}>
            <DemoRenderNodeContent/>
          </BaseVueComponent>
        ),
        ports: {
          ...createFlowConnectPorts(config),
        }
      }, true);
    }
  };
}
  • DemoRenderNodeContent.tsx
import {designPage, reactive} from "plain-ui-composition";
import './DemoRenderNodeContent.scss';
import {defer, DFD} from 'plain-utils/utils/defer';
import {FlowIcon} from "./FlowIcon";

export const DemoRenderNodeContent = designPage(() => {

  interface iFormData {
    taskName: string,
  }

  const state = reactive({
    formData: {
      taskName: '简历:前端工程师',
    } as iFormData
  });

  const edit = (() => {
    const innerState = reactive({
      editData: null as null | iFormData,
    });
    let dfd: DFD<iFormData> | null = null;
    const open = () => {
      innerState.editData = JSON.parse(JSON.stringify(state.formData));
      dfd = defer<iFormData>();
      dfd.promise.then(val => {
        state.formData = val;
      });
    };
    const cancel = () => {
      dfd!.reject();
      innerState.editData = null;
      dfd = null;
    };
    const confirm = () => {
      dfd!.resolve(innerState.editData!);
      innerState.editData = null;
      dfd = null;
    };
    const renderEdit = (notEditContent: any) => {
      return !innerState.editData ? notEditContent : <>
        <input type="text" value={innerState.editData.taskName} onInput={(e: any) => innerState.editData!.taskName = e.target.value}/>
      </>;
    };
    const renderButton = (externals: any) => {
      return !innerState.editData ? <>
        <button onClick={open}>编辑</button>
        {externals}
      </> : <>
        <button onClick={cancel}>取消</button>
        <button onClick={confirm}>确认</button>
      </>;
    };
    return { innerState, open, cancel, confirm, renderEdit, renderButton };
  })();

  return () => (
    <div class="demo-render-node-content">
      <div class="demo-render-node-content-form">
        {edit.renderEdit(<>
          <div class="demo-render-node-content-form-browse">
            <div class="demo-render-node-content-form-icon">
              <FlowIcon icon="node"/>
            </div>
            <div>{state.formData.taskName}</div>
            <div>{(() => {
              const rate = Math.min(10, state.formData.taskName.length);
              return "★★★★★★★★★★☆☆☆☆☆☆☆☆☆☆".slice(10 - rate, 10 - rate + 10);
            })()}</div>
          </div>
        </>)}
      </div>
      <div class="demo-render-node-content-options">
        {edit.renderButton(<>
          <button onClick={() => alert('accept')}>接受</button>
          <button onClick={() => alert('refuse')}>拒绝</button>
          <button onClick={() => alert('transfer')}>转交</button>
        </>)}
      </div>
    </div>
  );
});

Hooks钩子(拦截器)

  • editor有很多内置的钩子函数,用于监听(拦截)各种行为,比如监听节点的点击事件:
editor.hooks.onClickNode.use(({ node }) => {
    console.log('click node', node);
});
  • 钩子函数可以阻止,在添加的拦截函数中抛出异常或者返回Promise.reject()就可以阻止行为。比如添加节点的时候限制只能有一个开始(结束)节点的控制行为:
hooks.onAddNode.use(async ({ node }) => {
if (node.shape == START_NODE) {
  if (!config.onlyOneStartNode) {return; }
  const { cells } = await methods.getGraphData();
  if (!!cells.find(i => i.shape === START_NODE && i.id !== node.id)) {
    alert('只能存在一个开始节点!');
    return Promise.reject('only one start node!');
  }
} else if (node.shape == END_NODE) {
  if (!config.onlyOneEndNode) {return; }
  const { cells } = await methods.getGraphData();
  if (!!cells.find(i => i.shape === END_NODE && i.id !== node.id)) {
    alert('只能存在一个结束节点!');
    return Promise.reject('only one end node!');
  }
}
});

所有可用的Hook钩子

export function useFlowEditorHooks() {

  const hooks = {
    onMouseupCanvas: createHooks<(e: MouseEvent) => void>(),                                // 点击画布动作
    onGraphLoaded: createHooks<(module: iGraphModule) => void>(),                           // Graph模块加载完毕
    onGraphReady: createHooks<(module: iGraphModule) => void>(),                            // graph初始化准备完毕触发动作,onGraphLoaded执行完毕之后就会执行这个动作
    onFlowMounted: createHooks<() => void>(),                                               // ReactX6Editor组件的mounted动作
    onProcessOperators: createSyncHooks<(operators: iFlowEditorOperatorMeta[]) => void>(),  // 处理操作栏按钮

    /*节点钩子*/
    onClickNode: createHooks<(e: NodeView.PositionEventArgs<any>) => void>(),               // 单击节点
    onDblclickNode: createHooks<(e: NodeView.PositionEventArgs<any>) => void>(),            // 双击节点
    onNodeContextmenu: createHooks<(e: NodeView.PositionEventArgs<any>) => void>(),         // 右击节点触发动作
    onAddNode: createHooks<(e: { node: Node, cell: Cell, index: number }) => void>(),       // 添加节点钩子
    onDeleteNode: createHooks<(e: { node: Node, cell: Cell, index: number }) => void>(),    // 删除节点钩子

    /*连接线钩子*/
    onClickEdge: createHooks<(e: EdgeView.PositionEventArgs<any>) => void>(),               // 单击连接线
    onDblclickEdge: createHooks<(e: EdgeView.PositionEventArgs<any>) => void>(),            // 双击连接线
    onEdgeContextmenu: createHooks<(e: EdgeView.PositionEventArgs<any>) => void>(),         // 右击边触发动作
    onVertexContextmenu: createHooks<(e: EdgeView.PositionEventArgs<any>) => void>(),       // 右击拐点触发动作
    onAddEdge: createHooks<(e: { edge: Edge, cell: Cell, index: number }) => void>(),       // 添加连接线钩子
    onDeleteEdge: createHooks<(e: { edge: Edge, cell: Cell, index: number }) => void>(),    // 删除连接线钩子
    onConnectedEdge: createHooks<(e: EdgeView.EventArgs["edge:connected"]) => void>(),      // 连接线连接节点钩子

    /*画布钩子*/
    onBlankContextmenu: createHooks<(e: CellView.PositionEventArgs) => void>(),             // 右击拐点触发动作

    /*其他*/
    onCreateContextmenu: createHooks<(e: { edge?: Edge, node?: Node, view?: View, type: eCreateContextmenuType | keyof typeof eCreateContextmenuType, options: iFlowContextmenuOption[], pos: { x: number, y: number } }) => void>(),
  };

  return hooks;
}

自定义右击菜单

  editor.hooks.onCreateContextmenu.use(({ options, edge, node, type }) => {
    switch (type) {
      case 'node':
        options.push({
          label: '自定义node选项',
          icon: 'data',
          handler: () => alert('自定义node选项')
        });
        options.push({
          label: '自定义node选项2',
          icon: () => <i className="iconfont icon-pay_zhifubao" style={{ padding: '0 4px' }}/>,
          handler: () => alert('自定义node选项2')
        });
        return;
    }
  });

自定义操作栏按钮

  const editor = useFlowEditor({
    onlyOneStartNode: true,
    operators: [
      {
        label: '选项一',
        icon: () => (<i className="iconfont icon-pay_wechat" style={{ height: '27px', display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }}/>),
        handler: () => console.log('option 1...')
      }
    ]
  });

可用函数methods

  • useFlowEditor创建的对象有许多可用函数,使用示例如下所示:
// 获取x6创建的graph对象
const { graph } = await editor.getGraph();
// 获取所有的cell
const cells = graph.getSelectedCells();
  • 所有可用函数如下所示

|函数名称|类型|说明| |---|---|---| | getGraph | ()=>Promise | 异步获取graph实例对象,因为是按需加载antv/x6以及需要等待Editor挂载,所以graph初始化是一个异步的过程 | | update | (data:iFlowData)=>Promise | 更新数据 | | getGraphData | ()=><{ cells: iFlowMetaData[] }> | 从graph中解析json数据 | | deleteSelection | ()=>Promise) | 删除选中节点 | | undo | ()=>:Promise | 撤销 | | redo | ()=>:Promise | 重做 | | undoAndClearRedo | ()=>Promise | ,撤回,并且清空重做列表 | | setFrozen | (frozen?:boolean)=>Promise | 冻结 |

createFlowEditorUser

  • 可以通过函数createFlowEditorUser自定义默认配置的组合函数,比如useFlowEditor的源码如下所示
export const useFlowEditor = createFlowEditorUser({
  height: '100%',
  width: '100%',
  processGraphConfig: ({ graphConfig, module, config }) => {
    return Object.assign({}, {
      background: { color: '#fff' },        // 默认背景色为白色
      grid: {                               // 默认开启网格
        size: 10,
        visible: true,
      },
      connecting: {                         // 默认连接线为平滑连接线
        connector: 'smooth',
        allowBlank: false,
        allowLoop: false,
        allowEdge: false,
        highlight: true,
        createEdge() {
          return new module.Shape.Edge({
            shape: 'flow-edge',
            ...createDefaultEdge(config)
          });
        },
      },
      panning: true,                        // 拖拽画布移动所有节点
      resizing: true,                       // 节点选中之后可以调整大小
      rotating: true,                       // 节点选中之后可以旋转
      selecting: {                          // 默认开启框选功能
        enabled: true,
        rubberband: true,
        showNodeSelectionBox: true,
        modifiers: ['shift', 'ctrl', 'meta'],
      },
      snapline: {                           // 辅助对齐线
        enabled: true,
        clean: false,
      },
      keyboard: true,                       // 开启键盘快捷键功能
      clipboard: true,                      // 开启剪切板功能
      history: true,                        // 开启操作历史功能
    } as X6GraphConfig, graphConfig);
  },
  nodeConfig: {
    deepColor: '#1f74ff',                   // 深色(主色调)
    lightColor: '#deeaff',                  // 浅色(主色调背景色)
    edgeLightColor: '#b1cdfa',              // 连接线浅色
    edgeDeepColor: '#1f74ff',               // 连接线深色
    fontSize: '14',                         // 节点文字大小
    width: 80,                              // 默认宽度
    height: 40,                             // 默认高度
  },
  onlyOneStartNode: false,
  onlyOneEndNode: false,
  cannotDeleteStartNode: false,
  cannotDeleteEndNode: false,
});

操作说明

节点连接桩

  • 鼠标进入节点会显示节点的连接桩,拖拽连接桩到另一个节点或者另一个节点的连接桩,会创建一条连接线;
  • 点击节选会使得节点处于选中状态,处于选中状态的节点不会显示连接桩;但是此时可以拖拽调整节点大小。
  • 默认情况下,一个节点连接到另一个节点,不会保留连接线的开始节点连接桩以及目标节点的连接桩。右击节点,启用【保留连接桩】,此时从该节点连接的连线在创建的时候就不会自动去掉连接桩,同理如果这个节点为连接线的目标节点也是一样。如果需要保留连接线的连接桩,需要右击节点勾选保留连接桩;

连接线拐点

  • 默认情况下拐点的功能,是单击(双击)拖拽连接线添加拐点,或者移动拐点,双击存在的拐点删除拐点。
  • 为了支持业务需求,目前默认禁用了这些行为。单击拖拽连接线空白处不会添加拐点,双击拐点不会删除拐点。而是右击连接线,在右击的位置添加拐点,右击拐点选择删除拐点。
  • 单击连接线的时候会选中连接线,之后可以删除。同时预留双击连接线的功能用来处理更多业务需求;

可交互的React节点

  • 默认情况下,选中节点之后,选中框会挡住节点。此时如果节点是自定义React渲染的节点,那么选中框会挡住React节点的内容(按钮、输入框等等)的点击动作。
  • 目前为了优化这个逻辑,监听了选中的动作。当单选的时候,通过css选择器的方式,设置选中框的div的css属性pointer-eventsnone以避免挡住节点内容的操作;多选的时候则不做处理,允许选择框挡住节点(方便拖拽整体移动节点);

快捷键

多选

在画布上摁住shift可以框选多个节点,框选完毕之后,可以摁住ctrl或者meta键选中某个节点,以实现额外添加或者取消选中节点的功能;

复制

选中节点之后meta/ctrl + c可以复制节点;meta/ctrl + v可以粘贴节点;

剪切

选中节点之后meta/ctrl + x可以复制节点;meta/ctrl + v可以粘贴节点;

撤销

meta/ctrl + z 撤销刚刚的动作;

重做

meta/ctrl + shift + z 重做刚刚的动作;

全选

meta/ctrl + shift + a 全选画布中的节点;

删除

backspace/delete 删除选中节点;

放大

meta/ctrl + 1 放大画布

缩小

meta/ctrl + 2 缩小画布