Browse Source

feat: init

master
ly165334 5 years ago
committed by 拾邑
parent
commit
ae74ae7585
  1. 16
      .editorconfig
  2. 2
      .env
  3. 3
      .eslintrc
  4. 114
      .gitignore
  5. 37
      .umirc.ts
  6. 21
      LICENSE
  7. 2
      README.md
  8. 57
      package.json
  9. 7
      src/global.css
  10. 398
      src/pages/flowchart/data.ts
  11. 171
      src/pages/flowchart/index.less
  12. 71
      src/pages/flowchart/index.tsx
  13. 70
      src/pages/flowchart/settings/execute.tsx
  14. 138
      src/pages/flowchart/settings/index.tsx
  15. 58
      src/pages/flowchart/settings/state.tsx
  16. 112
      src/pages/flowchart/sidebar.tsx
  17. 250
      src/pages/flowchart/toolbar.tsx
  18. 185
      src/pages/flowchart/util.ts
  19. 6
      src/pages/index.tsx
  20. 44
      src/util.ts
  21. 25
      tsconfig.json
  22. 25
      tslint.json
  23. 1
      typings.d.ts

16
.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

2
.env

@ -0,0 +1,2 @@
BROWSER=none
ESLINT=1

3
.eslintrc

@ -0,0 +1,3 @@
{
"extends": "eslint-config-umi"
}

114
.gitignore

@ -1,104 +1,18 @@
# Logs # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html) # dependencies
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json /node_modules
/npm-debug.log*
/yarn-error.log
/yarn.lock
/package-lock.json
# Runtime data # production
pids /dist
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover # misc
lib-cov .DS_Store
# Coverage directory used by tools like istanbul # umi
coverage .umi
*.lcov .umi-production
# 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

37
.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

21
LICENSE

@ -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.

2
README.md

@ -1,2 +0,0 @@
# iMove
move your mouse, generate flow chart

57
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"
}
}

7
src/global.css

@ -0,0 +1,7 @@
html, body, #root {
height: 100%;
}
body {
margin: 0;
}

398
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:
'<svg viewBox="0 0 1024 1024" focusable="false" width="1em" height="1em" fill="currentColor" aria-hidden="true"><path d="M825.8 498L538.4 249.9c-10.7-9.2-26.4-.9-26.4 14v496.3c0 14.9 15.7 23.2 26.4 14L825.8 526c8.3-7.2 8.3-20.8 0-28zm-320 0L218.4 249.9c-10.7-9.2-26.4-.9-26.4 14v496.3c0 14.9 15.7 23.2 26.4 14L505.8 526c4.1-3.6 6.2-8.8 6.2-14 0-5.2-2.1-10.4-6.2-14z"></path></svg>'
}
},
{
width: 148,
height: 48,
data: {
type: "cond",
title: "条件节点",
icon:
'<svg viewBox="64 64 896 896" focusable="false" width="1em" height="1em" fill="currentColor" aria-hidden="true"><path d="M752 100c-61.8 0-112 50.2-112 112 0 47.7 29.9 88.5 72 104.6v27.6L512 601.4 312 344.2v-27.6c42.1-16.1 72-56.9 72-104.6 0-61.8-50.2-112-112-112s-112 50.2-112 112c0 50.6 33.8 93.5 80 107.3v34.4c0 9.7 3.3 19.3 9.3 27L476 672.3v33.6c-44.2 15-76 56.9-76 106.1 0 61.8 50.2 112 112 112s112-50.2 112-112c0-49.2-31.8-91-76-106.1v-33.6l226.7-291.6c6-7.7 9.3-17.3 9.3-27v-34.4c46.2-13.8 80-56.7 80-107.3 0-61.8-50.2-112-112-112zM224 212a48.01 48.01 0 0 1 96 0 48.01 48.01 0 0 1-96 0zm336 600a48.01 48.01 0 0 1-96 0 48.01 48.01 0 0 1 96 0zm192-552a48.01 48.01 0 0 1 0-96 48.01 48.01 0 0 1 0 96z"></path></svg>'
}
}
];
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:
'<svg viewBox="0 0 1024 1024" focusable="false" width="1em" height="1em" fill="currentColor" aria-hidden="true"><path d="M825.8 498L538.4 249.9c-10.7-9.2-26.4-.9-26.4 14v496.3c0 14.9 15.7 23.2 26.4 14L825.8 526c8.3-7.2 8.3-20.8 0-28zm-320 0L218.4 249.9c-10.7-9.2-26.4-.9-26.4 14v496.3c0 14.9 15.7 23.2 26.4 14L505.8 526c4.1-3.6 6.2-8.8 6.2-14 0-5.2-2.1-10.4-6.2-14z"></path></svg>'
},
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:
'<svg viewBox="0 0 1024 1024" focusable="false" width="1em" height="1em" fill="currentColor" aria-hidden="true"><path d="M825.8 498L538.4 249.9c-10.7-9.2-26.4-.9-26.4 14v496.3c0 14.9 15.7 23.2 26.4 14L825.8 526c8.3-7.2 8.3-20.8 0-28zm-320 0L218.4 249.9c-10.7-9.2-26.4-.9-26.4 14v496.3c0 14.9 15.7 23.2 26.4 14L505.8 526c4.1-3.6 6.2-8.8 6.2-14 0-5.2-2.1-10.4-6.2-14z"></path></svg>',
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"
}
]
};

171
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;
}
}

71
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 (
<div className="flowchart-wrap">
<SplitBox
split="vertical"
minSize={200}
maxSize={-320}
defaultSize={240}
primary="first"
>
<div className="flowchart-sidebar">
<div className="flowchart-sidebar-title">iMove</div>
<div className="flowchart-sidebar-title"></div>
<div className="flowchart-sidebar-content">
{this.graph && this.state.inited && (
<Sidebar graph={this.graph} />
)}
</div>
</div>
<SplitBox
split="vertical"
minSize={200}
maxSize={-320}
defaultSize={240}
primary="second"
>
<div className="flowchart-main">
<div className="flowchart-toolbar">
{this.graph && <GraphToolbar graph={this.graph} />}
</div>
<div className="flowchart-graph">
<div className="graph" ref={this.refContainer} tabIndex={-1} />
</div>
</div>
{this.graph && this.state.inited && <Setting graph={this.graph} />}
</SplitBox>
</SplitBox>
</div>
)
}
}
export namespace FlowChart {
export interface State {
inited: boolean
}
}

70
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 (
<div className="execute-setting">
<label>
<div></div>
<Select
defaultValue={this.props.data.title}
style={{ width: 120 }}
onChange={this.props.onTitleChange}
>
<Option value="empty">empty</Option>
<Option value="resolve">resolve</Option>
<Option value="reject">reject</Option>
</Select>
</label>
<label>
<div>action</div>
<Select
defaultValue={this.props.data.action}
style={{ width: 120 }}
onChange={this.props.onActionChange}
>
<Option value="empty">context赋值</Option>
</Select>
</label>
<label>
<div><b>context key</b></div>
<Input
defaultValue={this.props.data.contextKey}
type="text"
allowClear={true}
onBlur={this.props.onKeyChange}
/>
</label>
<label>
<div><b>value path</b></div>
<Input
defaultValue={this.props.data.contextValue}
type="text"
allowClear={true}
onBlur={this.props.onValueChange}
/>
</label>
</div>
);
}
}
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 {}
}

138
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<Setting.Props, Setting.State> {
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 (
<div className="flowchart-format">
<div className="flowchart-sidebar-title"></div>
<div className="flowchart-sidebar-content">
{data && data.type === "status" && (
<StateSetting
data={data}
onTitleChange={this.onStatusTitleChange}
onApiChange={this.onApiChange}
onSerTypeChange={this.onSerTypeChange}
/>
)}
{data && data.type === "execute" && (
<ExecuteSetting
data={data}
onTitleChange={this.onExcuteTitleChange}
onActionChange={this.onActionChange}
onKeyChange={this.onKeyChange}
onValueChange={this.onValueChange}
/>
)}
</div>
</div>
);
}
return null;
}
}
export namespace Setting {
export interface Props {
graph: Graph;
}
export interface State {
selectedCells: Cell[];
}
}

58
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 (
<div>
<label>
<div></div>
<Input
defaultValue={this.props.data.title}
type="text"
allowClear={true}
onBlur={this.props.onTitleChange}
/>
</label>
<label>
<div></div>
<Select
defaultValue={this.props.data.serviceType}
style={{ width: 120 }}
onChange={this.props.onSerTypeChange}
>
<Option value="none">none</Option>
<Option value="invoke">invoke</Option>
</Select>
</label>
<label>
<div>api</div>
<Input
defaultValue={this.props.data.api}
type="text"
allowClear={true}
onBlur={this.props.onApiChange}
/>
</label>
</div>
);
}
}
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 {}
}

112
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<Sidebar.Props> {
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<DataItem>(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 (
<div className="flowchart-source-nodes" ref={this.refContainer}>
{data.items.map(({ data, width, height }) => (
<div
key={data.type}
data-type={data.type}
style={{ width, height }}
className={classNames("flowchart-node-wrap", data.type)}
>
<div className={classNames("flowchart-node", data.type)}>
{data.icon && <Icon type={data.icon} svg={true} />}
<span className="text">{data.title}</span>
</div>
</div>
))}
</div>
);
}
}
export namespace Sidebar {
export interface Props {
graph: Graph;
}
}

250
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 (
<Menu hasIcon={false}>
<MenuItem name="25">25%</MenuItem>
<MenuItem name="50">50%</MenuItem>
<MenuItem name="75">75%</MenuItem>
<MenuItem name="100">100%</MenuItem>
<MenuItem name="125">125%</MenuItem>
<MenuItem name="150">150%</MenuItem>
<MenuItem name="200">200%</MenuItem>
<MenuItem name="400">400%</MenuItem>
</Menu>
);
}
render() {
const Item = Toolbar.Item;
const Group = Toolbar.Group;
return (
<Toolbar onClick={this.onClick} hoverEffect={true} align="left">
<Item dropdown={this.renderZoomDropdown()}>
{(this.graph.view.scale * 100).toFixed(0)}%
</Item>
<Group>
<Item name="resetView" tooltip="重置视口" icon="retweet" />
<Item name="fitWindow" tooltip="适应窗口" icon="fullscreen" />
</Group>
{this.commands.map(items => (
<Group key={items.map(i => i.name).join("-")}>
{items.map(item => (
<Item
key={item.name}
name={item.name}
icon={item.icon}
tooltip={
item.shortcut
? `${item.tooltip}(${item.shortcut})`
: item.tooltip
}
disabled={(this.state as any)[item.name] as boolean}
onClick={item.handler}
/>
))}
</Group>
))}
</Toolbar>
);
}
}
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;
}
}

185
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 `
<div class="flowchart-node${group ? " is-gruop" : ""} ${data.type}">
${data.icon ? `<i class="icon">${data.icon}</i>` : ""}
${group ? "" : `<span class="text">${data.title}</span>`}
</div>
`;
}
});
}
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()
// }

6
src/pages/index.tsx

@ -0,0 +1,6 @@
import React from "react";
import FlowChart from "./flowchart";
export default function() {
return <FlowChart />;
}

44
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);
}

25
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/*"
]
}
}
}

25
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
}
}

1
typings.d.ts

@ -0,0 +1 @@
declare module '*.less'
Loading…
Cancel
Save