import React, {Component} from 'react'
|
import PropTypes from 'prop-types'
|
import { is, fromJS } from 'immutable'
|
import { Popover, Tooltip, message } from 'antd'
|
import { ToolOutlined, DeleteOutlined, FontColorsOutlined, VerticalAlignTopOutlined, VerticalAlignBottomOutlined, SaveOutlined, ZoomInOutlined, ZoomOutOutlined, OneToOneOutlined, DoubleLeftOutlined } from '@ant-design/icons'
|
import { Graph, Shape } from '@antv/x6'
|
import { Stencil } from '@antv/x6-plugin-stencil'
|
import { Transform } from '@antv/x6-plugin-transform'
|
import { Selection } from '@antv/x6-plugin-selection'
|
import { Snapline } from '@antv/x6-plugin-snapline'
|
import { Keyboard } from '@antv/x6-plugin-keyboard'
|
import { Clipboard } from '@antv/x6-plugin-clipboard'
|
import { History } from '@antv/x6-plugin-history'
|
|
import MKEmitter from '@/utils/events.js'
|
import asyncComponent from '@/utils/asyncComponent'
|
import asyncIconComponent from '@/utils/asyncIconComponent'
|
import { resetStyle, getTables, checkComponent } from '@/utils/utils-custom.js'
|
import './index.scss'
|
|
const SettingComponent = asyncIconComponent(() => import('@/menu/datasource'))
|
const ChartCompileForm = asyncIconComponent(() => import('./chartcompile'))
|
const NodeUpdate = asyncIconComponent(() => import('./nodeupdate'))
|
const CopyComponent = asyncIconComponent(() => import('@/menu/components/share/copycomponent'))
|
const NormalHeader = asyncComponent(() => import('@/menu/components/share/normalheader'))
|
|
// #region 初始化图形
|
const groups = {
|
top: {
|
position: 'top',
|
attrs: {
|
circle: {
|
r: 4,
|
magnet: true,
|
stroke: 'var(--mk-sys-color)',
|
strokeWidth: 1,
|
fill: '#fff',
|
style: {
|
visibility: 'hidden'
|
}
|
}
|
}
|
},
|
right: {
|
position: 'right',
|
attrs: {
|
circle: {
|
r: 4,
|
magnet: true,
|
stroke: 'var(--mk-sys-color)',
|
strokeWidth: 1,
|
fill: '#fff',
|
style: {
|
visibility: 'hidden'
|
}
|
}
|
}
|
},
|
bottom: {
|
position: 'bottom',
|
attrs: {
|
circle: {
|
r: 4,
|
magnet: true,
|
stroke: 'var(--mk-sys-color)',
|
strokeWidth: 1,
|
fill: '#fff',
|
style: {
|
visibility: 'hidden'
|
}
|
}
|
}
|
},
|
left: {
|
position: 'left',
|
attrs: {
|
circle: {
|
r: 4,
|
magnet: true,
|
stroke: 'var(--mk-sys-color)',
|
strokeWidth: 1,
|
fill: '#fff',
|
style: {
|
visibility: 'hidden'
|
}
|
}
|
}
|
}
|
}
|
|
Graph.registerNode(
|
'mk-rect',
|
{
|
inherit: 'rect',
|
width: 66,
|
height: 36,
|
attrs: {
|
body: {
|
strokeWidth: 1,
|
stroke: '#5F95FF',
|
fill: '#EFF4FF'
|
},
|
text: {
|
fontSize: 12,
|
fill: '#262626'
|
}
|
},
|
ports: {
|
groups,
|
items: [
|
{ group: 'top' },
|
{ group: 'right' },
|
{ group: 'bottom' },
|
{ group: 'left' }
|
]
|
}
|
},
|
true
|
)
|
|
Graph.registerNode(
|
'mk-polygon',
|
{
|
inherit: 'polygon',
|
width: 66,
|
height: 36,
|
attrs: {
|
body: {
|
strokeWidth: 1,
|
stroke: '#5F95FF',
|
fill: '#EFF4FF'
|
},
|
text: {
|
fontSize: 12,
|
fill: '#262626'
|
}
|
},
|
ports: {
|
groups,
|
items: [
|
{ group: 'top' },
|
{ group: 'right' },
|
{ group: 'bottom' },
|
{ group: 'left' }
|
]
|
}
|
},
|
true
|
)
|
|
Graph.registerNode(
|
'mk-circle',
|
{
|
inherit: 'circle',
|
width: 36,
|
height: 36,
|
attrs: {
|
body: {
|
strokeWidth: 1,
|
stroke: '#5F95FF',
|
fill: '#EFF4FF'
|
},
|
text: {
|
fontSize: 12,
|
fill: '#262626'
|
}
|
},
|
ports: {
|
groups,
|
items: [
|
{ group: 'top' },
|
{ group: 'right' },
|
{ group: 'bottom' },
|
{ group: 'left' }
|
]
|
}
|
},
|
true
|
)
|
|
Graph.registerNode(
|
'mk-ellipse',
|
{
|
inherit: 'ellipse',
|
width: 66,
|
height: 36,
|
attrs: {
|
body: {
|
strokeWidth: 1,
|
stroke: '#5F95FF',
|
fill: '#EFF4FF'
|
},
|
text: {
|
fontSize: 12,
|
fill: '#262626'
|
}
|
},
|
ports: {
|
groups,
|
items: [
|
{ group: 'top' },
|
{ group: 'right' },
|
{ group: 'bottom' },
|
{ group: 'left' }
|
]
|
}
|
},
|
true
|
)
|
|
Graph.registerNode(
|
'mk-star',
|
{
|
inherit: 'polygon',
|
width: 36,
|
height: 36,
|
points: '100,10 40,198 190,78 10,78 160,198',
|
attrs: {
|
body: {
|
fill: '#EFF4FF',
|
stroke: '#5F95FF',
|
strokeWidth: 1,
|
fillRule: 'nonzero'
|
},
|
text: {
|
fontSize: 12,
|
fill: '#262626'
|
}
|
},
|
ports: {
|
groups,
|
items: [
|
{ group: 'top' },
|
{ group: 'right' },
|
{ group: 'bottom' },
|
{ group: 'left' }
|
]
|
}
|
},
|
true
|
)
|
|
class antvX6Chart extends Component {
|
static propTpyes = {
|
card: PropTypes.object,
|
updateConfig: PropTypes.func,
|
deletecomponent: PropTypes.func,
|
}
|
|
state = {
|
card: null,
|
eventListener: null,
|
toolunfold: true,
|
nodeunfold: true,
|
node: null
|
}
|
|
selectNode = null
|
mkGraph = null
|
|
UNSAFE_componentWillMount () {
|
const { card } = this.props
|
|
if (card.isNew) {
|
let _plot = {
|
width: card.width || 24,
|
height: 400,
|
subtype: card.subtype,
|
name: card.name
|
}
|
|
let _card = {
|
uuid: card.uuid,
|
type: card.type,
|
format: 'array', // 组件属性 - 数据格式
|
pageable: false, // 组件属性 - 是否可分页
|
switchable: true, // 组件属性 - 数据是否可切换
|
width: _plot.width,
|
name: _plot.name,
|
subtype: card.subtype,
|
setting: { interType: 'system' },
|
style: {
|
borderWidth: '1px', borderColor: 'rgb(217, 217, 217)',
|
marginLeft: '8px', marginRight: '8px', marginTop: '8px', marginBottom: '8px'
|
},
|
headerStyle: { fontSize: '16px', borderBottomWidth: '1px', borderBottomColor: '#e8e8e8' },
|
columns: [],
|
scripts: [],
|
search: [],
|
action: [],
|
plot: _plot,
|
}
|
|
this.updateComponent(_card)
|
} else {
|
this.setState({
|
card: fromJS(card).toJS()
|
})
|
}
|
}
|
|
shouldComponentUpdate (nextProps, nextState) {
|
return !is(fromJS(this.state), fromJS(nextState))
|
}
|
|
/**
|
* @description 组件销毁,清除state更新,清除快捷键设置
|
*/
|
componentWillUnmount () {
|
this.setState = () => {
|
return
|
}
|
}
|
|
componentDidMount () {
|
setTimeout(() => {
|
this.viewrender()
|
}, 1000)
|
}
|
|
viewrender = () => {
|
// const { card } = this.state
|
|
// if (card.plot.subtype === 'mindmap') {
|
// this.ponitrender()
|
// } else if (card.plot.subtype === 'indentTree') {
|
// this.indentrender()
|
// } else if (card.plot.subtype === 'kapmap') {
|
this.kapmaprender()
|
// }
|
}
|
|
kapmaprender = () => {
|
const { card } = this.state
|
|
// #region 初始化画布
|
const graph = new Graph({
|
container: document.getElementById(card.uuid + 'container'),
|
// grid: {
|
// visible: true,
|
// type: 'doubleMesh',
|
// args: [
|
// {
|
// color: '#eee', // 主网格线颜色
|
// thickness: 1 // 主网格线宽度
|
// },
|
// {
|
// color: '#ddd', // 次网格线颜色
|
// thickness: 1, // 次网格线宽度
|
// factor: 4 // 主次网格线间隔
|
// }
|
// ]
|
// },
|
scaling: {
|
min: 0.5,
|
max: 2
|
},
|
autoResize: true,
|
panning: true,
|
background: {
|
color: '#ffffff'
|
},
|
mousewheel: {
|
enabled: true,
|
zoomAtMousePosition: true,
|
modifiers: 'ctrl'
|
},
|
connecting: {
|
router: 'manhattan',
|
connector: {
|
name: 'rounded',
|
args: {
|
radius: 8
|
}
|
},
|
anchor: 'center',
|
connectionPoint: 'anchor',
|
allowBlank: false,
|
snap: {
|
radius: 20
|
},
|
createEdge() {
|
return new Shape.Edge({
|
attrs: {
|
line: {
|
stroke: '#A2B1C3',
|
strokeWidth: 2,
|
targetMarker: {
|
name: 'block',
|
width: 12,
|
height: 8
|
}
|
}
|
},
|
zIndex: 0
|
})
|
},
|
validateConnection({ targetMagnet }) {
|
return !!targetMagnet
|
}
|
},
|
highlighting: {
|
magnetAdsorbed: {
|
name: 'stroke',
|
args: {
|
attrs: {
|
fill: '#5F95FF',
|
stroke: '#5F95FF'
|
}
|
}
|
}
|
}
|
})
|
|
// #region 使用插件
|
graph
|
.use(new Transform({
|
resizing: true,
|
rotating: true
|
}))
|
.use(new Selection())
|
.use(new Snapline())
|
.use(new Keyboard())
|
.use(new Clipboard())
|
.use(new History())
|
|
// #region 初始化 stencil
|
const stencil = new Stencil({
|
title: '流程图',
|
target: graph,
|
stencilGraphWidth: 180,
|
stencilGraphHeight: 180,
|
groups: [
|
{
|
title: '通用节点',
|
name: 'group1'
|
},
|
{
|
title: '自定义',
|
name: 'group2',
|
graphHeight: 120,
|
layoutOptions: {
|
rowHeight: 70
|
}
|
}
|
],
|
layoutOptions: {
|
columns: 2,
|
columnWidth: 80,
|
rowHeight: 55
|
}
|
})
|
|
document.getElementById(card.uuid + 'stencil').appendChild(stencil.container)
|
|
// #region 快捷键与事件
|
graph.bindKey(['meta+c', 'ctrl+c'], () => {
|
const cells = graph.getSelectedCells()
|
if (cells.length) {
|
graph.copy(cells)
|
}
|
return false
|
})
|
graph.bindKey(['meta+x', 'ctrl+x'], () => {
|
const cells = graph.getSelectedCells()
|
if (cells.length) {
|
graph.cut(cells)
|
}
|
return false
|
})
|
graph.bindKey(['meta+v', 'ctrl+v'], () => {
|
if (!graph.isClipboardEmpty()) {
|
graph.paste({ offset: 32 })
|
}
|
return false
|
})
|
|
// undo redo
|
graph.bindKey(['meta+z', 'ctrl+z'], () => {
|
if (graph.canUndo()) {
|
graph.undo()
|
}
|
return false
|
})
|
graph.bindKey(['meta+shift+z', 'ctrl+shift+z'], () => {
|
if (graph.canRedo()) {
|
graph.redo()
|
}
|
return false
|
})
|
|
// 删除元素
|
graph.bindKey(['backspace', 'delete'], () => {
|
const cells = graph.getSelectedCells()
|
if (cells.length) {
|
graph.removeCells(cells)
|
this.selectNode = null
|
this.setState({node: null})
|
}
|
})
|
|
// 控制连接桩显示/隐藏
|
const showPorts = (ports, show) => {
|
for (let i = 0, len = ports.length; i < len; i += 1) {
|
ports[i].style.visibility = show ? 'visible' : 'hidden'
|
}
|
}
|
graph.on('node:mouseenter', () => {
|
const container = document.getElementById(card.uuid + 'container')
|
const ports = container.querySelectorAll('.x6-port-body')
|
showPorts(ports, true)
|
})
|
graph.on('node:mouseleave', () => {
|
const container = document.getElementById(card.uuid + 'container')
|
const ports = container.querySelectorAll('.x6-port-body')
|
showPorts(ports, false)
|
})
|
|
graph.on('node:click', ({ e, x, y, node, view }) => {
|
this.selectNode = node
|
|
this.setState({node: node.store.data})
|
})
|
graph.on('edge:click', ({ e, x, y, edge, view }) => {
|
this.selectNode = edge
|
|
this.setState({node: edge.store.data})
|
|
graph.clearTransformWidgets()
|
})
|
graph.on('blank:click', ({ e, x, y }) => {
|
this.selectNode = null
|
|
this.setState({node: null})
|
})
|
|
const r1 = graph.createNode({
|
shape: 'mk-rect',
|
label: '开始',
|
attrs: {
|
body: {
|
rx: 20,
|
ry: 26
|
}
|
}
|
})
|
const r2 = graph.createNode({
|
shape: 'mk-rect',
|
label: '过程'
|
})
|
const r3 = graph.createNode({
|
shape: 'mk-rect',
|
attrs: {
|
body: {
|
rx: 6,
|
ry: 6
|
}
|
},
|
label: '可选过程'
|
})
|
const r4 = graph.createNode({
|
shape: 'mk-polygon',
|
attrs: {
|
body: {
|
refPoints: '0,10 10,0 20,10 10,20'
|
}
|
},
|
label: '决策'
|
})
|
const r5 = graph.createNode({
|
shape: 'mk-polygon',
|
attrs: {
|
body: {
|
refPoints: '10,0 40,0 30,20 0,20'
|
}
|
},
|
label: '数据'
|
})
|
const r6 = graph.createNode({
|
shape: 'mk-circle',
|
label: '连接'
|
})
|
|
stencil.load([r1, r2, r3, r4, r5, r6], 'group1')
|
|
const p1 = graph.createNode({
|
shape: 'mk-ellipse',
|
label: 'ellipse'
|
})
|
const p2 = graph.createNode({
|
shape: 'mk-star',
|
label: ''
|
})
|
|
stencil.load([p1, p2], 'group2')
|
|
this.mkGraph = graph
|
}
|
|
setTop = () => {
|
if (!this.selectNode) {
|
message.warning('请选择节点!')
|
return
|
}
|
this.selectNode.toFront()
|
}
|
|
setBottom = () => {
|
if (!this.selectNode) {
|
message.warning('请选择节点!')
|
return
|
}
|
// let cells = this.mkGraph.getCells()
|
this.selectNode.toBack()
|
}
|
|
// zoom() 可获取或者设置缩放比例
|
setZoomIn = () => {
|
this.mkGraph.zoom(0.1)
|
}
|
|
setZoomOut = () => {
|
this.mkGraph.zoom(-0.1)
|
}
|
|
setZoomInt = () => {
|
this.mkGraph.zoomTo(1)
|
}
|
|
save = () => {
|
// let nodes = this.mkGraph.toJSON()
|
}
|
|
changeProps = (value, key) => {
|
const { node } = this.state
|
|
if (node.shape === 'edge') {
|
if (key === 'title') {
|
this.selectNode.setLabels(value)
|
} else if (key === 'stroke') {
|
this.selectNode.attr('line/stroke', value)
|
} else if (key === 'strokeWidth') {
|
this.selectNode.attr('line/strokeWidth', value)
|
} else if (key === 'lineType') {
|
if (value === 'dash') {
|
this.selectNode.attr('line/strokeDasharray', 5)
|
} else {
|
this.selectNode.attr('line/strokeDasharray', 0)
|
}
|
} else if (key === 'fontSize') {
|
this.selectNode.attr('text/fontSize', value)
|
} else if (key === 'fontFill') {
|
this.selectNode.attr('text/fill', value)
|
}
|
} else {
|
if (key === 'title') {
|
this.selectNode.attr('text/text', value)
|
} else if (key === 'fill') {
|
this.selectNode.attr('body/fill', value)
|
} else if (key === 'stroke') {
|
this.selectNode.attr('body/stroke', value)
|
} else if (key === 'fontSize') {
|
this.selectNode.attr('text/fontSize', value)
|
} else if (key === 'fontFill') {
|
this.selectNode.attr('text/fill', value)
|
}
|
}
|
}
|
|
updateComponent = (card) => {
|
card.width = card.plot.width
|
card.name = card.plot.name
|
card.subtype = card.plot.subtype
|
|
card.$c_ds = true
|
card.errors = checkComponent(card)
|
|
if (card.errors.length === 0) {
|
card.$tables = getTables(card)
|
}
|
|
this.setState({
|
card: card
|
})
|
this.props.updateConfig(card)
|
}
|
|
changeStyle = () => {
|
const { card } = this.state
|
|
MKEmitter.emit('changeStyle', ['background', 'border', 'padding', 'margin', 'shadow', 'clear'], card.style, this.getStyle)
|
}
|
|
getStyle = (style) => {
|
let _card = {...this.state.card, style}
|
|
this.updateComponent(_card)
|
}
|
|
clickComponent = (e) => {
|
if (sessionStorage.getItem('style-control') === 'true' || sessionStorage.getItem('style-control') === 'component') {
|
e.stopPropagation()
|
MKEmitter.emit('clickComponent', this.state.card.uuid, null, (style) => {
|
let _card = {...this.state.card}
|
_card.style = {..._card.style, ...style}
|
|
this.updateComponent(_card)
|
})
|
}
|
}
|
|
render() {
|
const { card, toolunfold, nodeunfold, node } = this.state
|
let _style = resetStyle(card.style)
|
|
return (
|
<div className="menu-x6-chart-edit-box" style={_style} onClick={this.clickComponent} id={card.uuid}>
|
<Popover overlayClassName="mk-popover-control-wrap" mouseLeaveDelay={0.2} mouseEnterDelay={0.2} content={
|
<div className="mk-popover-control">
|
<ChartCompileForm config={card} plotchange={this.updateComponent}/>
|
<CopyComponent type="antvG6" card={card}/>
|
<FontColorsOutlined className="style" title="调整样式" onClick={this.changeStyle}/>
|
<DeleteOutlined className="close" title="delete" onClick={() => this.props.deletecomponent(card.uuid)}/>
|
<SettingComponent config={card} updateConfig={this.updateComponent}/>
|
</div>
|
} trigger="hover">
|
<ToolOutlined/>
|
</Popover>
|
<NormalHeader config={card} updateComponent={this.updateComponent}/>
|
<div className="mk-toolbar">
|
<div className="left-tool">
|
<Tooltip title="置前">
|
<VerticalAlignTopOutlined onClick={this.setTop}/>
|
</Tooltip>
|
<Tooltip title="置后">
|
<VerticalAlignBottomOutlined onClick={this.setBottom}/>
|
</Tooltip>
|
<Tooltip title="保存">
|
<SaveOutlined onClick={this.save}/>
|
</Tooltip>
|
</div>
|
<div className="right-tool">
|
<Tooltip title="放大">
|
<ZoomInOutlined onClick={this.setZoomIn}/>
|
</Tooltip>
|
<Tooltip title="缩小">
|
<ZoomOutOutlined onClick={this.setZoomOut}/>
|
</Tooltip>
|
<Tooltip title="1:1">
|
<OneToOneOutlined onClick={this.setZoomInt}/>
|
</Tooltip>
|
</div>
|
</div>
|
<div className="canvas" style={{width: '100%', minHeight: card.plot.height, height: card.plot.height}} id={card.uuid + 'canvas'}>
|
<div id={card.uuid + 'stencil'} className={'mk-stencil ' + (toolunfold ? '' : 'merge')}>
|
<div className="tool-control" onClick={() => this.setState({toolunfold: !toolunfold})}>
|
<DoubleLeftOutlined />
|
</div>
|
</div>
|
<div id={card.uuid + 'container'} className="mk-container"></div>
|
<div className={'mk-node-edit ' + (nodeunfold ? '' : 'merge')}>
|
<div className="tool-control" onClick={() => this.setState({nodeunfold: !nodeunfold})}>
|
<DoubleLeftOutlined />
|
</div>
|
<div className="header">设置</div>
|
{!node ? <div className="empty">未选中</div> : <NodeUpdate node={node} onChange={this.changeProps}/>}
|
</div>
|
</div>
|
<div className="component-name">
|
<div className="center">
|
<div className="title">{card.name}</div>
|
<div className="content">
|
{card.errors && card.errors.map((err, index) => {
|
if (err.level === 0) {
|
return <span key={index} className="error">{err.detail}</span>
|
} else {
|
return <span key={index} className="waring">{err.detail};</span>
|
}
|
})}
|
</div>
|
</div>
|
</div>
|
</div>
|
)
|
}
|
}
|
|
export default antvX6Chart
|