diff --git a/.editorconfig b/.editorconfig
new file mode 100755
index 0000000..7e3649a
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,16 @@
+# http://editorconfig.org
+root = true
+
+[*]
+indent_style = space
+indent_size = 2
+end_of_line = lf
+charset = utf-8
+trim_trailing_whitespace = true
+insert_final_newline = true
+
+[*.md]
+trim_trailing_whitespace = false
+
+[Makefile]
+indent_style = tab
diff --git a/.env b/.env
new file mode 100644
index 0000000..847f6f8
--- /dev/null
+++ b/.env
@@ -0,0 +1,2 @@
+BROWSER=none
+ESLINT=1
diff --git a/.eslintrc b/.eslintrc
new file mode 100755
index 0000000..f626954
--- /dev/null
+++ b/.eslintrc
@@ -0,0 +1,3 @@
+{
+ "extends": "eslint-config-umi"
+}
diff --git a/.gitignore b/.gitignore
old mode 100644
new mode 100755
index 6704566..7d50fdf
--- a/.gitignore
+++ b/.gitignore
@@ -1,104 +1,18 @@
-# Logs
-logs
-*.log
-npm-debug.log*
-yarn-debug.log*
-yarn-error.log*
-lerna-debug.log*
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
-# Diagnostic reports (https://nodejs.org/api/report.html)
-report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
+# dependencies
+/node_modules
+/npm-debug.log*
+/yarn-error.log
+/yarn.lock
+/package-lock.json
-# Runtime data
-pids
-*.pid
-*.seed
-*.pid.lock
+# production
+/dist
-# Directory for instrumented libs generated by jscoverage/JSCover
-lib-cov
+# misc
+.DS_Store
-# Coverage directory used by tools like istanbul
-coverage
-*.lcov
-
-# nyc test coverage
-.nyc_output
-
-# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
-.grunt
-
-# Bower dependency directory (https://bower.io/)
-bower_components
-
-# node-waf configuration
-.lock-wscript
-
-# Compiled binary addons (https://nodejs.org/api/addons.html)
-build/Release
-
-# Dependency directories
-node_modules/
-jspm_packages/
-
-# TypeScript v1 declaration files
-typings/
-
-# TypeScript cache
-*.tsbuildinfo
-
-# Optional npm cache directory
-.npm
-
-# Optional eslint cache
-.eslintcache
-
-# Microbundle cache
-.rpt2_cache/
-.rts2_cache_cjs/
-.rts2_cache_es/
-.rts2_cache_umd/
-
-# Optional REPL history
-.node_repl_history
-
-# Output of 'npm pack'
-*.tgz
-
-# Yarn Integrity file
-.yarn-integrity
-
-# dotenv environment variables file
-.env
-.env.test
-
-# parcel-bundler cache (https://parceljs.org/)
-.cache
-
-# Next.js build output
-.next
-
-# Nuxt.js build / generate output
-.nuxt
-dist
-
-# Gatsby files
-.cache/
-# Comment in the public line in if your project uses Gatsby and *not* Next.js
-# https://nextjs.org/blog/next-9-1#public-directory-support
-# public
-
-# vuepress build output
-.vuepress/dist
-
-# Serverless directories
-.serverless/
-
-# FuseBox cache
-.fusebox/
-
-# DynamoDB Local files
-.dynamodb/
-
-# TernJS port file
-.tern-port
+# umi
+.umi
+.umi-production
diff --git a/.umirc.ts b/.umirc.ts
new file mode 100644
index 0000000..62afa6d
--- /dev/null
+++ b/.umirc.ts
@@ -0,0 +1,37 @@
+import { IConfig } from 'umi-types'
+
+// ref: https://umijs.org/config/
+const config: IConfig = {
+ treeShaking: true,
+ disableCSSModules: true,
+ plugins: [
+ // ref: https://umijs.org/plugin/umi-plugin-react.html
+ [
+ 'umi-plugin-react',
+ {
+ antd: true,
+ dva: false,
+ dynamicImport: false,
+ title: 'iMove',
+ dll: false,
+
+ routes: {
+ exclude: [/components\//],
+ },
+ },
+ ],
+ ],
+ extraBabelPlugins: [
+ [
+ 'import',
+ {
+ libraryName: '@antv/x6-components',
+ libraryDirectory: 'es',
+ transformToDefaultImport: false,
+ style: true,
+ },
+ ],
+ ],
+}
+
+export default config
diff --git a/LICENSE b/LICENSE
deleted file mode 100644
index a20fb03..0000000
--- a/LICENSE
+++ /dev/null
@@ -1,21 +0,0 @@
-MIT License
-
-Copyright (c) 2020 拾邑
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
diff --git a/README.md b/README.md
deleted file mode 100644
index 15829a4..0000000
--- a/README.md
+++ /dev/null
@@ -1,2 +0,0 @@
-# iMove
-move your mouse, generate flow chart
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..bdb3f66
--- /dev/null
+++ b/package.json
@@ -0,0 +1,57 @@
+{
+ "private": true,
+ "name": "@antv/x6-example-features",
+ "version": "0.2.3",
+ "scripts": {
+ "start": "umi dev",
+ "build": "umi build",
+ "lint": "yarn lint:ts",
+ "lint:ts": "tslint -c tslint.json -p tsconfig.json --fix",
+ "precommit": "lint-staged"
+ },
+ "dependencies": {
+ "@antv/x6": "^0.3.0",
+ "@antv/x6-components": "^0.2.0",
+ "@antv/x6-react-shape": "^0.2.0",
+ "antd": "^3.19.5",
+ "classnames": "^2.2.6",
+ "react": "^16.12.0",
+ "react-dom": "^16.8.6"
+ },
+ "devDependencies": {
+ "@types/classnames": "^2.2.10",
+ "@types/jest": "^23.3.12",
+ "@types/react": "^16.7.18",
+ "@types/react-dom": "^16.0.11",
+ "@types/react-test-renderer": "^16.0.3",
+ "babel-eslint": "^9.0.0",
+ "babel-plugin-import": "^1.13.0",
+ "eslint": "^5.4.0",
+ "eslint-config-umi": "^1.4.0",
+ "eslint-plugin-flowtype": "^2.50.0",
+ "eslint-plugin-import": "^2.14.0",
+ "eslint-plugin-jsx-a11y": "^5.1.1",
+ "eslint-plugin-react": "^7.11.1",
+ "lint-staged": "^7.2.2",
+ "react-test-renderer": "^16.7.0",
+ "tslint": "^5.12.0",
+ "tslint-eslint-rules": "^5.4.0",
+ "tslint-react": "^3.6.0",
+ "umi": "^2.9.0",
+ "umi-plugin-react": "^1.8.0",
+ "umi-types": "^0.3.0"
+ },
+ "lint-staged": {
+ "./src/**/*.{ts,tsx}": [
+ "tslint -c tslint.json -p ./tsconfig.json --fix",
+ "git add"
+ ],
+ "*.{js,jsx}": [
+ "eslint --fix",
+ "git add"
+ ]
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ }
+}
diff --git a/src/global.css b/src/global.css
new file mode 100644
index 0000000..e4a5833
--- /dev/null
+++ b/src/global.css
@@ -0,0 +1,7 @@
+html, body, #root {
+ height: 100%;
+}
+
+body {
+ margin: 0;
+}
diff --git a/src/pages/flowchart/data.ts b/src/pages/flowchart/data.ts
new file mode 100644
index 0000000..70cf6dd
--- /dev/null
+++ b/src/pages/flowchart/data.ts
@@ -0,0 +1,398 @@
+export interface DataItem {
+ width: number;
+ height: number;
+ data: {
+ type: string;
+ title: string;
+ icon?: string;
+ };
+}
+
+const items: DataItem[] = [
+ {
+ width: 56,
+ height: 56,
+ data: {
+ type: "start",
+ title: "开始"
+ }
+ },
+ {
+ width: 56,
+ height: 56,
+ data: {
+ type: "end",
+ title: "结束"
+ }
+ },
+ {
+ width: 72,
+ height: 72,
+ data: {
+ type: "status",
+ title: "状态节点"
+ }
+ },
+ {
+ width: 128,
+ height: 48,
+ data: {
+ type: "execute",
+ title: "执行节点",
+ icon:
+ ''
+ }
+ },
+ {
+ width: 148,
+ height: 48,
+ data: {
+ type: "cond",
+ title: "条件节点",
+ icon:
+ ''
+ }
+ }
+];
+
+const map: { [key: string]: DataItem } = {};
+
+items.forEach(item => {
+ map[item.data.type] = item;
+});
+
+export const data = { items, map };
+
+export function isCircle(item: DataItem) {
+ return item.data.type === "start" || item.data.type === "end";
+}
+
+export function isGroup(type: string) {
+ return type === "combine" || type === "group";
+}
+
+export function isFunc(type: string) {
+ return type === "approvement" || type === "execute";
+}
+
+export const mockGraphData = {
+ nodes: [
+ {
+ id: "cell-2",
+ data: {
+ type: "start",
+ title: "开始"
+ },
+ shape: "html",
+ resizable: false,
+ perimeter: "ellipse",
+ visible: true,
+ x: -200,
+ y: -292,
+ width: 56,
+ height: 56,
+ relative: false,
+ translateControlPoints: true
+ },
+ {
+ id: "cell-3",
+ data: {
+ type: "status",
+ title: "idle",
+ serviceType: "none"
+ },
+ shape: "html",
+ resizable: true,
+ perimeter: "rectangle",
+ visible: true,
+ x: -208,
+ y: -200,
+ width: 72,
+ height: 72,
+ relative: false,
+ translateControlPoints: true
+ },
+ {
+ id: "cell-4",
+ data: {
+ type: "status",
+ title: "loading",
+ serviceType: "invoke",
+ api: "douban.api"
+ },
+ shape: "html",
+ resizable: true,
+ perimeter: "rectangle",
+ visible: true,
+ x: -208,
+ y: -70,
+ width: 72,
+ height: 72,
+ relative: false,
+ translateControlPoints: true
+ },
+ {
+ id: "cell-5",
+ data: {
+ type: "execute",
+ title: "reject",
+ icon:
+ ''
+ },
+ shape: "html",
+ resizable: true,
+ perimeter: "rectangle",
+ visible: true,
+ x: -370,
+ y: 60,
+ width: 128,
+ height: 48,
+ relative: false,
+ translateControlPoints: true
+ },
+ {
+ id: "cell-6",
+ data: {
+ type: "execute",
+ title: "resolve",
+ icon:
+ '',
+ action: "empty",
+ contextKey: "file",
+ contextValue: "result.a"
+ },
+ shape: "html",
+ resizable: true,
+ perimeter: "rectangle",
+ visible: true,
+ x: -100,
+ y: 60,
+ width: 128,
+ height: 48,
+ relative: false,
+ translateControlPoints: true
+ },
+ {
+ id: "cell-7",
+ data: {
+ type: "status",
+ title: "fail"
+ },
+ shape: "html",
+ resizable: true,
+ perimeter: "rectangle",
+ visible: true,
+ x: -342,
+ y: 190,
+ width: 72,
+ height: 72,
+ relative: false,
+ translateControlPoints: true
+ },
+ {
+ id: "cell-9",
+ data: {
+ type: "status",
+ title: "success"
+ },
+ shape: "html",
+ resizable: true,
+ perimeter: "rectangle",
+ visible: true,
+ x: -72,
+ y: 190,
+ width: 72,
+ height: 72,
+ relative: false,
+ translateControlPoints: true
+ }
+ ],
+ edges: [
+ {
+ id: "cell-10",
+ data: null,
+ align: "center",
+ verticalAlign: "middle",
+ fontColor: "rgba(0, 0, 0, 1)",
+ shape: "connector",
+ endArrow: "classic",
+ stroke: "#888",
+ edge: "elbow",
+ elbow: "vertical",
+ labelBackgroundColor: "#f8f9fa",
+ rounded: true,
+ movable: false,
+ sourceAnchorX: "0.5",
+ sourceAnchorY: "1",
+ targetAnchorX: "0.5",
+ targetAnchorY: "0",
+ sourceAnchorDx: "0",
+ targetAnchorDx: "0",
+ targetAnchorDy: "0",
+ visible: true,
+ x: 0,
+ y: 0,
+ width: 0,
+ height: 0,
+ relative: true,
+ translateControlPoints: true,
+ source: "cell-2",
+ target: "cell-3"
+ },
+ {
+ id: "cell-11",
+ data: null,
+ align: "center",
+ verticalAlign: "middle",
+ fontColor: "rgba(0, 0, 0, 1)",
+ shape: "connector",
+ endArrow: "classic",
+ stroke: "#888",
+ edge: "elbow",
+ elbow: "vertical",
+ labelBackgroundColor: "#f8f9fa",
+ rounded: true,
+ movable: false,
+ sourceAnchorX: "0.5",
+ sourceAnchorY: "1",
+ targetAnchorX: "0.5",
+ targetAnchorY: "0",
+ sourceAnchorDx: "0",
+ targetAnchorDx: "0",
+ targetAnchorDy: "0",
+ visible: true,
+ x: 0,
+ y: 0,
+ width: 0,
+ height: 0,
+ relative: true,
+ translateControlPoints: true,
+ source: "cell-3",
+ target: "cell-4"
+ },
+ {
+ id: "cell-12",
+ data: null,
+ align: "center",
+ verticalAlign: "middle",
+ fontColor: "rgba(0, 0, 0, 1)",
+ shape: "connector",
+ endArrow: "classic",
+ stroke: "#888",
+ edge: "elbow",
+ elbow: "vertical",
+ labelBackgroundColor: "#f8f9fa",
+ rounded: true,
+ movable: false,
+ sourceAnchorX: "0.5",
+ sourceAnchorY: "1",
+ targetAnchorX: "0.5",
+ targetAnchorY: "0",
+ sourceAnchorDx: "0",
+ targetAnchorDx: "0",
+ targetAnchorDy: "0",
+ visible: true,
+ x: 0,
+ y: 0,
+ width: 0,
+ height: 0,
+ relative: true,
+ translateControlPoints: true,
+ source: "cell-4",
+ target: "cell-5"
+ },
+ {
+ id: "cell-13",
+ data: null,
+ align: "center",
+ verticalAlign: "middle",
+ fontColor: "rgba(0, 0, 0, 1)",
+ shape: "connector",
+ endArrow: "classic",
+ stroke: "#888",
+ edge: "elbow",
+ elbow: "vertical",
+ labelBackgroundColor: "#f8f9fa",
+ rounded: true,
+ movable: false,
+ sourceAnchorX: "0.5",
+ sourceAnchorY: "1",
+ targetAnchorX: "0.5",
+ targetAnchorY: "0",
+ sourceAnchorDx: "0",
+ targetAnchorDx: "0",
+ targetAnchorDy: "0",
+ visible: true,
+ x: 0,
+ y: 0,
+ width: 0,
+ height: 0,
+ relative: true,
+ translateControlPoints: true,
+ source: "cell-4",
+ target: "cell-6"
+ },
+ {
+ id: "cell-14",
+ data: null,
+ align: "center",
+ verticalAlign: "middle",
+ fontColor: "rgba(0, 0, 0, 1)",
+ shape: "connector",
+ endArrow: "classic",
+ stroke: "#888",
+ edge: "elbow",
+ elbow: "vertical",
+ labelBackgroundColor: "#f8f9fa",
+ rounded: true,
+ movable: false,
+ sourceAnchorX: "0.5",
+ sourceAnchorY: "1",
+ targetAnchorX: "0.5",
+ targetAnchorY: "0",
+ sourceAnchorDx: "0",
+ targetAnchorDx: "0",
+ targetAnchorDy: "0",
+ visible: true,
+ x: 0,
+ y: 0,
+ width: 0,
+ height: 0,
+ relative: true,
+ translateControlPoints: true,
+ source: "cell-5",
+ target: "cell-7"
+ },
+ {
+ id: "cell-15",
+ data: null,
+ align: "center",
+ verticalAlign: "middle",
+ fontColor: "rgba(0, 0, 0, 1)",
+ shape: "connector",
+ endArrow: "classic",
+ stroke: "#888",
+ edge: "elbow",
+ elbow: "vertical",
+ labelBackgroundColor: "#f8f9fa",
+ rounded: true,
+ movable: false,
+ sourceAnchorX: "0.5",
+ sourceAnchorY: "1",
+ targetAnchorX: "0.5",
+ targetAnchorY: "0",
+ sourceAnchorDx: "0",
+ targetAnchorDx: "0",
+ targetAnchorDy: "0",
+ visible: true,
+ x: 0,
+ y: 0,
+ width: 0,
+ height: 0,
+ relative: true,
+ translateControlPoints: true,
+ source: "cell-6",
+ target: "cell-9"
+ }
+ ]
+};
diff --git a/src/pages/flowchart/index.less b/src/pages/flowchart/index.less
new file mode 100644
index 0000000..9754d17
--- /dev/null
+++ b/src/pages/flowchart/index.less
@@ -0,0 +1,171 @@
+body {
+ overflow: hidden;
+}
+
+.flowchart-wrap {
+ width: 100%;
+ height: 100%;
+ overflow: hidden;
+}
+
+.flowchart-main {
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+ height: 100%;
+ overflow: hidden;
+}
+
+.flowchart-toolbar {
+ height: 41;
+ border-bottom: 1px solid #e9e9e9;
+ background-color: #fbfbfb;
+ padding: 4px 8px;
+}
+
+.flowchart-graph {
+ flex: 1;
+ overflow: hidden;
+
+ .graph {
+ box-shadow: none;
+ }
+}
+
+.flowchart-sidebar,
+.flowchart-format {
+ background-color: #fbfbfb;
+ width: 100%;
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+}
+
+.flowchart-sidebar {
+ border-right: 1px solid #e9e9e9;
+}
+
+.flowchart-format {
+ border-left: 1px solid #e9e9e9;
+}
+
+.flowchart-sidebar-title {
+ height: 41px;
+ text-align: center;
+ border-bottom: 1px solid #e9e9e9;
+ font-weight: bold;
+ line-height: 40px;
+ font-size: 16px;
+}
+
+.flowchart-sidebar-content {
+ flex: 1;
+ padding: 8px 16px;
+}
+
+.x6-node {
+ foreignobject {
+ filter: drop-shadow(2px 4px 4px rgba(80, 80, 80, 0.15));
+ overflow: visible;
+
+ > div {
+ overflow: visible !important;
+ }
+ }
+}
+
+.flowchart-node-wrap {
+ overflow: hidden;
+ margin: 16px auto;
+ cursor: move;
+
+ &.start,
+ &.end {
+ border-radius: 100%;
+ }
+}
+
+.flowchart-node {
+ font-family: 'PingFangSC-Regular', 'PingFang SC';
+ font-size: 14px;
+ font-weight: 400;
+ display: flex;
+ align-items: center;
+ width: 100%;
+ height: 100%;
+ color: rgba(0, 0, 0, 0.85);
+ border: 1px solid #e9e9e9;
+ text-align: center;
+ // box-shadow: 0 0 0 1px rgba(34, 36, 38, 0.15);
+ background: #ffffff;
+ user-select: none;
+ border-radius: 4px;
+
+ &.is-gruop {
+ align-items: flex-start;
+
+ .icon {
+ margin-left: 8px;
+ margin-top: 8px;
+ }
+ }
+
+ .anticon {
+ font-size: 16px;
+ margin-left: 16px;
+ }
+
+ .icon {
+ font-size: 16px;
+ margin-left: 16px;
+ font-style: normal;
+ line-height: 0;
+ display: inline-block;
+ text-align: center;
+ vertical-align: -0.125em;
+ text-transform: none;
+ color: #666666;
+
+ text-rendering: optimizelegibility;
+ -webkit-font-smoothing: antialiased;
+ }
+
+ span {
+ width: 100%;
+ }
+
+ &.start,
+ &.end {
+ border-radius: 100%;
+ justify-content: center;
+
+ .text {
+ margin-left: 0;
+ }
+ }
+
+ &.end {
+ border-color: #b0b0b0;
+ background: #cccccc;
+ }
+}
+
+.ping {
+ animation: ping 0.8s ease-in-out infinite both;
+ transform-origin: 50% 50%;
+}
+
+@keyframes ping {
+ 0% {
+ transform: scale(0.2);
+ opacity: 0.8;
+ }
+ 80% {
+ transform: scale(1.2);
+ opacity: 0;
+ }
+ 100% {
+ transform: scale(2.2);
+ opacity: 0;
+ }
+}
diff --git a/src/pages/flowchart/index.tsx b/src/pages/flowchart/index.tsx
new file mode 100644
index 0000000..d20ace9
--- /dev/null
+++ b/src/pages/flowchart/index.tsx
@@ -0,0 +1,71 @@
+import React from 'react'
+import { SplitBox } from '@antv/x6-components'
+import { Graph, DomEvent } from '@antv/x6'
+import { Sidebar } from './sidebar'
+import { GraphToolbar } from './toolbar'
+import { createGraph } from './util'
+import { Setting } from './settings'
+import './index.less'
+
+export default class FlowChart extends React.Component<{}, FlowChart.State> {
+ private graph: Graph
+ private container: HTMLDivElement
+
+ componentDidMount() {
+ DomEvent.disableContextMenu(this.container)
+ this.graph = createGraph(this.container)
+ this.graph.center()
+ this.setState({ inited: true })
+ }
+
+ refContainer = (container: HTMLDivElement) => {
+ this.container = container
+ }
+
+ render() {
+ return (
+
+
+
+
iMove
+
基础组件
+
+ {this.graph && this.state.inited && (
+
+ )}
+
+
+
+
+
+ {this.graph && }
+
+
+
+ {this.graph && this.state.inited && }
+
+
+
+ )
+ }
+}
+
+export namespace FlowChart {
+ export interface State {
+ inited: boolean
+ }
+}
diff --git a/src/pages/flowchart/settings/execute.tsx b/src/pages/flowchart/settings/execute.tsx
new file mode 100644
index 0000000..395f195
--- /dev/null
+++ b/src/pages/flowchart/settings/execute.tsx
@@ -0,0 +1,70 @@
+import * as React from "react";
+import { Select, Input } from "antd";
+
+const { Option } = Select;
+
+class ExecuteSetting extends React.Component<
+ ExecuteSetting.Props,
+ ExecuteSetting.State
+> {
+ render() {
+ return (
+
+
+
+
+
+
+ );
+ }
+}
+
+export default ExecuteSetting;
+
+export namespace ExecuteSetting {
+ export interface Props {
+ data: any;
+ onTitleChange: (e: any) => void;
+ onActionChange: (action: string) => void;
+ onKeyChange: (e: any) => void;
+ onValueChange: (e: any) => void;
+ }
+
+ export interface State {}
+}
diff --git a/src/pages/flowchart/settings/index.tsx b/src/pages/flowchart/settings/index.tsx
new file mode 100644
index 0000000..f8f5ff9
--- /dev/null
+++ b/src/pages/flowchart/settings/index.tsx
@@ -0,0 +1,138 @@
+import React from "react";
+import { Graph, Cell } from "@antv/x6";
+import StateSetting from "./state";
+import ExecuteSetting from "./execute";
+
+export class Setting extends React.Component {
+ constructor(props: Setting.Props) {
+ super(props);
+ this.state = this.getNextState(props);
+
+ props.graph.on("selection:changed", () => {
+ this.setState(this.getNextState());
+ });
+ }
+
+ getNextState(props: Setting.Props = this.props) {
+ return {
+ selectedCells: props.graph.getSelectedCells()
+ };
+ }
+
+ onStatusTitleChange = (e: any) => {
+ const value = e.target.value;
+ if (value && value.trim()) {
+ const cell = this.state.selectedCells[0];
+ const data = cell.data;
+ this.props.graph.setCellData(cell, {
+ ...data,
+ title: value.trim()
+ });
+ }
+ };
+
+ onExcuteTitleChange = (title: string) => {
+ const cell = this.state.selectedCells[0];
+ const data = cell.data;
+ this.props.graph.setCellData(cell, {
+ ...data,
+ title
+ });
+ };
+
+ onSerTypeChange = (serviceType: string) => {
+ const cell = this.state.selectedCells[0];
+ const data = cell.data;
+ this.props.graph.setCellData(cell, {
+ ...data,
+ serviceType
+ });
+ };
+
+ onActionChange = (action: string) => {
+ const cell = this.state.selectedCells[0];
+ const data = cell.data;
+ this.props.graph.setCellData(cell, {
+ ...data,
+ action
+ });
+ };
+
+ onApiChange = (e: any) => {
+ const value = e.target.value;
+ if (value && value.trim()) {
+ const cell = this.state.selectedCells[0];
+ const data = cell.data;
+ this.props.graph.setCellData(cell, {
+ ...data,
+ api: value.trim()
+ });
+ }
+ };
+
+ onKeyChange = (e: any) => {
+ const value = e.target.value;
+ if (value && value.trim()) {
+ const cell = this.state.selectedCells[0];
+ const data = cell.data;
+ this.props.graph.setCellData(cell, {
+ ...data,
+ contextKey: value.trim()
+ });
+ }
+ };
+
+ onValueChange = (e: any) => {
+ const value = e.target.value;
+ if (value && value.trim()) {
+ const cell = this.state.selectedCells[0];
+ const data = cell.data;
+ this.props.graph.setCellData(cell, {
+ ...data,
+ contextValue: value.trim()
+ });
+ }
+ };
+
+ render() {
+ if (this.state.selectedCells.length === 1) {
+ const cell = this.state.selectedCells[0];
+ const data = cell.data;
+ return (
+
+
流程设置
+
+ {data && data.type === "status" && (
+
+ )}
+ {data && data.type === "execute" && (
+
+ )}
+
+
+ );
+ }
+ return null;
+ }
+}
+
+export namespace Setting {
+ export interface Props {
+ graph: Graph;
+ }
+
+ export interface State {
+ selectedCells: Cell[];
+ }
+}
diff --git a/src/pages/flowchart/settings/state.tsx b/src/pages/flowchart/settings/state.tsx
new file mode 100644
index 0000000..fb30853
--- /dev/null
+++ b/src/pages/flowchart/settings/state.tsx
@@ -0,0 +1,58 @@
+import * as React from "react";
+import { Input, Select } from "antd";
+
+const { Option } = Select;
+
+class StateSetting extends React.Component<
+ StateSetting.Props,
+ StateSetting.State
+> {
+ render() {
+ return (
+
+
+
+
+
+ );
+ }
+}
+
+export default StateSetting;
+
+export namespace StateSetting {
+ export interface Props {
+ data: any;
+ onTitleChange: (e: any) => void;
+ onApiChange: (e: any) => void;
+ onSerTypeChange: (serviceType: string) => void;
+ }
+
+ export interface State {}
+}
diff --git a/src/pages/flowchart/sidebar.tsx b/src/pages/flowchart/sidebar.tsx
new file mode 100644
index 0000000..4179b8b
--- /dev/null
+++ b/src/pages/flowchart/sidebar.tsx
@@ -0,0 +1,112 @@
+import React from "react";
+import classNames from "classnames";
+import { Icon } from "@antv/x6-components";
+import { Graph, Dnd } from "@antv/x6";
+import { data, DataItem, isGroup } from "./data";
+import { addNode } from "./util";
+
+export class Sidebar extends React.Component {
+ private container: HTMLDivElement;
+
+ refContainer = (container: HTMLDivElement) => {
+ this.container = container;
+ };
+
+ componentDidMount() {
+ this.container.childNodes.forEach((elem: HTMLElement) => {
+ const type = elem.getAttribute("data-type") || "";
+ const itemData = data.map[type];
+
+ const dnd = new Dnd(elem, {
+ data: itemData,
+ getGraph: () => this.props.graph,
+ getTargetCell: ({ graph, graphX, graphY }) => {
+ const cell = graph.getCellAt(graphX, graphY);
+ if (cell != null && cell.data != null && isGroup(cell.data.type)) {
+ return cell;
+ }
+ return null;
+ },
+ createDragElement: ({ element }) => {
+ const elem = document.createElement("div") as HTMLDivElement;
+ const w = element.offsetWidth;
+ const h = element.offsetHeight;
+ elem.style.width = `${w}px`;
+ elem.style.height = `${h}px`;
+ elem.style.border = "1px dashed #000";
+ elem.style.cursor = "move";
+ return elem;
+ },
+ createPreviewElement: ({ graph, element }) => {
+ const elem = document.createElement("div") as HTMLDivElement;
+ const w = element.offsetWidth;
+ const h = element.offsetHeight;
+ const s = graph.view.scale;
+ elem.style.width = `${w * s}px`;
+ elem.style.height = `${h * s}px`;
+ elem.style.border = "1px dashed #000";
+ elem.style.cursor = "move";
+ return elem;
+ }
+ });
+
+ dnd.on("dragPrepare", ({ dragElement }) => {
+ dragElement.style.margin = "0";
+ });
+
+ dnd.on("drop", ({ data, graph, targetCell, targetPosition }) => {
+ if (data != null) {
+ const group = isGroup(data.data.type);
+ const parent = targetCell || undefined;
+ let x = targetPosition.x;
+ let y = targetPosition.y;
+ // relative parent position
+ if (parent != null) {
+ const geom = parent.getGeometry()!;
+ x -= geom.bounds.x;
+ y -= geom.bounds.y;
+ }
+
+ const cell = addNode(
+ graph,
+ data,
+ x,
+ y,
+ group ? 200 : null,
+ group ? 160 : null,
+ null,
+ parent
+ );
+
+ graph.unSelectCells(graph.getSelectedCells()).selectCell(cell);
+ }
+ });
+ });
+ }
+
+ render() {
+ return (
+
+ {data.items.map(({ data, width, height }) => (
+
+
+ {data.icon && }
+ {data.title}
+
+
+ ))}
+
+ );
+ }
+}
+
+export namespace Sidebar {
+ export interface Props {
+ graph: Graph;
+ }
+}
diff --git a/src/pages/flowchart/toolbar.tsx b/src/pages/flowchart/toolbar.tsx
new file mode 100644
index 0000000..fb53d9e
--- /dev/null
+++ b/src/pages/flowchart/toolbar.tsx
@@ -0,0 +1,250 @@
+import React from "react";
+import { Toolbar, Menu } from "@antv/x6-components";
+import { Graph, UndoManager } from "@antv/x6";
+
+export class GraphToolbar extends React.Component<
+ GraphToolbar.Props,
+ GraphToolbar.State
+> {
+ private graph: Graph;
+ private undoManager: UndoManager;
+ private commands: GraphToolbar.Command[][];
+
+ constructor(props: GraphToolbar.Props) {
+ super(props);
+
+ this.graph = props.graph;
+ this.undoManager = UndoManager.create(props.graph);
+ this.commands = [
+ [
+ {
+ name: "zoomIn",
+ icon: "zoom-in",
+ tooltip: "放大",
+ handler: () => {
+ let scale = this.graph.view.scale;
+ if (scale >= 8) {
+ scale += 8;
+ } else if (scale >= 4) {
+ scale += 4;
+ } else if (scale >= 2) {
+ scale += 1;
+ } else if (scale >= 1.5) {
+ scale += 0.5;
+ } else if (scale >= 1) {
+ scale += 0.25;
+ } else if (scale >= 0.7) {
+ scale += 0.15;
+ } else if (scale >= 0.4) {
+ scale += 0.1;
+ } else if (scale >= 0.15) {
+ scale += 0.05;
+ } else if (scale >= 0.01) {
+ scale += 0.01;
+ }
+ this.graph.zoomTo(scale);
+ }
+ },
+ {
+ name: "zoomOut",
+ icon: "zoom-out",
+ tooltip: "缩小",
+ handler: () => {
+ let scale = this.graph.view.scale;
+ if (scale <= 0.15) {
+ scale -= 0.01;
+ } else if (scale <= 0.4) {
+ scale -= 0.05;
+ } else if (scale <= 0.7) {
+ scale -= 0.1;
+ } else if (scale <= 1) {
+ scale -= 0.15;
+ } else if (scale <= 1.5) {
+ scale -= 0.25;
+ } else if (scale <= 2) {
+ scale -= 0.5;
+ } else if (scale <= 4) {
+ scale -= 1;
+ } else if (scale <= 8) {
+ scale -= 4;
+ } else if (scale <= 16) {
+ scale -= 8;
+ }
+ this.graph.zoomTo(scale);
+ }
+ }
+ ],
+ [
+ {
+ name: "undo",
+ icon: "undo",
+ tooltip: "撤销",
+ shortcut: "Cmd + Z",
+ handler: () => this.undoManager.undo()
+ },
+ {
+ name: "redo",
+ icon: "redo",
+ tooltip: "重做",
+ shortcut: "Cmd + Shift + Z",
+ handler: () => this.undoManager.redo()
+ }
+ ],
+ [
+ {
+ name: "delete",
+ icon: "delete",
+ tooltip: "删除",
+ shortcut: "Delete",
+ handler: () => this.graph.deleteCells()
+ },
+ {
+ name: "save",
+ icon: "save",
+ tooltip: "保存",
+ shortcut: "Cmd + S",
+ handler: () => {
+ const graphData = this.graph.toJSON();
+ console.log(JSON.stringify(graphData));
+ }
+ }
+ ]
+ ];
+
+ this.commands.forEach(items =>
+ items.forEach(item => {
+ if (item.shortcut) {
+ if (item.shortcut === "Delete") {
+ this.graph.bindKey(["del", "backspace"], item.handler);
+ } else {
+ this.graph.bindKey(item.shortcut, item.handler);
+ }
+ }
+ })
+ );
+
+ this.graph.on("selection:changed", this.updateState);
+ this.graph.view.on("scale", this.updateState);
+ this.graph.view.on("scaleAndTranslate", this.updateState);
+ this.graph.model.on("change", this.updateState);
+ this.undoManager.on("undo", this.updateState);
+ this.undoManager.on("redo", this.updateState);
+
+ this.state = this.getNextState();
+ }
+
+ updateState = () => {
+ this.setState(this.getNextState());
+ };
+
+ getNextState() {
+ return {
+ zoomIn: this.graph.view.scale === this.graph.maxScale,
+ zoomOut: this.graph.view.scale === this.graph.minScale,
+ undo: !this.undoManager.canUndo(),
+ redo: !this.undoManager.canRedo(),
+ delete: this.graph.getSelectedCells().length <= 0
+ };
+ }
+
+ resetZoom = () => {
+ if (this.graph.view.scale !== 1) {
+ this.graph.zoomTo(1);
+ }
+ };
+
+ onClick = (name: string) => {
+ if (name === "resetView") {
+ this.graph.zoomTo(1);
+ this.graph.center();
+ } else if (name === "fitWindow") {
+ this.graph.fit(8);
+ // this.graph.center()
+ } else if (
+ name === "25" ||
+ name === "50" ||
+ name === "75" ||
+ name === "100" ||
+ name === "125" ||
+ name === "150" ||
+ name === "200" ||
+ name === "400"
+ ) {
+ const scale = parseInt(name, 10) / 100;
+ this.graph.zoomTo(scale);
+ }
+ };
+
+ renderZoomDropdown() {
+ const MenuItem = Menu.Item;
+ return (
+
+ );
+ }
+
+ render() {
+ const Item = Toolbar.Item;
+ const Group = Toolbar.Group;
+
+ return (
+
+ -
+ {(this.graph.view.scale * 100).toFixed(0)}%
+
+
+
+
+
+ {this.commands.map(items => (
+ i.name).join("-")}>
+ {items.map(item => (
+
+ ))}
+
+ ))}
+
+ );
+ }
+}
+
+export namespace GraphToolbar {
+ export interface Props {
+ graph: Graph;
+ }
+
+ export interface State {
+ zoomIn: boolean;
+ zoomOut: boolean;
+ redo: boolean;
+ undo: boolean;
+ delete: boolean;
+ }
+
+ export interface Command {
+ name: string;
+ icon: string;
+ tooltip: string;
+ shortcut?: string;
+ handler: () => void;
+ }
+}
diff --git a/src/pages/flowchart/util.ts b/src/pages/flowchart/util.ts
new file mode 100644
index 0000000..c95506d
--- /dev/null
+++ b/src/pages/flowchart/util.ts
@@ -0,0 +1,185 @@
+import { Cell, Graph, Style } from "@antv/x6";
+import { DataItem, isCircle, isGroup, mockGraphData } from "./data";
+
+export function createGraph(container: HTMLDivElement) {
+ return new Graph(container, {
+ rotate: false,
+ resize: true,
+ folding: false,
+ infinite: true, // 无限大画布
+ // pageVisible: true,
+ // pageBreak: {
+ // enabled: true,
+ // dsahed: true,
+ // stroke: '#c0c0c0',
+ // },
+ // pageFormat: {
+ // width: 800,
+ // height: 960,
+ // },
+ // mouseWheel: true,
+ rubberband: true,
+ allowLoops: true,
+ backgroundColor: "#f8f9fa",
+ grid: {
+ type: "dot",
+ color: "#bcbcbc"
+ },
+ guide: {
+ enabled: true,
+ dashed: true,
+ stroke: "#ff5500"
+ },
+ connection: {
+ enabled: true,
+ hotspotable: false,
+ livePreview: true,
+ createEdge(options) {
+ const style = options.style;
+ fixEdgeStyle(style, style);
+ return this.createEdge(options);
+ }
+ },
+ connectionPreview: {
+ stroke: "#1890ff"
+ },
+ connectionHighlight: {},
+ allowDanglingEdges: false,
+ keyboard: {
+ enabled: true,
+ global: false,
+ escape: true
+ },
+ selectionPreview: {
+ dashed: false,
+ strokeWidth: 2
+ },
+ nodeStyle: {
+ fill: "rgba(0, 0, 0, 0)",
+ stroke: "none",
+ label: false,
+ editable: false
+ },
+ edgeStyle: {
+ edge: "elbow",
+ elbow: "vertical",
+ labelBackgroundColor: "#f8f9fa",
+ rounded: true,
+ movable: false
+ },
+ dropEnabled: true,
+ dropTargetHighlight: {
+ stroke: "#87d068",
+ opacity: 1
+ },
+ anchor: {
+ inductiveSize: 24
+ },
+ // anchorTip: {
+ // enabled: true,
+ // opacity: 1,
+ // className: 'ping',
+ // },
+ anchorHighlight: {
+ strokeWidth: 16
+ },
+ isValidDropTarget(target) {
+ if (target && target.data) {
+ return isGroup(target.data.type);
+ }
+ },
+ isLabelMovable() {
+ return false;
+ },
+ shouldRedrawOnDataChange() {
+ return true;
+ },
+ getAnchors(cell) {
+ if (cell != null && this.model.isNode(cell)) {
+ const type = cell.data.type;
+ if (type === "start") {
+ return [[0.5, 1]];
+ }
+
+ if (isGroup(type)) {
+ return [
+ [0.25, 0],
+ [0.5, 0],
+ [0.75, 0],
+
+ [0, 0.25],
+ [0, 0.5],
+ [0, 0.75],
+
+ [1, 0.25],
+ [1, 0.5],
+ [1, 0.75],
+
+ [0.25, 1],
+ [0.5, 1],
+ [0.75, 1]
+ ];
+ }
+
+ return [[0.5, 0], [0, 0.5], [1, 0.5], [0.5, 1]];
+ }
+ return null;
+ },
+ getHtml(cell) {
+ const data = cell.data;
+ const group = isGroup(data.type);
+ return `
+
+ ${data.icon ? `${data.icon}` : ""}
+ ${group ? "" : `${data.title}`}
+
+ `;
+ }
+ });
+}
+
+export function addNode(
+ graph: Graph,
+ item: DataItem,
+ x: number,
+ y: number,
+ width?: number | null,
+ height?: number | null,
+ title?: string | null,
+ parent?: Cell
+) {
+ const data = { ...item.data };
+ if (title) {
+ data.title = title;
+ }
+
+ return graph.addNode({
+ x,
+ y,
+ data,
+ parent,
+ width: width || item.width,
+ height: height || item.height,
+ shape: "html",
+ resizable: !isCircle(item),
+ perimeter: isCircle(item) ? "ellipse" : "rectangle"
+ });
+}
+
+function fixEdgeStyle(raw: Style, result: Style) {
+ if (raw.targetAnchorX === 1 || raw.targetAnchorX === 0) {
+ result.elbow = "horizontal";
+ }
+}
+
+export function addEdge(graph: Graph, options: any) {
+ const style: Style = {};
+ fixEdgeStyle(options, style);
+ graph.addEdge({ ...options, style });
+}
+
+// export function graph2flow(graphJson: any) {
+// const { nodes, edges } = mockGraphData;
+
+// nodes.forEach()
+// }
diff --git a/src/pages/index.tsx b/src/pages/index.tsx
new file mode 100755
index 0000000..3d49937
--- /dev/null
+++ b/src/pages/index.tsx
@@ -0,0 +1,6 @@
+import React from "react";
+import FlowChart from "./flowchart";
+
+export default function() {
+ return ;
+}
diff --git a/src/util.ts b/src/util.ts
new file mode 100644
index 0000000..2ebd9db
--- /dev/null
+++ b/src/util.ts
@@ -0,0 +1,44 @@
+export function randomColor() {
+ var letters = "0123456789ABCDEF";
+ var color = "#";
+ for (var i = 0; i < 6; i += 1) {
+ color += letters[Math.floor(Math.random() * 16)];
+ }
+ return color;
+}
+
+export function invertColor(hex: string, bw: boolean) {
+ if (hex.indexOf("#") === 0) {
+ hex = hex.slice(1);
+ }
+ // convert 3-digit hex to 6-digits.
+ if (hex.length === 3) {
+ hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
+ }
+
+ if (hex.length !== 6) {
+ throw new Error("Invalid HEX color.");
+ }
+
+ const r = parseInt(hex.slice(0, 2), 16);
+ const g = parseInt(hex.slice(2, 4), 16);
+ const b = parseInt(hex.slice(4, 6), 16);
+
+ if (bw) {
+ // http://stackoverflow.com/a/3943023/112731
+ return r * 0.299 + g * 0.587 + b * 0.114 > 186 ? "#000000" : "#FFFFFF";
+ }
+
+ // invert color components
+ const rr = (255 - r).toString(16);
+ const gg = (255 - g).toString(16);
+ const bb = (255 - b).toString(16);
+
+ // pad each with zeros and return
+ return "#" + padZero(rr) + padZero(gg) + padZero(bb);
+}
+
+function padZero(str: string, len: number = 2) {
+ var zeros = new Array(len).join("0");
+ return (zeros + str).slice(-len);
+}
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..71d78b9
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,25 @@
+{
+ "compilerOptions": {
+ "target": "esnext",
+ "module": "esnext",
+ "moduleResolution": "node",
+ "jsx": "react",
+ "baseUrl": ".",
+ "importHelpers": true,
+ "sourceMap": true,
+ "esModuleInterop": true,
+ "noImplicitAny": true,
+ "noEmitOnError": true,
+ "noUnusedLocals": true,
+ "strictNullChecks": true,
+ "resolveJsonModule": true,
+ "experimentalDecorators": true,
+ "strictPropertyInitialization": false,
+ "allowSyntheticDefaultImports": true,
+ "paths": {
+ "@/*": [
+ "src/*"
+ ]
+ }
+ }
+}
diff --git a/tslint.json b/tslint.json
new file mode 100644
index 0000000..07ed8c2
--- /dev/null
+++ b/tslint.json
@@ -0,0 +1,25 @@
+{
+ "extends": ["tslint-react", "tslint-eslint-rules"],
+ "rules": {
+ "semicolon": [true, "never"],
+ "quotemark": [true, "single", "jsx-double"],
+ "trailing-comma": [
+ true,
+ {
+ "singleline": "never",
+ "multiline": {
+ "objects": "always",
+ "arrays": "always",
+ "functions": "always",
+ "typeLiterals": "ignore"
+ },
+ "esSpecCompliant": true
+ }
+ ],
+ "no-construct": true,
+ "no-debugger": true,
+ "no-reference": true,
+ "jsx-no-multiline-js": false,
+ "jsx-no-lambda": false
+ }
+}
diff --git a/typings.d.ts b/typings.d.ts
new file mode 100644
index 0000000..6fe5a28
--- /dev/null
+++ b/typings.d.ts
@@ -0,0 +1 @@
+declare module '*.less'