80 changed files with 6211 additions and 1897 deletions
File diff suppressed because it is too large
@ -0,0 +1,97 @@ |
|||||
|
import {Shape} from '@antv/x6'; |
||||
|
|
||||
|
const schema = { |
||||
|
base: Shape.Rect, |
||||
|
shape: 'imove-behavior', |
||||
|
width: 80, |
||||
|
height: 40, |
||||
|
label: '处理', |
||||
|
attrs: { |
||||
|
body: { |
||||
|
fill: '#AACCF7', |
||||
|
stroke: '#5E9CEE' |
||||
|
}, |
||||
|
label: { |
||||
|
fill: '#333' |
||||
|
} |
||||
|
}, |
||||
|
ports: { |
||||
|
groups: { |
||||
|
top: { |
||||
|
position: 'top', |
||||
|
attrs: { |
||||
|
circle: { |
||||
|
r: 3, |
||||
|
magnet: true, |
||||
|
stroke: '#666', |
||||
|
strokeWidth: 1, |
||||
|
fill: '#fff' |
||||
|
}, |
||||
|
}, |
||||
|
}, |
||||
|
right: { |
||||
|
position: 'right', |
||||
|
attrs: { |
||||
|
circle: { |
||||
|
r: 3, |
||||
|
magnet: true, |
||||
|
stroke: '#666', |
||||
|
strokeWidth: 1, |
||||
|
fill: '#fff' |
||||
|
}, |
||||
|
}, |
||||
|
}, |
||||
|
bottom: { |
||||
|
position: 'bottom', |
||||
|
attrs: { |
||||
|
circle: { |
||||
|
r: 3, |
||||
|
magnet: true, |
||||
|
stroke: '#666', |
||||
|
strokeWidth: 1, |
||||
|
fill: '#fff' |
||||
|
}, |
||||
|
}, |
||||
|
}, |
||||
|
left: { |
||||
|
position: 'left', |
||||
|
attrs: { |
||||
|
circle: { |
||||
|
r: 3, |
||||
|
magnet: true, |
||||
|
stroke: '#666', |
||||
|
strokeWidth: 1, |
||||
|
fill: '#fff' |
||||
|
}, |
||||
|
}, |
||||
|
}, |
||||
|
}, |
||||
|
items: [ |
||||
|
{ |
||||
|
id: 'top', |
||||
|
group: 'top' |
||||
|
}, |
||||
|
{ |
||||
|
id: 'right', |
||||
|
group: 'right' |
||||
|
}, |
||||
|
{ |
||||
|
id: 'bottom', |
||||
|
group: 'bottom' |
||||
|
}, |
||||
|
{ |
||||
|
id: 'left', |
||||
|
group: 'left' |
||||
|
} |
||||
|
] |
||||
|
}, |
||||
|
data: { |
||||
|
label: '处理', |
||||
|
configSchema: '{\n \n}', |
||||
|
configData: {}, |
||||
|
dependencies: '{\n \n}', |
||||
|
code: 'export default async function(ctx) {\n \n}' |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
export default schema; |
@ -0,0 +1,130 @@ |
|||||
|
import {Shape} from '@antv/x6'; |
||||
|
|
||||
|
const schema = { |
||||
|
base: Shape.Polygon, |
||||
|
shape: 'imove-branch', |
||||
|
width: 80, |
||||
|
height: 40, |
||||
|
attrs: { |
||||
|
body: { |
||||
|
strokeWidth: 2, |
||||
|
stroke: '#333', |
||||
|
fill: '#FFFC7C', |
||||
|
refPoints: '0,10 10,0 20,10 10,20' |
||||
|
}, |
||||
|
label: { |
||||
|
refX: 0.5, |
||||
|
refY: 0.5, |
||||
|
text: '判断', |
||||
|
fill: '#333', |
||||
|
fontSize: 14, |
||||
|
textAnchor: 'middle', |
||||
|
textVerticalAnchor: 'middle' |
||||
|
} |
||||
|
}, |
||||
|
ports: { |
||||
|
groups: { |
||||
|
top: { |
||||
|
position: 'top', |
||||
|
attrs: { |
||||
|
circle: { |
||||
|
r: 3, |
||||
|
magnet: true, |
||||
|
stroke: '#666', |
||||
|
strokeWidth: 1, |
||||
|
fill: '#fff' |
||||
|
}, |
||||
|
}, |
||||
|
}, |
||||
|
right: { |
||||
|
zIndex: 999999999, |
||||
|
position: 'right', |
||||
|
attrs: { |
||||
|
circle: { |
||||
|
r: 3, |
||||
|
magnet: true, |
||||
|
stroke: '#666', |
||||
|
strokeWidth: 1, |
||||
|
fill: '#fff' |
||||
|
}, |
||||
|
}, |
||||
|
label: { |
||||
|
position: 'right' |
||||
|
} |
||||
|
}, |
||||
|
bottom: { |
||||
|
zIndex: 999999999, |
||||
|
position: 'bottom', |
||||
|
attrs: { |
||||
|
circle: { |
||||
|
r: 3, |
||||
|
magnet: true, |
||||
|
stroke: '#666', |
||||
|
strokeWidth: 1, |
||||
|
fill: '#fff' |
||||
|
}, |
||||
|
}, |
||||
|
label: { |
||||
|
position: 'bottom' |
||||
|
} |
||||
|
}, |
||||
|
left: { |
||||
|
position: 'left', |
||||
|
attrs: { |
||||
|
circle: { |
||||
|
r: 3, |
||||
|
magnet: true, |
||||
|
stroke: '#666', |
||||
|
strokeWidth: 1, |
||||
|
fill: '#fff' |
||||
|
}, |
||||
|
}, |
||||
|
}, |
||||
|
}, |
||||
|
items: [ |
||||
|
{ |
||||
|
id: 'top', |
||||
|
group: 'top' |
||||
|
}, |
||||
|
{ |
||||
|
id: 'right', |
||||
|
group: 'right', |
||||
|
attrs: { |
||||
|
text: { |
||||
|
text: '是' |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
{ |
||||
|
id: 'bottom', |
||||
|
group: 'bottom', |
||||
|
attrs: { |
||||
|
text: { |
||||
|
text: '否' |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
{ |
||||
|
id: 'left', |
||||
|
group: 'left' |
||||
|
} |
||||
|
] |
||||
|
}, |
||||
|
data: { |
||||
|
label: '判断', |
||||
|
configSchema: '{\n \n}', |
||||
|
configData: {}, |
||||
|
dependencies: '{\n \n}', |
||||
|
ports: { |
||||
|
right: { |
||||
|
condition: 'true' |
||||
|
}, |
||||
|
bottom: { |
||||
|
condition: 'false' |
||||
|
} |
||||
|
}, |
||||
|
code: 'export default async function(ctx) {\n return true;\n}' |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
export default schema; |
@ -0,0 +1,11 @@ |
|||||
|
import start from './start'; |
||||
|
import branch from './branch'; |
||||
|
import behavior from './behavior'; |
||||
|
|
||||
|
const cellSchemaMap: {[key: string]: any} = { |
||||
|
'imove-start': start, |
||||
|
'imove-branch': branch, |
||||
|
'imove-behavior': behavior |
||||
|
}; |
||||
|
|
||||
|
export default cellSchemaMap; |
@ -0,0 +1,99 @@ |
|||||
|
import {Shape} from '@antv/x6'; |
||||
|
|
||||
|
const schema = { |
||||
|
base: Shape.Circle, |
||||
|
shape: 'imove-start', |
||||
|
width: 80, |
||||
|
height: 80, |
||||
|
label: '开始', |
||||
|
attrs: { |
||||
|
body: { |
||||
|
fill: '#333', |
||||
|
strokeWidth: 1, |
||||
|
stroke: '#333' |
||||
|
}, |
||||
|
label: { |
||||
|
fill: '#FFF' |
||||
|
} |
||||
|
}, |
||||
|
ports: { |
||||
|
groups: { |
||||
|
top: { |
||||
|
position: 'top', |
||||
|
attrs: { |
||||
|
circle: { |
||||
|
r: 3, |
||||
|
magnet: true, |
||||
|
stroke: '#666', |
||||
|
strokeWidth: 1, |
||||
|
fill: '#fff' |
||||
|
}, |
||||
|
}, |
||||
|
}, |
||||
|
right: { |
||||
|
position: 'right', |
||||
|
attrs: { |
||||
|
circle: { |
||||
|
r: 3, |
||||
|
magnet: true, |
||||
|
stroke: '#666', |
||||
|
strokeWidth: 1, |
||||
|
fill: '#fff' |
||||
|
}, |
||||
|
}, |
||||
|
}, |
||||
|
bottom: { |
||||
|
position: 'bottom', |
||||
|
attrs: { |
||||
|
circle: { |
||||
|
r: 3, |
||||
|
magnet: true, |
||||
|
stroke: '#666', |
||||
|
strokeWidth: 1, |
||||
|
fill: '#fff' |
||||
|
}, |
||||
|
}, |
||||
|
}, |
||||
|
left: { |
||||
|
position: 'left', |
||||
|
attrs: { |
||||
|
circle: { |
||||
|
r: 3, |
||||
|
magnet: true, |
||||
|
stroke: '#666', |
||||
|
strokeWidth: 1, |
||||
|
fill: '#fff' |
||||
|
}, |
||||
|
}, |
||||
|
}, |
||||
|
}, |
||||
|
items: [ |
||||
|
{ |
||||
|
id: 'top', |
||||
|
group: 'top' |
||||
|
}, |
||||
|
{ |
||||
|
id: 'right', |
||||
|
group: 'right' |
||||
|
}, |
||||
|
{ |
||||
|
id: 'bottom', |
||||
|
group: 'bottom' |
||||
|
}, |
||||
|
{ |
||||
|
id: 'left', |
||||
|
group: 'left' |
||||
|
} |
||||
|
] |
||||
|
}, |
||||
|
data: { |
||||
|
label: '开始', |
||||
|
configSchema: '{\n \n}', |
||||
|
configData: {}, |
||||
|
trigger: 'start', |
||||
|
dependencies: '{\n \n}', |
||||
|
code: 'export default async function(ctx) {\n \n}' |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
export default schema; |
@ -0,0 +1,21 @@ |
|||||
|
import {Shape} from '@antv/x6'; |
||||
|
|
||||
|
const schema = { |
||||
|
base: Shape.Rect, |
||||
|
shape: 'imove-behavior-preview', |
||||
|
top: 20, |
||||
|
width: 80, |
||||
|
height: 40, |
||||
|
label: '处理', |
||||
|
attrs: { |
||||
|
body: { |
||||
|
fill: '#AACCF7', |
||||
|
stroke: '#5E9CEE' |
||||
|
}, |
||||
|
label: { |
||||
|
fill: '#333' |
||||
|
} |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
export default schema; |
@ -0,0 +1,29 @@ |
|||||
|
import {Shape} from '@antv/x6'; |
||||
|
|
||||
|
const schema = { |
||||
|
base: Shape.Polygon, |
||||
|
shape: 'imove-branch-preview', |
||||
|
top: 20, |
||||
|
width: 80, |
||||
|
height: 40, |
||||
|
label: '分支', |
||||
|
attrs: { |
||||
|
body: { |
||||
|
strokeWidth: 2, |
||||
|
stroke: '#333', |
||||
|
fill: '#FFFC7C', |
||||
|
refPoints: '0,10 10,0 20,10 10,20' |
||||
|
}, |
||||
|
label: { |
||||
|
refX: 0.5, |
||||
|
refY: 0.5, |
||||
|
text: '判断', |
||||
|
fill: '#333', |
||||
|
fontSize: 14, |
||||
|
textAnchor: 'middle', |
||||
|
textVerticalAnchor: 'middle' |
||||
|
} |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
export default schema; |
@ -0,0 +1,11 @@ |
|||||
|
import start from './start'; |
||||
|
import branch from './branch'; |
||||
|
import behavior from './behavior'; |
||||
|
|
||||
|
const cellSchemaMap: {[key: string]: any} = { |
||||
|
'imove-start-preview': start, |
||||
|
'imove-branch-preview': branch, |
||||
|
'imove-behavior-preview': behavior |
||||
|
}; |
||||
|
|
||||
|
export default cellSchemaMap; |
@ -0,0 +1,22 @@ |
|||||
|
import {Shape} from '@antv/x6'; |
||||
|
|
||||
|
const schema = { |
||||
|
base: Shape.Circle, |
||||
|
shape: 'imove-start-preview', |
||||
|
top: 0, |
||||
|
width: 80, |
||||
|
height: 80, |
||||
|
label: '开始', |
||||
|
attrs: { |
||||
|
body: { |
||||
|
strokeWidth: 0, |
||||
|
stroke: '#333', |
||||
|
fill: '#333' |
||||
|
}, |
||||
|
label: { |
||||
|
fill: '#FFF' |
||||
|
} |
||||
|
}, |
||||
|
}; |
||||
|
|
||||
|
export default schema; |
@ -0,0 +1,139 @@ |
|||||
|
import {Graph} from '@antv/x6'; |
||||
|
import {safeGet} from '../utils'; |
||||
|
|
||||
|
interface Shortcut { |
||||
|
keys: string | string[]; |
||||
|
handler: (flowChart: Graph) => void; |
||||
|
} |
||||
|
|
||||
|
const MIN_ZOOM = 0.5; |
||||
|
const MAX_ZOOM = 1.5; |
||||
|
const ZOOM_STEP = 0.1; |
||||
|
|
||||
|
const shortcuts: {[key: string]: Shortcut} = { |
||||
|
save: { |
||||
|
keys: `meta + s`, |
||||
|
handler(flowChart: Graph) { |
||||
|
// TODO: save data
|
||||
|
console.log(JSON.stringify(flowChart.toJSON(), null, 2)); |
||||
|
fetch('http://localhost:3456/api/save', { |
||||
|
method: 'POST', |
||||
|
headers: { |
||||
|
'content-type': 'application/json' |
||||
|
}, |
||||
|
body: JSON.stringify({dsl: flowChart.toJSON()}) |
||||
|
}); |
||||
|
return false; |
||||
|
} |
||||
|
}, |
||||
|
undo: { |
||||
|
keys: `meta + z`, |
||||
|
handler(flowChart: Graph) { |
||||
|
flowChart.undo(); |
||||
|
return false; |
||||
|
} |
||||
|
}, |
||||
|
redo: { |
||||
|
keys: `meta + shift + z`, |
||||
|
handler(flowChart: Graph) { |
||||
|
flowChart.redo(); |
||||
|
return false; |
||||
|
} |
||||
|
}, |
||||
|
zoomIn: { |
||||
|
keys: `meta + shift + \+`, |
||||
|
handler(flowChart: Graph) { |
||||
|
const nextZoom = (flowChart.zoom() + ZOOM_STEP).toPrecision(2); |
||||
|
flowChart.zoomTo(Number(nextZoom), {maxScale: MAX_ZOOM}); |
||||
|
return false; |
||||
|
} |
||||
|
}, |
||||
|
zoomOut: { |
||||
|
keys: `meta + shift + \-`, |
||||
|
handler(flowChart: Graph) { |
||||
|
const nextZoom = (flowChart.zoom() - ZOOM_STEP).toPrecision(2); |
||||
|
flowChart.zoomTo(Number(nextZoom), {minScale: MIN_ZOOM}); |
||||
|
return false; |
||||
|
} |
||||
|
}, |
||||
|
copy: { |
||||
|
keys: 'meta + c', |
||||
|
handler(flowChart: Graph) { |
||||
|
const cells = flowChart.getSelectedCells(); |
||||
|
if (cells.length > 0) { |
||||
|
flowChart.copy(cells); |
||||
|
} |
||||
|
return false; |
||||
|
} |
||||
|
}, |
||||
|
paste: { |
||||
|
keys: 'meta + v', |
||||
|
handler(flowChart: Graph) { |
||||
|
if (!flowChart.isClipboardEmpty()) { |
||||
|
const cells = flowChart.paste({offset: 32}); |
||||
|
flowChart.cleanSelection(); |
||||
|
flowChart.select(cells); |
||||
|
} |
||||
|
return false; |
||||
|
} |
||||
|
}, |
||||
|
delete: { |
||||
|
keys: ['backspace', 'del'], |
||||
|
handler(flowChart: Graph) { |
||||
|
flowChart.removeCells(flowChart.getSelectedCells()); |
||||
|
flowChart.trigger('toolBar:forceUpdate'); |
||||
|
return false; |
||||
|
} |
||||
|
}, |
||||
|
selectAll: { |
||||
|
keys: 'meta + a', |
||||
|
handler(flowChart: Graph) { |
||||
|
flowChart.select(flowChart.getCells()); |
||||
|
return false; |
||||
|
} |
||||
|
}, |
||||
|
bold: { |
||||
|
keys: 'meta + b', |
||||
|
handler(flowChart: Graph) { |
||||
|
const cells = flowChart.getSelectedCells(); |
||||
|
if (cells.length > 0) { |
||||
|
const isAlreadyBold = safeGet(cells, '0.attrs.label.fontWeight', 'normal') === 'bold'; |
||||
|
cells.forEach((cell) => { |
||||
|
cell.setAttrs({label: {fontWeight: isAlreadyBold ? 'normal' : 'bold'}}); |
||||
|
}); |
||||
|
flowChart.trigger('toolBar:forceUpdate'); |
||||
|
} |
||||
|
return false; |
||||
|
} |
||||
|
}, |
||||
|
italic: { |
||||
|
keys: 'meta + i', |
||||
|
handler(flowChart: Graph) { |
||||
|
const cells = flowChart.getSelectedCells(); |
||||
|
if(cells.length > 0) { |
||||
|
const isAlreadyItalic = safeGet(cells, '0.attrs.label.fontStyle', 'normal') === 'italic'; |
||||
|
cells.forEach((cell) => { |
||||
|
cell.setAttrs({label: {fontStyle: isAlreadyItalic ? 'normal' : 'italic'}}); |
||||
|
}); |
||||
|
flowChart.trigger('toolBar:forceUpdate'); |
||||
|
} |
||||
|
return false; |
||||
|
} |
||||
|
}, |
||||
|
underline: { |
||||
|
keys: 'meta + u', |
||||
|
handler(flowChart: Graph) { |
||||
|
const cells = flowChart.getSelectedCells(); |
||||
|
if(cells.length > 0) { |
||||
|
const isAlreadyUnderline = safeGet(cells, '0.attrs.label.textDecoration', 'none') === 'underline'; |
||||
|
cells.forEach((cell) => { |
||||
|
cell.setAttrs({label: {textDecoration: isAlreadyUnderline ? 'none' : 'underline'}}); |
||||
|
}); |
||||
|
flowChart.trigger('toolBar:forceUpdate'); |
||||
|
} |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
export default shortcuts; |
@ -1,34 +0,0 @@ |
|||||
/** @jsx jsx */ |
|
||||
import { jsx, css } from '@emotion/core'; |
|
||||
import { CellProps } from '../../model'; |
|
||||
import Label from '../../utils/styles/cells'; |
|
||||
|
|
||||
const StateCell = ({ label, scale = 1 }: CellProps): JSX.Element => { |
|
||||
const width = 48 * scale; |
|
||||
const height = 30 * scale; |
|
||||
return ( |
|
||||
<div> |
|
||||
<svg |
|
||||
css={css` |
|
||||
width: ${width}px; |
|
||||
height: ${height}px; |
|
||||
`}
|
|
||||
> |
|
||||
<rect |
|
||||
css={css` |
|
||||
fill: #daeffa; |
|
||||
stroke: #82caed; |
|
||||
`}
|
|
||||
x="1" |
|
||||
y="1" |
|
||||
width={width - 2} |
|
||||
height={height - 2} |
|
||||
strokeWidth="2" |
|
||||
/> |
|
||||
</svg> |
|
||||
{label && <Label>{label}</Label>} |
|
||||
</div> |
|
||||
); |
|
||||
}; |
|
||||
|
|
||||
export default StateCell; |
|
@ -1,33 +0,0 @@ |
|||||
/** @jsx jsx */ |
|
||||
import { jsx, css } from '@emotion/core'; |
|
||||
import { CellProps } from '../../model'; |
|
||||
import Label from '../../utils/styles/cells'; |
|
||||
|
|
||||
const DecisionCell = ({ label, scale = 1 }: CellProps): JSX.Element => { |
|
||||
const width = 48 * scale; |
|
||||
const height = 30 * scale; |
|
||||
return ( |
|
||||
<div> |
|
||||
<svg |
|
||||
css={css` |
|
||||
width: ${width}px; |
|
||||
height: ${height}px; |
|
||||
`}
|
|
||||
> |
|
||||
<polygon |
|
||||
css={css` |
|
||||
fill: #fef0dd; |
|
||||
stroke: #fccd8e; |
|
||||
`}
|
|
||||
points={`${1 * scale},${15 * scale} ${24 * scale},${29 * scale} ${47 * scale},${ |
|
||||
15 * scale |
|
||||
} ${24 * scale},${1 * scale}`}
|
|
||||
strokeWidth="2" |
|
||||
/> |
|
||||
</svg> |
|
||||
{label && <Label>{label}</Label>} |
|
||||
</div> |
|
||||
); |
|
||||
}; |
|
||||
|
|
||||
export default DecisionCell; |
|
@ -1,27 +0,0 @@ |
|||||
/** @jsx jsx */ |
|
||||
import { jsx, css } from '@emotion/core'; |
|
||||
import { CellProps } from '../../model'; |
|
||||
|
|
||||
const EndCell = ({ scale = 1 }: CellProps): JSX.Element => { |
|
||||
const width = 20 * scale; |
|
||||
const radius = 10 * scale; |
|
||||
return ( |
|
||||
<svg |
|
||||
css={css` |
|
||||
width: ${width}px; |
|
||||
height: ${width}px; |
|
||||
`}
|
|
||||
> |
|
||||
<circle |
|
||||
css={{ fill: '#f55753' }} |
|
||||
cx={radius} |
|
||||
cy={radius} |
|
||||
r={radius} |
|
||||
strokeWidth={2 * scale} |
|
||||
/> |
|
||||
<circle css={{ fill: '#faaba9' }} cx={radius} cy={radius} r={8 * scale} /> |
|
||||
</svg> |
|
||||
); |
|
||||
}; |
|
||||
|
|
||||
export default EndCell; |
|
@ -1,15 +0,0 @@ |
|||||
import Start from './start'; |
|
||||
import Action from './action'; |
|
||||
import Decision from './decision'; |
|
||||
import End from './end'; |
|
||||
|
|
||||
const CellMap = { |
|
||||
start: Start, |
|
||||
action: Action, |
|
||||
decision: Decision, |
|
||||
end: End, |
|
||||
}; |
|
||||
|
|
||||
export default CellMap; |
|
||||
|
|
||||
export type CellTypes = keyof typeof CellMap; |
|
@ -1,27 +0,0 @@ |
|||||
/** @jsx jsx */ |
|
||||
import { jsx, css } from '@emotion/core'; |
|
||||
import { CellProps } from '../../model'; |
|
||||
|
|
||||
const StartCell = ({ scale = 1 }: CellProps): JSX.Element => { |
|
||||
const width = 20 * scale; |
|
||||
const radius = 10 * scale; |
|
||||
return ( |
|
||||
<svg |
|
||||
css={css` |
|
||||
width: ${width}px; |
|
||||
height: ${width}px; |
|
||||
`}
|
|
||||
> |
|
||||
<circle |
|
||||
css={{ fill: '#76ca02' }} |
|
||||
cx={radius} |
|
||||
cy={radius} |
|
||||
r={radius} |
|
||||
strokeWidth={2 * scale} |
|
||||
/> |
|
||||
<circle css={{ fill: '#bbe581' }} cx={radius} cy={radius} r={8 * scale} /> |
|
||||
</svg> |
|
||||
); |
|
||||
}; |
|
||||
|
|
||||
export default StartCell; |
|
@ -0,0 +1,31 @@ |
|||||
|
import React, {useState, useEffect} from 'react'; |
||||
|
|
||||
|
import {SketchPicker, ColorResult} from 'react-color'; |
||||
|
|
||||
|
interface IProps { |
||||
|
color: string; |
||||
|
onChangeComplete: (color: string) => void; |
||||
|
} |
||||
|
|
||||
|
const ColorPicker: React.FC<IProps> = (props) => { |
||||
|
|
||||
|
const {color, onChangeComplete: changeCb} = props; |
||||
|
const [curColor, setCurColor] = useState(color); |
||||
|
|
||||
|
// life
|
||||
|
useEffect(() => setCurColor(color), [color]); |
||||
|
|
||||
|
// events
|
||||
|
const onChange = (color: ColorResult): void => setCurColor(color.hex); |
||||
|
const onChangeComplete = (color: ColorResult): void => changeCb(color.hex); |
||||
|
|
||||
|
return ( |
||||
|
<SketchPicker |
||||
|
color={curColor} |
||||
|
onChange={onChange} |
||||
|
onChangeComplete={onChangeComplete} |
||||
|
/> |
||||
|
); |
||||
|
}; |
||||
|
|
||||
|
export default ColorPicker; |
@ -1,76 +0,0 @@ |
|||||
import * as React from 'react'; |
|
||||
import { Form, Input, InputNumber } from 'antd'; |
|
||||
import 'antd/lib/input-number/style'; |
|
||||
|
|
||||
const { Item } = Form; |
|
||||
|
|
||||
const labelCol = { span: 7 }; |
|
||||
|
|
||||
export interface SchemaFormProps { |
|
||||
schema: any; |
|
||||
data: any; |
|
||||
} |
|
||||
|
|
||||
export interface SchemaFormState { |
|
||||
data: any; |
|
||||
} |
|
||||
|
|
||||
class SchemaForm extends React.Component<SchemaFormProps, SchemaFormState> { |
|
||||
constructor(props: SchemaFormProps) { |
|
||||
super(props); |
|
||||
|
|
||||
this.state = { |
|
||||
data: props.data, |
|
||||
}; |
|
||||
} |
|
||||
|
|
||||
changeValue = (e: React.ChangeEvent<HTMLInputElement>, key: string): void => { |
|
||||
const { data } = this.state; |
|
||||
const value = e.target.value.trim(); |
|
||||
if (!data.input) { |
|
||||
data.input = {}; |
|
||||
} |
|
||||
data.input[key] = value; |
|
||||
this.setState({ data }); |
|
||||
}; |
|
||||
|
|
||||
changeNumValue = (value: number | undefined, key: string): void => { |
|
||||
const { data } = this.state; |
|
||||
if (!data.input) { |
|
||||
data.input = {}; |
|
||||
} |
|
||||
data.input[key] = value; |
|
||||
this.setState({ data }); |
|
||||
}; |
|
||||
|
|
||||
render(): JSX.Element { |
|
||||
const { data } = this.state; |
|
||||
const { |
|
||||
schema: { properties }, |
|
||||
} = this.props; |
|
||||
|
|
||||
return ( |
|
||||
<Form labelCol={labelCol} initialValues={data}> |
|
||||
{Object.keys(properties).map((key) => ( |
|
||||
<Item key={key} label={properties[key].title} name={`input.${properties[key].title}`}> |
|
||||
{properties[key].type === 'string' ? ( |
|
||||
<Input |
|
||||
onChange={(e): void => { |
|
||||
this.changeValue(e, key); |
|
||||
}} |
|
||||
/> |
|
||||
) : ( |
|
||||
<InputNumber |
|
||||
onChange={(value): void => { |
|
||||
this.changeNumValue(value, key); |
|
||||
}} |
|
||||
/> |
|
||||
)} |
|
||||
</Item> |
|
||||
))} |
|
||||
</Form> |
|
||||
); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
export default SchemaForm; |
|
@ -1,164 +0,0 @@ |
|||||
/** @jsx jsx */ |
|
||||
import { Component } from 'react'; |
|
||||
import { jsx, css } from '@emotion/core'; |
|
||||
import styled from '@emotion/styled'; |
|
||||
import { Graph, Cell } from '@antv/x6'; |
|
||||
import { Collapse, Form, Input } from 'antd'; |
|
||||
import SchemaForm from './schema-form'; |
|
||||
import 'antd/lib/form/style'; |
|
||||
import 'antd/lib/input/style'; |
|
||||
|
|
||||
const { Panel } = Collapse; |
|
||||
const { Item } = Form; |
|
||||
|
|
||||
const labelCol = { span: 7 }; |
|
||||
|
|
||||
const Header = styled.h3` |
|
||||
margin-bottom: 0; |
|
||||
padding-left: 20px; |
|
||||
height: 36px; |
|
||||
line-height: 36px; |
|
||||
font-size: 12px; |
|
||||
color: #555; |
|
||||
border-bottom: 1px solid #ddd; |
|
||||
`;
|
|
||||
|
|
||||
const SettingCollapse = styled(Collapse)` |
|
||||
font-size: 14px; |
|
||||
border: none; |
|
||||
|
|
||||
> .ant-collapse-item > .ant-collapse-header { |
|
||||
height: 36px; |
|
||||
line-height: 12px; |
|
||||
font-size: 12px; |
|
||||
color: #666; |
|
||||
} |
|
||||
`;
|
|
||||
|
|
||||
interface SettingProps { |
|
||||
graph: Graph; |
|
||||
} |
|
||||
|
|
||||
interface SettingState { |
|
||||
cell: Cell<any> | null; |
|
||||
} |
|
||||
|
|
||||
class Setting extends Component<SettingProps, SettingState> { |
|
||||
constructor(props: SettingProps) { |
|
||||
super(props); |
|
||||
|
|
||||
this.state = { |
|
||||
cell: null, |
|
||||
}; |
|
||||
|
|
||||
const { graph } = this.props; |
|
||||
graph.on('selection:changed', () => { |
|
||||
const cells = graph.getSelectedCells(); |
|
||||
if (cells.length === 0) { |
|
||||
this.setState({ cell: null }); |
|
||||
return; |
|
||||
} |
|
||||
|
|
||||
const cell = cells[0]; |
|
||||
|
|
||||
if (cell.isEdge()) { |
|
||||
cell.data = {}; |
|
||||
} |
|
||||
|
|
||||
this.setState({ cell }); |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
render(): JSX.Element { |
|
||||
const { graph } = this.props; |
|
||||
const { cell } = this.state; |
|
||||
let cellType = ''; |
|
||||
|
|
||||
if (cell) { |
|
||||
if (cell.isNode()) { |
|
||||
cellType = cell.data.type.toUpperCase(); |
|
||||
} else { |
|
||||
cellType = 'EDGE'; |
|
||||
} |
|
||||
} else { |
|
||||
cellType = 'FLOW'; |
|
||||
} |
|
||||
|
|
||||
return ( |
|
||||
<div |
|
||||
css={css` |
|
||||
height: 100%; |
|
||||
overflow: auto; |
|
||||
border-left: 1px solid #ddd; |
|
||||
`}
|
|
||||
> |
|
||||
<Header>{cellType}</Header> |
|
||||
{cell && !['start', 'end'].includes(cell.data.type) && ( |
|
||||
<SettingCollapse |
|
||||
defaultActiveKey={['general', 'event', 'input', 'output']} |
|
||||
key={cell && cell.id ? cell.id : ''} |
|
||||
> |
|
||||
<Panel header="General" key="general"> |
|
||||
<Form labelCol={labelCol} initialValues={{ label: cell.data.label }}> |
|
||||
<Item label="名称" name="label"> |
|
||||
<Input |
|
||||
onChange={(e): void => { |
|
||||
cell.data.label = e.target.value.trim(); |
|
||||
graph.setCellData(cell, cell.data); |
|
||||
}} |
|
||||
/> |
|
||||
</Item> |
|
||||
</Form> |
|
||||
</Panel> |
|
||||
{/* <Panel header="Event" key="event"> |
|
||||
<Form |
|
||||
labelCol={labelCol} |
|
||||
initialValues={{ event: cell.data && cell.data.data && cell.data.data.event }} |
|
||||
> |
|
||||
<Item label="触发事件" name="event"> |
|
||||
<Input |
|
||||
onChange={(e): void => { |
|
||||
if (!cell.data.data) { |
|
||||
cell.data.data = {}; |
|
||||
} |
|
||||
cell.data.data.event = e.target.value.trim(); |
|
||||
graph.setCellData(cell, cell.data); |
|
||||
}} |
|
||||
/> |
|
||||
</Item> |
|
||||
</Form> |
|
||||
</Panel> */} |
|
||||
{cell.data && cell.data.schema && Object.keys(cell.data.schema.properties).length && ( |
|
||||
<Panel header="Input" key="input"> |
|
||||
<SchemaForm schema={cell.data.schema} data={cell.data.data} /> |
|
||||
</Panel> |
|
||||
)} |
|
||||
<Panel header="Output" key="output"> |
|
||||
<Form |
|
||||
labelCol={labelCol} |
|
||||
initialValues={{ event: cell.data && cell.data.data && cell.data.data.contextKey }} |
|
||||
> |
|
||||
<Item label="存储字段" name="output.contextKey"> |
|
||||
<Input |
|
||||
onChange={(e): void => { |
|
||||
if (!cell.data.data) { |
|
||||
cell.data.data = {}; |
|
||||
} |
|
||||
if (!cell.data.data.output) { |
|
||||
cell.data.data.output = {}; |
|
||||
} |
|
||||
cell.data.data.output.contextKey = e.target.value.trim(); |
|
||||
graph.setCellData(cell, cell.data); |
|
||||
}} |
|
||||
/> |
|
||||
</Item> |
|
||||
</Form> |
|
||||
</Panel> |
|
||||
</SettingCollapse> |
|
||||
)} |
|
||||
</div> |
|
||||
); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
export default Setting; |
|
@ -1,131 +0,0 @@ |
|||||
/** @jsx jsx */ |
|
||||
import { useRef, useEffect, ReactNode } from 'react'; |
|
||||
import { jsx, css } from '@emotion/core'; |
|
||||
import styled from '@emotion/styled'; |
|
||||
import { Graph } from '@antv/x6'; |
|
||||
import { Collapse } from 'antd'; |
|
||||
import { generals, DataItem } from '../data/cells'; |
|
||||
import patchDnd from '../utils/patch-dnd'; |
|
||||
import Cells from './cells'; |
|
||||
import 'antd/lib/collapse/style'; |
|
||||
|
|
||||
const { Panel } = Collapse; |
|
||||
|
|
||||
const ToolCollapse = styled(Collapse)` |
|
||||
border: none; |
|
||||
|
|
||||
> .ant-collapse-item > .ant-collapse-header { |
|
||||
height: 36px; |
|
||||
line-height: 12px; |
|
||||
font-size: 12px; |
|
||||
color: #666; |
|
||||
} |
|
||||
`;
|
|
||||
|
|
||||
const ToolPanel = styled(Panel)` |
|
||||
margin-right: -1px; |
|
||||
|
|
||||
.ant-collapse-content-box { |
|
||||
margin-bottom: -1px; |
|
||||
padding: 0; |
|
||||
overflow: hidden; |
|
||||
} |
|
||||
`;
|
|
||||
|
|
||||
interface SidebarProps { |
|
||||
graph: Graph; |
|
||||
cells: DataItem[]; |
|
||||
} |
|
||||
|
|
||||
const CellWrapper = ({ data, children }: { data: DataItem; children: ReactNode }): JSX.Element => { |
|
||||
return ( |
|
||||
<div |
|
||||
css={css` |
|
||||
box-sizing: border-box; |
|
||||
display: inline-block; |
|
||||
padding: 10px 5px; |
|
||||
width: 50%; |
|
||||
height: 80px; |
|
||||
text-align: center; |
|
||||
vertical-align: top; |
|
||||
border-right: 1px solid #ddd; |
|
||||
border-bottom: 1px solid #ddd; |
|
||||
|
|
||||
:hover { |
|
||||
box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.1); |
|
||||
} |
|
||||
`}
|
|
||||
> |
|
||||
<div |
|
||||
css={css` |
|
||||
display: flex; |
|
||||
align-items: center; |
|
||||
justify-content: center; |
|
||||
height: 40px; |
|
||||
`}
|
|
||||
> |
|
||||
{children} |
|
||||
</div> |
|
||||
<span |
|
||||
css={css` |
|
||||
font-size: 11px; |
|
||||
line-height: 19px; |
|
||||
`}
|
|
||||
> |
|
||||
{data.label} |
|
||||
</span> |
|
||||
</div> |
|
||||
); |
|
||||
}; |
|
||||
|
|
||||
const Sidebar = ({ graph, cells }: SidebarProps): JSX.Element => { |
|
||||
const generalRef = useRef<HTMLDivElement>(null); |
|
||||
const gistRef = useRef<HTMLDivElement>(null); |
|
||||
|
|
||||
useEffect(() => { |
|
||||
const generalContainer = generalRef.current; |
|
||||
patchDnd(generalContainer, graph, generals); |
|
||||
|
|
||||
const gistContainer = gistRef.current; |
|
||||
patchDnd(gistContainer, graph, cells); |
|
||||
}); |
|
||||
|
|
||||
return ( |
|
||||
<div |
|
||||
css={css` |
|
||||
height: 100%; |
|
||||
overflow: auto; |
|
||||
border-right: 1px solid #ddd; |
|
||||
`}
|
|
||||
> |
|
||||
<ToolCollapse defaultActiveKey={['general', 'gist']}> |
|
||||
<ToolPanel header="General" key="general"> |
|
||||
<div ref={generalRef}> |
|
||||
{generals.map((cell) => { |
|
||||
const Cell = Cells[cell.type]; |
|
||||
return ( |
|
||||
<CellWrapper key={cell.type} data={cell}> |
|
||||
<Cell /> |
|
||||
</CellWrapper> |
|
||||
); |
|
||||
})} |
|
||||
</div> |
|
||||
</ToolPanel> |
|
||||
<ToolPanel header="业务元件" key="gist"> |
|
||||
<div ref={gistRef}> |
|
||||
{cells.map((cell) => { |
|
||||
const Cell = Cells[cell.type]; |
|
||||
return ( |
|
||||
<CellWrapper key={`${cell.type}${cell.label}`} data={cell}> |
|
||||
<Cell /> |
|
||||
</CellWrapper> |
|
||||
); |
|
||||
})} |
|
||||
</div> |
|
||||
</ToolPanel> |
|
||||
</ToolCollapse> |
|
||||
</div> |
|
||||
); |
|
||||
}; |
|
||||
|
|
||||
export default Sidebar; |
|
@ -1,84 +0,0 @@ |
|||||
import * as React from 'react'; |
|
||||
import styled from '@emotion/styled'; |
|
||||
import { Graph, UndoManager } from '@antv/x6'; |
|
||||
import { Tooltip } from 'antd'; |
|
||||
import { bindKey } from '../utils/tool'; |
|
||||
import getCommands from '../data/commands'; |
|
||||
import 'antd/lib/tooltip/style'; |
|
||||
|
|
||||
export interface ToolbarProps { |
|
||||
graph: Graph; |
|
||||
onSave: (data: { nodes: any; edges: any }) => void; |
|
||||
} |
|
||||
|
|
||||
const Group = styled.div` |
|
||||
position: relative; |
|
||||
display: inline-block; |
|
||||
box-sizing: border-box; |
|
||||
padding: 5px; |
|
||||
line-height: 25px; |
|
||||
|
|
||||
:first-of-type { |
|
||||
:before { |
|
||||
display: none; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
:before { |
|
||||
content: ''; |
|
||||
display: block; |
|
||||
position: absolute; |
|
||||
width: 1px; |
|
||||
height: 12px; |
|
||||
background: #d2d2d2; |
|
||||
top: 50%; |
|
||||
left: 0; |
|
||||
transform: translateY(-50%); |
|
||||
} |
|
||||
`;
|
|
||||
|
|
||||
const Item = styled.span` |
|
||||
display: inline-block; |
|
||||
margin: 0 2px; |
|
||||
padding: 0 5px; |
|
||||
border-radius: 3px; |
|
||||
cursor: pointer; |
|
||||
|
|
||||
:hover { |
|
||||
background: #ddd; |
|
||||
} |
|
||||
`;
|
|
||||
|
|
||||
const Toolbar = ({ graph, onSave }: ToolbarProps): JSX.Element => { |
|
||||
const undoManager = UndoManager.create(graph); |
|
||||
const commands = getCommands(graph, undoManager); |
|
||||
|
|
||||
bindKey(commands, graph); |
|
||||
|
|
||||
return ( |
|
||||
<div> |
|
||||
{commands.map((items) => ( |
|
||||
<Group key={items.map((i) => i.name).join('-')}> |
|
||||
{items.map((item) => ( |
|
||||
<Tooltip title={item.tooltip} key={item.name}> |
|
||||
<Item |
|
||||
onClick={(): void => { |
|
||||
if (item.name === 'save') { |
|
||||
const graphData = graph.toJSON(); |
|
||||
onSave(graphData); |
|
||||
return; |
|
||||
} |
|
||||
item.handler(); |
|
||||
}} |
|
||||
> |
|
||||
{item.icon} |
|
||||
</Item> |
|
||||
</Tooltip> |
|
||||
))} |
|
||||
</Group> |
|
||||
))} |
|
||||
</div> |
|
||||
); |
|
||||
}; |
|
||||
|
|
||||
export default Toolbar; |
|
@ -0,0 +1,13 @@ |
|||||
|
import React from 'react'; |
||||
|
import {createFromIconfontCN} from '@ant-design/icons'; |
||||
|
|
||||
|
interface IconFontProps { |
||||
|
type: string; |
||||
|
className?: string; |
||||
|
} |
||||
|
|
||||
|
const XIcon: React.SFC<IconFontProps> = createFromIconfontCN({ |
||||
|
scriptUrl: '//at.alicdn.com/t/font_2024452_lw8k0l261fe.js' |
||||
|
}); |
||||
|
|
||||
|
export default XIcon; |
@ -1,68 +0,0 @@ |
|||||
import { CellTypes } from '../components/cells'; |
|
||||
|
|
||||
interface Style { |
|
||||
width: number; |
|
||||
height: number; |
|
||||
scale: number; |
|
||||
} |
|
||||
|
|
||||
export interface DataItem { |
|
||||
label: string; |
|
||||
type: CellTypes; |
|
||||
style: Style; |
|
||||
data?: any; |
|
||||
isEdge?: boolean; |
|
||||
schema?: any; |
|
||||
} |
|
||||
|
|
||||
export const generals: DataItem[] = [ |
|
||||
{ |
|
||||
label: 'Start Event', |
|
||||
type: 'start', |
|
||||
style: { |
|
||||
width: 20, |
|
||||
height: 20, |
|
||||
scale: 2, |
|
||||
}, |
|
||||
}, |
|
||||
{ |
|
||||
label: 'End Event', |
|
||||
type: 'end', |
|
||||
style: { |
|
||||
width: 20, |
|
||||
height: 20, |
|
||||
scale: 2, |
|
||||
}, |
|
||||
}, |
|
||||
]; |
|
||||
|
|
||||
const items: any = []; |
|
||||
|
|
||||
new Array(localStorage.length).fill(1).forEach((val, index) => { |
|
||||
const item = localStorage.key(index); |
|
||||
if (item && item?.startsWith('cell')) { |
|
||||
let data: any = localStorage.getItem(item); |
|
||||
if (data) { |
|
||||
data = JSON.parse(data); |
|
||||
(data as any).type = data.type.toLowerCase(); |
|
||||
items.push(data); |
|
||||
} |
|
||||
} |
|
||||
}); |
|
||||
|
|
||||
export const gists: DataItem[] = items.map((item: any) => { |
|
||||
return { |
|
||||
label: item.label, |
|
||||
type: item.type, |
|
||||
style: { |
|
||||
width: 48, |
|
||||
height: 30, |
|
||||
scale: 2, |
|
||||
}, |
|
||||
data: { |
|
||||
code: item.code, |
|
||||
dependencies: [{ '@ali/rax-base': '^2.1.9' }], |
|
||||
}, |
|
||||
schema: item.schema, |
|
||||
}; |
|
||||
}); |
|
@ -1,89 +0,0 @@ |
|||||
import * as React from 'react'; |
|
||||
import { |
|
||||
UndoOutlined, |
|
||||
RedoOutlined, |
|
||||
ZoomInOutlined, |
|
||||
ZoomOutOutlined, |
|
||||
RetweetOutlined, |
|
||||
FullscreenOutlined, |
|
||||
DeleteOutlined, |
|
||||
SaveOutlined, |
|
||||
} from '@ant-design/icons'; |
|
||||
import { Graph, UndoManager, Platform } from '@antv/x6'; |
|
||||
import * as Tool from '../utils/tool'; |
|
||||
import { Commands } from '../model'; |
|
||||
|
|
||||
export default function getCommands(graph: Graph, undoManager: UndoManager): Commands { |
|
||||
const ctrlKey = Platform.IS_MAC ? 'Cmd' : 'Ctrl'; |
|
||||
return [ |
|
||||
[ |
|
||||
{ |
|
||||
name: 'undo', |
|
||||
icon: <UndoOutlined />, |
|
||||
tooltip: '撤销', |
|
||||
shortcut: `${ctrlKey} + Z`, |
|
||||
handler: (): void => undoManager.undo(), |
|
||||
}, |
|
||||
{ |
|
||||
name: 'redo', |
|
||||
icon: <RedoOutlined />, |
|
||||
tooltip: '重做', |
|
||||
shortcut: `${ctrlKey} + Shift + Z`, |
|
||||
handler: (): void => undoManager.redo(), |
|
||||
}, |
|
||||
], |
|
||||
[ |
|
||||
{ |
|
||||
name: 'zoomIn', |
|
||||
icon: <ZoomInOutlined />, |
|
||||
tooltip: '放大', |
|
||||
handler: Tool.zoomIn(graph), |
|
||||
}, |
|
||||
{ |
|
||||
name: 'zoomOut', |
|
||||
icon: <ZoomOutOutlined />, |
|
||||
tooltip: '缩小', |
|
||||
handler: Tool.zoomOut(graph), |
|
||||
}, |
|
||||
], |
|
||||
[ |
|
||||
{ |
|
||||
name: 'resetView', |
|
||||
icon: <RetweetOutlined />, |
|
||||
tooltip: '重置视口', |
|
||||
handler: (): void => { |
|
||||
graph.zoomTo(1); |
|
||||
graph.center(); |
|
||||
}, |
|
||||
}, |
|
||||
{ |
|
||||
name: 'fitWindow', |
|
||||
icon: <FullscreenOutlined />, |
|
||||
tooltip: '适应窗口', |
|
||||
handler: (): void => { |
|
||||
graph.fit(8); |
|
||||
}, |
|
||||
}, |
|
||||
], |
|
||||
[ |
|
||||
{ |
|
||||
name: 'delete', |
|
||||
icon: <DeleteOutlined />, |
|
||||
tooltip: '删除', |
|
||||
shortcut: 'Delete', |
|
||||
handler: (): void => graph.deleteCells(), |
|
||||
}, |
|
||||
], |
|
||||
[ |
|
||||
{ |
|
||||
name: 'save', |
|
||||
icon: <SaveOutlined />, |
|
||||
tooltip: '保存', |
|
||||
shortcut: `${ctrlKey} + S`, |
|
||||
handler: (): void => { |
|
||||
// code
|
|
||||
}, |
|
||||
}, |
|
||||
], |
|
||||
]; |
|
||||
} |
|
@ -0,0 +1,22 @@ |
|||||
|
.container { |
||||
|
|
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
|
||||
|
.body { |
||||
|
|
||||
|
flex: 1; |
||||
|
display: flex; |
||||
|
flex-direction: row; |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
|
||||
|
.full { |
||||
|
flex: 1; |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
} |
||||
|
} |
||||
|
} |
@ -1,109 +1,49 @@ |
|||||
import * as React from 'react'; |
import React, {useState} from 'react'; |
||||
import { Layout } from 'antd'; |
|
||||
import { Graph, Shape, DomEvent } from '@antv/x6'; |
|
||||
import { ReactShape } from '@antv/x6-react-shape'; |
|
||||
import styled from '@emotion/styled'; |
|
||||
import Sidebar from './components/sidebar'; |
|
||||
import Toolbar from './components/toolbar'; |
|
||||
import Setting from './components/setting'; |
|
||||
import createGraph from './utils/create-graph'; |
|
||||
import { DataItem } from './data/cells'; |
|
||||
import 'antd/lib/layout/style'; |
|
||||
|
|
||||
Shape.register('react', ReactShape, true); |
import '@antv/x6/dist/x6.css'; |
||||
|
import styles from './index.module.less'; |
||||
|
|
||||
const { Header, Sider, Content } = Layout; |
import {Graph} from '@antv/x6'; |
||||
|
import Header from './mods/header'; |
||||
|
import SideBar from './mods/sideBar'; |
||||
|
import ToolBar from './mods/toolBar'; |
||||
|
import FlowChart from './mods/flowChart'; |
||||
|
import SettingBar from './mods/settingBar'; |
||||
|
|
||||
const IMoveLayout = styled(Layout)` |
interface IProps { |
||||
height: 100%; |
onSave: (data: {nodes: any; edges: any}) => void; |
||||
`;
|
|
||||
|
|
||||
const IMoveHeader = styled(Header)` |
|
||||
height: 50px; |
|
||||
background: #fff; |
|
||||
border-bottom: 1px solid #ddd; |
|
||||
`;
|
|
||||
|
|
||||
const IMoveTitle = styled.h2` |
|
||||
line-height: 50px; |
|
||||
`;
|
|
||||
|
|
||||
const IMoveSider = styled(Sider)` |
|
||||
background: #fff; |
|
||||
`;
|
|
||||
|
|
||||
const IMoveContent = styled(Content)` |
|
||||
position: relative; |
|
||||
padding-top: 35px; |
|
||||
`;
|
|
||||
|
|
||||
const ToolWrapper = styled.div` |
|
||||
position: absolute; |
|
||||
top: 0; |
|
||||
width: 100%; |
|
||||
height: 35px; |
|
||||
line-height: 35px; |
|
||||
background: #fff; |
|
||||
`;
|
|
||||
|
|
||||
interface CoreProps { |
|
||||
cells: DataItem[]; |
|
||||
onSave: (data: { nodes: any; edges: any }) => void; |
|
||||
} |
} |
||||
|
|
||||
interface CoreState { |
const Core: React.FC<IProps> = props => { |
||||
inited: boolean; |
|
||||
} |
const {onSave} = props; |
||||
|
const [flowChart, setGraph] = useState<Graph>(); |
||||
class Core extends React.Component<CoreProps, CoreState> { |
|
||||
private graph: Graph | null = null; |
const onFlowChartReady = (flowChart: Graph): void => { |
||||
|
setGraph(flowChart); |
||||
private container = React.createRef<HTMLDivElement>(); |
flowChart.on('node:added', (args) => { |
||||
|
flowChart.cleanSelection(); |
||||
constructor(props: CoreProps) { |
flowChart.select(args.cell); |
||||
super(props); |
}); |
||||
this.state = { |
flowChart.on('selection:changed', () => { |
||||
inited: false, |
flowChart.trigger('toolBar:forceUpdate'); |
||||
}; |
flowChart.trigger('settingBar:forceUpdate'); |
||||
} |
}); |
||||
|
}; |
||||
componentDidMount(): void { |
|
||||
const container = this.container.current; |
return ( |
||||
if (container) { |
<div className={styles.container}> |
||||
DomEvent.disableContextMenu(container); |
<Header title={'iMove'}/> |
||||
this.graph = createGraph(container); |
<div className={styles.body}> |
||||
this.graph.center(); |
<SideBar flowChart={flowChart}/> |
||||
this.setState({ |
<div className={styles.full}> |
||||
inited: true, |
<ToolBar flowChart={flowChart}/> |
||||
}); |
<FlowChart onReady={onFlowChartReady}/> |
||||
} |
</div> |
||||
} |
<SettingBar flowChart={flowChart}/> |
||||
|
</div> |
||||
render(): JSX.Element { |
</div> |
||||
const { inited } = this.state; |
); |
||||
const { cells, onSave } = this.props; |
}; |
||||
return ( |
|
||||
<IMoveLayout> |
|
||||
<IMoveHeader> |
|
||||
<IMoveTitle>iMove</IMoveTitle> |
|
||||
</IMoveHeader> |
|
||||
<IMoveLayout> |
|
||||
<IMoveSider> |
|
||||
{this.graph && inited && <Sidebar graph={this.graph} cells={cells} />}2 |
|
||||
</IMoveSider> |
|
||||
<IMoveContent> |
|
||||
<ToolWrapper> |
|
||||
{this.graph && inited && <Toolbar graph={this.graph} onSave={onSave} />} |
|
||||
</ToolWrapper> |
|
||||
<div ref={this.container} tabIndex={-1} /> |
|
||||
</IMoveContent> |
|
||||
<IMoveSider width="300"> |
|
||||
{this.graph && inited && <Setting graph={this.graph} />} |
|
||||
</IMoveSider> |
|
||||
</IMoveLayout> |
|
||||
</IMoveLayout> |
|
||||
); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
export default Core; |
export default Core; |
||||
|
@ -1,4 +0,0 @@ |
|||||
export interface CellProps { |
|
||||
label?: string; |
|
||||
scale?: number; |
|
||||
} |
|
@ -1,9 +0,0 @@ |
|||||
interface Command { |
|
||||
name: string; |
|
||||
icon: JSX.Element; |
|
||||
tooltip: string; |
|
||||
shortcut?: string; |
|
||||
handler: () => void; |
|
||||
} |
|
||||
|
|
||||
export type Commands = Command[][]; |
|
@ -1,2 +0,0 @@ |
|||||
export * from './cells'; |
|
||||
export * from './commands'; |
|
@ -0,0 +1,19 @@ |
|||||
|
.container { |
||||
|
|
||||
|
position: relative; |
||||
|
display: flex; |
||||
|
flex: 1; |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
|
||||
|
.flowChart { |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
} |
||||
|
|
||||
|
.miniMap { |
||||
|
position: absolute; |
||||
|
right: 40px; |
||||
|
bottom: 40px; |
||||
|
} |
||||
|
} |
@ -0,0 +1,30 @@ |
|||||
|
import React, {useRef, useEffect} from 'react'; |
||||
|
|
||||
|
import styles from './index.module.less'; |
||||
|
|
||||
|
import {Graph} from '@antv/x6'; |
||||
|
import createGraph from '../../utils/createGraph'; |
||||
|
|
||||
|
interface IProps { |
||||
|
onReady: (graph: Graph) => void; |
||||
|
} |
||||
|
|
||||
|
const FlowChart: React.FC<IProps> = props => { |
||||
|
const {onReady} = props; |
||||
|
const graphRef = useRef<HTMLDivElement>(null); |
||||
|
const miniMapRef = useRef<HTMLDivElement>(null); |
||||
|
useEffect(() => { |
||||
|
if(graphRef.current && miniMapRef.current) { |
||||
|
const graph = createGraph(graphRef.current, miniMapRef.current); |
||||
|
onReady(graph); |
||||
|
} |
||||
|
}, []); |
||||
|
return ( |
||||
|
<div className={styles.container}> |
||||
|
<div className={styles.flowChart} ref={graphRef}/> |
||||
|
<div className={styles.miniMap} ref={miniMapRef}/> |
||||
|
</div> |
||||
|
); |
||||
|
}; |
||||
|
|
||||
|
export default FlowChart; |
@ -0,0 +1,12 @@ |
|||||
|
.container { |
||||
|
height: 48px; |
||||
|
line-height: 48px; |
||||
|
background-color: #FEFEFE; |
||||
|
border-bottom: 1px solid #D9D9D9; |
||||
|
} |
||||
|
|
||||
|
.titleText { |
||||
|
margin-left: 20px; |
||||
|
font-size: 24px; |
||||
|
font-weight: 500; |
||||
|
} |
@ -0,0 +1,18 @@ |
|||||
|
import React from 'react'; |
||||
|
|
||||
|
import styles from './index.module.less'; |
||||
|
|
||||
|
interface IProps { |
||||
|
title?: string; |
||||
|
} |
||||
|
|
||||
|
const Header: React.FC<IProps> = props => { |
||||
|
const {title = 'iMove'} = props; |
||||
|
return ( |
||||
|
<div className={styles.container}> |
||||
|
<span className={styles.titleText}>{title}</span> |
||||
|
</div> |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
export default Header; |
@ -0,0 +1,9 @@ |
|||||
|
.container { |
||||
|
|
||||
|
margin-top: 20px; |
||||
|
|
||||
|
.titleText { |
||||
|
font-size: 14px; |
||||
|
margin-bottom: 10px; |
||||
|
} |
||||
|
} |
@ -0,0 +1,34 @@ |
|||||
|
import React from 'react'; |
||||
|
|
||||
|
import 'antd/es/checkbox/style'; |
||||
|
import styles from './index.module.less'; |
||||
|
|
||||
|
import {Checkbox as AntdCheckbox} from 'antd'; |
||||
|
import {CheckboxChangeEvent} from 'antd/lib/checkbox'; |
||||
|
|
||||
|
interface IProps { |
||||
|
value: any; |
||||
|
name: string; |
||||
|
title: string; |
||||
|
disabled?: boolean; |
||||
|
description?: string; |
||||
|
onValueChange: (val: boolean) => void; |
||||
|
} |
||||
|
|
||||
|
const Checkbox: React.FC<IProps> = props => { |
||||
|
const {name, title, value, disabled, onValueChange} = props; |
||||
|
const onChange = (evt: CheckboxChangeEvent): void => { |
||||
|
console.log(evt.target.checked) |
||||
|
if(evt.target.checked !== value) { |
||||
|
onValueChange(evt.target.checked); |
||||
|
} |
||||
|
}; |
||||
|
return ( |
||||
|
<div className={styles.container}> |
||||
|
<p className={styles.titleText}>{title}</p> |
||||
|
<AntdCheckbox name={name} checked={value} disabled={disabled} onChange={onChange}/> |
||||
|
</div> |
||||
|
); |
||||
|
}; |
||||
|
|
||||
|
export default Checkbox; |
@ -0,0 +1,27 @@ |
|||||
|
.container { |
||||
|
|
||||
|
margin-top: 20px; |
||||
|
|
||||
|
.titleText { |
||||
|
font-size: 14px; |
||||
|
margin-bottom: 10px; |
||||
|
} |
||||
|
|
||||
|
.btn { |
||||
|
margin-top: 5px; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.editModal { |
||||
|
|
||||
|
:global { |
||||
|
.ant-modal-body { |
||||
|
padding: 0; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.editor { |
||||
|
width: 100%; |
||||
|
height: 400; |
||||
|
} |
||||
|
} |
@ -0,0 +1,130 @@ |
|||||
|
import React, {useState, useEffect} from 'react'; |
||||
|
|
||||
|
import 'antd/es/modal/style'; |
||||
|
import 'antd/es/button/style'; |
||||
|
import styles from './index.module.less'; |
||||
|
|
||||
|
import AceEditor from 'react-ace'; |
||||
|
import {Button, Modal} from 'antd'; |
||||
|
import 'ace-builds/src-noconflict/theme-dracula'; |
||||
|
import 'ace-builds/src-noconflict/mode-javascript'; |
||||
|
import 'ace-builds/src-noconflict/snippets/javascript'; |
||||
|
import 'ace-builds/src-noconflict/ext-language_tools'; |
||||
|
|
||||
|
interface IProps { |
||||
|
value: any; |
||||
|
name: string; |
||||
|
title: string; |
||||
|
onValueChange: (value: string) => void; |
||||
|
} |
||||
|
|
||||
|
const Code: React.FC<IProps> = props => { |
||||
|
|
||||
|
const {title, value, onValueChange} = props; |
||||
|
const [visible, setVisible] = useState<boolean>(false); |
||||
|
|
||||
|
// events
|
||||
|
const onClickEdit = (): void => { |
||||
|
setVisible(true); |
||||
|
}; |
||||
|
const onClickCancel = (): void => { |
||||
|
setVisible(false); |
||||
|
}; |
||||
|
const onClickOk = (newJson: string): void => { |
||||
|
setVisible(false); |
||||
|
onValueChange(newJson); |
||||
|
}; |
||||
|
|
||||
|
return ( |
||||
|
<div className={styles.container}> |
||||
|
<p className={styles.titleText}>{title}</p> |
||||
|
<Button |
||||
|
block={true} |
||||
|
className={styles.btn} |
||||
|
onClick={onClickEdit} |
||||
|
> |
||||
|
编辑 |
||||
|
</Button> |
||||
|
<EditModal |
||||
|
title={title} |
||||
|
value={value} |
||||
|
visible={visible} |
||||
|
onOk={onClickOk} |
||||
|
onCancel={onClickCancel} |
||||
|
/> |
||||
|
</div> |
||||
|
); |
||||
|
}; |
||||
|
|
||||
|
interface IEditorModalProps { |
||||
|
visible: boolean; |
||||
|
title: string; |
||||
|
value: string; |
||||
|
onOk: (val: string) => void; |
||||
|
onCancel: () => void; |
||||
|
} |
||||
|
|
||||
|
const CODE_EDITOR_STYLE = { |
||||
|
width: '100%', |
||||
|
height: 400 |
||||
|
}; |
||||
|
const CODE_EDITOR_OPTIONS = { |
||||
|
useWorker: false |
||||
|
}; |
||||
|
|
||||
|
const EditModal: React.FC<IEditorModalProps> = props => { |
||||
|
|
||||
|
const {visible, title, value, onOk, onCancel} = props; |
||||
|
const [code, setCode] = useState<string>(value); |
||||
|
|
||||
|
// life
|
||||
|
useEffect(() => { |
||||
|
// set value when opening modal
|
||||
|
// clear content when closing modal
|
||||
|
if(visible) { |
||||
|
setCode(value); |
||||
|
} else { |
||||
|
setCode(''); |
||||
|
} |
||||
|
}, [visible]); |
||||
|
|
||||
|
// events
|
||||
|
const onClickOk = (): void => { |
||||
|
onOk(code); |
||||
|
}; |
||||
|
const onChangeCode = (newCode: string): void => { |
||||
|
if(newCode !== code) { |
||||
|
setCode(newCode); |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
return ( |
||||
|
<Modal |
||||
|
className={styles.editModal} |
||||
|
width={800} |
||||
|
title={title} |
||||
|
okText={'保存'} |
||||
|
visible={visible} |
||||
|
cancelText={'取消'} |
||||
|
onOk={onClickOk} |
||||
|
onCancel={onCancel} |
||||
|
> |
||||
|
<AceEditor |
||||
|
style={CODE_EDITOR_STYLE} |
||||
|
tabSize={2} |
||||
|
value={code} |
||||
|
focus={true} |
||||
|
theme={'dracula'} |
||||
|
mode={'javascript'} |
||||
|
name={'code-editor'} |
||||
|
enableSnippets={true} |
||||
|
enableLiveAutocompletion={true} |
||||
|
enableBasicAutocompletion={true} |
||||
|
setOptions={CODE_EDITOR_OPTIONS} |
||||
|
onChange={onChangeCode} |
||||
|
/> |
||||
|
</Modal> |
||||
|
); |
||||
|
}; |
||||
|
|
||||
|
export default Code; |
@ -0,0 +1,9 @@ |
|||||
|
.container { |
||||
|
|
||||
|
margin-top: 20px; |
||||
|
|
||||
|
.titleText { |
||||
|
font-size: 14px; |
||||
|
margin-bottom: 10px; |
||||
|
} |
||||
|
} |
@ -0,0 +1,32 @@ |
|||||
|
import React from 'react'; |
||||
|
|
||||
|
import 'antd/es/input/style'; |
||||
|
import styles from './index.module.less'; |
||||
|
|
||||
|
import {Input as AntdInput} from 'antd'; |
||||
|
|
||||
|
interface IProps { |
||||
|
value: any; |
||||
|
name: string; |
||||
|
title: string; |
||||
|
disabled?: boolean; |
||||
|
description?: string; |
||||
|
onValueChange: (val: string) => void; |
||||
|
} |
||||
|
|
||||
|
const Input: React.FC<IProps> = props => { |
||||
|
const {name, title, value, disabled, onValueChange} = props; |
||||
|
const onChange = (evt: React.ChangeEvent<HTMLInputElement>): void => { |
||||
|
if(evt.target.value !== value) { |
||||
|
onValueChange(evt.target.value); |
||||
|
} |
||||
|
}; |
||||
|
return ( |
||||
|
<div className={styles.container}> |
||||
|
<p className={styles.titleText}>{title}</p> |
||||
|
<AntdInput name={name} value={value} disabled={disabled} onChange={onChange}/> |
||||
|
</div> |
||||
|
); |
||||
|
}; |
||||
|
|
||||
|
export default Input; |
@ -0,0 +1,27 @@ |
|||||
|
.container { |
||||
|
|
||||
|
margin-top: 20px; |
||||
|
|
||||
|
.titleText { |
||||
|
font-size: 14px; |
||||
|
margin-bottom: 10px; |
||||
|
} |
||||
|
|
||||
|
.btn { |
||||
|
margin-top: 5px; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.editModal { |
||||
|
|
||||
|
:global { |
||||
|
.ant-modal-body { |
||||
|
padding: 0; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.editor { |
||||
|
width: 100%; |
||||
|
height: 400; |
||||
|
} |
||||
|
} |
@ -0,0 +1,145 @@ |
|||||
|
import React, {useState, useEffect} from 'react'; |
||||
|
|
||||
|
import 'antd/es/modal/style'; |
||||
|
import 'antd/es/button/style'; |
||||
|
import 'antd/es/message/style'; |
||||
|
import styles from './index.module.less'; |
||||
|
|
||||
|
import AceEditor from 'react-ace'; |
||||
|
import JsonView from 'react-json-view'; |
||||
|
import {Button, Modal, message} from 'antd'; |
||||
|
import {safeParse} from '../../../../utils'; |
||||
|
import 'ace-builds/src-noconflict/mode-json'; |
||||
|
import 'ace-builds/src-noconflict/theme-dracula'; |
||||
|
import 'ace-builds/src-noconflict/snippets/json'; |
||||
|
import 'ace-builds/src-noconflict/ext-language_tools'; |
||||
|
|
||||
|
interface IJsonProps { |
||||
|
value: any; |
||||
|
name: string; |
||||
|
title: string; |
||||
|
onValueChange: (value: string) => void; |
||||
|
} |
||||
|
|
||||
|
const Json: React.FC<IJsonProps> = props => { |
||||
|
|
||||
|
const {title, value, onValueChange} = props; |
||||
|
const [visible, setVisible] = useState<boolean>(false); |
||||
|
|
||||
|
// events
|
||||
|
const onClickEdit = (): void => { |
||||
|
setVisible(true); |
||||
|
}; |
||||
|
const onClickCancel = (): void => { |
||||
|
setVisible(false); |
||||
|
}; |
||||
|
const onClickOk = (newJson: string): void => { |
||||
|
setVisible(false); |
||||
|
onValueChange(newJson); |
||||
|
}; |
||||
|
|
||||
|
return ( |
||||
|
<div className={styles.container}> |
||||
|
<p className={styles.titleText}>{title}</p> |
||||
|
<JsonView |
||||
|
name={null} |
||||
|
collapsed={true} |
||||
|
enableClipboard={false} |
||||
|
displayDataTypes={false} |
||||
|
displayObjectSize={false} |
||||
|
src={safeParse(value)} |
||||
|
/> |
||||
|
<Button |
||||
|
block={true} |
||||
|
className={styles.btn} |
||||
|
onClick={onClickEdit} |
||||
|
> |
||||
|
编辑 |
||||
|
</Button> |
||||
|
<EditModal |
||||
|
title={title} |
||||
|
value={value} |
||||
|
visible={visible} |
||||
|
onOk={onClickOk} |
||||
|
onCancel={onClickCancel} |
||||
|
/> |
||||
|
</div> |
||||
|
); |
||||
|
}; |
||||
|
|
||||
|
interface IEditorModalProps { |
||||
|
visible: boolean; |
||||
|
title: string; |
||||
|
value: string; |
||||
|
onOk: (val: string) => void; |
||||
|
onCancel: () => void; |
||||
|
} |
||||
|
|
||||
|
const CODE_EDITOR_STYLE = { |
||||
|
width: '100%', |
||||
|
height: 400 |
||||
|
}; |
||||
|
const CODE_EDITOR_OPTIONS = { |
||||
|
useWorker: false |
||||
|
}; |
||||
|
|
||||
|
const EditModal: React.FC<IEditorModalProps> = props => { |
||||
|
|
||||
|
const {visible, title, value, onOk, onCancel} = props; |
||||
|
const [json, setJson] = useState<string>(''); |
||||
|
|
||||
|
// life
|
||||
|
useEffect(() => { |
||||
|
// set value when opening modal
|
||||
|
// clear content when closing modal
|
||||
|
if(visible) { |
||||
|
setJson(value); |
||||
|
} else { |
||||
|
setJson(''); |
||||
|
} |
||||
|
}, [visible]); |
||||
|
|
||||
|
// events
|
||||
|
const onClickOk = (): void => { |
||||
|
try { |
||||
|
JSON.parse(json); |
||||
|
onOk(json); |
||||
|
} catch(err) { |
||||
|
message.error('内容必须是合法json'); |
||||
|
console.log('save failed, the error is:', err.message); |
||||
|
} |
||||
|
}; |
||||
|
const onChangeJson = (newJson: string): void => { |
||||
|
if(newJson !== json) { |
||||
|
setJson(newJson); |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
return ( |
||||
|
<Modal |
||||
|
className={styles.editModal} |
||||
|
title={title} |
||||
|
okText={'保存'} |
||||
|
visible={visible} |
||||
|
cancelText={'取消'} |
||||
|
onOk={onClickOk} |
||||
|
onCancel={onCancel} |
||||
|
> |
||||
|
<AceEditor |
||||
|
style={CODE_EDITOR_STYLE} |
||||
|
tabSize={2} |
||||
|
value={json} |
||||
|
focus={true} |
||||
|
mode={'json'} |
||||
|
theme={'dracula'} |
||||
|
name={'json-editor'} |
||||
|
enableSnippets={true} |
||||
|
enableBasicAutocompletion={true} |
||||
|
setOptions={CODE_EDITOR_OPTIONS} |
||||
|
onChange={onChangeJson} |
||||
|
/> |
||||
|
</Modal> |
||||
|
); |
||||
|
}; |
||||
|
|
||||
|
export default Json; |
@ -0,0 +1,35 @@ |
|||||
|
.container { |
||||
|
|
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
width: 350px; |
||||
|
height: 100%; |
||||
|
border-left: 1px solid #D9D9D9; |
||||
|
|
||||
|
&.center { |
||||
|
justify-content: center; |
||||
|
align-items: center; |
||||
|
} |
||||
|
|
||||
|
:global { |
||||
|
|
||||
|
.ant-tabs-nav { |
||||
|
margin-bottom: 0; |
||||
|
} |
||||
|
|
||||
|
.ant-tabs-nav-list { |
||||
|
display: flex; |
||||
|
flex: 1; |
||||
|
} |
||||
|
|
||||
|
.ant-tabs-tab { |
||||
|
display: flex; |
||||
|
flex: 1; |
||||
|
justify-content: center; |
||||
|
align-items: center; |
||||
|
padding: 10px 0; |
||||
|
background-color: #FAFAFA; |
||||
|
border-bottom: 1px solid #D9D9D9; |
||||
|
} |
||||
|
} |
||||
|
} |
@ -0,0 +1,58 @@ |
|||||
|
import React, {useEffect, useReducer} from 'react'; |
||||
|
|
||||
|
import 'antd/es/tabs/style'; |
||||
|
import 'antd/es/empty/style'; |
||||
|
import styles from './index.module.less'; |
||||
|
|
||||
|
import {Empty, Tabs} from 'antd'; |
||||
|
import Basic from './mods/basic'; |
||||
|
import Config from './mods/config'; |
||||
|
import {Graph} from '@antv/x6'; |
||||
|
|
||||
|
const {TabPane} = Tabs; |
||||
|
|
||||
|
interface IProps { |
||||
|
flowChart: Graph | undefined; |
||||
|
} |
||||
|
|
||||
|
const SettingBar: React.FC<IProps> = props => { |
||||
|
const {flowChart} = props; |
||||
|
const [ignored, forceUpdate] = useReducer(n => n + 1, 0); |
||||
|
useEffect(() => { |
||||
|
flowChart && flowChart.on('settingBar:forceUpdate', forceUpdate); |
||||
|
return () => { |
||||
|
flowChart && flowChart.off('settingBar:forceUpdate'); |
||||
|
}; |
||||
|
}, [flowChart]); |
||||
|
if(!flowChart) { |
||||
|
return ( |
||||
|
<div className={`${styles.container} ${styles.center}`}> |
||||
|
<Empty description={'请选择一个节点'} image={Empty.PRESENTED_IMAGE_SIMPLE}/> |
||||
|
</div> |
||||
|
); |
||||
|
} else { |
||||
|
const nodes = flowChart.getSelectedCells().filter(v => v.shape !== 'edge'); |
||||
|
if(nodes.length === 1) { |
||||
|
return ( |
||||
|
<div className={styles.container}> |
||||
|
<Tabs tabBarGutter={0} defaultActiveKey={'basic'} tabBarStyle={{display: 'flex', flex: 1, justifyContent: 'center', alignItems: 'center'}}> |
||||
|
<TabPane tab={'基础信息'} key={'basic'}> |
||||
|
<Basic selectedCell={nodes[0]}/> |
||||
|
</TabPane> |
||||
|
<TabPane tab={'投放配置'} key={'config'}> |
||||
|
<Config selectedCell={nodes[0]}/> |
||||
|
</TabPane> |
||||
|
</Tabs> |
||||
|
</div> |
||||
|
); |
||||
|
} else { |
||||
|
return ( |
||||
|
<div className={`${styles.container} ${styles.center}`}> |
||||
|
<Empty description={'请选择一个节点'} image={Empty.PRESENTED_IMAGE_SIMPLE}/> |
||||
|
</div> |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
export default SettingBar; |
@ -0,0 +1,5 @@ |
|||||
|
.container { |
||||
|
padding: 0 20px; |
||||
|
height: 100%; |
||||
|
overflow: scroll; |
||||
|
} |
@ -0,0 +1,88 @@ |
|||||
|
import React, {useState, useEffect} from 'react'; |
||||
|
|
||||
|
import styles from './index.module.less'; |
||||
|
|
||||
|
import {Cell} from '@antv/x6'; |
||||
|
import Json from '../../components/json'; |
||||
|
import Code from '../../components/code'; |
||||
|
import Input from '../../components/input'; |
||||
|
|
||||
|
interface IProps { |
||||
|
selectedCell: Cell; |
||||
|
} |
||||
|
|
||||
|
interface IBasicData { |
||||
|
label: string; |
||||
|
code: string; |
||||
|
trigger?: string; |
||||
|
dependencies: string; |
||||
|
configSchema: string; |
||||
|
} |
||||
|
|
||||
|
const Basic: React.FC<IProps> = props => { |
||||
|
|
||||
|
const {selectedCell} = props; |
||||
|
const [data, setData] = useState<IBasicData>(); |
||||
|
const {label, code, trigger, dependencies, configSchema} = data || {}; |
||||
|
|
||||
|
// life
|
||||
|
useEffect(() => { |
||||
|
setData(selectedCell.getData()); |
||||
|
}, [selectedCell]); |
||||
|
|
||||
|
// events
|
||||
|
const commonChange = (key: string, val: string): void => { |
||||
|
selectedCell.setData({[key]: val}); |
||||
|
setData(Object.assign({}, data, {[key]: val})); |
||||
|
}; |
||||
|
const onChangeLabel = (val: string): void => { |
||||
|
commonChange('label', val); |
||||
|
selectedCell.setAttrs({label: {text: val}}); |
||||
|
}; |
||||
|
const onChangeConfigSchema = (val: string): void => { |
||||
|
commonChange('configSchema', val); |
||||
|
selectedCell.trigger('change:configSchema', {configSchema: val}); |
||||
|
}; |
||||
|
const onChangeCode = (val: string): void => commonChange('code', val); |
||||
|
const onChangeTrigger = (val: string): void => commonChange('trigger', val); |
||||
|
const onChangeDependencies = (val: string): void => commonChange('dependencies', val); |
||||
|
|
||||
|
return ( |
||||
|
<div className={styles.container}> |
||||
|
<Input |
||||
|
name={'label'} |
||||
|
title={'显示名称'} |
||||
|
value={label} |
||||
|
onValueChange={onChangeLabel} |
||||
|
/> |
||||
|
{selectedCell.shape === 'imove-start' && ( |
||||
|
<Input |
||||
|
name={'trigger'} |
||||
|
title={'逻辑触发名称'} |
||||
|
value={trigger} |
||||
|
onValueChange={onChangeTrigger} |
||||
|
/> |
||||
|
)} |
||||
|
<Code |
||||
|
name={'code'} |
||||
|
title={'代码'} |
||||
|
value={code} |
||||
|
onValueChange={onChangeCode} |
||||
|
/> |
||||
|
<Json |
||||
|
name={'configSchema'} |
||||
|
title={'投放配置schema'} |
||||
|
value={configSchema} |
||||
|
onValueChange={onChangeConfigSchema} |
||||
|
/> |
||||
|
<Json |
||||
|
name={'dependencies'} |
||||
|
title={'依赖'} |
||||
|
value={dependencies} |
||||
|
onValueChange={onChangeDependencies} |
||||
|
/> |
||||
|
</div> |
||||
|
); |
||||
|
}; |
||||
|
|
||||
|
export default Basic; |
@ -0,0 +1,23 @@ |
|||||
|
.container { |
||||
|
|
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
padding: 0 20px; |
||||
|
height: 100%; |
||||
|
|
||||
|
.empty { |
||||
|
margin-top: 100px; |
||||
|
} |
||||
|
|
||||
|
.footContainer { |
||||
|
|
||||
|
display: flex; |
||||
|
flex-direction: row; |
||||
|
justify-content: flex-end; |
||||
|
margin-top: 20px; |
||||
|
|
||||
|
.saveBtn { |
||||
|
margin-left: 20px; |
||||
|
} |
||||
|
} |
||||
|
} |
@ -0,0 +1,120 @@ |
|||||
|
import React, {useState, useEffect} from 'react'; |
||||
|
|
||||
|
import styles from './index.module.less'; |
||||
|
|
||||
|
import {Cell} from '@antv/x6'; |
||||
|
import {Button ,Empty} from 'antd'; |
||||
|
import Input from '../../components/input'; |
||||
|
import Checkbox from '../../components/checkbox'; |
||||
|
import {safeParse} from '../../../../utils'; |
||||
|
|
||||
|
// FIXME
|
||||
|
// save original config in tempConfig when change to edit mode
|
||||
|
// and recover original config when click cancel button
|
||||
|
let tempConfig: any = null; |
||||
|
|
||||
|
enum Mode { |
||||
|
edit = 'edit', |
||||
|
readOnly = 'readOnly' |
||||
|
} |
||||
|
|
||||
|
interface IConfig { |
||||
|
[key: string]: any; |
||||
|
} |
||||
|
|
||||
|
interface IProps { |
||||
|
selectedCell: Cell; |
||||
|
} |
||||
|
|
||||
|
const Config: React.FC<IProps> = props => { |
||||
|
|
||||
|
const {selectedCell} = props; |
||||
|
const [mode, setMode] = useState<Mode>(Mode.readOnly); |
||||
|
const [configData, setConfigData] = useState<IConfig>({}); |
||||
|
const [configSchema, setConfigSchema] = useState<IConfig>({}); |
||||
|
|
||||
|
// life
|
||||
|
useEffect(() => { |
||||
|
const {configSchema, configData = {}} = selectedCell.getData() || {}; |
||||
|
setConfigData(configData); |
||||
|
setConfigSchema(safeParse(configSchema)); |
||||
|
selectedCell.on('change:configSchema', (data: {configSchema: string}) => { |
||||
|
setConfigSchema(safeParse(data.configSchema)); |
||||
|
}); |
||||
|
return () => { |
||||
|
selectedCell.off('change:configSchema'); |
||||
|
}; |
||||
|
}, [selectedCell]); |
||||
|
|
||||
|
// events
|
||||
|
const onFieldValueChange = (key: string, value: any) => { |
||||
|
setConfigData(Object.assign({}, configData, {[key]: value})); |
||||
|
}; |
||||
|
const onClickEdit = (): void => { |
||||
|
setMode(Mode.edit); |
||||
|
tempConfig = configData; |
||||
|
}; |
||||
|
const onClickCancel = (): void => { |
||||
|
setMode(Mode.readOnly); |
||||
|
setConfigData(tempConfig); |
||||
|
}; |
||||
|
const onClickSave = (): void => { |
||||
|
setMode(Mode.readOnly); |
||||
|
selectedCell.setData({configData}); |
||||
|
}; |
||||
|
|
||||
|
// no config schema
|
||||
|
if(!configSchema || Object.keys(configSchema).length === 0) { |
||||
|
return ( |
||||
|
<div className={styles.container}> |
||||
|
<Empty |
||||
|
className={styles.empty} |
||||
|
description={'请编辑投放配置schema'} |
||||
|
image={Empty.PRESENTED_IMAGE_SIMPLE} |
||||
|
/> |
||||
|
</div> |
||||
|
); |
||||
|
} else { |
||||
|
return ( |
||||
|
<div className={styles.container}> |
||||
|
{Object.keys(configSchema).map((key, idx) => { |
||||
|
const {title, description, type} = configSchema[key]; |
||||
|
const FieldComponent = Helper.getFieldComponent(type); |
||||
|
return ( |
||||
|
<FieldComponent |
||||
|
key={idx} |
||||
|
name={key} |
||||
|
title={title} |
||||
|
value={configData[key]} |
||||
|
description={description} |
||||
|
disabled={mode === Mode.readOnly} |
||||
|
onValueChange={(value: any) => onFieldValueChange(key, value)} |
||||
|
/> |
||||
|
); |
||||
|
})} |
||||
|
{mode === Mode.readOnly ? ( |
||||
|
<div className={styles.footContainer}> |
||||
|
<Button block onClick={onClickEdit}>编辑</Button> |
||||
|
</div> |
||||
|
) : ( |
||||
|
<div className={styles.footContainer}> |
||||
|
<Button onClick={onClickCancel}>取消</Button> |
||||
|
<Button className={styles.saveBtn} type={'primary'} onClick={onClickSave}>保存</Button> |
||||
|
</div> |
||||
|
)} |
||||
|
</div> |
||||
|
); |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
const Helper = { |
||||
|
getFieldComponent(type: string) { |
||||
|
if(type === 'boolean') { |
||||
|
return Checkbox; |
||||
|
} else { |
||||
|
return Input; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export default Config; |
@ -0,0 +1,20 @@ |
|||||
|
.container { |
||||
|
|
||||
|
width: 235px; |
||||
|
border-right: 1px solid #D9D9D9; |
||||
|
|
||||
|
:global { |
||||
|
.ant-collapse > .ant-collapse-item > .ant-collapse-header { |
||||
|
padding: 10px 16px 10px 40px; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.collapse { |
||||
|
border: 0; |
||||
|
border-radius: 0; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.chart { |
||||
|
width: 100%; |
||||
|
} |
@ -0,0 +1,109 @@ |
|||||
|
import React, {useState, useEffect, useRef} from 'react'; |
||||
|
|
||||
|
import 'antd/es/collapse/style'; |
||||
|
import styles from './index.module.less'; |
||||
|
|
||||
|
import {Collapse} from 'antd'; |
||||
|
import {Addon, Events, Graph, Node} from '@antv/x6'; |
||||
|
import previewCellSchemaMap from '../../common/previewCell'; |
||||
|
|
||||
|
const {Dnd} = Addon; |
||||
|
const {Panel} = Collapse; |
||||
|
const CELL_SIZE = 80; |
||||
|
const CELL_SCALE = 0.7; |
||||
|
const GENERAL_GROUP = { |
||||
|
key: 'general', |
||||
|
name: '通用元件', |
||||
|
cells: ['imove-start-preview', 'imove-branch-preview', 'imove-behavior-preview'] |
||||
|
}; |
||||
|
const SHAPE_REFLECT_MAP: {[key: string]: string} = { |
||||
|
'imove-start-preview': 'imove-start', |
||||
|
'imove-branch-preview': 'imove-branch', |
||||
|
'imove-behavior-preview': 'imove-behavior', |
||||
|
}; |
||||
|
|
||||
|
interface IGroupItem { |
||||
|
key: string; |
||||
|
name: string; |
||||
|
cells: string[]; |
||||
|
} |
||||
|
|
||||
|
interface ISideBarPorps { |
||||
|
flowChart: Graph | undefined; |
||||
|
} |
||||
|
|
||||
|
const SideBar: React.FC<ISideBarPorps> = props => { |
||||
|
|
||||
|
const {flowChart} = props; |
||||
|
const [dnd, setDnd] = useState<Addon.Dnd>(); |
||||
|
const [groups, setGroups] = useState<IGroupItem[]>([]); |
||||
|
|
||||
|
// life
|
||||
|
useEffect(() => { |
||||
|
// TODO: fetch to get custom group data
|
||||
|
setGroups([GENERAL_GROUP]); |
||||
|
}, []); |
||||
|
useEffect(() => { |
||||
|
if(flowChart) { |
||||
|
setDnd(new Dnd({target: flowChart, scaled: true})); |
||||
|
} |
||||
|
}, [flowChart]); |
||||
|
|
||||
|
return ( |
||||
|
<div className={styles.container}> |
||||
|
{flowChart && dnd && ( |
||||
|
<Collapse className={styles.collapse} defaultActiveKey={['general', 'custom']}> |
||||
|
{groups.map(group => ( |
||||
|
<Panel key={group.key} header={group.name}> |
||||
|
<PanelContent dnd={dnd} cells={group.cells} flowChart={flowChart}/> |
||||
|
</Panel> |
||||
|
))} |
||||
|
</Collapse> |
||||
|
)} |
||||
|
</div> |
||||
|
); |
||||
|
}; |
||||
|
|
||||
|
interface IPanelContentProps { |
||||
|
dnd: Addon.Dnd; |
||||
|
cells: string[]; |
||||
|
flowChart: Graph; |
||||
|
} |
||||
|
|
||||
|
const PanelContent: React.FC<IPanelContentProps> = props => { |
||||
|
const {dnd, cells, flowChart} = props; |
||||
|
const ref = useRef<HTMLDivElement>(null); |
||||
|
useEffect(() => { |
||||
|
if(ref.current && flowChart) { |
||||
|
const graph = new Graph({ |
||||
|
container: ref.current, |
||||
|
width: ref.current.offsetWidth, |
||||
|
height: Math.ceil(cells.length / 3) * CELL_SIZE * CELL_SCALE, |
||||
|
interacting: false, |
||||
|
preventDefaultBlankAction: false, |
||||
|
}); |
||||
|
cells.forEach((cell: any, index: number) => { |
||||
|
const rowIdx = Math.floor(index / 3); |
||||
|
const colIdx = index % 3; |
||||
|
const {top} = previewCellSchemaMap[cell] || {}; |
||||
|
graph.addNode({ |
||||
|
shape: cell, |
||||
|
x: colIdx * (CELL_SIZE + 16 / CELL_SCALE), |
||||
|
y: rowIdx * (CELL_SIZE + 16 / CELL_SCALE) + top |
||||
|
}); |
||||
|
}); |
||||
|
graph.scale(CELL_SCALE, CELL_SCALE); |
||||
|
graph.on('cell:mousedown', (args: Events.EventArgs['cell:mousedown']) => { |
||||
|
const {node, e} = args; |
||||
|
let newNode = node; |
||||
|
if(SHAPE_REFLECT_MAP[node.shape]) { |
||||
|
newNode = Node.create({shape: SHAPE_REFLECT_MAP[node.shape]}); |
||||
|
} |
||||
|
dnd.start(newNode, e); |
||||
|
}); |
||||
|
} |
||||
|
}, [flowChart]); |
||||
|
return <div className={styles.chart} ref={ref}/>; |
||||
|
}; |
||||
|
|
||||
|
export default SideBar; |
@ -0,0 +1,18 @@ |
|||||
|
.container { |
||||
|
|
||||
|
display: flex; |
||||
|
flex-direction: row; |
||||
|
justify-content: flex-start; |
||||
|
align-items: center; |
||||
|
height: 43px; |
||||
|
background-color: #FAFAFA; |
||||
|
border-bottom: 1px solid #D9D9D9; |
||||
|
|
||||
|
.group { |
||||
|
display: flex; |
||||
|
flex-direction: row; |
||||
|
align-items: center; |
||||
|
height: 43px; |
||||
|
border-right: 1px solid #D9D9D9; |
||||
|
} |
||||
|
} |
@ -0,0 +1,35 @@ |
|||||
|
import React, {useEffect, useReducer} from 'react'; |
||||
|
|
||||
|
import 'antd/es/tooltip/style'; |
||||
|
import styles from './index.module.less'; |
||||
|
|
||||
|
import {Graph} from '@antv/x6'; |
||||
|
import widgets from './widgets'; |
||||
|
|
||||
|
interface IProps { |
||||
|
flowChart: Graph | undefined; |
||||
|
} |
||||
|
|
||||
|
const ToolBar: React.FC<IProps> = props => { |
||||
|
const {flowChart} = props; |
||||
|
const [ignored, forceUpdate] = useReducer(n => n + 1, 0); |
||||
|
useEffect(() => { |
||||
|
flowChart && flowChart.on('toolBar:forceUpdate', forceUpdate); |
||||
|
return () => { |
||||
|
flowChart && flowChart.off('toolBar:forceUpdate'); |
||||
|
}; |
||||
|
}, [flowChart]); |
||||
|
return ( |
||||
|
<div className={styles.container}> |
||||
|
{flowChart && widgets.map((group, index) => ( |
||||
|
<div key={index} className={styles.group}> |
||||
|
{group.map((ToolItem, index) => { |
||||
|
return <ToolItem key={index} flowChart={flowChart}/>; |
||||
|
})} |
||||
|
</div> |
||||
|
))} |
||||
|
</div> |
||||
|
); |
||||
|
}; |
||||
|
|
||||
|
export default ToolBar; |
@ -0,0 +1,54 @@ |
|||||
|
import React from 'react'; |
||||
|
|
||||
|
import 'antd/es/menu/style'; |
||||
|
import styles from './index.module.less'; |
||||
|
|
||||
|
import {Graph} from '@antv/x6'; |
||||
|
import {safeGet} from '../../../utils'; |
||||
|
import XIcon from '../../../components/xIcon'; |
||||
|
import makeDropdownWidget from './makeDropdownWidget'; |
||||
|
import ColorPicker from '../../../components/colorPicker'; |
||||
|
|
||||
|
interface IProps { |
||||
|
flowChart: Graph; |
||||
|
} |
||||
|
|
||||
|
const BgColor: React.FC<IProps> = makeDropdownWidget({ |
||||
|
tooltip: '填充颜色', |
||||
|
getIcon(flowChart: Graph) { |
||||
|
let bgColor = '#DDD'; |
||||
|
const cells = flowChart.getSelectedCells(); |
||||
|
if(cells.length > 0) { |
||||
|
bgColor = safeGet(cells, '0.attrs.body.fill', '#575757'); |
||||
|
} |
||||
|
return ( |
||||
|
<div className={styles.bgColorContainer}> |
||||
|
<XIcon className={styles.fillIcon} type={'icon-tianchong'}/> |
||||
|
<div className={styles.colorPreview} style={{backgroundColor: bgColor}}/> |
||||
|
</div> |
||||
|
); |
||||
|
}, |
||||
|
getOverlay(flowChart: Graph, onChange: (data: any) => void) { |
||||
|
let bgColor = '#DDD'; |
||||
|
const cells = flowChart.getSelectedCells(); |
||||
|
if(cells.length > 0) { |
||||
|
bgColor = safeGet(cells, '0.attrs.body.fill', '#575757'); |
||||
|
} |
||||
|
const onChangeComplete = (color: string) => onChange(color); |
||||
|
return ( |
||||
|
<ColorPicker color={bgColor} onChangeComplete={onChangeComplete}/> |
||||
|
); |
||||
|
}, |
||||
|
handler: (flowChart: Graph, value: any) => { |
||||
|
const cells = flowChart.getSelectedCells(); |
||||
|
if(cells.length > 0) { |
||||
|
cells.forEach(cell => cell.setAttrs({body: {fill: value}})); |
||||
|
flowChart.trigger('toolBar:forceUpdate'); |
||||
|
} |
||||
|
}, |
||||
|
disabled(flowChart: Graph) { |
||||
|
return flowChart.getSelectedCellCount() === 0; |
||||
|
}, |
||||
|
}); |
||||
|
|
||||
|
export default BgColor; |
@ -0,0 +1,32 @@ |
|||||
|
import React from 'react'; |
||||
|
|
||||
|
import {Graph} from '@antv/x6'; |
||||
|
import {safeGet} from '../../../utils'; |
||||
|
import makeBtnWidget from './makeBtnWidget'; |
||||
|
import {BoldOutlined} from '@ant-design/icons'; |
||||
|
import shortcuts from '../../../common/shortcuts'; |
||||
|
|
||||
|
interface IProps { |
||||
|
flowChart: Graph |
||||
|
} |
||||
|
|
||||
|
const Bold: React.FC<IProps> = makeBtnWidget({ |
||||
|
tooltip: '加粗', |
||||
|
handler: shortcuts['bold'].handler, |
||||
|
getIcon() { |
||||
|
return <BoldOutlined/>; |
||||
|
}, |
||||
|
disabled(flowChart: Graph) { |
||||
|
return flowChart.getSelectedCellCount() === 0; |
||||
|
}, |
||||
|
selected(flowChart: Graph) { |
||||
|
const cells = flowChart.getSelectedCells(); |
||||
|
if(cells.length > 0) { |
||||
|
return safeGet(cells, '0.attrs.label.fontWeight', 'normal') === 'bold'; |
||||
|
} else { |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
export default Bold; |
@ -0,0 +1,27 @@ |
|||||
|
import React from 'react'; |
||||
|
|
||||
|
import {Graph} from '@antv/x6'; |
||||
|
import makeBtnWidget from './makeBtnWidget'; |
||||
|
import XIcon from '../../../components/xIcon'; |
||||
|
|
||||
|
interface IProps { |
||||
|
flowChart: Graph |
||||
|
} |
||||
|
|
||||
|
const BringToBack: React.FC<IProps> = makeBtnWidget({ |
||||
|
tooltip: '置于底层', |
||||
|
getIcon() { |
||||
|
return <XIcon type={'icon-bringtobottom'}/>; |
||||
|
}, |
||||
|
handler(flowChart: Graph) { |
||||
|
const cells = flowChart.getSelectedCells(); |
||||
|
if(cells.length === 1) { |
||||
|
cells[0].toBack(); |
||||
|
} |
||||
|
}, |
||||
|
disabled(flowChart: Graph) { |
||||
|
return flowChart.getSelectedCellCount() === 0; |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
export default BringToBack; |
@ -0,0 +1,29 @@ |
|||||
|
//at.alicdn.com/t/font_2024452_s39le1337k9.js
|
||||
|
import React from 'react'; |
||||
|
|
||||
|
import {Graph} from '@antv/x6'; |
||||
|
import makeBtnWidget from './makeBtnWidget'; |
||||
|
import XIcon from '../../../components/xIcon'; |
||||
|
import shortcuts from '../../../common/shortcuts'; |
||||
|
|
||||
|
interface IProps { |
||||
|
flowChart: Graph |
||||
|
} |
||||
|
|
||||
|
const BringToTop: React.FC<IProps> = makeBtnWidget({ |
||||
|
tooltip: '置于顶层', |
||||
|
getIcon() { |
||||
|
return <XIcon type={'icon-bringtotop'}/>; |
||||
|
}, |
||||
|
handler(flowChart: Graph) { |
||||
|
const cells = flowChart.getSelectedCells(); |
||||
|
if(cells.length === 1) { |
||||
|
cells[0].toFront(); |
||||
|
} |
||||
|
}, |
||||
|
disabled(flowChart: Graph) { |
||||
|
return flowChart.getSelectedCellCount() === 0; |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
export default BringToTop; |
@ -0,0 +1,21 @@ |
|||||
|
import React from 'react'; |
||||
|
|
||||
|
import {Graph} from '@antv/x6'; |
||||
|
import makeBtnWidget from './makeBtnWidget'; |
||||
|
import {FullscreenExitOutlined} from '@ant-design/icons'; |
||||
|
|
||||
|
interface IProps { |
||||
|
flowChart: Graph |
||||
|
} |
||||
|
|
||||
|
const FitWindow: React.FC<IProps> = makeBtnWidget({ |
||||
|
tooltip: '适配窗口', |
||||
|
getIcon() { |
||||
|
return <FullscreenExitOutlined/>; |
||||
|
}, |
||||
|
handler(flowChart: Graph) { |
||||
|
flowChart.zoomToFit({minScale: 0.5, maxScale: 1}); |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
export default FitWindow; |
@ -0,0 +1,52 @@ |
|||||
|
import React from 'react'; |
||||
|
|
||||
|
import 'antd/es/menu/style'; |
||||
|
|
||||
|
import {Menu} from 'antd'; |
||||
|
import {Graph} from '@antv/x6'; |
||||
|
import {safeGet} from '../../../utils'; |
||||
|
import makeDropdownWidget from './makeDropdownWidget'; |
||||
|
|
||||
|
interface IProps { |
||||
|
flowChart: Graph |
||||
|
} |
||||
|
|
||||
|
const MenuItem = Menu.Item; |
||||
|
const FONT_SIZE_SET = [ |
||||
|
9, 10, 11, 12, 13, 14, 15, 16, 19, 22, 24, 29, 32 |
||||
|
]; |
||||
|
|
||||
|
const FontSize: React.FC<IProps> = makeDropdownWidget({ |
||||
|
tooltip: '字号', |
||||
|
getIcon(flowChart: Graph) { |
||||
|
let fontSize = 14; |
||||
|
const cells = flowChart.getSelectedCells(); |
||||
|
if(cells.length > 0) { |
||||
|
fontSize = safeGet(cells, '0.attrs.label.fontSize', 14); |
||||
|
} |
||||
|
return <span style={{fontSize: 14}}>{fontSize}px</span>; |
||||
|
}, |
||||
|
getOverlay(flowChart: Graph, onChange: (data: any) => void) { |
||||
|
return ( |
||||
|
<Menu onClick={(args) => onChange(args.key)}> |
||||
|
{FONT_SIZE_SET.map(fontSize => ( |
||||
|
<MenuItem key={fontSize}> |
||||
|
<span>{fontSize} px</span> |
||||
|
</MenuItem> |
||||
|
))} |
||||
|
</Menu> |
||||
|
); |
||||
|
}, |
||||
|
handler: (flowChart: Graph, value: any) => { |
||||
|
const cells = flowChart.getSelectedCells(); |
||||
|
if(cells.length > 0) { |
||||
|
cells.forEach(cell => cell.setAttrs({label: {fontSize: value}})); |
||||
|
flowChart.trigger('toolBar:forceUpdate'); |
||||
|
} |
||||
|
}, |
||||
|
disabled(flowChart: Graph) { |
||||
|
return flowChart.getSelectedCellCount() === 0; |
||||
|
}, |
||||
|
}); |
||||
|
|
||||
|
export default FontSize; |
@ -0,0 +1,102 @@ |
|||||
|
import React, { ReactElement } from 'react'; |
||||
|
|
||||
|
import 'antd/es/menu/style'; |
||||
|
|
||||
|
import {Menu} from 'antd'; |
||||
|
import {Graph} from '@antv/x6'; |
||||
|
import {safeGet} from '../../../utils'; |
||||
|
import makeDropdownWidget from './makeDropdownWidget'; |
||||
|
import {AlignLeftOutlined, AlignCenterOutlined, AlignRightOutlined} from '@ant-design/icons'; |
||||
|
|
||||
|
interface IProps { |
||||
|
flowChart: Graph; |
||||
|
} |
||||
|
|
||||
|
interface AlignItem { |
||||
|
text: string; |
||||
|
icon: ReactElement; |
||||
|
attrs: { |
||||
|
refX: number; |
||||
|
refX2?: number; |
||||
|
textAnchor: string; |
||||
|
align: { |
||||
|
horizontal: string; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const MenuItem = Menu.Item; |
||||
|
const ALIGN_MAP: {[key: string]: AlignItem} = { |
||||
|
left: { |
||||
|
text: '左对齐', |
||||
|
icon: <AlignLeftOutlined/>, |
||||
|
attrs: { |
||||
|
refX: 0, |
||||
|
refX2: 5, |
||||
|
textAnchor: 'start', |
||||
|
align: { |
||||
|
horizontal: 'left' |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
center: { |
||||
|
text: '居中对齐', |
||||
|
icon: <AlignCenterOutlined/>, |
||||
|
attrs: { |
||||
|
refX: 0.5, |
||||
|
refX2: 0, |
||||
|
textAnchor: 'middle', |
||||
|
align: { |
||||
|
horizontal: 'center' |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
right: { |
||||
|
text: '右对齐', |
||||
|
icon: <AlignRightOutlined/>, |
||||
|
attrs: { |
||||
|
refX: 0.99, |
||||
|
refX2: -5, |
||||
|
textAnchor: 'end', |
||||
|
align: { |
||||
|
horizontal: 'right' |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
const HorizontalAlign: React.FC<IProps> = makeDropdownWidget({ |
||||
|
tooltip: '水平对齐', |
||||
|
getIcon(flowChart: Graph) { |
||||
|
let alignType = 'center'; |
||||
|
const cells = flowChart.getSelectedCells(); |
||||
|
if(cells.length > 0) { |
||||
|
alignType = safeGet(cells, '0.attrs.label.align.horizontal', 'center'); |
||||
|
} |
||||
|
return ALIGN_MAP[alignType].icon; |
||||
|
}, |
||||
|
getOverlay(flowChart: Graph, onChange: (data: any) => void) { |
||||
|
return ( |
||||
|
<Menu onClick={({key}) => onChange(ALIGN_MAP[key].attrs)}> |
||||
|
{Object.keys(ALIGN_MAP).map((alignType) => ( |
||||
|
<MenuItem key={alignType}> |
||||
|
{ALIGN_MAP[alignType].icon} |
||||
|
<span>{ALIGN_MAP[alignType].text}</span> |
||||
|
</MenuItem> |
||||
|
))} |
||||
|
</Menu> |
||||
|
); |
||||
|
}, |
||||
|
handler: (flowChart: Graph, value: any) => { |
||||
|
const cells = flowChart.getSelectedCells(); |
||||
|
if(cells.length > 0) { |
||||
|
cells.forEach(cell => cell.setAttrs({label: value})); |
||||
|
flowChart.trigger('toolBar:forceUpdate'); |
||||
|
} |
||||
|
}, |
||||
|
disabled(flowChart: Graph) { |
||||
|
return flowChart.getSelectedCellCount() === 0; |
||||
|
}, |
||||
|
}); |
||||
|
|
||||
|
export default HorizontalAlign; |
@ -0,0 +1,91 @@ |
|||||
|
.btnWidget { |
||||
|
|
||||
|
box-sizing: border-box; |
||||
|
display: flex; |
||||
|
justify-content: center; |
||||
|
align-items: center; |
||||
|
margin-left: 4px; |
||||
|
padding: 0 8px; |
||||
|
min-width: 34px; |
||||
|
height: 34px; |
||||
|
font-size: 16px; |
||||
|
border-radius: 2px; |
||||
|
border: 0.5px solid transparent; |
||||
|
|
||||
|
.caret { |
||||
|
margin-left: 4px; |
||||
|
font-size: 10px; |
||||
|
} |
||||
|
|
||||
|
&:last-child { |
||||
|
margin-right: 4px; |
||||
|
} |
||||
|
|
||||
|
&:hover { |
||||
|
cursor: pointer; |
||||
|
border: 0.5px solid #CCC; |
||||
|
} |
||||
|
|
||||
|
&.selected { |
||||
|
border: 0.5px solid #CCC; |
||||
|
box-shadow: inset 0 1px 2px rgba(0,0,0,.1); |
||||
|
background-color: #EEE; |
||||
|
background-image: linear-gradient(top,#eee,#e0e0e0); |
||||
|
} |
||||
|
|
||||
|
&.disabled { |
||||
|
color: #CCC; |
||||
|
} |
||||
|
|
||||
|
&.disabled:hover { |
||||
|
cursor: default; |
||||
|
border: 0.5px solid transparent; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.zoomContainer { |
||||
|
|
||||
|
display: flex; |
||||
|
flex-direction: row; |
||||
|
align-items: center; |
||||
|
|
||||
|
.zoomText { |
||||
|
margin-left: 4px; |
||||
|
width: 45px; |
||||
|
text-align: center; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.bgColorContainer, .textColorContainer, .lineColorContainer { |
||||
|
|
||||
|
position: relative; |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
justify-content: center; |
||||
|
|
||||
|
.fillIcon { |
||||
|
position: relative; |
||||
|
top: -2px; |
||||
|
font-size: 15px; |
||||
|
} |
||||
|
|
||||
|
.textIcon { |
||||
|
position: relative; |
||||
|
top: -2px; |
||||
|
font-size: 15px; |
||||
|
} |
||||
|
|
||||
|
.lineIcon { |
||||
|
position: relative; |
||||
|
top: -2px; |
||||
|
font-size: 15px; |
||||
|
} |
||||
|
|
||||
|
.colorPreview { |
||||
|
position: absolute; |
||||
|
bottom: 0; |
||||
|
left: 0; |
||||
|
right: 0; |
||||
|
height: 1.6px; |
||||
|
} |
||||
|
} |
@ -0,0 +1,35 @@ |
|||||
|
import React from 'react'; |
||||
|
|
||||
|
import {Graph} from '@antv/x6'; |
||||
|
|
||||
|
import Save from './save'; |
||||
|
import Undo from './undo'; |
||||
|
import Redo from './redo'; |
||||
|
import Zoom from './zoom'; |
||||
|
import Bold from './bold'; |
||||
|
import Italic from './italic'; |
||||
|
import BgColor from './bgColor'; |
||||
|
import FontSize from './fontSize'; |
||||
|
import TextColor from './textColor'; |
||||
|
import LineColor from './lineColor'; |
||||
|
import Underline from './underline'; |
||||
|
import FitWindow from './fitWindow'; |
||||
|
import BringToTop from './bringToTop'; |
||||
|
import BringToBack from './bringToBack'; |
||||
|
import VerticalAlign from './verticalAlign'; |
||||
|
import HorizontalAlign from './horizontalAlign'; |
||||
|
|
||||
|
interface IProps { |
||||
|
flowChart: Graph; |
||||
|
} |
||||
|
|
||||
|
const tools: React.FC<IProps>[][] = [ |
||||
|
[Save, FitWindow, Undo, Redo], |
||||
|
[Zoom], |
||||
|
[FontSize, Bold, Italic, Underline, TextColor], |
||||
|
[HorizontalAlign, VerticalAlign], |
||||
|
[BgColor, LineColor], |
||||
|
[BringToTop, BringToBack] |
||||
|
]; |
||||
|
|
||||
|
export default tools; |
@ -0,0 +1,32 @@ |
|||||
|
import React from 'react'; |
||||
|
|
||||
|
import {Graph} from '@antv/x6'; |
||||
|
import {safeGet} from '../../../utils'; |
||||
|
import makeBtnWidget from './makeBtnWidget'; |
||||
|
import {ItalicOutlined} from '@ant-design/icons'; |
||||
|
import shortcuts from '../../../common/shortcuts'; |
||||
|
|
||||
|
interface IProps { |
||||
|
flowChart: Graph |
||||
|
} |
||||
|
|
||||
|
const Italic: React.FC<IProps> = makeBtnWidget({ |
||||
|
tooltip: '斜体', |
||||
|
handler: shortcuts['italic'].handler, |
||||
|
getIcon() { |
||||
|
return <ItalicOutlined/>; |
||||
|
}, |
||||
|
disabled(flowChart: Graph) { |
||||
|
return flowChart.getSelectedCellCount() === 0; |
||||
|
}, |
||||
|
selected(flowChart: Graph) { |
||||
|
const cells = flowChart.getSelectedCells(); |
||||
|
if(cells.length > 0) { |
||||
|
return safeGet(cells, '0.attrs.label.fontStyle', 'normal') === 'italic'; |
||||
|
} else { |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
export default Italic; |
@ -0,0 +1,54 @@ |
|||||
|
import React from 'react'; |
||||
|
|
||||
|
import 'antd/es/menu/style'; |
||||
|
import styles from './index.module.less'; |
||||
|
|
||||
|
import {Graph} from '@antv/x6'; |
||||
|
import {safeGet} from '../../../utils'; |
||||
|
import {HighlightOutlined} from '@ant-design/icons'; |
||||
|
import makeDropdownWidget from './makeDropdownWidget'; |
||||
|
import ColorPicker from '../../../components/colorPicker'; |
||||
|
|
||||
|
interface IProps { |
||||
|
flowChart: Graph; |
||||
|
} |
||||
|
|
||||
|
const LineColor: React.FC<IProps> = makeDropdownWidget({ |
||||
|
tooltip: '线条颜色', |
||||
|
getIcon(flowChart: Graph) { |
||||
|
let lineColor = '#DDD'; |
||||
|
const cells = flowChart.getSelectedCells(); |
||||
|
if(cells.length > 0) { |
||||
|
lineColor = safeGet(cells, '0.attrs.body.stroke', '#333'); |
||||
|
} |
||||
|
return ( |
||||
|
<div className={styles.lineColorContainer}> |
||||
|
<HighlightOutlined className={styles.lineIcon}/> |
||||
|
<div className={styles.colorPreview} style={{backgroundColor: lineColor}}/> |
||||
|
</div> |
||||
|
); |
||||
|
}, |
||||
|
getOverlay(flowChart: Graph, onChange: (data: any) => void) { |
||||
|
let lineColor = '#DDD'; |
||||
|
const cells = flowChart.getSelectedCells(); |
||||
|
if(cells.length > 0) { |
||||
|
lineColor = safeGet(cells, '0.attrs.body.stroke', '#333'); |
||||
|
} |
||||
|
const onChangeComplete = (color: string) => onChange(color); |
||||
|
return ( |
||||
|
<ColorPicker color={lineColor} onChangeComplete={onChangeComplete}/> |
||||
|
); |
||||
|
}, |
||||
|
handler: (flowChart: Graph, value: any) => { |
||||
|
const cells = flowChart.getSelectedCells(); |
||||
|
if(cells.length > 0) { |
||||
|
cells.forEach(cell => cell.setAttrs({body: {stroke: value}})); |
||||
|
flowChart.trigger('toolBar:forceUpdate'); |
||||
|
} |
||||
|
}, |
||||
|
disabled(flowChart: Graph) { |
||||
|
return flowChart.getSelectedCellCount() === 0; |
||||
|
}, |
||||
|
}); |
||||
|
|
||||
|
export default LineColor; |
@ -0,0 +1,51 @@ |
|||||
|
import React, {ReactElement} from 'react'; |
||||
|
|
||||
|
import 'antd/es/tooltip/style'; |
||||
|
import styles from './index.module.less'; |
||||
|
|
||||
|
import {Tooltip} from 'antd'; |
||||
|
import {Graph} from '@antv/x6'; |
||||
|
|
||||
|
interface IOptions { |
||||
|
tooltip: string; |
||||
|
getIcon: (flowChart: Graph) => ReactElement; |
||||
|
handler: (flowChart: Graph) => void; |
||||
|
disabled?: (flowChart: Graph) => boolean; |
||||
|
selected?: (flowChart: Graph) => boolean; |
||||
|
} |
||||
|
|
||||
|
interface IBtnWidgetProps { |
||||
|
flowChart: Graph; |
||||
|
} |
||||
|
|
||||
|
const makeBtnWidget = (options: IOptions) => { |
||||
|
const Widget: React.FC<IBtnWidgetProps> = (props) => { |
||||
|
const {flowChart} = props; |
||||
|
const {tooltip, getIcon, handler} = options; |
||||
|
const iconWrapperCls = [styles.btnWidget]; |
||||
|
let {disabled = false, selected = false} = options; |
||||
|
if(typeof disabled === 'function') { |
||||
|
disabled = disabled(flowChart); |
||||
|
disabled && iconWrapperCls.push(styles.disabled); |
||||
|
} |
||||
|
if(typeof selected === 'function') { |
||||
|
selected = selected(flowChart); |
||||
|
selected && iconWrapperCls.push(styles.selected); |
||||
|
} |
||||
|
const onClick = (): void => { |
||||
|
if(disabled) return; |
||||
|
handler(flowChart); |
||||
|
flowChart.trigger('toolBar:forceUpdate'); |
||||
|
} |
||||
|
return ( |
||||
|
<Tooltip title={tooltip}> |
||||
|
<div className={iconWrapperCls.join(' ')} onClick={onClick}> |
||||
|
{getIcon(flowChart)} |
||||
|
</div> |
||||
|
</Tooltip> |
||||
|
); |
||||
|
}; |
||||
|
return Widget; |
||||
|
}; |
||||
|
|
||||
|
export default makeBtnWidget; |
@ -0,0 +1,51 @@ |
|||||
|
import React, {ReactElement} from 'react'; |
||||
|
|
||||
|
import 'antd/es/tooltip/style'; |
||||
|
import 'antd/es/dropdown/style'; |
||||
|
import styles from './index.module.less'; |
||||
|
|
||||
|
import {Graph} from '@antv/x6'; |
||||
|
import {Tooltip, Dropdown} from 'antd'; |
||||
|
import {CaretDownOutlined} from '@ant-design/icons'; |
||||
|
|
||||
|
interface IOptions { |
||||
|
tooltip: string; |
||||
|
getIcon: (flowChart: Graph) => ReactElement; |
||||
|
getOverlay: (flowChart: Graph, onChange: (data: any) => void) => ReactElement; |
||||
|
handler: (flowChart: Graph, data: any) => void; |
||||
|
disabled?: (flowChart: Graph) => boolean; |
||||
|
} |
||||
|
|
||||
|
interface IDropdownWidgetProps { |
||||
|
flowChart: Graph; |
||||
|
} |
||||
|
|
||||
|
const makeDropdownWidget = (options: IOptions) => { |
||||
|
const Widget: React.FC<IDropdownWidgetProps> = (props) => { |
||||
|
const {flowChart} = props; |
||||
|
const {tooltip, getIcon, getOverlay, handler} = options; |
||||
|
const iconWrapperCls = [styles.btnWidget]; |
||||
|
let {disabled = false} = options; |
||||
|
if(typeof disabled === 'function') { |
||||
|
disabled = disabled(flowChart); |
||||
|
disabled && iconWrapperCls.push(styles.disabled); |
||||
|
} |
||||
|
const onChange = (data: any): void => { |
||||
|
if(disabled) return; |
||||
|
handler(flowChart, data); |
||||
|
flowChart.trigger('toolBar:forceUpdate'); |
||||
|
}; |
||||
|
return ( |
||||
|
<Tooltip title={tooltip}> |
||||
|
<Dropdown disabled={disabled} overlay={getOverlay(flowChart, onChange)} trigger={['click']}> |
||||
|
<div className={iconWrapperCls.join(' ')}> |
||||
|
{getIcon(flowChart)} <CaretDownOutlined className={styles.caret}/> |
||||
|
</div> |
||||
|
</Dropdown> |
||||
|
</Tooltip> |
||||
|
); |
||||
|
}; |
||||
|
return Widget; |
||||
|
}; |
||||
|
|
||||
|
export default makeDropdownWidget; |
@ -0,0 +1,23 @@ |
|||||
|
import React from 'react'; |
||||
|
|
||||
|
import {Graph} from '@antv/x6'; |
||||
|
import makeBtnWidget from './makeBtnWidget'; |
||||
|
import {RedoOutlined} from '@ant-design/icons'; |
||||
|
import shortcuts from '../../../common/shortcuts'; |
||||
|
|
||||
|
interface IProps { |
||||
|
flowChart: Graph |
||||
|
} |
||||
|
|
||||
|
const Save: React.FC<IProps> = makeBtnWidget({ |
||||
|
tooltip: '重做', |
||||
|
handler: shortcuts['redo'].handler, |
||||
|
getIcon() { |
||||
|
return <RedoOutlined/>; |
||||
|
}, |
||||
|
disabled(flowChart: Graph) { |
||||
|
return !flowChart.canRedo(); |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
export default Save; |
@ -0,0 +1,20 @@ |
|||||
|
import React from 'react'; |
||||
|
|
||||
|
import {Graph} from '@antv/x6'; |
||||
|
import makeBtnWidget from './makeBtnWidget'; |
||||
|
import {SaveOutlined} from '@ant-design/icons'; |
||||
|
import shortcuts from '../../../common/shortcuts'; |
||||
|
|
||||
|
interface IProps { |
||||
|
flowChart: Graph |
||||
|
} |
||||
|
|
||||
|
const Save: React.FC<IProps> = makeBtnWidget({ |
||||
|
tooltip: '保存', |
||||
|
handler: shortcuts['save'].handler, |
||||
|
getIcon() { |
||||
|
return <SaveOutlined/>; |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
export default Save; |
@ -0,0 +1,54 @@ |
|||||
|
import React from 'react'; |
||||
|
|
||||
|
import 'antd/es/menu/style'; |
||||
|
import styles from './index.module.less'; |
||||
|
|
||||
|
import {Graph} from '@antv/x6'; |
||||
|
import {safeGet} from '../../../utils'; |
||||
|
import XIcon from '../../../components/xIcon'; |
||||
|
import makeDropdownWidget from './makeDropdownWidget'; |
||||
|
import ColorPicker from '../../../components/colorPicker'; |
||||
|
|
||||
|
interface IProps { |
||||
|
flowChart: Graph; |
||||
|
} |
||||
|
|
||||
|
const TextColor: React.FC<IProps> = makeDropdownWidget({ |
||||
|
tooltip: '文本颜色', |
||||
|
getIcon(flowChart: Graph) { |
||||
|
let textColor = '#DDD'; |
||||
|
const cells = flowChart.getSelectedCells(); |
||||
|
if(cells.length > 0) { |
||||
|
textColor = safeGet(cells, '0.attrs.label.fill', '#575757'); |
||||
|
} |
||||
|
return ( |
||||
|
<div className={styles.textColorContainer}> |
||||
|
<XIcon className={styles.textIcon} type={'icon-A'}/> |
||||
|
<div className={styles.colorPreview} style={{backgroundColor: textColor}}/> |
||||
|
</div> |
||||
|
); |
||||
|
}, |
||||
|
getOverlay(flowChart: Graph, onChange: (data: any) => void) { |
||||
|
let textColor = '#DDD'; |
||||
|
const cells = flowChart.getSelectedCells(); |
||||
|
if(cells.length > 0) { |
||||
|
textColor = safeGet(cells, '0.attrs.label.fill', '#575757'); |
||||
|
} |
||||
|
const onChangeComplete = (color: string) => onChange(color); |
||||
|
return ( |
||||
|
<ColorPicker color={textColor} onChangeComplete={onChangeComplete}/> |
||||
|
); |
||||
|
}, |
||||
|
handler: (flowChart: Graph, value: any) => { |
||||
|
const cells = flowChart.getSelectedCells(); |
||||
|
if(cells.length > 0) { |
||||
|
cells.forEach(cell => cell.setAttrs({label: {fill: value}})); |
||||
|
flowChart.trigger('toolBar:forceUpdate'); |
||||
|
} |
||||
|
}, |
||||
|
disabled(flowChart: Graph) { |
||||
|
return flowChart.getSelectedCellCount() === 0; |
||||
|
}, |
||||
|
}); |
||||
|
|
||||
|
export default TextColor; |
@ -0,0 +1,32 @@ |
|||||
|
import React from 'react'; |
||||
|
|
||||
|
import {Graph} from '@antv/x6'; |
||||
|
import {safeGet} from '../../../utils'; |
||||
|
import makeBtnWidget from './makeBtnWidget'; |
||||
|
import {UnderlineOutlined} from '@ant-design/icons'; |
||||
|
import shortcuts from '../../../common/shortcuts'; |
||||
|
|
||||
|
interface IProps { |
||||
|
flowChart: Graph |
||||
|
} |
||||
|
|
||||
|
const Underline: React.FC<IProps> = makeBtnWidget({ |
||||
|
tooltip: '下划线', |
||||
|
handler: shortcuts['underline'].handler, |
||||
|
getIcon() { |
||||
|
return <UnderlineOutlined/>; |
||||
|
}, |
||||
|
disabled(flowChart: Graph) { |
||||
|
return flowChart.getSelectedCellCount() === 0; |
||||
|
}, |
||||
|
selected(flowChart: Graph) { |
||||
|
const cells = flowChart.getSelectedCells(); |
||||
|
if(cells.length > 0) { |
||||
|
return safeGet(cells, '0.attrs.label.textDecoration', 'none') === 'underline'; |
||||
|
} else { |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
export default Underline; |
@ -0,0 +1,23 @@ |
|||||
|
import React from 'react'; |
||||
|
|
||||
|
import {Graph} from '@antv/x6'; |
||||
|
import makeBtnWidget from './makeBtnWidget'; |
||||
|
import {UndoOutlined} from '@ant-design/icons'; |
||||
|
import shortcuts from '../../../common/shortcuts'; |
||||
|
|
||||
|
interface IProps { |
||||
|
flowChart: Graph |
||||
|
} |
||||
|
|
||||
|
const Save: React.FC<IProps> = makeBtnWidget({ |
||||
|
tooltip: '撤销', |
||||
|
handler: shortcuts['undo'].handler, |
||||
|
getIcon() { |
||||
|
return <UndoOutlined/>; |
||||
|
}, |
||||
|
disabled(flowChart: Graph) { |
||||
|
return !flowChart.canUndo(); |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
export default Save; |
@ -0,0 +1,102 @@ |
|||||
|
import React, {ReactElement} from 'react'; |
||||
|
|
||||
|
import 'antd/es/menu/style'; |
||||
|
|
||||
|
import {Menu} from 'antd'; |
||||
|
import {Graph} from '@antv/x6'; |
||||
|
import {safeGet} from '../../../utils'; |
||||
|
import makeDropdownWidget from './makeDropdownWidget'; |
||||
|
import {VerticalAlignTopOutlined, VerticalAlignMiddleOutlined, VerticalAlignBottomOutlined} from '@ant-design/icons'; |
||||
|
|
||||
|
interface IProps { |
||||
|
flowChart: Graph; |
||||
|
} |
||||
|
|
||||
|
interface AlignItem { |
||||
|
text: string; |
||||
|
icon: ReactElement; |
||||
|
attrs: { |
||||
|
refY: number; |
||||
|
refY2?: number; |
||||
|
textVerticalAlign: string; |
||||
|
align: { |
||||
|
vertical: string; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const MenuItem = Menu.Item; |
||||
|
const ALIGN_MAP: {[key: string]: AlignItem} = { |
||||
|
top: { |
||||
|
text: '上对齐', |
||||
|
icon: <VerticalAlignTopOutlined/>, |
||||
|
attrs: { |
||||
|
refY: 0, |
||||
|
refY2: 10, |
||||
|
textVerticalAlign: 'start', |
||||
|
align: { |
||||
|
vertical: 'top' |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
center: { |
||||
|
text: '居中对齐', |
||||
|
icon: <VerticalAlignMiddleOutlined/>, |
||||
|
attrs: { |
||||
|
refY: 0.5, |
||||
|
refY2: 0, |
||||
|
textVerticalAlign: 'middle', |
||||
|
align: { |
||||
|
vertical: 'center' |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
bottom: { |
||||
|
text: '下对齐', |
||||
|
icon: <VerticalAlignBottomOutlined/>, |
||||
|
attrs: { |
||||
|
refY: 0.99, |
||||
|
refY2: -10, |
||||
|
textVerticalAlign: 'end', |
||||
|
align: { |
||||
|
vertical: 'bottom' |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
const VerticalAlign: React.FC<IProps> = makeDropdownWidget({ |
||||
|
tooltip: '垂直对齐', |
||||
|
getIcon(flowChart: Graph) { |
||||
|
let alignType = 'center'; |
||||
|
const cells = flowChart.getSelectedCells(); |
||||
|
if(cells.length > 0) { |
||||
|
alignType = safeGet(cells, '0.attrs.label.align.vertical', 'center'); |
||||
|
} |
||||
|
return ALIGN_MAP[alignType].icon; |
||||
|
}, |
||||
|
getOverlay(flowChart: Graph, onChange: (data: any) => void) { |
||||
|
return ( |
||||
|
<Menu onClick={({key}) => onChange(ALIGN_MAP[key].attrs)}> |
||||
|
{Object.keys(ALIGN_MAP).map((alignType: string) => ( |
||||
|
<MenuItem key={alignType}> |
||||
|
{ALIGN_MAP[alignType].icon} |
||||
|
<span>{ALIGN_MAP[alignType].text}</span> |
||||
|
</MenuItem> |
||||
|
))} |
||||
|
</Menu> |
||||
|
); |
||||
|
}, |
||||
|
handler: (flowChart: Graph, value: any) => { |
||||
|
const cells = flowChart.getSelectedCells(); |
||||
|
if(cells.length > 0) { |
||||
|
cells.forEach(cell => cell.setAttrs({label: value})); |
||||
|
flowChart.trigger('toolBar:forceUpdate'); |
||||
|
} |
||||
|
}, |
||||
|
disabled(flowChart: Graph) { |
||||
|
return flowChart.getSelectedCellCount() === 0; |
||||
|
}, |
||||
|
}); |
||||
|
|
||||
|
export default VerticalAlign; |
@ -0,0 +1,64 @@ |
|||||
|
import React, {useState, useEffect} from 'react'; |
||||
|
|
||||
|
import styles from './index.module.less'; |
||||
|
|
||||
|
import {Graph} from '@antv/x6'; |
||||
|
import makeBtnWidget from './makeBtnWidget'; |
||||
|
import shortcuts from '../../../common/shortcuts'; |
||||
|
import {ZoomOutOutlined, ZoomInOutlined} from '@ant-design/icons'; |
||||
|
|
||||
|
interface IProps { |
||||
|
flowChart: Graph |
||||
|
} |
||||
|
|
||||
|
const MIN_ZOOM = 0.5; |
||||
|
const MAX_ZOOM = 1.5; |
||||
|
|
||||
|
const ZoomOut: React.FC<IProps> = makeBtnWidget({ |
||||
|
tooltip: '缩小', |
||||
|
handler: shortcuts['zoomOut'].handler, |
||||
|
getIcon() { |
||||
|
return <ZoomOutOutlined/>; |
||||
|
}, |
||||
|
disabled(flowChart: Graph) { |
||||
|
return flowChart.zoom() <= MIN_ZOOM; |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
const ZoomIn: React.FC<IProps> = makeBtnWidget({ |
||||
|
tooltip: '放大', |
||||
|
handler: shortcuts['zoomIn'].handler, |
||||
|
getIcon() { |
||||
|
return <ZoomInOutlined/>; |
||||
|
}, |
||||
|
disabled(flowChart: Graph) { |
||||
|
return flowChart.zoom() >= MAX_ZOOM; |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
const Zoom: React.FC<IProps> = props => { |
||||
|
const {flowChart} = props; |
||||
|
const [scale, setScale] = useState<number>(flowChart.zoom()); |
||||
|
useEffect(() => { |
||||
|
flowChart.on('scale', () => { |
||||
|
setScale(flowChart.zoom()); |
||||
|
}); |
||||
|
}, [flowChart]); |
||||
|
return ( |
||||
|
<div className={styles.zoomContainer}> |
||||
|
<ZoomOut {...props}/> |
||||
|
<span className={styles.zoomText}> |
||||
|
{Helper.scaleFormatter(scale)} |
||||
|
</span> |
||||
|
<ZoomIn {...props}/> |
||||
|
</div> |
||||
|
); |
||||
|
}; |
||||
|
|
||||
|
const Helper = { |
||||
|
scaleFormatter(scale: number): string { |
||||
|
return (scale * 100).toFixed(0) + '%'; |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
export default Zoom; |
@ -0,0 +1,9 @@ |
|||||
|
declare module '*.module.css' { |
||||
|
const classes: {[key: string]: string}; |
||||
|
export default classes; |
||||
|
} |
||||
|
|
||||
|
declare module '*.less' { |
||||
|
const classes: {[key: string]: string}; |
||||
|
export default classes; |
||||
|
} |
@ -1,88 +0,0 @@ |
|||||
import * as React from 'react'; |
|
||||
import * as ReactDom from 'react-dom'; |
|
||||
import { Graph } from '@antv/x6'; |
|
||||
|
|
||||
export default function createGraph(container: HTMLDivElement): Graph { |
|
||||
return new Graph(container, { |
|
||||
rotate: false, |
|
||||
resize: false, |
|
||||
infinite: true, |
|
||||
rubberband: true, |
|
||||
backgroundColor: '#f8f9fa', |
|
||||
grid: { |
|
||||
type: 'dot', |
|
||||
color: '#bcbcbc', |
|
||||
}, |
|
||||
guide: { |
|
||||
enabled: true, |
|
||||
dashed: true, |
|
||||
stroke: '#ff5500', |
|
||||
}, |
|
||||
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, |
|
||||
}, |
|
||||
connection: { |
|
||||
enabled: true, |
|
||||
hotspotable: false, |
|
||||
livePreview: true, |
|
||||
}, |
|
||||
connectionPreview: { |
|
||||
stroke: '#1890ff', |
|
||||
}, |
|
||||
anchor: { |
|
||||
inductiveSize: 10, |
|
||||
}, |
|
||||
anchorHighlight: { |
|
||||
strokeWidth: 16, |
|
||||
}, |
|
||||
shouldRedrawOnDataChange(): boolean { |
|
||||
return true; |
|
||||
}, |
|
||||
getAnchors(cell): [number, number][] | null { |
|
||||
if (cell === null || cell.data === null) return null; |
|
||||
|
|
||||
if (cell.data.type === 'start') { |
|
||||
return [[0.5, 1]]; |
|
||||
} |
|
||||
|
|
||||
if (cell.data.type === 'end') { |
|
||||
return [[0.5, 0]]; |
|
||||
} |
|
||||
|
|
||||
return [ |
|
||||
[0.5, 0], |
|
||||
[0, 0.5], |
|
||||
|
|
||||
[1, 0.5], |
|
||||
[0.5, 1], |
|
||||
]; |
|
||||
}, |
|
||||
getLabel: (cell): HTMLElement | string => { |
|
||||
if (cell.isEdge()) { |
|
||||
const div = document.createElement('div'); |
|
||||
ReactDom.render(<div>{cell.data.label}</div>, div); |
|
||||
return div; |
|
||||
} |
|
||||
return ''; |
|
||||
}, |
|
||||
}); |
|
||||
} |
|
@ -0,0 +1,88 @@ |
|||||
|
import {Graph} from '@antv/x6'; |
||||
|
import shortcuts from '../common/shortcuts'; |
||||
|
import baseCellSchemaMap from '../common/baseCell'; |
||||
|
import previewCellSchemaMap from '../common/previewCell'; |
||||
|
|
||||
|
// X6 register base/preview cell shape
|
||||
|
[baseCellSchemaMap, previewCellSchemaMap] |
||||
|
.forEach(schemas => Object.values(schemas).forEach(schema => { |
||||
|
const {base, ...rest} = schema; |
||||
|
base.define(rest); |
||||
|
})); |
||||
|
|
||||
|
const createGraph = (container: HTMLDivElement, miniMapContainer: HTMLDivElement): Graph => { |
||||
|
|
||||
|
// create graph instance
|
||||
|
const graph = new Graph({ |
||||
|
container, |
||||
|
rotating: false, |
||||
|
resizing: true, |
||||
|
clipboard: true, |
||||
|
// https://x6.antv.vision/zh/docs/tutorial/intermediate/connector
|
||||
|
connecting: { |
||||
|
snap: true, |
||||
|
dangling: true, |
||||
|
highlight: true, |
||||
|
router: { |
||||
|
name: 'manhattan' |
||||
|
}, |
||||
|
validateConnection({sourceView, targetView}) { |
||||
|
return sourceView !== targetView; |
||||
|
} |
||||
|
}, |
||||
|
// https://x6.antv.vision/zh/docs/tutorial/basic/background
|
||||
|
background: { |
||||
|
color: '#f8f9fa' |
||||
|
}, |
||||
|
// https://x6.antv.vision/zh/docs/tutorial/basic/grid
|
||||
|
grid: { |
||||
|
visible: true |
||||
|
}, |
||||
|
// https://x6.antv.vision/zh/docs/tutorial/basic/selection
|
||||
|
selecting: { |
||||
|
enabled: true, |
||||
|
multiple: true, |
||||
|
rubberband: true, |
||||
|
movable: true, |
||||
|
showNodeSelectionBox: true |
||||
|
}, |
||||
|
// https://x6.antv.vision/zh/docs/tutorial/basic/snapline
|
||||
|
snapline: { |
||||
|
enabled: true, |
||||
|
clean: 100 |
||||
|
}, |
||||
|
// https://x6.antv.vision/zh/docs/tutorial/basic/keyboard
|
||||
|
keyboard: { |
||||
|
enabled: true, |
||||
|
global: false |
||||
|
}, |
||||
|
// https://x6.antv.vision/zh/docs/tutorial/basic/history
|
||||
|
history: { |
||||
|
enabled: true |
||||
|
}, |
||||
|
// https://x6.antv.vision/zh/docs/tutorial/basic/minimap
|
||||
|
minimap: { |
||||
|
enabled: true, |
||||
|
padding: 0, |
||||
|
width: 200, |
||||
|
height: 160, |
||||
|
minScale: 0.5, |
||||
|
maxScale: 1.5, |
||||
|
container: miniMapContainer, |
||||
|
}, |
||||
|
// https://x6.antv.vision/zh/docs/tutorial/basic/scroller
|
||||
|
scroller: { |
||||
|
enabled: true |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
// register shortcuts
|
||||
|
Object.values(shortcuts).forEach(shortcut => { |
||||
|
const {keys, handler} = shortcut; |
||||
|
graph.bindKey(keys, () => handler(graph)); |
||||
|
}); |
||||
|
|
||||
|
return graph; |
||||
|
} |
||||
|
|
||||
|
export default createGraph; |
@ -0,0 +1,26 @@ |
|||||
|
export const safeParse = (json: string): Object => { |
||||
|
try { |
||||
|
return JSON.parse(json); |
||||
|
} catch(err) { |
||||
|
return {}; |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
export const safeGet = (obj: any, keyChain: string, defaultVal: any): any => { |
||||
|
|
||||
|
if(typeof obj !== 'object' || obj === null) { |
||||
|
return defaultVal; |
||||
|
} |
||||
|
|
||||
|
let retVal = obj; |
||||
|
const keys = keyChain.split('.'); |
||||
|
for(const key of keys) { |
||||
|
if(retVal[key] === undefined) { |
||||
|
return defaultVal; |
||||
|
} else { |
||||
|
retVal = retVal[key]; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return retVal; |
||||
|
}; |
@ -1,69 +0,0 @@ |
|||||
import * as React from 'react'; |
|
||||
import { Graph, Dnd } from '@antv/x6'; |
|
||||
import CellMap from '../components/cells'; |
|
||||
import { DataItem } from '../data/cells'; |
|
||||
|
|
||||
export default function patchDnd( |
|
||||
container: HTMLDivElement | null, |
|
||||
graph: Graph, |
|
||||
cellData: DataItem[] |
|
||||
): void { |
|
||||
if (!container) return; |
|
||||
|
|
||||
(container.childNodes as NodeListOf<HTMLElement>).forEach((node, index) => { |
|
||||
const itemData = cellData[index]; |
|
||||
const dnd = new Dnd<DataItem>(node, { |
|
||||
data: itemData, |
|
||||
getGraph: (): Graph => graph, |
|
||||
createDragElement: ({ data }): HTMLDivElement => { |
|
||||
const elem = document.createElement('div'); |
|
||||
if (typeof data === 'undefined') return elem; |
|
||||
|
|
||||
const { width, height, scale } = data.style; |
|
||||
elem.style.width = `${width * scale}px`; |
|
||||
elem.style.height = `${height * scale}px`; |
|
||||
elem.style.border = '1px dashed #000'; |
|
||||
elem.style.cursor = 'move'; |
|
||||
return elem; |
|
||||
}, |
|
||||
createPreviewElement: ({ data }): HTMLDivElement => { |
|
||||
const elem = document.createElement('div'); |
|
||||
if (typeof data === 'undefined') return elem; |
|
||||
|
|
||||
const { width, height, scale } = data.style; |
|
||||
elem.style.width = `${width * scale}px`; |
|
||||
elem.style.height = `${height * scale}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, targetPosition }) => { |
|
||||
if (typeof data === 'undefined') return; |
|
||||
|
|
||||
const { width, height, scale } = data.style; |
|
||||
const { x, y } = targetPosition; |
|
||||
const Cell = CellMap[data.type]; |
|
||||
const cell = graph.addNode({ |
|
||||
x, |
|
||||
y, |
|
||||
data, |
|
||||
width: width * scale, |
|
||||
height: height * scale, |
|
||||
shape: 'react', |
|
||||
component: (newData) => { |
|
||||
if (!cell) return null; |
|
||||
const { label } = newData.data; |
|
||||
return <Cell label={label} scale={scale} />; |
|
||||
}, |
|
||||
}); |
|
||||
|
|
||||
graph.unSelectCells(graph.getSelectedCells()).selectCell(cell); |
|
||||
}); |
|
||||
}); |
|
||||
} |
|
@ -1,16 +0,0 @@ |
|||||
import styled from '@emotion/styled'; |
|
||||
|
|
||||
const Label = styled.div` |
|
||||
position: absolute; |
|
||||
width: 100%; |
|
||||
top: 50%; |
|
||||
padding: 0 5px; |
|
||||
transform: translateY(-50%); |
|
||||
overflow: hidden; |
|
||||
text-overflow: ellipsis; |
|
||||
white-space: nowrap; |
|
||||
font-size: 12px; |
|
||||
text-align: center; |
|
||||
`;
|
|
||||
|
|
||||
export default Label; |
|
@ -1,68 +0,0 @@ |
|||||
import { Graph } from '@antv/x6'; |
|
||||
import { Commands } from '../model'; |
|
||||
|
|
||||
export function zoomIn(graph: Graph) { |
|
||||
return (): void => { |
|
||||
let { scale } = graph.view; |
|
||||
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; |
|
||||
} |
|
||||
graph.zoomTo(scale); |
|
||||
}; |
|
||||
} |
|
||||
|
|
||||
export function zoomOut(graph: Graph) { |
|
||||
return (): void => { |
|
||||
let { scale } = graph.view; |
|
||||
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; |
|
||||
} |
|
||||
graph.zoomTo(scale); |
|
||||
}; |
|
||||
} |
|
||||
|
|
||||
export function bindKey(commands: Commands, graph: Graph): void { |
|
||||
commands.forEach((items) => |
|
||||
items.forEach((item) => { |
|
||||
if (item.shortcut) { |
|
||||
if (item.shortcut === 'Delete') { |
|
||||
graph.bindKey(['del', 'backspace'], item.handler); |
|
||||
} else { |
|
||||
graph.bindKey(item.shortcut, item.handler); |
|
||||
} |
|
||||
} |
|
||||
}) |
|
||||
); |
|
||||
} |
|
@ -1,7 +1,10 @@ |
|||||
{ |
{ |
||||
"extends": "../../tsconfig", |
"extends": "../../tsconfig", |
||||
"compilerOptions": { |
"compilerOptions": { |
||||
"outDir": "dist/types" |
"outDir": "dist/types", |
||||
|
"jsx": "react", |
||||
|
"esModuleInterop": true |
||||
}, |
}, |
||||
"include": ["./src"] |
"include": ["./src"], |
||||
|
"exclude": ["node_modules"] |
||||
} |
} |
||||
|
Loading…
Reference in new issue