You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

471 lines
19 KiB

<template>
<el-container v-loading="loading"
element-loading-text="Loading..."
element-loading-spinner="el-icon-loading"
element-loading-background="rgba(0, 0, 0, 0.8)"
>
<el-container direction="vertical">
<flowable-header :modeler="bpmnModeler"
:can-redo="canRedo"
:can-undo="canUndo"
:default-zoom="defaultZoom"
@restart="restart"
@handleExportSvg="handleExportSvg"
@handleExportBpmn="handleExportBpmn"
@handleProcessZoomOut="handleProcessZoomOut"
@handleProcessZoomIn="handleProcessZoomIn"
@handleProcessReZoom="handleProcessReZoom"
@importDiagram="importDiagram"
@save="handleSubmitModel"
/>
<el-main class="bpmn-viewer-container">
<div ref="bpmnViewer" class="canvas"/>
</el-main>
</el-container>
<flowable-panel v-if="bpmnModeler" :modeler="bpmnModeler" :width="panelWidth"/>
</el-container>
</template>
<script>
import BpmnModeler from 'entfrm-bpmn/lib/Modeler'
import customTranslate from '@/common/translate/customTranslate'
import templateXml from '@/common/template'
import FlowableHeader from '@components/Header'
import flowableDescriptor from '@/common/config/flowable'
import FlowablePanel from '@components/Panel'
import lodash from 'lodash-es'
import { validateNull } from '@utils'
import { activityExtensionDataSave, activityExtensionPropertySave, deployModel, addModel, editModel } from '@/api/flowable-designer'
export default {
name: 'FlowableDesigner',
components: {
FlowableHeader,
FlowablePanel
},
props: {
panelWidth: {
type: Number,
default: 500
}
},
data () {
return {
loading: false,
modelData: {},
bpmnModeler: undefined,
canRedo: false,
canUndo: false,
defaultZoom: 1,
// todo: 目前配合微应用乾坤使用兼容,重构vue3删除
createMessage: this.$useMessage.createMessage,
}
},
mounted () {
this.init()
},
methods: {
/** 初始化加载BpmnModeler */
init () {
const _moddleExtensions = this.getModdleExtensions()
const _additionalModules = this.getAdditionalModules()
this.canvas = this.$refs.bpmnViewer
this.bpmnModeler = new BpmnModeler({
container: this.canvas,
keyboard: { bindTo: document },
additionalModules: _additionalModules,
moddleExtensions: _moddleExtensions
})
window.bpmn = this.bpmnModeler
this.importDiagram(templateXml.initTemplate())
this.initModelListeners()
},
/** 初始化监听事件 */
initModelListeners () {
const EventBus = this.bpmnModeler.get('eventBus')
EventBus.on('commandStack.changed', () => {
try {
// 设置历史记录目前状态
this.canRedo = this.bpmnModeler.get('commandStack').canRedo()
this.canUndo = this.bpmnModeler.get('commandStack').canUndo()
} catch (e) {
console.error(`[Process Designer Warn]: ${e.message || e}`)
}
})
EventBus.on('canvas.viewbox.changed', ({ viewbox }) => {
// 设置目前画布的大小
const { scale } = viewbox
this.defaultZoom = Math.ceil(scale * 100) / 100
})
},
/** 获取modeler附加模板(覆盖modeler内部已经存在的模块,也称重新附加模块) */
getAdditionalModules () {
return [{ translate: ['value', customTranslate] }]
},
/** 获取modeler扩展模板(新加入到modeler中的扩展模块,可以通过ioc调用,例如this.bpmnModeler.get(...)) */
getModdleExtensions () {
return { flowable: flowableDescriptor }
},
/** 导入bpmnXml文件 */
importDiagram (xml) {
this.bpmnModeler.importXML(xml).catch(() => this.createMessage.error('打开模型出错,请确认该模型符合Bpmn2.0规范'))
},
/** 创建新的流程图 */
restart () {
this.importDiagram(templateXml.initTemplate())
},
/** 导出diagram.bpmn文件 */
handleExportBpmn () {
this.bpmnModeler.saveXML({
format: true
}).then(result => {
const { filename, href } = this.setEncoded('BPMN', result.xml)
if (href && filename) {
const a = document.createElement('a')
a.download = filename // 指定下载的文件名
a.href = href // URL对象
a.click() // 模拟点击
URL.revokeObjectURL(a.href) // 释放URL 对象
}
})
},
/** 导出diagram.svg文件 */
handleExportSvg () {
this.bpmnModeler.saveSVG().then(result => {
const { filename, href } = this.setEncoded('SVG', result.svg)
if (href && filename) {
const a = document.createElement('a')
a.download = filename
a.href = href
a.click()
URL.revokeObjectURL(a.href)
}
})
},
/** 设置导出格式编码 */
setEncoded (type, data) {
const encodedData = encodeURIComponent(data)
if (data) {
if (type === 'XML') {
return {
filename: 'diagram.bpmn20.xml',
href: 'data:application/bpmn20-xml;charset=UTF-8,' + encodedData,
data: data
}
}
if (type === 'BPMN') {
return {
filename: 'diagram.bpmn',
href: 'data:application/bpmn20-xml;charset=UTF-8,' + encodedData,
data: data
}
}
if (type === 'SVG') {
return {
filename: 'diagram.svg',
href: 'data:application/text/xml;charset=UTF-8,' + encodedData,
data: data
}
}
}
},
/** 处理画布缩小 */
handleProcessZoomOut (zoomStep) {
const newZoom = Math.ceil(this.defaultZoom * 100 - zoomStep * 100) / 100
this.defaultZoom = newZoom.toFixed(1)
this.bpmnModeler.get('canvas').zoom(this.defaultZoom)
},
/** 处理画布放大 */
handleProcessZoomIn (zoomStep) {
const newZoom = Math.ceil(this.defaultZoom * 100 + zoomStep * 100) / 100
this.defaultZoom = newZoom.toFixed(1)
this.bpmnModeler.get('canvas').zoom(this.defaultZoom)
},
/** 处理画布重置 */
handleProcessReZoom () {
this.defaultZoom = 1
this.bpmnModeler.get('canvas').zoom('fit-viewport', 'auto')
},
/** 设置bpmnXml */
setBpmnXml (xml) {
if (xml) {
this.importDiagram(xml)
} else {
this.restart()
}
},
/** 设置模型数据 */
setModelData (data) {
this.modelData = data || {}
},
/** 获取模型数据 */
getModelData () {
return this.modelData
},
/** 模型数据重置 */
reset () {
this.modelData = {
id: undefined,
key: undefined,
name: undefined,
description: undefined,
createdBy: undefined,
lastUpdatedBy: undefined,
lastUpdated: undefined,
latestVersion: undefined,
version: undefined,
modelType: undefined
}
},
/** 处理模型流程交互转换 */
handleModelProcess (opts) {
const options = {
flowElements: opts.flowElements || [],
process: opts.process || {},
activityExtensionProperty: opts.activityExtensionProperty || [],
activityExtensionData: opts.activityExtensionData || [],
validateErrorData: opts.validateErrorData || []
}
for (let i = 0; i < options.flowElements.length; ++i) {
const bpmnElement = options.flowElements[i]
const bpmnElementParent = lodash.get(bpmnElement, '$parent', {})
const extensionElements = lodash.get(bpmnElement, 'extensionElements', {})
const conditionType = lodash.get(bpmnElement, '$attrs.flowable:conditionType')
const bpmnType = lodash.get(bpmnElement, '$type', '')
const bpmnElementKeys = Object.keys(bpmnElement)
// ----------------扩展活动属性存储------------------
if (bpmnElementKeys.includes('formType')) {
options.activityExtensionProperty.push({
key: 'formType',
processDefId: options.process.id,
activityDefId: bpmnElement.id,
value: bpmnElement.formType
})
}
if (bpmnElementKeys.includes('formReadOnly')) {
options.activityExtensionProperty.push({
key: 'formReadOnly',
processDefId: options.process.id,
activityDefId: bpmnElement.id,
value: bpmnElement.formReadOnly
})
}
if (conditionType !== undefined) {
options.activityExtensionProperty.push({
key: 'conditionType',
processDefId: options.process.id,
activityDefId: bpmnElement.id,
value: conditionType
})
}
// ---------------扩展活动属性数据存储----------------
const values = lodash.get(extensionElements, 'values', [])
// ------------------用户分配人---------------------
const assignee = values.filter(element => element.$type === 'flowable:Assignee')
// ------------------常用按钮-----------------------
const button = values.filter(element => element.$type === 'flowable:Button')
// ------------------流转条件-----------------------
const condition = values.filter(element => element.$type === 'flowable:Condition')
if ((assignee.length + button.length + condition.length) > 0) {
options.activityExtensionData.push({
activityDefId: bpmnElement.id,
processDefId: options.process.id,
workflowAssigneeList: assignee,
workflowButtonList: button,
workflowConditionList: condition
})
}
// ------------------制定校验规则--------------------
const formKey = lodash.get(bpmnElement, 'formKey')
switch (bpmnType) {
case 'bpmn:StartEvent':
if (validateNull(formKey) && bpmnElementParent.$type !== 'bpmn:SubProcess') {
options.validateErrorData.push(`<p>节点【${bpmnElement.name || bpmnElement.id}】没有配置表单。</p>`)
}
break
case 'bpmn:UserTask':
if (!assignee.length) {
options.validateErrorData.push(`<p>节点【${bpmnElement.name || bpmnElement.id}】没有指定办理人。</p>`)
}
if (!button.length) {
options.validateErrorData.push(`<p>节点【${bpmnElement.name || bpmnElement.id}】没有配置按钮。</p>`)
}
if (validateNull(formKey)) {
options.validateErrorData.push(`<p>节点【${bpmnElement.name || bpmnElement.id}】没有配置表单。</p>`)
}
break
}
if (bpmnElement.$type === 'bpmn:SubProcess') {
const flowElements = lodash.get(bpmnElement, 'flowElements', [])
this.handleModelProcess({
flowElements,
process: options.process,
activityExtensionProperty: options.activityExtensionProperty,
activityExtensionData: options.activityExtensionData,
validateErrorData: options.validateErrorData || []
})
}
}
},
/** 处理模型提交 */
handleSubmitModel (code) {
new Promise((resolve, reject) => {
// fixme:第一层处理流程模型空值相关校验
if (this.bpmnModeler) {
const definitions = this.bpmnModeler.getDefinitions()
const rootElements = lodash.get(definitions, 'rootElements', [])
// 检测画布流程名称不能为空
const canvasRootElement = this.bpmnModeler.get('canvas').getRootElement()
const canvasProcess = lodash.get(canvasRootElement, 'businessObject', {})
if (validateNull(canvasProcess.name)) {
return reject('检测到主体画布流程名称为空,请检查,这个为必填项!')
}
// 检测协助池中的一些非空校验
const collaboration = rootElements.find(item => item.$type === 'bpmn:Collaboration')
if (collaboration) {
const participants = lodash.get(collaboration, 'participants', [])
for (let i = 0; i < participants.length; ++i) {
const participant = participants[i]
const process = lodash.get(participant, 'processRef', {})
// 检测池中的泳道不能为空
const lanes = lodash.get(process, 'laneSets[0].lanes', [])
if (lanes.length === 0) {
return reject(`池子【${participant.name || participant.id}】没有配置泳道,请检测!`)
}
// 检测池中的bpmn元素不能为空
const flowElements = lodash.get(process, 'flowElements', [])
if (flowElements.length === 0) {
return reject(`池子【${participant.name || participant.id}】没有配置bpmn元素,请设计bpmn流程!`)
}
}
// 检测正常单流程的一些非空校验
} else {
const process = lodash.get(rootElements, '[0]', {})
const flowElements = lodash.get(process, 'flowElements', [])
if (flowElements.length === 0) {
return reject(`流程【${process.name || process.id}】没有配置bpmn元素,请设计bpmn流程!`)
}
}
// 处理一些需要初始化的值
this.loading = true
resolve({ rootElements, canvasProcess })
} else reject('bpmn建模对象不存在,请检查!')
}).then(({ rootElements, canvasProcess }) => {
// fixme:第二层处理流程模型新增
const processRelationIds = []
const collaboration = rootElements.find(item => item.$type === 'bpmn:Collaboration')
if (collaboration) {
const participants = lodash.get(collaboration, 'participants', [])
for (let i = 0; i < participants.length; ++i) {
const participant = participants[i]
const process = lodash.get(participant, 'processRef', {})
processRelationIds.push(String(process.id).replaceAll(',', ''))
}
} else {
const process = lodash.get(rootElements, '[0]', {})
processRelationIds.push(String(process.id).replaceAll(',', ''))
}
return new Promise((resolve, reject) => {
if (this.modelData.id === undefined) {
addModel({
key: processRelationIds.join(),
name: canvasProcess.name,
modelType: 0,
description: ''
}).then(response => {
this.modelData = response
resolve({ rootElements, canvasProcess, processRelationIds })
}).catch(() => {
reject('保存流程模型失败!')
})
} else resolve({ rootElements, canvasProcess, processRelationIds })
})
}).then(({ rootElements, canvasProcess, processRelationIds }) => {
// fixme:第三层处理bpmnXml
return this.bpmnModeler.saveXML({
format: true
}).then(result => {
return Promise.resolve({ rootElements, canvasProcess, processRelationIds, result })
})
}).then(({ rootElements, canvasProcess, processRelationIds, result }) => {
// fixme:第四层处理流程模型修改
return editModel(this.modelData.id, {
key: processRelationIds.join(),
name: canvasProcess.name,
json_xml: result.xml,
// 这个字段为后期版本冲突功能做准备
newversion: false,
description: '',
comment: '',
lastUpdated: this.modelData.lastUpdated
}).then(response => {
this.modelData = response
const chain = []
const activityExtensionProperty = []
const activityExtensionData = []
let validateErrorData = []
// 查找并存储扩展属性跟扩展数据,方便后台拿取做对应功能需求
const collaboration = rootElements.find(item => item.$type === 'bpmn:Collaboration')
// 处理协助池
if (collaboration) {
const participants = lodash.get(collaboration, 'participants', [])
for (let i = 0; i < participants.length; ++i) {
const participant = participants[i]
const process = lodash.get(participant, 'processRef', {})
const flowElements = lodash.get(process, 'flowElements', [])
const tempValidateErrorData = []
this.handleModelProcess({
flowElements,
process,
activityExtensionProperty,
activityExtensionData,
validateErrorData: tempValidateErrorData
})
if (tempValidateErrorData.length > 0) {
validateErrorData = validateErrorData.concat(
[`<p>池子【${participant.name || participant.id}】:</p>`], tempValidateErrorData)
}
}
// 处理正常单流程
} else {
const process = lodash.get(rootElements, '[0]', {})
const flowElements = lodash.get(process, 'flowElements', [])
const tempValidateErrorData = []
this.handleModelProcess({
flowElements,
process,
activityExtensionProperty,
activityExtensionData,
validateErrorData: tempValidateErrorData
})
if (tempValidateErrorData.length > 0) {
validateErrorData = validateErrorData.concat(
[`<p>流程【${process.name || process.id}】:</p>`], tempValidateErrorData)
}
}
chain.push(validateErrorData)
code === 1 && chain.push(deployModel({ id: response.id, category: '未分类' }))
chain.push(activityExtensionPropertySave(activityExtensionProperty))
chain.push(activityExtensionDataSave(activityExtensionData))
return Promise.all(chain)
}).catch(() => {
return Promise.reject('保存流程模型失败!')
})
}).then(results => {
results[0].length && this.$notify({
title: '提示',
message: results[0].join(''),
type: 'warning',
dangerouslyUseHTMLString: true
})
this.createMessage.success(results[1].data || '保存流程模型成功!')
this.$emit('refresh')
this.loading = false
}).catch(err => {
this.createMessage.error(err)
this.loading = false
})
}
}
}
</script>