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 { 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'; |
|||
import React, {useState} from 'react'; |
|||
|
|||
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)` |
|||
height: 100%; |
|||
`;
|
|||
|
|||
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 IProps { |
|||
onSave: (data: {nodes: any; edges: any}) => void; |
|||
} |
|||
|
|||
interface CoreState { |
|||
inited: boolean; |
|||
} |
|||
|
|||
class Core extends React.Component<CoreProps, CoreState> { |
|||
private graph: Graph | null = null; |
|||
|
|||
private container = React.createRef<HTMLDivElement>(); |
|||
|
|||
constructor(props: CoreProps) { |
|||
super(props); |
|||
this.state = { |
|||
inited: false, |
|||
}; |
|||
} |
|||
|
|||
componentDidMount(): void { |
|||
const container = this.container.current; |
|||
if (container) { |
|||
DomEvent.disableContextMenu(container); |
|||
this.graph = createGraph(container); |
|||
this.graph.center(); |
|||
this.setState({ |
|||
inited: true, |
|||
}); |
|||
} |
|||
} |
|||
|
|||
render(): JSX.Element { |
|||
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> |
|||
); |
|||
} |
|||
} |
|||
const Core: React.FC<IProps> = props => { |
|||
|
|||
const {onSave} = props; |
|||
const [flowChart, setGraph] = useState<Graph>(); |
|||
|
|||
const onFlowChartReady = (flowChart: Graph): void => { |
|||
setGraph(flowChart); |
|||
flowChart.on('node:added', (args) => { |
|||
flowChart.cleanSelection(); |
|||
flowChart.select(args.cell); |
|||
}); |
|||
flowChart.on('selection:changed', () => { |
|||
flowChart.trigger('toolBar:forceUpdate'); |
|||
flowChart.trigger('settingBar:forceUpdate'); |
|||
}); |
|||
}; |
|||
|
|||
return ( |
|||
<div className={styles.container}> |
|||
<Header title={'iMove'}/> |
|||
<div className={styles.body}> |
|||
<SideBar flowChart={flowChart}/> |
|||
<div className={styles.full}> |
|||
<ToolBar flowChart={flowChart}/> |
|||
<FlowChart onReady={onFlowChartReady}/> |
|||
</div> |
|||
<SettingBar flowChart={flowChart}/> |
|||
</div> |
|||
</div> |
|||
); |
|||
}; |
|||
|
|||
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", |
|||
"compilerOptions": { |
|||
"outDir": "dist/types" |
|||
"outDir": "dist/types", |
|||
"jsx": "react", |
|||
"esModuleInterop": true |
|||
}, |
|||
"include": ["./src"] |
|||
"include": ["./src"], |
|||
"exclude": ["node_modules"] |
|||
} |
|||
|
Loading…
Reference in new issue