这是多年前做的一个项目经历。公司需要个能画流程图或者时序图或者其他诡异不规则图的软件或者工具,并且节点间能配合逻辑可交互(点击、高亮、跳转等),即有开始就有结束,能攻能守,能动能静,可长可短,可盐可甜。鉴于立项时间和现有人力都不算富裕,所以本着快速响应实现原则,胡搜海搜了一波,国内少之又少,要么就特么需要登录,要么就需要付费,我呸。无意间发现某个付费的web版本process啊on啊的,仔细端详了下这个web版的代码调用,发现这个核心就是一个编辑器界面,随之发现重点核心就是那个mxclient,然后奔着这个名字搜了一波,功夫不负有心人找到了出处,原来墙外的世界真大,早就有了这个玩意,废话说了一堆,见笑,链接点此处。(英文不好的,查字典)
废话一堆,真把式拿出来溜溜,欢迎唾弃~~~
真实运行图如下
一段简单粗暴的spa常见开场代码
<div id="app">
<div class="pannel">
<div class="editor__action">
<el-button type="default" size="small" @click="onCancel">取消</el-button>
<el-button type="default" size="small" @click="onSave()">保存</el-button>
</div>
</div>
<div ref="editorRef"></div>
</div>
接下来拿vue2.x练手,上菜
import '^/mxgraph/editor/styles/grapheditor.css'
import '^/mxgraph/editor/resources/grapheditor.txt.js'
import '^/mxgraph/editor/styles/default.xml.js'
import '^/mxgraph/editor/sanitizer/sanitizer.min.js'
import '^/mxgraph/editor/js/EditorUi.js'
import '^/mxgraph/editor/js/Editor.js'
import '^/mxgraph/editor/js/Sidebar.js'
import '^/mxgraph/editor/js/Graph.js'
import '^/mxgraph/editor/js/Format.js'
import '^/mxgraph/editor/js/Shapes.js'
import '^/mxgraph/editor/js/Actions.js'
import '^/mxgraph/editor/js/Menus.js'
import '^/mxgraph/editor/js/Toolbar.js'
import '^/mxgraph/editor/js/Dialogs.js'
import FlowHelper from './flow.js'
export default {
name: 'editor',
data() {
return {
message: '',
xml: ''
}
},
created() {
this.cellErrors = {}
},
mounted() {
this.initEditor()
},
destroyed() {
let children = [...document.body.children]
children.forEach(c => {
if (
c.nodeType === 1 &&
((c.className &&
(c.className.indexOf('ge') == 0 || c.className.indexOf('mx') == 0)) ||
(c.style.touchAction === 'none' &&
c.firstChild.nodeName.toLowerCase() === 'svg'))
) {
document.body.removeChild(c)
}
})
},
methods: {
initEditor() {
this.graph = FlowHelper.initEditor(
this.$refs.editorRef,
grapheditorTXT,
defaultThemeStylesheet,
cell => {
this.taskOnClick(cell)
}
)
this.renderXml()
},
renderFromXml(data) {
this.xml = this.preParseXML(data.flowXml)
let flowTree = JSON.parse(this.xml)
flowTree.tasks.forEach(t => this.createTask(t, flowTree))
if (
data.processModelAttribute.label &&
Number(this.$route.params.modifyType) === 0 &&
data.processModelAttribute.label.length <= 26
) {
data.processModelAttribute.label += '(复制)'
}
this.processModelAttribute = data.processModelAttribute
this.renderXml()
},
preParseXML(xml) {
let xmlObj = JSON.parse(xml)
xmlObj.cells.forEach(c => {
if (c.flow) {
c.style.fillColor = '#fff'
c.style.strokeColor = '#999'
}
})
xmlObj.tasks.forEach(t => {
t.value = t.attrs.label
if (t.flow) {
t.style.fillColor = '#fff'
t.style.strokeColor = '#999'
t.style.strokeWidth = 1
}
})
return JSON.stringify(xmlObj)
},
renderXml() {
if (this.xml && this.graph) {
const cells = []
FlowHelper.display(this.xml, null, null, this.graph, false, false)
FlowHelper.getTaskCells(this.graph).forEach(t => {
const { label, executorPost: post, assignee } = this.tempTasks[
'task' + t.id
]
const executor = this.persons.find(val => val.id === assignee) || {}
FlowHelper.updateCellAttrs(this.graph, t, {
label: t.value,
assigneeName: executor.name || assignee,
executorPost: post
})
if (!assignee && /task|condition/.test(t.flow)) {
cells.push(t)
}
})
if (cells.length) {
this.$message.error('请填写任务执行人')
this.setCellStyle(cells, '#f00')
}
}
},
createTask(task, flowTree) {
let id = 'task' + task.id
let temp = { label: '', assignee: '', description: '' }
if (!this.tempTasks) {
this.tempTasks = {}
}
if (!this.tempTasks[id]) {
this.tempTasks[id] = {}
}
if (task.attrs) {
this.tempTasks[id] = task.attrs
}
if (task.flow == 'condition') {
temp = { agree: '', reject: '' }
if (flowTree) {
/**
* 编辑时候需要设置决策节点的agree属性为指定的edge.id
* 编辑时候需要设置决策节点的reject属性为指定的edge.id
*/
flowTree.cells
.filter(({ source }) => source == task.id)
.forEach(e => {
if (e.conditionExpression == 0) {
temp.agree = e.id
}
if (e.conditionExpression == 1) {
temp.reject = e.id
}
})
}
} else if (task.flow == 'task') {
temp = { tools: '', knowledge: '' }
} else if (task.flow == 'flow') {
temp = { label: '', description: '', subFlow: task.subFlow }
}
!this.tempTasks[id].agree && delete this.tempTasks[id].agree
!this.tempTasks[id].reject && delete this.tempTasks[id].reject
this.tempTasks[id] = Object.assign(temp, this.tempTasks[id], {
label: this.tempTasks[id].label || this.getValue(task)
})
},
onCancel() {
this.$confirm('是否确定取消流程模板编辑?取消后不可恢复', '提示', {
confirmButtonText: '确定取消',
cancelButtonText: '暂不取消',
type: 'danger'
})
.then(_ => this.$router.go(-1))
.catch(e => e)
},
onSave(releaseTag) {
this.toggleSaveLoading()
this.validFlow()
if (this.validErrors()) {
return this.toggleSaveLoading()
}
this.setCellStyle(FlowHelper.getCells(this.graph).filter(c => c.flow))
let flowTree = FlowHelper.encodeXML2JSON(this.graph)
if (this.tempTasks) {
Object.keys(this.tempTasks).forEach(t => {
let id = t.slice(4)
flowTree.tasks.forEach(c => {
if (c.id == id) {
c.attrs = JSON.parse(JSON.stringify(this.tempTasks[t]))
if ('agree' in c.attrs) delete c.attrs.agree
if ('reject' in c.attrs) delete c.attrs.reject
if ('assigneeName' in c.attrs) delete c.attrs.assigneeName
if (c.flow === 'flow') {
c.subFlow = c.attrs.subFlow
delete c.attrs.subFlow
}
}
})
})
}
// 子流程更换后重置recode标识
this.modifySubFlowList &&
flowTree.tasks.forEach(val => {
if (this.modifySubFlowList.includes(val.id)) {
val.recode = ''
}
})
let params = {
flowTree,
processModelAttribute: this.processModelAttribute,
releaseTag: !releaseTag ? 0 : 1
}
if (
this.$route.params.modelId &&
this.$route.params.modifyType &&
Number(this.$route.params.modifyType) === 1
) {
params.modelId = this.$route.params.modelId
}
this.$api.template
.save(params, {
headers: { groupId: this.$route.params.storeId },
needToast: false
})
.then(r => {
this.$message.success('保存成功')
if (this.$route.params.storeId != undefined) {
this.$router.replace(
`/${this.$route.params.storeId}/setting/operation`
)
}
})
.catch(e => {
this.toggleSaveLoading()
if (parseInt(e.code, 10) === 10001) {
this.showTemplateForm = true
this.showTaskForm = false
this.existTemplateLabel.push(this.processModelAttribute.label)
} else {
this.$message.error(e.message)
}
})
},
validFlow() {
this.validStartEnd()
this.validTasks()
this.validConditions()
this.validMergeBranch()
},
validStartEnd() {
let start = FlowHelper.getStartCells(this.graph)
let end = FlowHelper.getEndCells(this.graph)
if (start && start[0] && start[0].edges && start[0].edges.length > 1) {
this.makeErrors({
key: start[0].id,
message: '开始节点只能关联一个其它节点',
cell: start[0]
})
}
if (start.length <= 0 || start.length > 1) {
if (start.length <= 0) {
this.makeErrors({ key: 'start', message: '缺少开始节点' })
} else {
start.forEach(s =>
this.makeErrors({ key: s.id, message: '节点仅限一个', cell: s })
)
}
} else {
start = start[0]
if (
!start.edges ||
start.edges.length < 1 ||
!start.edges.every(
e =>
e &&
e.target &&
e.target !== e.source &&
e.target !== start &&
e.target !== end &&
e.source !== end &&
e.target.isVertex() &&
e.source.isVertex()
)
) {
console.log('1')
this.makeErrors({
key: start.id,
message: '节点必须至少关联一个非开始和结束的节点,不能被关联',
cell: start,
type: 0
})
}
}
if (end.length <= 0) {
this.makeErrors({ key: 'terminal', message: '缺少结束节点' })
} else {
for (let i = 0; i < end.length; i++) {
let ed = end[i]
let b =
!ed.edges ||
ed.edges.length < 1 ||
!ed.edges.every(
e =>
e &&
e.target &&
e.target === ed &&
e.source &&
e.source.isVertex() &&
!/terminal|start/.test(e.source.flow)
)
if (b) {
this.makeErrors({
key: ed.id,
cell: ed,
type: 0,
message: '节点必须至少被一个非开始和结束的节点关联,只能被关联'
})
}
if (end.length > 1) {
this.makeErrors({
key: ed.id,
cell: ed,
type: 0,
message: '节点仅限一个'
})
}
}
}
},
validMergeBranch() {
let cells = FlowHelper.getCells(this.graph).filter(
t => t.flow == 'merge' || t.flow == 'branch'
)
for (let i = 0; i < cells.length; i++) {
let c = cells[i]
let f = c.flow
let edges = c.edges
let node = '节点'
if (!edges || edges.length < 1) {
this.makeErrors({
key: c.id,
cell: c,
type: 0,
message: `${node}不能独立存在`
})
} else {
let x
let w
let message
if (f == 'merge') {
x = edges.findIndex(e => e.source == c)
w =
edges
.filter((e, n) => n != x)
.findIndex(e => /merge|branch/.test(e.source.flow)) > -1
message = `${node}必须关联一个其他类型的节点`
}
if (f == 'branch') {
x = edges.findIndex(e => e.target == c)
w =
edges
.filter((e, n) => n != x)
.findIndex(e => /merge|branch/.test(e.target.flow)) > -1
message = `${node}必须被一个其他类型的节点关联`
}
if (x == -1) {
this.makeErrors({ key: c.id, cell: c, type: 0, message })
}
if (w) {
this.makeErrors({
key: c.id,
cell: c,
type: 0,
message: `${node}不能互联分支或合并节点`
})
}
let z = edges.length < 3
if (z) {
message =
cells.flow == 'branch'
? `${node}必须关联两个以上的其他类型节点`
: `${node}必须被两个以上的其他类型节点关联`
this.makeErrors({ key: c.id, cell: c, type: 0, message })
}
}
}
},
validConditions() {
let conditions = FlowHelper.getConditions(this.graph)
for (let i = 0; i < conditions.length; i++) {
let c = conditions[i]
if (!c.edges) {
this.makeErrors({
key: c.id,
cell: c,
type: 0,
message: '决策节点不能独立存在'
})
} else {
let t
c.edges.forEach(e => {
if (e.target.flow == 'start' || e.source.flow == 'start') {
this.makeErrors({
key: c.id,
cell: c,
type: 0,
message: '节点不能关连开始节点'
})
}
if (
e.source.flow == 'condition' &&
e.conditionExpression != 0 &&
e.conditionExpression != 1
) {
this.makeErrors({
key: c.id,
cell: c,
type: 1,
message: '请完善节点的流转规则配置'
})
}
})
}
}
},
validTasks() {
let tasks = FlowHelper.getTaskCells(this.graph)
if (tasks.length <= 1) {
this.makeErrors({
key: 'task',
cell: tasks[0],
message: '至少包含两个任务节点'
})
} else {
for (let i = 0; i < tasks.length; i++) {
let t = tasks[i]
const validMsg = this.validTask(tasks[i])
validMsg.forEach(val => {
this.makeErrors({
key: t.id,
cell: t,
type: 0,
message: val
})
})
if (!this.validTaskAttr(tasks[i])) {
this.makeErrors({
key: t.id,
cell: t,
type: 0,
message: '请检查节点的必填信息是否填写完善'
})
}
}
}
},
validTask(task) {
const taskId = task.id
const edges = task.edges || []
const msg = []
// 决策结点连接校验
if (task.flow === 'condition') {
let targetNum = 0
let sourceNum = 0
edges.forEach(val => {
if (val.source.id === taskId) {
sourceNum++
}
if (val.target.id === taskId) {
targetNum++
}
})
if (sourceNum !== 2) {
msg.push('该节点必须关联两个其它节点')
}
if (targetNum < 1) {
msg.push('该节点至少被一个节点关联')
}
}
// 任务及子流程结点连接校验
if (/task|robot|flow/.test(task.flow)) {
let sourceNum = 0
let targetNum = 0
edges.forEach(val => {
if (val.source.id === taskId) {
sourceNum++
}
if (val.target.id === taskId) {
targetNum++
}
})
if (sourceNum > 1) {
msg.push('该节点只能关联一个节点')
}
if (!sourceNum) {
msg.push('该节点没有关联其它节点')
}
if (targetNum < 1) {
msg.push('该节点至少被一个节点关联')
}
}
return msg
},
validTaskAttr(cell) {
let value = cell.value
if (
!this.tempTasks ||
(!this.tempTasks['task' + cell.id] ||
!this.tempTasks['task' + cell.id][
/task|condition|robot/.test(cell.flow) ? 'label' : 'subFlow'
])
) {
return false
}
return true
},
taskOnClick(cell) {
if (!/task|condition|flow|robot/.test(cell.flow)) return
this.activeCell = cell
// this.taskType = cell.flow == 'task' ? 1 : cell.flow == 'condition' ? 2 : 3
switch (cell.flow) {
case 'task':
this.taskType = 1
break
case 'condition':
this.taskType = 2
break
case 'flow':
this.taskType = 3
break
case 'robot':
this.taskType = 4
break
}
if (cell.flow == 'condition') {
this.setYesNoTask(cell)
}
this.showTemplateForm = false
this.showTaskForm = true
this.createTask(cell)
this.taskFormAtts = { ...this.tempTasks['task' + cell.id] }
},
updateCellAttrs(attrs) {
attrs = JSON.parse(JSON.stringify(attrs))
let id = 'task' + this.activeCell.id
if (attrs.assignee) {
const executor =
this.persons.find(val => val.id === attrs.assignee) || {}
attrs.assigneeName = executor.name || attrs.assignee
this.setCellStyle([this.activeCell])
} else {
attrs.assigneeName = ''
}
Object.assign(this.tempTasks[id], attrs)
FlowHelper.updateCellAttrs(this.graph, this.activeCell, attrs)
const executor = this.persons.find(val => val.id === attrs.assignee)
},
toggleSaveLoading() {
this.saveLoading = !this.saveLoading
},
updateCategories(data) {
this.categories = data
},
setYesNoTask(cell) {
if (!cell.edges || cell.edges.length <= 0) {
this.nodesYesNo = []
return
}
this.nodesYesNo = cell.edges
.filter(e => e.source == cell && e.target)
.map(({ id, target }) => {
return {
id,
name: target.flow == 'terminal' ? '结束' : this.getValue(target)
}
})
let old
if (this.tempTasks && this.tempTasks['task' + cell.id]) {
old = this.tempTasks['task' + cell.id]
// 编辑后更新决策节点的缓存数据
if (!this.nodesYesNo.some(e => e.id == old.agree)) {
old.agree = ''
}
if (!this.nodesYesNo.some(e => e.id == old.reject)) {
old.reject = ''
}
}
},
getValue(target) {
let div = document.createElement('div')
div.innerHTML = target.value
return div.textContent || div.innerText
},
validErrors() {
let keys = Object.keys(this.cellErrors)
if (keys.length <= 0) {
return false
} else {
let message = []
let cells = []
let n = 1
keys.forEach(k => {
let err = this.cellErrors[k]
err.forEach(e => {
let name = ''
if (e.cell) {
name =
'【' +
(e.cell.flow == 'start'
? '开始'
: e.cell.flow == 'terminal'
? '结束'
: this.getValue(e.cell)) +
'】'
}
message.push(n + '. ' + name + e.message)
n += 1
if (e.cell) {
cells.push(e.cell)
}
})
})
this.$message.error({
dangerouslyUseHTMLString: true,
duration: 7000,
message: message.join('<br/><br/>')
})
if (cells.length > 0) {
this.setCellStyle(cells, '#ED4040')
//this.graph.setSelectionCells(cells)
let ids = cells.map(c => c.id)
this.setCellStyle(
FlowHelper.getCells(this.graph).filter(
c => ids.indexOf(c.id) < 0 && c.flow
)
)
}
if (keys.length == 1 && keys[0] == 'template') {
this.setCellStyle(FlowHelper.getCells(this.graph).filter(c => c.flow))
}
this.cellErrors = {}
return true
}
},
makeErrors({ key, message, cell, type }) {
this.cellErrors[key] = this.cellErrors[key] || []
this.cellErrors[key].push({ message, cell, type })
},
setCellStyle(cells, color = '#999') {
cells = cells || FlowHelper.getCells(this.graph).filter(c => c.flow)
this.graph.setCellStyles('strokeColor', color, cells)
}
}
}
代码中flow.js是个人精心整理的快速接入mxclient的落地工具函数,鉴于篇幅原因,不做展示。其应用场景可用于各种时序图、流程图、电路图、作业图场景,支持动效果,可独立二开。有需要或者疑问的小伙伴们欢迎唾弃~~