仿钉钉设计流程:增加并行分支

This commit is contained in:
jason 2024-04-07 15:53:21 +08:00
parent 556cac8138
commit 32d2eb11f0
8 changed files with 300 additions and 95 deletions

View File

@ -6,7 +6,7 @@
<div class="add-node-popover-body"> <div class="add-node-popover-body">
<a class="add-node-popover-item approver" @click="addType(1)"> <a class="add-node-popover-item approver" @click="addType(1)">
<div class="item-wrapper"> <div class="item-wrapper">
<span class="iconfont"></span> <span class="iconfont icon-approve"></span>
</div> </div>
<p>审批人</p> <p>审批人</p>
</a> </a>
@ -26,20 +26,26 @@
--> -->
<a class="add-node-popover-item notifier" @click="addType(2)"> <a class="add-node-popover-item notifier" @click="addType(2)">
<div class="item-wrapper"> <div class="item-wrapper">
<span class="iconfont"></span> <span class="iconfont icon-copy"></span>
</div> </div>
<p>抄送人</p> <p>抄送人</p>
</a> </a>
<a class="add-node-popover-item condition" @click="addType(4)"> <a class="add-node-popover-item condition" @click="addType(4)">
<div class="item-wrapper"> <div class="item-wrapper">
<span class="iconfont"></span> <span class="iconfont icon-exclusive"></span>
</div> </div>
<p>条件分支</p> <p>条件分支</p>
</a> </a>
<a class="add-node-popover-item condition" @click="addType(5)">
<div class="item-wrapper">
<span class="iconfont icon-parallel"></span>
</div>
<p>并行分支</p>
</a>
</div> </div>
<template #reference> <template #reference>
<button class="btn" type="button"> <button class="btn" type="button" v-if="showAddButton">
<span class="iconfont"></span> <span><Icon icon="ep:plus" class="addIcon" :size="14" /></span>
</button> </button>
</template> </template>
</el-popover> </el-popover>
@ -47,66 +53,60 @@
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { NodeType } from './consts'
import { ref } from 'vue' import { ref } from 'vue'
import { generateUUID } from '@/utils' import { generateUUID } from '@/utils'
let props = defineProps({ let props = defineProps({
childNodeP: { childNodeP: {
type: Object, type: Object,
default: () => ({}) default: () => ({})
},
showAddButton:{
type:Boolean,
default:true
} }
}) })
let emits = defineEmits(['update:childNodeP']) let emits = defineEmits(['update:childNodeP'])
let visible = ref(false) let visible = ref(false)
const addType = (type) => { const addType = (type: number) => {
visible.value = false visible.value = false
if (type != 4) { //
var data if (type === NodeType.APPROVE_USER_NODE) {
if (type == 1) { const data = {
// data = { name: '审核人',
// name: '', error: true,
// error: true, type: 1,
// type: 1, //
// settype: 1, attributes: {
// selectMode: 0, approveMethod: undefined,
// selectRange: 0, candidateStrategy: undefined,
// directorLevel: 1, candidateParam: undefined
// examineMode: 1, },
// noHanderAction: 1, childNode: props.childNodeP
// examineEndDirectorLevel: 0,
// childNode: props.childNodeP,
// nodeUserList: []
// }
data = {
name: '审核人',
error: true,
type: 1,
//
attributes : {
approveMethod : undefined,
candidateStrategy: undefined,
candidateParam: undefined
},
childNode: props.childNodeP
}
} else if (type == 2) {
data = {
name: '抄送人',
type: 2,
error: true,
//
attributes : {
candidateStrategy: undefined,
candidateParam: undefined
},
childNode: props.childNodeP
}
} }
emits('update:childNodeP', data) emits('update:childNodeP', data)
} else { }
emits('update:childNodeP', { //
name: '路由', if (type === NodeType.CC_USER_NODE) {
const data = {
name: '抄送人',
type: 2,
error: true,
//
attributes: {
candidateStrategy: undefined,
candidateParam: undefined
},
childNode: props.childNodeP
}
emits('update:childNodeP', data)
}
//
if (type === NodeType.EXCLUSIVE_NODE) {
const data = {
name: '条件分支',
type: 4, type: 4,
id : 'GateWay_'+ generateUUID(), id: 'GateWay_' + generateUUID(),
childNode: props.childNodeP, childNode: props.childNodeP,
conditionNodes: [ conditionNodes: [
{ {
@ -125,8 +125,92 @@ const addType = (type) => {
childNode: null childNode: null
} }
] ]
}) }
emits('update:childNodeP', data)
} }
// fork
if (type === NodeType.PARALLEL_NODE_FORK) {
const data = {
name: '并行分支_FORK',
type: 5,
id: 'GateWay_' + generateUUID(),
conditionNodes: [
{
name: '并行1',
error: true,
type: 3,
childNode: null
},
{
name: '并行2',
type: 3,
childNode: null
}
],
childNode: {
id: 'GateWay_' + generateUUID(),
name: '并行分支_JOIN',
type: 6,
error: true,
childNode: props.childNodeP,
}
}
emits('update:childNodeP', data)
}
// if (type != 4) {
// var data
// if (type == 1) {
// data = {
// name: '',
// error: true,
// type: 1,
// //
// attributes : {
// approveMethod : undefined,
// candidateStrategy: undefined,
// candidateParam: undefined
// },
// childNode: props.childNodeP
// }
// } else if (type == 2) {
// data = {
// name: '',
// type: 2,
// error: true,
// //
// attributes : {
// candidateStrategy: undefined,
// candidateParam: undefined
// },
// childNode: props.childNodeP
// }
// }
// emits('update:childNodeP', data)
// } else {
// emits('update:childNodeP', {
// name: '',
// type: 4,
// id : 'GateWay_'+ generateUUID(),
// childNode: props.childNodeP,
// conditionNodes: [
// {
// name: '1',
// error: true,
// type: 3,
// priorityLevel: 1,
// conditionList: [],
// childNode: null
// },
// {
// name: '',
// type: 3,
// priorityLevel: 2,
// conditionList: [],
// childNode: null
// }
// ]
// })
// }
} }
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@ -173,9 +257,9 @@ const addType = (type) => {
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.1); box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.1);
-webkit-transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1); -webkit-transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1); transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
.iconfont { .addIcon {
font-size: 16px; line-height: 30px;
color: #fff; color: #fff;
} }

View File

@ -18,9 +18,17 @@ export enum NodeType {
*/ */
CONDITION_NODE = 3, CONDITION_NODE = 3,
/** /**
* *
*/ */
ROUTER_NODE = 4 EXCLUSIVE_NODE = 4,
/**
*
*/
PARALLEL_NODE_FORK = 5,
/**
*
*/
PARALLEL_NODE_JOIN = 6
} }
export const NODE_BG_COLOR = new Map() export const NODE_BG_COLOR = new Map()

View File

@ -7,17 +7,15 @@
--> -->
<template> <template>
<div class="node-wrap" v-if="nodeConfig.type < 3"> <div class="node-wrap" v-if="nodeConfig.type < 3">
<div <div class="node-wrap-box" :class="isTried && nodeConfig.error ? 'active error' : ''">
class="node-wrap-box"
:class="
(nodeConfig.type == 0 ? 'start-node ' : '') +
(isTried && nodeConfig.error ? 'active error' : '')
"
>
<div class="title" :style="`background: rgb(${bgColors[nodeConfig.type]});`"> <div class="title" :style="`background: rgb(${bgColors[nodeConfig.type]});`">
<span v-if="nodeConfig.type == 0">发起人</span> <span v-if="nodeConfig.type == 0">发起人</span>
<template v-else> <template v-else>
<span class="iconfont">{{ nodeConfig.type == 1 ? '' : '' }}</span> <span
class="iconfont"
:class="nodeConfig.type === NodeType.APPROVE_USER_NODE ? 'icon-approve' : 'icon-copy'"
>
</span>
<input <input
v-if="isInput" v-if="isInput"
type="text" type="text"
@ -37,9 +35,9 @@
<span class="placeholder" v-if="!showText">请选择{{ defaultText }}</span> <span class="placeholder" v-if="!showText">请选择{{ defaultText }}</span>
<span v-html="showText" class="ellipsis-text" v-else></span> <span v-html="showText" class="ellipsis-text" v-else></span>
</div> </div>
<div class="icon-box"> <!-- <div class="icon-box">
<i class="anticon anticon-edit" @click="editNode"></i> <i class="anticon anticon-edit" @click="editNode"></i>
</div> </div> -->
<i class="anticon anticon-right arrow"></i> <i class="anticon anticon-right arrow"></i>
</div> </div>
<div class="error_tip" v-if="isTried && nodeConfig.error"> <div class="error_tip" v-if="isTried && nodeConfig.error">
@ -51,7 +49,7 @@
<div class="branch-wrap" v-if="nodeConfig.type == 4"> <div class="branch-wrap" v-if="nodeConfig.type == 4">
<div class="branch-box-wrap"> <div class="branch-box-wrap">
<div class="branch-box"> <div class="branch-box">
<button class="add-branch" @click="addTerm">添加条件</button> <button class="add-branch" @click="addTerm(nodeConfig.type)">添加条件</button>
<div class="col-box" v-for="(item, index) in nodeConfig.conditionNodes" :key="index"> <div class="col-box" v-for="(item, index) in nodeConfig.conditionNodes" :key="index">
<div class="condition-node"> <div class="condition-node">
<div class="condition-node-box"> <div class="condition-node-box">
@ -89,7 +87,7 @@
<!-- <div class="content" @click="setPerson(item.priorityLevel)">{{ <!-- <div class="content" @click="setPerson(item.priorityLevel)">{{
conditionStr(nodeConfig, index) conditionStr(nodeConfig, index)
}}</div> --> }}</div> -->
<div class="content">{{conditionStr(nodeConfig, index) }}</div> <div class="content">{{ conditionStr(nodeConfig, index) }}</div>
<div class="error_tip" v-if="isTried && item.error"> <div class="error_tip" v-if="isTried && item.error">
<i class="anticon anticon-exclamation-circle"></i> <i class="anticon anticon-exclamation-circle"></i>
</div> </div>
@ -111,6 +109,59 @@
<addNode v-model:childNodeP="nodeConfig.childNode" /> <addNode v-model:childNodeP="nodeConfig.childNode" />
</div> </div>
</div> </div>
<div class="branch-wrap" v-if="nodeConfig.type == 5">
<div class="branch-box-wrap">
<div class="branch-box">
<button class="add-branch" @click="addTerm(nodeConfig.type)">添加分支</button>
<div class="col-box" v-for="(item, index) in nodeConfig.conditionNodes" :key="index">
<div class="condition-node">
<div class="condition-node-box">
<div class="title-wrapper" :style="`background: rgb(${bgColors[nodeConfig.type]});`">
<input
v-if="isInputList[index]"
type="text"
class="ant-input editable-title-input"
@blur="blurEvent(index)"
@focus="$event.currentTarget?.select()"
v-mountedFoucs
v-model="item.name"
/>
<span v-else class="editable-title" @click="clickEvent(index)">{{
item.name
}}</span>
<i class="anticon anticon-close close" @click="delTerm(nodeConfig.type, index)"></i>
</div>
<div class="auto-judge" :class="isTried && item.error ? 'error active' : ''">
<div class="content">并行执行</div>
<div class="error_tip" v-if="isTried && item.error">
<i class="anticon anticon-exclamation-circle"></i>
</div>
</div>
<addNode v-model:childNodeP="item.childNode" />
</div>
</div>
<nodeWrap v-if="item.childNode" v-model:nodeConfig="item.childNode" />
<template v-if="index == 0">
<div class="top-left-cover-line"></div>
<div class="bottom-left-cover-line"></div>
</template>
<template v-if="index == nodeConfig.conditionNodes.length - 1">
<div class="top-right-cover-line"></div>
<div class="bottom-right-cover-line"></div>
</template>
</div>
</div>
<addNode v-model:childNodeP="nodeConfig.childNode" :show-add-button="false" />
</div>
</div>
<div class="node-wrap" v-if="nodeConfig.type === 6">
<div class="node-wrap-box">
<div class="content">
<div class="text">聚合</div>
</div>
</div>
<addNode v-model:childNodeP="nodeConfig.childNode" />
</div>
<nodeWrap v-if="nodeConfig.childNode" v-model:nodeConfig="nodeConfig.childNode" /> <nodeWrap v-if="nodeConfig.childNode" v-model:nodeConfig="nodeConfig.childNode" />
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@ -124,7 +175,7 @@ import {
placeholderList, placeholderList,
getApproverShowText getApproverShowText
} from './util' } from './util'
import { WorkFlowNode } from './consts' import { WorkFlowNode, NodeType } from './consts'
import { useWorkFlowStoreWithOut } from '@/store/modules/simpleWorkflow' import { useWorkFlowStoreWithOut } from '@/store/modules/simpleWorkflow'
let _uid = getCurrentInstance().uid let _uid = getCurrentInstance().uid
@ -170,7 +221,7 @@ let {
setUserTaskConfig setUserTaskConfig
} = store } = store
// ??? // ???
const isTried = computed(() => store.isTried) const isTried = computed(() => store.isTried)
// //
const userTaskConfig = computed(() => store.userTaskConfig) const userTaskConfig = computed(() => store.userTaskConfig)
// //
@ -188,13 +239,12 @@ const showText = computed(() => {
return '' return ''
} }
} }
if(props.nodeConfig.type === 2) { if (props.nodeConfig.type === 2) {
if(props.nodeConfig.attributes) { if (props.nodeConfig.attributes) {
return copyerStr( props.nodeConfig.attributes.candidateStrategy) return copyerStr(props.nodeConfig.attributes.candidateStrategy)
} else { } else {
return '' return ''
} }
} }
return '' return ''
}) })
@ -204,9 +254,9 @@ watch(userTaskConfig, (approver) => {
} }
}) })
watch(copyerConfig, (copyer) => { watch(copyerConfig, (copyer) => {
console.log('copyer',copyer) console.log('copyer', copyer)
if (copyer.flag && copyer.id === _uid) { if (copyer.flag && copyer.id === _uid) {
console.log('copyer id is equal',copyer) console.log('copyer id is equal', copyer)
emits('update:nodeConfig', copyer.value) emits('update:nodeConfig', copyer.value)
} }
}) })
@ -230,7 +280,7 @@ const blurEvent = (index) => {
isInputList.value[index] = false isInputList.value[index] = false
// eslint-disable-next-line vue/no-mutating-props // eslint-disable-next-line vue/no-mutating-props
props.nodeConfig.conditionNodes[index].name = props.nodeConfig.conditionNodes[index].name =
props.nodeConfig.conditionNodes[index].name || '条件' props.nodeConfig.conditionNodes[index].name || '条件'
} else { } else {
isInput.value = false isInput.value = false
// eslint-disable-next-line vue/no-mutating-props // eslint-disable-next-line vue/no-mutating-props
@ -240,42 +290,66 @@ const blurEvent = (index) => {
const delNode = () => { const delNode = () => {
emits('update:nodeConfig', props.nodeConfig.childNode) emits('update:nodeConfig', props.nodeConfig.childNode)
} }
const addTerm = () => { const addTerm = (type:number) => {
const len = props.nodeConfig.conditionNodes.length const len = props.nodeConfig.conditionNodes.length
let lastIndex = props.nodeConfig.conditionNodes.length - 1 let lastIndex = props.nodeConfig.conditionNodes.length - 1
let nodeName = '条件' + len
if(type === NodeType.PARALLEL_NODE_FORK) {
nodeName = '并行' + (len+1);
lastIndex = props.nodeConfig.conditionNodes.length;
}
// eslint-disable-next-line vue/no-mutating-props // eslint-disable-next-line vue/no-mutating-props
props.nodeConfig.conditionNodes.splice(lastIndex, 0, { props.nodeConfig.conditionNodes.splice(lastIndex, 0, {
name: '条件' + len, name: nodeName,
type: 3, type: 3,
priorityLevel: len,
conditionList: [], conditionList: [],
childNode: null childNode: null
}) })
resetConditionNodesErr() resetConditionNodesErr()
emits('update:nodeConfig', props.nodeConfig) emits('update:nodeConfig', props.nodeConfig)
} }
const delTerm = (index) => { const delTerm = (nodeType: number, index: number) => {
if (props.nodeConfig.conditionNodes) { if (props.nodeConfig.conditionNodes) {
// eslint-disable-next-line vue/no-mutating-props // eslint-disable-next-line vue/no-mutating-props
props.nodeConfig.conditionNodes.splice(index, 1) props.nodeConfig.conditionNodes.splice(index, 1)
props.nodeConfig.conditionNodes.map((item, index) => { if (nodeType === NodeType.PARALLEL_NODE_FORK) {
props.nodeConfig.conditionNodes.map((item, index) => {
item.name = `并行${index + 1}`
})
} else {
props.nodeConfig.conditionNodes.map((item, index) => {
// item.priorityLevel = index + 1 // item.priorityLevel = index + 1
if (index !== props.nodeConfig.conditionNodes.length - 1) { if (index !== props.nodeConfig.conditionNodes.length - 1) {
item.name = `条件${index + 1}` item.name = `条件${index + 1}`
} }
}) })
}
resetConditionNodesErr() resetConditionNodesErr()
emits('update:nodeConfig', props.nodeConfig) emits('update:nodeConfig', props.nodeConfig)
if (props.nodeConfig.conditionNodes.length == 1) { if (props.nodeConfig.conditionNodes.length == 1) {
if (props.nodeConfig.childNode) { if (nodeType === NodeType.PARALLEL_NODE_FORK) {
if (props.nodeConfig.conditionNodes[0].childNode) { const joinNode = props.nodeConfig.childNode;
reData(props.nodeConfig.conditionNodes[0].childNode, props.nodeConfig.childNode) if (joinNode?.childNode) {
} else { if (props.nodeConfig.conditionNodes[0].childNode) {
// eslint-disable-next-line vue/no-mutating-props reData(props.nodeConfig.conditionNodes[0].childNode, joinNode?.childNode)
props.nodeConfig.conditionNodes[0].childNode = props.nodeConfig.childNode } else {
// eslint-disable-next-line vue/no-mutating-props
props.nodeConfig.conditionNodes[0].childNode = joinNode?.childNode
}
} }
emits('update:nodeConfig', props.nodeConfig.conditionNodes[0].childNode)
} else {
if (props.nodeConfig.childNode) {
if (props.nodeConfig.conditionNodes[0].childNode) {
reData(props.nodeConfig.conditionNodes[0].childNode, props.nodeConfig.childNode)
} else {
// eslint-disable-next-line vue/no-mutating-props
props.nodeConfig.conditionNodes[0].childNode = props.nodeConfig.childNode
}
}
emits('update:nodeConfig', props.nodeConfig.conditionNodes[0].childNode)
} }
emits('update:nodeConfig', props.nodeConfig.conditionNodes[0].childNode)
} }
} }
} }
@ -287,9 +361,9 @@ const reData = (data, addData) => {
} }
} }
const setPerson = (priorityLevel) => { const setPerson = (priorityLevel) => {
console.log('priorityLevel',priorityLevel) console.log('priorityLevel', priorityLevel)
const { type } = props.nodeConfig const { type } = props.nodeConfig
console.log('type',type) console.log('type', type)
if (type == 0) { if (type == 0) {
// setPromoter(true) // setPromoter(true)
} else if (type == 1) { } else if (type == 1) {

View File

@ -187,7 +187,7 @@ export const removeEle = (arr, elem, key = 'id') => {
arr.splice(includesIndex, 1) arr.splice(includesIndex, 1)
} }
export const bgColors = ['87, 106, 149', '255, 148, 62', '50, 150, 250','50, 150, 250','248, 107, 248'] export const bgColors = ['87, 106, 149', '255, 148, 62', '50, 150, 250','50, 150, 250','248, 107, 248','244, 118, 118']
export const placeholderList = ['发起人', '审核人', '抄送人'] export const placeholderList = ['发起人', '审核人', '抄送人']
export const setTypes = [ export const setTypes = [
{ value: 1, label: '指定成员' }, { value: 1, label: '指定成员' },

View File

@ -37,7 +37,7 @@
font-family: anticon!important font-family: anticon!important
} }
.anticon-close:before { .anticon-close:before {
content: "\E633" content: "\E633"
} }
.anticon-right:before { .anticon-right:before {
content: "\E61F" content: "\E61F"
@ -486,7 +486,7 @@ html {
height: 100% height: 100%
} }
@font-face { /* @font-face {
font-family: IconFont; font-family: IconFont;
src: url("//at.alicdn.com/t/font_135284_ph2thxxbzgf.eot"); src: url("//at.alicdn.com/t/font_135284_ph2thxxbzgf.eot");
src: url("//at.alicdn.com/t/font_135284_ph2thxxbzgf.eot?#iefix") format("embedded-opentype"), url("//at.alicdn.com/t/font_135284_ph2thxxbzgf.woff") format("woff"), url("//at.alicdn.com/t/font_135284_ph2thxxbzgf.ttf") format("truetype"), url("//at.alicdn.com/t/font_135284_ph2thxxbzgf.svg#IconFont") format("svg") src: url("//at.alicdn.com/t/font_135284_ph2thxxbzgf.eot?#iefix") format("embedded-opentype"), url("//at.alicdn.com/t/font_135284_ph2thxxbzgf.woff") format("woff"), url("//at.alicdn.com/t/font_135284_ph2thxxbzgf.ttf") format("truetype"), url("//at.alicdn.com/t/font_135284_ph2thxxbzgf.svg#IconFont") format("svg")
@ -499,6 +499,45 @@ html {
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-webkit-text-stroke-width: .2px; -webkit-text-stroke-width: .2px;
-moz-osx-font-smoothing: grayscale -moz-osx-font-smoothing: grayscale
} */
@font-face {
font-family: "iconfont"; /* Project id 4495938 */
src: url('iconfont.woff2?t=1712392083512') format('woff2'),
url('iconfont.woff?t=1712392083512') format('woff'),
url('iconfont.ttf?t=1712392083512') format('truetype');
}
.iconfont {
font-family: "iconfont" !important;
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.icon-Inclusive:before {
content: "\e602";
}
.icon-copy:before {
content: "\e7eb";
}
.icon-handle:before {
content: "\e61c";
}
.icon-exclusive:before {
content: "\e717";
}
.icon-approve:before {
content: "\e715";
}
.icon-parallel:before {
content: "\e688";
} }
.fd-nav { .fd-nav {