流程部分setup

This commit is contained in:
xiangheng
2024-07-05 12:47:27 +08:00
parent 97da6f37bb
commit 34c4eaaa5b
5 changed files with 431 additions and 480 deletions

View File

@@ -48,13 +48,13 @@
v-show="activeStep === 'formDesign'"
tabName="formDesign"
/>
<Diagram
ref="processDesign"
v-show="activeStep === 'processDesign'"
tabName="processDesign"
<FlowEdit
ref="flowEdit"
v-show="activeStep === 'flowEdit'"
tabName="flowEdit"
:fieldList="fieldList"
:conf="mockData.flowProcessData"
></Diagram>
></FlowEdit>
</div>
</el-dialog>
</template>
@@ -62,7 +62,7 @@
<script setup lang="ts">
import { ref, shallowRef, watch } from 'vue'
import XForm from './XForm/index.vue'
import Diagram from './flowEdit/Diagram.vue'
import FlowEdit from './flowEdit/index.vue'
import BasicSetting from './BasicSetting/index.vue'
import feedback from '@/utils/feedback'
@@ -89,7 +89,7 @@ const props = defineProps({
const basicSetting = shallowRef<InstanceType<typeof BasicSetting>>()
const formDesign = shallowRef<InstanceType<typeof XForm>>()
const processDesign = shallowRef<InstanceType<typeof Diagram>>()
const flowEdit = shallowRef<InstanceType<typeof FlowEdit>>()
const dialogVisible = ref(false)
const activeStep = ref('basicSetting')
const mockData = ref({
@@ -102,7 +102,7 @@ const fieldList = ref([])
const steps = [
{ label: '基础设置', key: 'basicSetting' },
{ label: '表单设计', key: 'formDesign' },
{ label: '流程设计', key: 'processDesign' }
{ label: '流程设计', key: 'flowEdit' }
]
watch(
() => dialogVisible.value,
@@ -151,7 +151,7 @@ function changeSteps(item) {
function publish() {
const p1 = basicSetting.value.getData()
const p2 = formDesign.value.getData()
const p3 = processDesign.value.getData()
const p3 = flowEdit.value.getData()
Promise.all([p1, p2, p3])
.then((res) => {
console.log('res', res)

View File

@@ -1,338 +0,0 @@
<template>
<div class="diagram">
<diagram-toolbar
v-if="lf"
class="diagram-toolbar"
:lf="lf"
:active-edges="activeEdges"
@saveGraph="$_saveGraph"
@importData="importData"
/>
<div class="diagram-main">
<diagram-sidebar class="diagram-sidebar" @dragInNode="$_dragInNode" />
<div ref="container" class="diagram-container">
<div class="diagram-wrapper">
<div ref="diagram" class="lf-diagram"></div>
</div>
</div>
</div>
<!-- 右侧属性面板 -->
<PropertyPanel ref="panelRef" @setProperties="$setProperties" />
</div>
</template>
<script>
import LogicFlow from '@logicflow/core'
import { SelectionSelect, Menu, BpmnElement } from '@logicflow/extension'
import '@logicflow/core/dist/style/index.css'
import '@logicflow/extension/lib/style/index.css'
import DiagramToolbar from './DiagramToolbar.vue'
import DiagramSidebar from './DiagramSidebar.vue'
import PropertyPanel from './PropertyPanel.vue'
import { registerCustomElement } from './node'
// import { Control } from "@logicflow/extension";
export default {
name: 'Diagram',
components: {
DiagramToolbar,
DiagramSidebar,
PropertyPanel
},
props: {
tabName: {
type: String,
default: ''
},
fieldList: {
type: Array,
default: () => {
return []
}
},
conf: {
type: Object,
default: () => {
return {}
}
}
},
data() {
return {
sidebarWidth: 200,
diagramWidth: 0,
diagramHeight: 0,
lf: null,
filename: '',
activeEdges: []
}
},
mounted() {
// const data = ''
this.filename = 'export.json'
// const d = window.sessionStorage.getItem(this.filename)
// if (d) {
// data = JSON.parse(d)
// }
this.initLogicFlow(this.conf)
},
methods: {
initLogicFlow(data) {
// 引入框选插件
LogicFlow.use(SelectionSelect)
LogicFlow.use(Menu)
LogicFlow.use(BpmnElement)
const lf = new LogicFlow({
container: this.$refs.diagram,
overlapMode: 1,
autoWrap: true,
metaKeyMultipleSelected: true,
keyboard: {
enabled: true
},
grid: {
size: 10,
type: 'dot'
}
})
lf.setTheme({
baseEdge: { strokeWidth: 1 },
baseNode: { strokeWidth: 1 },
nodeText: { overflowMode: 'autoWrap', lineHeight: 1.5 },
edgeText: { overflowMode: 'autoWrap', lineHeight: 1.5 }
})
// 注册自定义元素
registerCustomElement(lf)
lf.setDefaultEdgeType('pro-polyline')
lf.render(data)
this.lf = lf
this.lf.on('node:click', (e) => {
console.log('点击节点', e.data)
this.$refs.panelRef.open(e.data, this.fieldList)
})
// lf.renderRawData( )
},
$_dragInNode(type, text = '') {
this.lf.dnd.startDrag({
type,
text
})
},
// 获取可以进行设置的属性
$_getProperty() {
// this.lf.getProperties(node.id)
},
$setProperties(node, item) {
this.lf.setProperties(node.id, item)
},
$_setZIndex(node, type) {
this.lf.setElementZIndex(node.id, type)
},
importData(text) {
this.lf.renderRawData(text)
},
$_saveGraph() {
const data = this.lf.getGraphData()
this.download(this.filename, JSON.stringify(data))
},
download(filename, text) {
window.sessionStorage.setItem(filename, text)
const element = document.createElement('a')
element.setAttribute(
'href',
'data:text/plain;charset=utf-8,' + encodeURIComponent(text)
)
element.setAttribute('download', filename)
element.style.display = 'none'
document.body.appendChild(element)
element.click()
document.body.removeChild(element)
},
getData() {
/**
* 校验目标
* 1. 必须存在开始节点和结束节点
* 2. 连线方向正确
* 3. 多余的节点(不重要)
* 4. 所有分支必须结束节点
* 5. 检查审批节点设置情况
* 6. 一个节点可以有多个子网关,子网关只能通过一个,不能有多个普通子节点
*
*/
return new Promise((resolve, reject) => {
const data = this.lf.getGraphData()
const nodes = data.nodes
const edges = data.edges
let haveMoreChildNode = false
const sourceNodeIdSum = {} //节点id->[子节点id]
edges.forEach((edge) => {
const targetNode = nodes.find((item) => item.id == edge.targetNodeId)
if (sourceNodeIdSum[edge.sourceNodeId]) {
sourceNodeIdSum[edge.sourceNodeId].push(targetNode)
for (const n of sourceNodeIdSum[edge.sourceNodeId]) {
if (n.type != 'bpmn:exclusiveGateway') {
haveMoreChildNode = true
break
}
}
} else {
sourceNodeIdSum[edge.sourceNodeId] = [targetNode]
}
})
console.log('sourceNodeIdSum', sourceNodeIdSum)
if (haveMoreChildNode) {
// 如果都是网关就可以,等优化
return reject({
target: this.tabName,
message: '流程设计-一个节点只能有一个子节点,可以有多个网关'
})
}
// if (data.nodes.length != data.edges.length + 1) {
// return reject({
// message: "流程设计-节点之间必须连线,可以清理多余的节点",//不是很重要
// });
// }
// 检查开始节点和结束节点是否存在
const findStartNode = nodes.filter((item) => item.type == 'bpmn:startEvent')
const findEndNode = nodes.filter((item) => item.type == 'bpmn:endEvent')
if (findStartNode.length != 1 || findEndNode.length != 1) {
return reject({
target: this.tabName,
message: '流程设计-流程必须有且只有一个开始节点和结束节点'
})
}
function handel(arr, pid) {
const newArr = []
arr.forEach((node) => {
// console.log('sourceNodeIdSum', sourceNodeIdSum, node.id)
const newNode = {
id: node.id,
pid: pid,
label: node?.text?.value,
type: node.type,
fieldAuth: node?.properties?.fieldAuth,
gateway: node?.properties?.gateway,
userType: node?.properties?.userType || 0,
userId: node?.properties?.userId || 0,
deptId: node?.properties?.deptId || 0,
postId: node?.properties?.postId || 0
}
if (sourceNodeIdSum[node.id]) {
newNode.children = handel(sourceNodeIdSum[node.id], node.id)
}
newArr.push(newNode)
})
return newArr
}
const TreeNode = handel(findStartNode, 0)
// tree转list
function treeToList(tree) {
const arr = []
tree.forEach((item) => {
arr.push(item)
if (item.children) {
arr.push(...treeToList(item.children))
}
})
return arr
}
console.log('TreeNode', TreeNode)
console.log('treeToList', treeToList(TreeNode))
// 检查连线方向是否正确;
resolve({ formData: data, treeToList: treeToList(TreeNode) })
})
}
}
}
</script>
<style lang="scss">
.diagram {
width: 100%;
height: 100%;
position: relative;
}
.diagram-toolbar {
position: absolute;
top: 10px;
right: 20px;
height: 40px;
padding: 0 10px;
/* width: 310px; */
display: flex;
align-items: center;
/* border-bottom: 1px solid #e5e5e5; */
z-index: 10;
background: #fff;
box-shadow: 0px 0px 4px rgba($color: #000000, $alpha: 0.5);
}
.diagram-main {
position: relative;
width: 100%;
height: 100%;
overflow: hidden;
.diagram-sidebar {
position: absolute;
left: 20px;
z-index: 10;
top: 50px;
width: 60px;
background: #fff;
box-shadow: 0px 0px 4px rgba($color: #000000, $alpha: 0.5);
}
.diagram-container {
height: 100%;
.diagram-wrapper {
box-sizing: border-box;
width: 100%;
height: 100%;
.lf-diagram {
// box-shadow: 0px 0px 4px #838284;
width: 100%;
height: 100%;
}
}
}
/* 由于背景图和gird不对齐需要css处理一下 */
.diagram-container :v-deep .lf-background {
left: -9px;
}
}
::-webkit-scrollbar {
width: 9px;
height: 9px;
background: white;
border-left: 1px solid #e8e8e8;
}
::-webkit-scrollbar-thumb {
border-width: 1px;
border-style: solid;
border-color: #fff;
border-radius: 6px;
background: #c9c9c9;
}
::-webkit-scrollbar-thumb:hover {
background: #b5b5b5;
}
</style>

View File

@@ -1,133 +0,0 @@
<template>
<el-drawer
v-model="drawerVisible"
size="600px"
:title="'节点:' + node?.text?.value"
@close="close"
>
<!-- fieldList:{{ fieldList }}
<div>properties:{{ properties }}</div> -->
<!-- 开始节点 -->
<!-- {{ node }} -->
<div v-if="node.type == 'bpmn:startEvent'">
开始节点
<FieldAuth :node="node" :properties="properties" :fieldList="fieldList"></FieldAuth>
</div>
<div v-if="node.type == 'bpmn:userTask'">
<UserTask :node="node" :properties="properties" :fieldList="fieldList"></UserTask>
<FieldAuth :node="node" :properties="properties" :fieldList="fieldList"></FieldAuth>
</div>
<div v-if="node.type == 'bpmn:serviceTask'">
<div>系统任务</div>
<div>抄送</div>
<div>发送邮件</div>
<div>发送短信</div>
<div>发送站内消息</div>
<div>数据入库</div>
</div>
<div v-if="node.type == 'bpmn:exclusiveGateway'">
<Gateway :node="node" :properties="properties" :fieldList="fieldList"></Gateway>
</div>
<div v-if="node.type == 'bpmn:endEvent'">结束</div>
</el-drawer>
</template>
<script>
import UserTask from './PropertyPanel/UserTask.vue'
import FieldAuth from './PropertyPanel/FieldAuth.vue'
import Gateway from './PropertyPanel/Gateway.vue'
export default {
name: 'PropertyPanel',
props: {},
components: {
UserTask,
FieldAuth,
Gateway
},
data() {
return {
drawerVisible: false,
node: {},
properties: {
userType: '',
userId: '', //审批人id
deptId: '', //审批部门id
postId: '', //岗位id
fieldAuth: {}, // 字段权限
gateway: [] //网关条件列表
},
/**
* 表单列表
* [{
* id: 1,
* label: '表单1',
* auth: 1,
* }]
*/
fieldList: []
}
},
methods: {
open(node, fieldList) {
this.node = node
this.properties.userType = node?.properties?.userType || ''
this.properties.userId = node?.properties?.userId || ''
this.properties.deptId = node?.properties?.deptId || ''
this.properties.postId = node?.properties?.postId || ''
this.properties.gateway = node?.properties?.gateway || []
this.properties.fieldAuth = node?.properties?.fieldAuth
? { ...node?.properties?.fieldAuth }
: {}
this.fieldList = fieldList.map((item) => {
let auth = 1
const formId = item?.field?.id
if (node?.properties?.fieldAuth?.[formId]) {
auth = node.properties.fieldAuth[formId]
}
return {
id: formId,
label: item?.field?.options?.label,
auth: auth
}
})
this.drawerVisible = true
},
close() {
const fieldAuth = {}
this.fieldList.forEach((item) => {
fieldAuth[item.id] = item.auth
})
this.$emit('setProperties', this.node, { ...this.properties })
// this.setProperties('fieldAuth', {
// ...fieldAuth
// })
// this.setProperties('userType', this.properties.userType)
// this.setProperties('userId', this.properties.userId)
// this.setProperties('deptId', this.properties.deptId)
// this.setProperties('postId', this.properties.postId)
// this.setProperties('gateway', this.properties.gateway)
},
setProperties(key, val) {
this.$emit('setProperties', this.node, {
[key]: val
})
}
}
}
</script>
<style scoped></style>

View File

@@ -0,0 +1,99 @@
<template>
<el-drawer
v-model="drawerVisible"
size="600px"
:title="'节点:' + node?.text?.value"
@close="close"
>
<!-- fieldList:{{ fieldList }}
<div>properties:{{ properties }}</div> -->
<!-- 开始节点 -->
<!-- {{ node }} -->
<div v-if="node.type == 'bpmn:startEvent'">
开始节点
<FieldAuth :node="node" :properties="properties" :fieldList="fieldList"></FieldAuth>
</div>
<div v-if="node.type == 'bpmn:userTask'">
<UserTask :node="node" :properties="properties" :fieldList="fieldList"></UserTask>
<FieldAuth :node="node" :properties="properties" :fieldList="fieldList"></FieldAuth>
</div>
<div v-if="node.type == 'bpmn:serviceTask'">
<div>系统任务</div>
<div>抄送</div>
<div>发送邮件</div>
<div>发送短信</div>
<div>发送站内消息</div>
<div>数据入库</div>
</div>
<div v-if="node.type == 'bpmn:exclusiveGateway'">
<Gateway :node="node" :properties="properties" :fieldList="fieldList"></Gateway>
</div>
<div v-if="node.type == 'bpmn:endEvent'">结束</div>
</el-drawer>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import UserTask from './UserTask.vue'
import FieldAuth from './FieldAuth.vue'
import Gateway from './Gateway.vue'
const emit = defineEmits(['setProperties'])
const drawerVisible = ref(false)
const node = ref<{
type?: string
text?: {
value?: string
}
}>({})
const properties = reactive({
userType: '',
userId: '',
deptId: '',
postId: '',
fieldAuth: {},
gateway: []
})
const fieldList = ref([])
const open = (newNode, newFieldList) => {
node.value = newNode
properties.userType = newNode?.properties?.userType || ''
properties.userId = newNode?.properties?.userId || ''
properties.deptId = newNode?.properties?.deptId || ''
properties.postId = newNode?.properties?.postId || ''
properties.gateway = newNode?.properties?.gateway || []
properties.fieldAuth = newNode?.properties?.fieldAuth
? { ...newNode?.properties?.fieldAuth }
: {}
fieldList.value = newFieldList.map((item) => ({
id: item?.field?.id,
label: item?.field?.options?.label,
auth: newNode?.properties?.fieldAuth?.[item?.field?.id] || 1
}))
drawerVisible.value = true
}
const close = () => {
const fieldAuth = {}
fieldList.value.forEach((item) => {
fieldAuth[item.id] = item.auth
})
emit('setProperties', node.value, { ...properties })
drawerVisible.value = false
}
defineExpose({
open
})
</script>
<style scoped></style>

View File

@@ -0,0 +1,323 @@
<template>
<div class="diagram">
<diagram-toolbar
v-if="lf"
class="diagram-toolbar"
:lf="lf"
:active-edges="activeEdges"
@saveGraph="saveGraph"
@importData="importData"
/>
<div class="diagram-main">
<diagram-sidebar class="diagram-sidebar" @dragInNode="dragInNode" />
<div ref="container" class="diagram-container">
<div class="diagram-wrapper">
<div ref="diagramRef" class="lf-diagram"></div>
</div>
</div>
</div>
<!-- Right-side property panel -->
<PropertyPanel ref="PropertyPanelRef" @setProperties="setProperties" />
</div>
</template>
<script setup lang="ts">
// Importing necessary functions and components
import { ref, onMounted } from 'vue'
import LogicFlow from '@logicflow/core'
import { SelectionSelect, Menu, BpmnElement } from '@logicflow/extension'
import '@logicflow/core/dist/style/index.css'
import '@logicflow/extension/lib/style/index.css'
import DiagramToolbar from './DiagramToolbar.vue'
import DiagramSidebar from './DiagramSidebar.vue'
import PropertyPanel from './PropertyPanel/PropertyPanel.vue'
import { registerCustomElement } from './node'
defineOptions({
name: 'flowEdit'
})
// Define component props
const props = defineProps({
tabName: {
type: String,
default: ''
},
fieldList: {
type: Array,
default: () => []
},
conf: {
type: Object,
default: () => ({})
}
})
// Define refs for reactive data and component references
const lf = ref(null) // Reference to LogicFlow instance
const activeEdges = ref([]) // Reactive array for active edges
const diagramRef = ref(null) // Reference to the diagram container
const PropertyPanelRef = ref(null) // Reference to the PropertyPanel component
// Lifecycle hook to initialize LogicFlow when the component is mounted
onMounted(() => {
initLogicFlow(props.conf)
})
// Function to initialize LogicFlow
function initLogicFlow(data) {
// 引入框选插件
LogicFlow.use(SelectionSelect)
LogicFlow.use(Menu)
LogicFlow.use(BpmnElement)
// Creating a new LogicFlow instance
const logicFlowInstance = new LogicFlow({
container: diagramRef.value, // Setting the container where LogicFlow will be rendered
overlapMode: 1,
autoWrap: true,
metaKeyMultipleSelected: true,
keyboard: {
enabled: true
},
grid: {
size: 10,
type: 'dot'
}
})
// Setting theme for LogicFlow
logicFlowInstance.setTheme({
baseEdge: { strokeWidth: 1 },
baseNode: { strokeWidth: 1 },
nodeText: { overflowMode: 'autoWrap', lineHeight: 1.5 },
edgeText: { overflowMode: 'autoWrap', lineHeight: 1.5 }
})
// Registering custom elements for LogicFlow
registerCustomElement(logicFlowInstance)
// Setting default edge type and rendering initial data
logicFlowInstance.setDefaultEdgeType('pro-polyline')
logicFlowInstance.render(data)
// Assigning the LogicFlow instance to the 'lf' ref
lf.value = logicFlowInstance
// Event listener for node clicks
lf.value.on('node:click', (e) => {
console.log('Click on node', e.data)
PropertyPanelRef.value.open(e.data, props.fieldList)
})
}
// Function to handle dragging nodes into the diagram
function dragInNode(type, text = '') {
lf.value.dnd.startDrag({
type,
text
})
}
// Function to set properties of a node
function setProperties(node, item) {
lf.value.setProperties(node.id, item)
}
// function setZIndex(node, type) {
// lf.value.setElementZIndex(node.id, type)
// }
// Function to import data into the LogicFlow instance
function importData(text) {
lf.value.renderRawData(text)
}
// Function to save the graph data
function saveGraph() {
const data = lf.value.getGraphData()
download('export.json', JSON.stringify(data))
}
// Function to download the graph data as a file
function download(filename, text) {
window.sessionStorage.setItem(filename, text)
const element = document.createElement('a')
element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text))
element.setAttribute('download', filename)
element.style.display = 'none'
document.body.appendChild(element)
element.click()
document.body.removeChild(element)
}
async function getData() {
/**
* 校验目标
* 1. 必须存在开始节点和结束节点
* 2. 连线方向正确
* 3. 多余的节点(不重要)
* 4. 所有分支必须结束节点
* 5. 检查审批节点设置情况
* 6. 一个节点可以有多个子网关,子网关只能通过一个,不能有多个普通子节点
*
*/
return new Promise<{
formData: any
treeToList: any
}>((resolve, reject) => {
const data = lf.value.getGraphData()
const nodes = data.nodes
const edges = data.edges
let haveMoreChildNode = false
const sourceNodeIdSum = {} // Node ID -> child nodes mapping
edges.forEach((edge) => {
const targetNode = nodes.find((item) => item.id === edge.targetNodeId)
if (sourceNodeIdSum[edge.sourceNodeId]) {
sourceNodeIdSum[edge.sourceNodeId].push(targetNode)
for (const n of sourceNodeIdSum[edge.sourceNodeId]) {
if (n.type !== 'bpmn:exclusiveGateway') {
haveMoreChildNode = true
break
}
}
} else {
sourceNodeIdSum[edge.sourceNodeId] = [targetNode]
}
})
if (haveMoreChildNode) {
return reject({
target: props.tabName,
message: '流程设计-一个节点只能有一个子节点,可以有多个网关'
})
}
// 检查开始节点和结束节点是否存在
const findStartNode = nodes.filter((item) => item.type === 'bpmn:startEvent')
const findEndNode = nodes.filter((item) => item.type === 'bpmn:endEvent')
if (findStartNode.length !== 1 || findEndNode.length !== 1) {
return reject({
target: props.tabName,
message: '流程设计-流程必须有且只有一个开始节点和结束节点'
})
}
const TreeNode = handel(nodes, 0)
function handel(arr, pid) {
const newArr = []
arr.forEach((node) => {
const newNode = {
id: node.id,
pid: pid,
label: node?.text?.value,
type: node.type,
fieldAuth: node?.properties?.fieldAuth,
gateway: node?.properties?.gateway,
userType: node?.properties?.userType || 0,
userId: node?.properties?.userId || 0,
deptId: node?.properties?.deptId || 0,
postId: node?.properties?.postId || 0,
children: null
}
if (sourceNodeIdSum[node.id]) {
newNode.children = handel(sourceNodeIdSum[node.id], node.id)
}
newArr.push(newNode)
})
return newArr
}
function treeToList(tree) {
const arr = []
tree.forEach((item) => {
arr.push(item)
if (item.children) {
arr.push(...treeToList(item.children))
}
})
return arr
}
// 检查连线方向是否正确;
resolve({ formData: data, treeToList: treeToList(TreeNode) })
})
}
defineExpose({
getData
})
</script>
<style lang="scss">
.diagram {
width: 100%;
height: 100%;
position: relative;
}
.diagram-toolbar {
position: absolute;
top: 10px;
right: 20px;
height: 40px;
padding: 0 10px;
/* width: 310px; */
display: flex;
align-items: center;
/* border-bottom: 1px solid #e5e5e5; */
z-index: 10;
background: #fff;
box-shadow: 0px 0px 4px rgba($color: #000000, $alpha: 0.5);
}
.diagram-main {
position: relative;
width: 100%;
height: 100%;
overflow: hidden;
.diagram-sidebar {
position: absolute;
left: 20px;
z-index: 10;
top: 50px;
width: 60px;
background: #fff;
box-shadow: 0px 0px 4px rgba($color: #000000, $alpha: 0.5);
}
.diagram-container {
height: 100%;
.diagram-wrapper {
box-sizing: border-box;
width: 100%;
height: 100%;
.lf-diagram {
// box-shadow: 0px 0px 4px #838284;
width: 100%;
height: 100%;
}
}
}
/* 由于背景图和gird不对齐需要css处理一下 */
.diagram-container :v-deep .lf-background {
left: -9px;
}
}
::-webkit-scrollbar {
width: 9px;
height: 9px;
background: white;
border-left: 1px solid #e8e8e8;
}
::-webkit-scrollbar-thumb {
border-width: 1px;
border-style: solid;
border-color: #fff;
border-radius: 6px;
background: #c9c9c9;
}
::-webkit-scrollbar-thumb:hover {
background: #b5b5b5;
}
</style>