模型基础信息

This commit is contained in:
2024-05-06 13:57:06 +08:00
parent d7fbf3fe64
commit 917dc4e665
67 changed files with 9894 additions and 1104 deletions

View File

@@ -0,0 +1,57 @@
<template>
<start-events v-if="childNode.type === 'startEvent'" v-model="childNode" :currentActivityId="currentActivityId" />
<start-tasks v-if="childNode.type === 'startTask'" v-model="childNode" :currentActivityId="currentActivityId" />
<user-tasks v-if="childNode.type === 'userTask'" v-model="childNode" :currentActivityId="currentActivityId" />
<service-tasks v-if="childNode.type === 'serviceTask'" v-model="childNode" :currentActivityId="currentActivityId" />
<exclusive-gateways v-if="childNode.type === 'exclusiveGateway'" v-model="childNode" :currentActivityId="currentActivityId">
<template #default="slot">
<c-node-wrap v-if="slot.node" v-model="slot.node.childNode" :currentActivityId="currentActivityId" />
</template>
</exclusive-gateways>
<parallel-gateways v-if="childNode.type === 'parallelGateway'" v-model="childNode" :currentActivityId="currentActivityId">
<template #default="slot">
<c-node-wrap v-if="slot.node" v-model="slot.node.childNode" :currentActivityId="currentActivityId" />
</template>
</parallel-gateways>
<c-node-wrap v-if="childNode.childNode" v-model="childNode.childNode" :currentActivityId="currentActivityId" />
</template>
<script>
import startEvents from './nodes/cStartEvent.vue'
import startTasks from './nodes/cStartTask.vue'
import userTasks from './nodes/cUserTask.vue'
import exclusiveGateways from './nodes/cExclusiveGateway.vue'
import parallelGateways from './nodes/cParallelGateway.vue'
import serviceTasks from './nodes/cServiceTask.vue'
export default {
components: {
startEvents,
startTasks,
userTasks,
exclusiveGateways,
parallelGateways,
serviceTasks
},
props: {
modelValue: { type: Object, default: () => {} },
currentActivityId: { type: String, default: () => '' }
},
data() {
return {
childNode: {}
}
},
watch: {
modelValue(val) {
this.childNode = val
},
childNode(val) {
this.$emit('update:modelValue', val)
}
},
mounted() {
this.childNode = this.modelValue
}
}
</script>

View File

@@ -0,0 +1,80 @@
<template>
<div class="workflow-design">
<div class="workflow-design-btns">
<a-space>
<a-tooltip>
<template #title>放大</template>
<a-button @click="handleZoom(true)" :disabled="zoom > 2">
<template #icon><plus-outlined /></template>
</a-button>
</a-tooltip>
<a-tooltip>
<template #title>缩小</template>
<a-button @click="handleZoom(false)" :disabled="zoom < 1">
<template #icon><minus-outlined /></template>
</a-button>
</a-tooltip>
</a-space>
</div>
<div id="div1">
<div class="box-scale" :style="style">
<node-wrap-chart v-if="childNode" v-model="childNode.childNode" :currentActivityId="currentActivityId" />
<div class="end-node">
<div class="end-node-circle"></div>
<div class="end-node-text">流程结束</div>
</div>
</div>
</div>
</div>
</template>
<script>
import nodeWrapChart from '@/components/XnWorkflow/chart/cNodeWrap.vue'
import FullscreenPreviewHelper from './zoom_helper'
const FullScreenRightSpace = 20
export default {
components: {
nodeWrapChart
},
props: {
modelValue: { type: Object, default: () => {} },
currentActivityId: { type: String, default: () => '' }
},
data() {
return {
childNode: this.modelValue,
style: {},
zoom: 1
}
},
computed: {},
watch: {
modelValue(val) {
this.childNode = val
},
childNode(val) {
this.$emit('update:modelValue', val)
}
},
methods: {
handleGetStyle(zoom) {
return FullscreenPreviewHelper.getZoomStyles(zoom, 1, 0, FullScreenRightSpace)
},
handleZoom(zoomIn) {
const zoom = this.zoom
const zoomData = FullscreenPreviewHelper.getZoomData(zoomIn, zoom)
const style = this.handleGetStyle(zoomData.zoom)
this.style = style
this.zoom = zoomData.zoom
}
}
}
</script>
<style lang="less">
@import '../flowIndex.less';
.workflow-design-btns {
width: 100px;
}
</style>

View File

@@ -0,0 +1,5 @@
<template>
<div class="add-node-btn-box">
<div class="add-node-btn"></div>
</div>
</template>

View File

@@ -0,0 +1,141 @@
<template>
<div class="branch-wrap">
<div class="branch-box-wrap">
<div class="branch-box" style="margin-top: 0px">
<div v-for="(item, index) in childNode.conditionNodeList" :key="index" class="col-box">
<div class="condition-node">
<div class="condition-node-box">
<div class="auto-judge">
<div class="title">
<span class="node-title">{{ item.title }}</span>
<span class="priority-title">优先级{{ item.properties.configInfo.priorityLevel }}</span>
</div>
<div class="content">
<span v-if="toText(childNode, index)">{{ toText(childNode, index) }}</span>
<span v-else class="placeholder">请设置条件</span>
</div>
</div>
<add-nodes v-model="item.childNode" />
</div>
</div>
<slot v-if="item.childNode" :node="item"></slot>
<div v-if="index == 0" class="top-left-cover-line"></div>
<div v-if="index == 0" class="bottom-left-cover-line"></div>
<div v-if="index == childNode.conditionNodeList.length - 1" class="top-right-cover-line"></div>
<div v-if="index == childNode.conditionNodeList.length - 1" class="bottom-right-cover-line"></div>
</div>
</div>
<add-nodes v-model="childNode.childNode" :parent-data="childNode" />
</div>
</div>
</template>
<script>
import addNodes from './cAddNode.vue'
export default {
components: {
addNodes
},
props: {
modelValue: { type: Object, default: () => {} },
currentActivityId: { type: String, default: () => '' }
},
data() {
return {
childNode: {},
index: 0,
operatorList: [
{
label: '等于',
value: '=='
},
{
label: '不等于',
value: '!='
},
{
label: '大于',
value: '>'
},
{
label: '大于等于',
value: '>='
},
{
label: '小于',
value: '<'
},
{
label: '小于等于',
value: '<='
},
{
label: '包含',
value: 'include'
},
{
label: '不包含',
value: 'notInclude'
}
]
}
},
watch: {
modelValue() {
this.childNode = this.modelValue
}
},
mounted() {
this.childNode = this.modelValue
},
methods: {
toText(childNode, index) {
const conditionList = childNode.conditionNodeList[index].properties.conditionInfo
const priorityLevel = childNode.conditionNodeList[index].properties.configInfo.priorityLevel
const len = this.childNode.conditionNodeList.length
const priorityLevelMax = this.childNode.conditionNodeList[len - 1].properties.configInfo.priorityLevel
if (JSON.stringify(conditionList) !== undefined && conditionList.length > 0) {
let text = ''
for (let i = 0; i < conditionList.length; i++) {
for (let j = 0; j < conditionList[i].length; j++) {
if (j + 1 !== conditionList[i].length) {
text =
text +
conditionList[i][j].label +
this.getOperatorLabel(conditionList[i][j].operator) +
conditionList[i][j].value +
' 且 '
} else {
text =
text +
conditionList[i][j].label +
this.getOperatorLabel(conditionList[i][j].operator) +
conditionList[i][j].value
}
}
if (i + 1 !== conditionList.length) {
text = text + ' 或 '
}
}
return text
} else if (conditionList.length === 0 && priorityLevel < priorityLevelMax) {
return false
} else {
return '其他条件进入此流程'
}
},
// 通过value 获取界面显示的label汉字
getOperatorLabel(value) {
return this.operatorList.find((item) => item.value === value).label
}
}
}
</script>
<style scoped type="less">
.workflow-design .condition-node {
min-height: 200px !important;
}
</style>

View File

@@ -0,0 +1,50 @@
<template>
<div class="branch-wrap">
<div class="branch-box-wrap">
<div class="branch-box">
<div v-for="(item, index) in childNode.conditionNodeList" :key="index" class="col-box">
<div class="condition-node">
<div class="condition-node-box">
<user-tasks v-model="childNode.conditionNodeList[index]" :currentActivityId="currentActivityId" />
</div>
</div>
<slot v-if="item.childNode" :node="item"></slot>
<div v-if="index == 0" class="top-left-cover-line"></div>
<div v-if="index == 0" class="bottom-left-cover-line"></div>
<div v-if="index == childNode.conditionNodeList.length - 1" class="top-right-cover-line"></div>
<div v-if="index == childNode.conditionNodeList.length - 1" class="bottom-right-cover-line"></div>
</div>
</div>
<add-nodes v-model="childNode.childNode" :parent-data="childNode" />
</div>
</div>
</template>
<script>
import addNodes from './cAddNode.vue'
import userTasks from './cUserTask.vue'
export default {
components: {
addNodes,
userTasks
},
props: {
modelValue: { type: Object, default: () => {} },
currentActivityId: { type: String, default: () => '' }
},
data() {
return {
childNode: {},
index: 0
}
},
watch: {
modelValue() {
this.childNode = this.modelValue
}
},
mounted() {
this.childNode = this.modelValue
}
}
</script>

View File

@@ -0,0 +1,80 @@
<template>
<div class="node-wrap">
<a-tooltip placement="left">
<template #title v-if="childNode.properties && childNode.properties.commentList.length > 0">
<div v-for="comment in childNode.properties.commentList" :key="comment.id">
{{ comment.userName }}{{ comment.approveTime }}
<a-tag color="rgb(212, 212, 212)">{{ comment.operateText }}</a-tag
>意见{{ comment.comment }}
</div>
</template>
<div :class="childNode.properties && childNode.properties.commentList.length > 0 ? 'node-state-label' : ''">
<div class="node-wrap-box">
<div class="title" style="background: #3296fa">
<send-outlined class="icon" />
<span>{{ childNode.title }}</span>
</div>
<div class="content">
<span v-if="toText(childNode)">{{ toText(childNode) }}</span>
<span v-else class="placeholder">未选择人员</span>
</div>
</div>
</div>
</a-tooltip>
<add-nodes v-model="childNode.childNode" />
</div>
</template>
<script>
import addNodes from './cAddNode.vue'
export default {
components: {
addNodes
},
props: {
modelValue: { type: Object, default: () => {} },
currentActivityId: { type: String, default: () => '' }
},
data() {
return {
childNode: {}
}
},
watch: {
modelValue() {
this.childNode = this.modelValue
}
},
mounted() {
this.childNode = this.modelValue
},
methods: {
toText(childNode) {
if (JSON.stringify(childNode) !== '{}') {
const participateInfo = childNode.properties.participateInfo
if (participateInfo.length > 0) {
let resultArray = []
if (participateInfo[0].label.indexOf(',') !== -1) {
resultArray = participateInfo[0].label.split(',')
} else {
resultArray.push(participateInfo[0].label)
}
return resultArray.toString()
} else {
return false
}
} else {
return false
}
}
}
}
</script>
<style scoped>
.node-state-label {
padding: 1px;
border: 3px solid rgb(24, 144, 255);
border-radius: 2px;
}
</style>

View File

@@ -0,0 +1,24 @@
<template>
<div class="node-wrap"></div>
</template>
<script>
export default {
props: {
modelValue: { type: Object, default: () => {} }
},
data() {
return {
childNode: {}
}
},
watch: {
modelValue() {
this.childNode = this.modelValue
}
},
mounted() {
this.childNode = this.modelValue
}
}
</script>

View File

@@ -0,0 +1,76 @@
<template>
<div class="node-wrap">
<a-tooltip placement="left">
<template #title v-if="childNode.properties && childNode.properties.commentList.length > 0">
<div v-for="comment in childNode.properties.commentList" :key="comment.id">
{{ comment.userName }}{{ comment.approveTime }}
<a-tag color="rgb(212, 212, 212)">{{ comment.operateText }}</a-tag
>意见{{ comment.comment }}
</div>
</template>
<div :class="getClassName()">
<div class="node-wrap-box start-node">
<div class="title" style="background: #576a95">
<user-outlined class="icon" />
<span>{{ childNode.title }}</span>
</div>
<div class="content">
<span>{{ toText(childNode) }}</span>
</div>
</div>
</div>
</a-tooltip>
<add-nodes v-model="childNode.childNode" />
</div>
</template>
<script>
import addNodes from './cAddNode.vue'
export default {
components: {
addNodes
},
props: {
modelValue: { type: Object, default: () => {} },
currentActivityId: { type: String, default: () => '' }
},
data() {
return {
childNode: {}
}
},
watch: {
modelValue() {
this.childNode = this.modelValue
}
},
mounted() {
this.childNode = this.modelValue
},
methods: {
toText() {
return '系统自动配置参与人'
},
getClassName() {
if (this.childNode.id === this.currentActivityId) {
return 'node-state-label-activity'
} else {
return this.childNode.properties && this.childNode.properties.commentList.length > 0 ? 'node-state-label' : ''
}
}
}
}
</script>
<style scoped>
.node-state-label {
padding: 1px;
border: 3px solid rgb(24, 144, 255);
border-radius: 2px;
}
.node-state-label-activity {
padding: 1px;
border: 3px dashed #00e97c;
border-radius: 2px;
}
</style>

View File

@@ -0,0 +1,147 @@
<template>
<div class="node-wrap">
<a-tooltip placement="left">
<template #title v-if="childNode.properties && childNode.properties.commentList.length > 0">
<div v-for="comment in childNode.properties.commentList" :key="comment.id">
{{ comment.userName }}{{ comment.approveTime }}
<a-tag color="rgb(212, 212, 212)">{{ comment.operateText }}</a-tag
>意见{{ comment.comment }}
</div>
</template>
<div :class="getClassName()">
<div class="node-wrap-box">
<div class="title" style="background: #ff943e">
<user-outlined class="icon" />
<span>{{ childNode.title }}</span>
</div>
<div class="content">
<span v-if="toText(childNode)">{{ toText(childNode) }}</span>
<span v-else class="placeholder">未选择审批人</span>
</div>
</div>
</div>
</a-tooltip>
<add-nodes v-model="childNode.childNode" />
</div>
</template>
<script>
import addNodes from './cAddNode.vue'
export default {
components: {
addNodes
},
props: {
modelValue: { type: Object, default: () => {} },
currentActivityId: { type: String, default: () => '' }
},
data() {
return {
childNode: {}
}
},
watch: {
modelValue() {
this.childNode = this.modelValue
}
},
mounted() {
this.childNode = this.modelValue
},
methods: {
toText(childNode) {
if (JSON.stringify(childNode) !== '{}') {
const strArray = this.toTag(childNode.properties.participateInfo[0])
if (strArray.length > 0) {
let value = ''
// eslint-disable-next-line no-plusplus
for (let i = 0; i < strArray.length; i++) {
if (strArray.length === i + 1) {
value = value + strArray[i]
} else {
value = value + strArray[i] + ''
}
}
return value
} else {
return false
}
} else {
return false
}
},
toTag(participateInfo) {
// eslint-disable-next-line no-undefined
if (participateInfo === undefined) {
return []
}
if (participateInfo.label === '') {
return []
} else {
let resultArray = []
if (participateInfo.label.indexOf(',') !== -1) {
resultArray = participateInfo.label.split(',')
} else {
resultArray.push(participateInfo.label)
}
return resultArray
}
},
getClassName() {
if (this.childNode.properties && this.childNode.properties.commentList.length > 0) {
if (this.childNode.properties.commentList.length === 1) {
if (this.childNode.properties.commentList[0].operateType === 'PASS') {
return 'node-state-label-pass'
}
if (this.childNode.properties.commentList[0].operateType === 'REJECT') {
return 'node-state-label-reject'
}
if (this.childNode.properties.commentList[0].operateType === 'JUMP') {
return 'node-state-label-jump'
}
if (this.childNode.properties.commentList[0].operateType === 'BACK') {
return 'node-state-label-back'
}
}
return 'node-state-label'
} else {
if (this.childNode.id === this.currentActivityId) {
return 'node-state-label-activity'
}
}
}
}
}
</script>
<style scoped>
.node-state-label {
padding: 1px;
border: 3px solid #1890ff;
border-radius: 2px;
}
.node-state-label-pass {
padding: 1px;
border: 3px solid #52c41a;
border-radius: 2px;
}
.node-state-label-reject {
padding: 1px;
border: 3px solid #ff4d4f;
border-radius: 2px;
}
.node-state-label-jump {
padding: 1px;
border: 3px solid #bfbfbf;
border-radius: 2px;
}
.node-state-label-back {
padding: 1px;
border: 3px solid #bfbfbf;
border-radius: 2px;
}
.node-state-label-activity {
padding: 1px;
border: 3px dashed #00e97c;
border-radius: 2px;
}
</style>

View File

@@ -0,0 +1,39 @@
const defaultZoomMargin = 360
const perZoom = 0.25 // 每次放大缩小0.25倍
const ZoomHelper = {
// 处理全屏放大的样式
getZoomStyles(zoom, MinZoom = 1, ZoomMargin = defaultZoomMargin, rightSpace = 0) {
const width = document.querySelector('.workflow-design').clientWidth
let style = {}
// 兼容Firefox浏览器的放大缩小
if (zoom !== MinZoom) {
style = {
transform: `scale(${zoom})`,
'transform-origin': '0 0',
width: (width - ZoomMargin - rightSpace) / zoom + 'px'
}
}
return style
},
// 获取最大的放大倍数
getMaxZoom() {
const width = window.innerWidth
const mediumWidth = 1600
const smallScreenScale = 2.5 // 小屏幕下附件放大3倍会有样式问题, 所以取2.5
const bigScreenScale = 3 // 大于1600的最大倍数为3
const maxZoom = width > mediumWidth ? bigScreenScale : smallScreenScale
return maxZoom
},
// 获取点击放大缩小之后生成的最终的放大倍数和样式
getZoomData(zoomIn, zoom) {
const zoomResult = zoomIn ? zoom + perZoom : zoom - perZoom // 放大倍数加一次或者减少一次
const zoomData = {
style: this.getZoomStyles(zoomResult),
zoom: zoomResult
}
return zoomData
}
}
export default ZoomHelper

View File

@@ -0,0 +1,9 @@
审批中 蓝色 #1890FF
已挂起 黄色 #FCC02E
已完成 绿色 #52C41A
已终止 黄色 #FF5A5A
已撤回 灰色 #BFBFBF
已拒绝 红色 #FF4D4F
同意 绿色 #52C41A

View File

@@ -0,0 +1,3 @@
<template>
<a-result status="404" title="未找到表单" sub-title="对不起该节点配置的自定义表单不存在请联系管理员" />
</template>

View File

@@ -0,0 +1,28 @@
/**
* 自定义表单导入
*
* @author yubaoshan
* @date 2023-05-11 00:12:22
*/
const modules = import.meta.glob('/src/views/flw/customform/**/**.vue')
const notFound = () => import(/* @vite-ignore */ `/src/components/XnWorkflow/customForm/404.vue`)
// 直接渲染组件
export const loadComponent = (component) => {
if (component) {
const link = modules[`/src/views/flw/customform/${component}.vue`]
return markRaw(defineAsyncComponent(link ? link : notFound))
} else {
return markRaw(defineAsyncComponent(notFound))
}
}
// 给出判断如果使用的地方取到是404那么他的下一步就不走了
export const verdictComponent = (component) => {
if (component) {
const link = modules[`/src/views/flw/customform/${component}.vue`]
return !!link
} else {
return false
}
}

View File

@@ -0,0 +1,425 @@
.workflow-design {
overflow: auto;
width: 100%;
}
.workflow-design .box-scale {
display: inline-block;
position: relative;
width: 100%;
padding-top: 10px;
padding-bottom: 55px;
align-items: flex-start;
justify-content: center;
flex-wrap: wrap;
min-width: min-content;
}
.nodeLegal {
border: 2px solid red;
border-radius: 1px;
}
.workflow-design {
.node-wrap {
display: inline-flex;
width: 100%;
flex-flow: column wrap;
justify-content: flex-start;
align-items: center;
padding: 0px 50px;
position: relative;
z-index: 1;
}
.node-wrap-box {
display: inline-flex;
flex-direction: column;
position: relative;
width: 220px;
min-height: 72px;
flex-shrink: 0;
background: var(--node-wrap-box-color);
border-radius: 1px;
cursor: pointer;
box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.2);
}
.node-wrap-box::before {
background: var(--auto-judge-before-color);
content: '';
position: absolute;
top: -12px;
left: 50%;
transform: translateX(-50%);
width: 0px;
border-style: solid;
border-width: 8px 6px 4px;
border-color: #cacaca transparent transparent;
}
.node-wrap-box.start-node:before {
content: none;
}
.node-wrap-box .title {
height: 24px;
line-height: 24px;
color: #fff;
padding-left: 16px;
padding-right: 30px;
border-radius: 2px 2px 0 0;
position: relative;
display: flex;
align-items: center;
}
.node-wrap-box .title .icon {
margin-right: 5px;
}
.node-wrap-box .title .close {
font-size: 15px;
position: absolute;
top: 50%;
transform: translateY(-50%);
right: 10px;
display: none;
}
.node-wrap-box .title .success {
font-size: 20px;
position: absolute;
top: 50%;
transform: translateY(-50%);
right: -25px;
display: block;
color: #00bb00;
}
.node-wrap-box .content {
position: relative;
padding: 15px;
}
.node-wrap-box .content .placeholder {
color: red;
}
.node-wrap-box:hover .close {
display: block;
}
.add-node-btn-box {
width: 240px;
display: inline-flex;
flex-shrink: 0;
position: relative;
z-index: 1;
}
.add-node-btn-box:before {
content: '';
position: absolute;
top: 0px;
left: 0px;
right: 0px;
bottom: 0px;
z-index: -1;
margin: auto;
width: 2px;
height: 100%;
background-color: rgb(202, 202, 202);
}
.add-node-btn {
user-select: none;
width: 240px;
padding: 20px 0px 32px;
display: flex;
justify-content: center;
flex-shrink: 0;
flex-grow: 1;
}
.add-node-btn span {
}
.add-branch {
justify-content: center;
padding: 0px 10px;
position: absolute;
top: -16px;
left: 50%;
transform: translateX(-50%);
transform-origin: center center;
z-index: 1;
display: inline-flex;
align-items: center;
}
.branch-wrap {
display: inline-flex;
width: 100%;
}
.branch-box-wrap {
display: flex;
flex-flow: column wrap;
align-items: center;
min-height: 270px;
width: 100%;
flex-shrink: 0;
}
.col-box {
display: inline-flex;
flex-direction: column;
align-items: center;
position: relative;
background: var(--component-background);
}
// 分支 上面横线
.branch-box {
display: flex;
overflow: visible;
min-height: 180px;
height: auto;
border-bottom: 2px solid #ccc;
border-top: 2px solid #ccc;
position: relative;
margin-top: 15px;
}
.branch-box .col-box::before {
content: '';
position: absolute;
top: 0px;
left: 0px;
right: 0px;
bottom: 0px;
z-index: 0;
margin: auto;
width: 2px;
height: 100%;
background-color: rgb(202, 202, 202);
}
.condition-node {
display: inline-flex;
flex-direction: column;
min-height: 220px;
}
.condition-node-box {
padding-top: 30px;
padding-right: 50px;
padding-left: 50px;
justify-content: center;
align-items: center;
flex-grow: 1;
position: relative;
display: inline-flex;
flex-direction: column;
}
.auto-judge {
position: relative;
width: 220px;
min-height: 72px;
background: var(--node-wrap-box-color);
border-radius: 2px;
padding: 15px 15px;
cursor: pointer;
box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.2);
}
// 箭头框框
.auto-judge::before {
content: '';
position: absolute;
top: -12px;
left: 50%;
transform: translateX(-50%);
width: 0px;
border-style: solid;
border-width: 8px 6px 4px;
border-color: #cacaca transparent transparent;
background: var(--auto-judge-before-color);
}
.auto-judge .title {
line-height: 16px;
}
.auto-judge .title .node-title {
color: #15bc83;
}
.auto-judge .title .close {
font-size: 15px;
position: absolute;
top: 15px;
right: 15px;
color: #999;
display: none;
}
.auto-judge .title .success {
font-size: 15px;
position: absolute;
top: 15px;
right: 15px;
color: #999;
display: block;
}
.auto-judge .title .priority-title {
position: absolute;
top: 15px;
right: 15px;
color: #999;
}
.auto-judge .content {
position: relative;
padding-top: 15px;
}
.auto-judge .content .placeholder {
color: red;
}
.auto-judge:hover {
.close {
display: block;
}
.priority-title {
display: none;
}
}
.top-left-cover-line,
.top-right-cover-line {
position: absolute;
height: 3px;
width: 50%;
background-color: var(--component-background);
top: -2px;
}
.bottom-left-cover-line,
.bottom-right-cover-line {
position: absolute;
height: 3px;
width: 50%;
background-color: var(--component-background);
bottom: -2px;
}
.top-left-cover-line {
left: -1px;
}
.top-right-cover-line {
right: -1px;
}
.bottom-left-cover-line {
left: -1px;
}
.bottom-right-cover-line {
right: -1px;
}
.end-node {
border-radius: 50%;
font-size: 14px;
color: rgba(25, 31, 37, 0.4);
text-align: left;
}
// 结束的小点点
.end-node-circle {
width: 10px;
height: 10px;
margin: auto;
border-radius: 50%;
background: #dbdcdc;
}
.end-node-text {
margin-top: 5px;
text-align: center;
}
.auto-judge:hover {
.sort-left {
display: flex;
}
.sort-right {
display: flex;
}
}
.auto-judge .sort-left {
position: absolute;
top: 0;
bottom: 0;
z-index: 1;
left: 0;
display: none;
justify-content: center;
align-items: center;
flex-direction: column;
}
.auto-judge .sort-right {
position: absolute;
top: 0;
bottom: 0;
z-index: 1;
right: 0;
display: none;
justify-content: center;
align-items: center;
flex-direction: column;
}
.auto-judge .sort-left:hover,
.auto-judge .sort-right:hover {
background: var(--auto-judge-before-color);
}
.auto-judge:after {
pointer-events: none;
content: '';
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
z-index: 2;
border-radius: 2px;
transition: all 0.1s;
}
.auto-judge:hover:after {
border: 1px solid #3296fa;
box-shadow: 0 0 6px 0 rgba(50, 150, 250, 0.3);
}
.node-wrap-box:after {
pointer-events: none;
content: '';
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
z-index: 2;
border-radius: 2px;
transition: all 0.1s;
}
.node-wrap-box:hover:after {
border: 1px solid #3296fa;
box-shadow: 0 0 6px 0 rgba(50, 150, 250, 0.3);
}
}
.tags-list {
margin-top: 15px;
width: 100%;
}
.add-node-popover-body {
height: 81px;
}
.add-node-popover-body li {
display: inline-block;
width: 80px;
text-align: center;
padding: 10px 0;
}
.add-node-popover-body li i {
border: 1px solid var(--el-border-color-light);
width: 40px;
height: 40px;
border-radius: 50%;
text-align: center;
line-height: 38px;
font-size: 18px;
cursor: pointer;
}
.add-node-popover-body li i:hover {
border: 1px solid #3296fa;
background: #3296fa;
color: #fff !important;
}
.add-node-popover-body li p {
font-size: 12px;
margin-top: 5px;
}
.node-wrap-drawer__title {
padding-right: 40px;
}
.node-wrap-drawer__title label {
cursor: pointer;
}
.node-wrap-drawer__title label:hover {
border-bottom: 1px dashed #409eff;
}
.node-wrap-drawer__title .node-wrap-drawer-title-edit {
color: #409eff;
margin-left: 10px;
vertical-align: middle;
}

View File

@@ -0,0 +1,139 @@
<!--
* @Descripttion: 仿钉钉流程设计器
* @version: 1.2
* @Author: sakuya
* @Date: 2021年9月14日08:38:35
* @LastEditors: yubaoshan
* @LastEditTime: 2022年2月9日16:48:49
-->
<template>
<div class="workflow-design">
<!-- 配置流程全局属性 -->
<div style="float: right; padding-right: 10px">
<span v-if="!toDataLegal(childNode)" style="padding-right: 5px">
<!-- <exclamation-circle-outlined style="color: red; font-size: 18px" />-->
<!-- <Warning style="color: red; font-size: 18px"/>-->
</span>
<!-- <el-tooltip>-->
<!-- <template #title></template>-->
<!-- <el-button @click="$refs.process.showDrawer()">-->
<!-- <template #icon>-->
<!--&lt;!&ndash; <setting-outlined />&ndash;&gt;-->
<!-- <Setting />-->
<!-- </template>-->
<!-- -->
<!-- </el-button>-->
<!-- </el-tooltip>-->
<el-tooltip
class="box-item"
effect="dark"
content="配置流程全局属性"
placement="top"
>
<el-button :icon='Setting'>
全局配置
<template #icon>
<Setting />
</template>
</el-button>
</el-tooltip>
</div>
<div class="box-scale">
<node-wrap
v-if="childNode"
v-model="childNode.childNode"
:form-field-list-value="childFormFieldListValue"
:record-data="childRecordData"
:process-config-info="childNode.properties.configInfo"
:execution-listener-array="executionListenerArray"
:task-listener-array="taskListenerArray"
:selector-api-function="selectorApiFunction"
/>
<div class="end-node">
<div class="end-node-circle"></div>
<div class="end-node-text">流程结束</div>
</div>
</div>
<process
ref="process"
v-model="childNode"
:form-field-list-value="childFormFieldListValue"
:record-data="childRecordData"
:sn-template-array="snTemplateArray"
:print-template-array="printTemplateArray"
:execution-listener-array="executionListenerArray"
:execution-listener-selector-for-custom-event-array="executionListenerSelectorForCustomEventArray"
:task-listener-array="taskListenerArray"
:selector-api-function="selectorApiFunction"
/>
</div>
</template>
<script>
import nodeWrap from './nodeWrap.vue'
import { Warning, Setting, Plus } from '@element-plus/icons-vue'
import process from './process.vue'
export default {
computed: {
Plus() {
return Plus
}
},
components: {
nodeWrap,
Warning,
Setting,
process
},
props: {
modelValue: { type: Object, default: () => {} },
formFieldListValue: { type: Array, default: () => [] },
recordData: { type: Object, default: () => {} },
snTemplateArray: { type: Array, default: () => [] },
printTemplateArray: { type: Array, default: () => [] },
executionListenerArray: { type: Array, default: () => [] },
executionListenerSelectorForCustomEventArray: { type: Array, default: () => [] },
listenerType: { type: String, default: () => 'default' },
taskListenerArray: { type: Array, default: () => [] },
selectorApiFunction: { type: Object, default: () => {} }
},
data() {
return {
childNode: this.modelValue,
childFormFieldListValue: this.formFieldListValue,
childRecordData: this.recordData
}
},
watch: {
modelValue(val) {
this.childNode = val
},
// 监听字段列表传输的相关动静
formFieldListValue(val) {
this.childFormFieldListValue = val
},
recordData(val) {
this.childRecordData = val
},
childNode(val) {
this.$emit('update:modelValue', val)
}
},
methods: {
toDataLegal(childNode) {
if (childNode === undefined) {
return false
} else {
return childNode.dataLegal
}
}
}
}
</script>
<style lang="less">
@import './flowIndex.less';
</style>

View File

@@ -0,0 +1,131 @@
<template>
<start-event v-if="childNode.type === 'startEvent'" v-model="childNode" />
<start-task
v-if="childNode.type === 'startTask'"
v-model="childNode"
:formFieldListValue="formFieldListValue"
:recordData="recordData"
:processConfigInfo="processConfigInfo"
:execution-listener-array="executionListenerArray"
:task-listener-array="taskListenerArray"
:selector-api-function="selectorApiFunction"
/>
<user-task
v-if="childNode.type === 'userTask'"
v-model="childNode"
:formFieldListValue="formFieldListValue"
:recordData="recordData"
:processConfigInfo="processConfigInfo"
:execution-listener-array="executionListenerArray"
:task-listener-array="taskListenerArray"
:selector-api-function="selectorApiFunction"
/>
<service-task
v-if="childNode.type === 'serviceTask'"
v-model="childNode"
:execution-listener-array="executionListenerArray"
:selector-api-function="selectorApiFunction"
:form-field-list-value="formFieldListValue"
:record-data="recordData"
/>
<exclusive-gateway
v-if="childNode.type === 'exclusiveGateway'"
v-model="childNode"
:form-field-list-value="formFieldListValue"
:record-data="recordData"
:execution-listener-array="executionListenerArray"
:task-listener-array="taskListenerArray"
:selector-api-function="selectorApiFunction"
>
<template #default="slot">
<node-wrap
v-if="slot.node"
v-model="slot.node.childNode"
:formFieldListValue="formFieldListValue"
:recordData="recordData"
:processConfigInfo="processConfigInfo"
:execution-listener-array="executionListenerArray"
:task-listener-array="taskListenerArray"
:selector-api-function="selectorApiFunction"
/>
</template>
</exclusive-gateway>
<parallel-gateway
v-if="childNode.type === 'parallelGateway'"
v-model="childNode"
:formFieldListValue="formFieldListValue"
:recordData="recordData"
:processConfigInfo="processConfigInfo"
:execution-listener-array="executionListenerArray"
:task-listener-array="taskListenerArray"
:selector-api-function="selectorApiFunction"
>
<template #default="slot">
<node-wrap
v-if="slot.node"
v-model="slot.node.childNode"
:formFieldListValue="formFieldListValue"
:recordData="recordData"
:processConfigInfo="processConfigInfo"
:execution-listener-array="executionListenerArray"
:task-listener-array="taskListenerArray"
:selector-api-function="selectorApiFunction"
/>
</template>
</parallel-gateway>
<node-wrap
v-if="childNode.childNode"
v-model="childNode.childNode"
:formFieldListValue="formFieldListValue"
:recordData="recordData"
:processConfigInfo="processConfigInfo"
:execution-listener-array="executionListenerArray"
:task-listener-array="taskListenerArray"
:selector-api-function="selectorApiFunction"
/>
</template>
<script>
import startEvent from './nodes/startEvent.vue'
import startTask from './nodes/startTask.vue'
import userTask from './nodes/userTask.vue'
import exclusiveGateway from './nodes/exclusiveGateway.vue'
import parallelGateway from './nodes/parallelGateway.vue'
import serviceTask from './nodes/serviceTask.vue'
export default {
components: {
startEvent,
startTask,
userTask,
exclusiveGateway,
parallelGateway,
serviceTask
},
props: {
modelValue: { type: Object, default: () => {} },
formFieldListValue: { type: Array, default: () => [] },
recordData: { type: Object, default: () => {} },
processConfigInfo: { type: Object, default: () => {} },
executionListenerArray: { type: Array, default: () => [] },
taskListenerArray: { type: Array, default: () => [] },
selectorApiFunction: { type: Object, default: () => {} }
},
data() {
return {
childNode: {}
}
},
watch: {
modelValue(val) {
this.childNode = val
},
childNode(val) {
this.$emit('update:modelValue', val)
}
},
mounted() {
this.childNode = this.modelValue
}
}
</script>

View File

@@ -0,0 +1,152 @@
<template>
<div class="add-node-btn-box">
<div class="add-node-btn">
<a-popover v-model:visible="visible" placement="rightTop" trigger="click" :width="270">
<template #content>
<div class="add-node-popover-body">
<ul style="height: 80px">
<li>
<a-button shape="circle" size="large" @click="addType('userTask')">
<template #icon>
<user-outlined style="color: #ff943e; font-size: 18px" />
</template>
</a-button>
<p>审批节点</p>
</li>
<li>
<a-button shape="circle" size="large" @click="addType('serviceTask')">
<template #icon>
<send-outlined style="color: #3296fa; font-size: 18px" />
</template>
</a-button>
<p>抄送节点</p>
</li>
<li v-if="addExclusiveGateway">
<a-button shape="circle" size="large" @click="addType('exclusiveGateway')">
<template #icon>
<share-alt-outlined style="color: #15bc83; font-size: 18px" />
</template>
</a-button>
<p>条件分支</p>
</li>
<li v-if="addParallelGateway">
<a-button shape="circle" size="large" @click="addType('parallelGateway')">
<template #icon>
<partition-outlined :rotate="180" style="color: #ac28f5; font-size: 18px" />
</template>
</a-button>
<p>并行分支</p>
</li>
</ul>
</div>
</template>
<a-button type="primary" shape="circle">
<!-- @click="addNodeButton" -->
<template #icon><plus-outlined /></template>
</a-button>
</a-popover>
</div>
</div>
</template>
<script>
import { cloneDeep } from 'lodash-es'
import config from '@/components/XnWorkflow/nodes/config/config'
const NodeTitleMap = {
userTask: '审核人',
serviceTask: '抄送人',
exclusiveGateway: '条件路由',
parallelGateway: '并行路由'
}
export default {
props: {
modelValue: { type: Object, default: () => {} },
parentData: { type: Object, default: () => {} },
nodeItem: { type: Object, default: () => {} }
},
emits: ['update:modelValue'],
data() {
return {
visible: false,
addExclusiveGateway: true,
addParallelGateway: true
}
},
mounted() {},
methods: {
addNodeButton() {
// 他的上级是条件分支或并行分支,将其不在添加 // 控制节点下面
if (!this.parentData) {
this.disabledChildren()
} else {
if (this.parentData.type === 'exclusiveGateway' || this.parentData.type === 'parallelGateway') {
this.addExclusiveGateway = false
this.addParallelGateway = false
}
}
},
disabledChildren() {
// 如果下级是条件分支或并行分支,将其不在添加 // 控制节点上面
if (this.modelValue && this.modelValue.type) {
if (this.modelValue.type === 'exclusiveGateway' || this.modelValue.type === 'parallelGateway') {
this.addExclusiveGateway = false
this.addParallelGateway = false
}
}
// 不管其他的,如果是条件分支的项,那么他的下面无法添加条件
if (this.nodeItem) {
this.addExclusiveGateway = false
}
},
getBaseCondition(type, title) {
const condition = cloneDeep(config.nodeModel.node)
condition.id = this.$TOOL.snowyUuid()
condition.type = type
condition.title = title
return condition
},
addType(type) {
const nodeModel = this.getBaseCondition(type, NodeTitleMap[type]) || {}
nodeModel.childNode = this.modelValue
if (type === 'userTask') {
// 创建 configInfo
const configInfo = cloneDeep(config.nodeConfigInfo.userTaskConfigInfo)
nodeModel.properties.configInfo = configInfo
} else if (type === 'exclusiveGateway') {
nodeModel.dataLegal = true
// 创建分支节点1
const condition1 = this.getBaseCondition('sequenceFlow', '条件1')
// 创建分支节点1 configInfo
const condition1ConfigInfo1 = cloneDeep(config.nodeConfigInfo.conditionConfigInfo)
condition1ConfigInfo1.priorityLevel = 1
condition1.properties.configInfo = condition1ConfigInfo1
// 创建分支节点2
const condition2 = this.getBaseCondition('sequenceFlow', '条件2')
// 创建分支节点2 configInfo
const condition1ConfigInfo2 = cloneDeep(config.nodeConfigInfo.conditionConfigInfo)
condition1ConfigInfo2.priorityLevel = 2
condition2.properties.configInfo = condition1ConfigInfo2
// 装进去
nodeModel.conditionNodeList.push(condition1)
nodeModel.conditionNodeList.push(condition2)
} else if (type === 'parallelGateway') {
// 创建主节点
nodeModel.dataLegal = true
// 创建分支节点1
const condition1 = this.getBaseCondition('userTask', '审批人1')
condition1.properties.configInfo = cloneDeep(config.nodeConfigInfo.userTaskConfigInfo)
condition1.dataLegal = true
// 创建分支节点2
const condition2 = this.getBaseCondition('userTask', '审批人2')
condition2.properties.configInfo = cloneDeep(config.nodeConfigInfo.userTaskConfigInfo)
condition2.dataLegal = true
// 装进去
nodeModel.conditionNodeList.push(condition1)
nodeModel.conditionNodeList.push(condition2)
}
this.visible = false
this.$emit('update:modelValue', nodeModel)
}
}
}
</script>

View File

@@ -0,0 +1,5 @@
<template>
<div class="add-node-btn-box">
<div class="add-node-btn"></div>
</div>
</template>

View File

@@ -0,0 +1,41 @@
<template>
<a-modal
title="预览"
:width="700"
:visible="visible"
:destroy-on-close="true"
:footer-style="{ textAlign: 'right' }"
:mask="false"
:confirmLoading="confirmLoading"
@ok="onSubmit"
@cancel="onClose"
>
<component ref="customFormRef" :is="customFormsLayouts" />
</a-modal>
</template>
<script setup name="previewCustomForm">
import { ElMessage } from 'element-plus'
import { loadComponent } from '../../customForm'
const visible = ref(false)
const confirmLoading = ref(false)
const customFormRef = ref()
const customFormsLayouts = ref()
const onOpen = (url) => {
if (url) {
visible.value = true
customFormsLayouts.value = loadComponent(url)
}
}
const onSubmit = () => {
customFormRef.value.getData().then((value) => {
message.info(JSON.stringify(value))
})
}
const onClose = () => {
visible.value = false
}
defineExpose({
onOpen
})
</script>

View File

@@ -0,0 +1,581 @@
export default {
// 模型结构,就是每个节点,都有的
nodeModel: {
// 节点
node: {
id: '', // 节点id
title: '', // 节点名称
type: '', // 节点类型
dataLegal: false, // 信息是否完整
// content:'', // 内容、备注
properties: {
configInfo: {}, // 除条件路由和分合流节点外均有
conditionInfo: [], // 条件信息, 条件节点特有
participateInfo: [], // 参与人信息,开始节点、审批节点、抄送节点才有
buttonInfo: [], // 按钮信息, 开始节点、审批节点、抄送节点才有
fieldInfo: [], // 字段信息, 开始节点、审批节点、抄送节点才有
commentList: [], // 流转记录内容
formInfo: [], // 自定义表单 发起、审批节点都有
executionListenerInfo: [], // 执行监听器信息,所有节点都有
taskListenerInfo: [] // 任务监听器信息(审批节点特有)
// noticeInfo: [], // 通知信息, 开始节点、审批节点、抄送节点才有
},
childNode: {}, // 子节点
conditionNodeList: [] // 条件子节点
},
// 通用按钮默认配置,不需要改动,除非自定义开发的时候,再加什么按钮
buttonInfo: [
{
key: 'SAVE',
label: '保存',
value: 'HIDE',
type: 'default',
icon: 'save-outlined'
},
{
key: 'SUBMIT',
label: '提交',
value: 'HIDE',
type: 'primary',
icon: 'check-circle-outlined'
},
{
key: 'PASS',
label: '同意',
value: 'HIDE',
type: 'primary',
icon: 'check-outlined'
},
{
key: 'REJECT',
label: '拒绝',
value: 'HIDE',
type: 'danger',
icon: 'close-outlined'
},
{
key: 'BACK',
label: '退回',
value: 'HIDE',
type: 'default',
icon: 'rollback-outlined'
},
{
key: 'JUMP',
label: '跳转',
value: 'HIDE',
type: 'default',
icon: 'ungroup-outlined'
},
{
key: 'ADD_SIGN',
label: '加签',
value: 'HIDE',
type: 'default',
icon: 'tags-outlined'
},
{
key: 'PRINT',
label: '打印',
value: 'HIDE',
type: 'default',
icon: 'printer-outlined'
},
{
key: 'TURN',
label: '转办',
value: 'HIDE',
type: 'default',
icon: 'user-switch-outlined'
}
],
fieldInfo: [],
formInfo: [
{
key: 'FORM_URL',
label: 'PC端表单组件',
value: ''
},
{
key: 'MOBILE_FORM_URL',
label: '移动端表单组件',
value: ''
}
],
executionListenerInfo: [],
taskListenerInfo: [],
noticeInfo: []
},
// 各个节点的configInfo,他们长的都不一样
nodeConfigInfo: {
// 全局配置
processConfigInfo: {
// 基础配置
processSnTemplateId: undefined, // 流水号模板 id
processPrintTemplateId: undefined, // 打印模板 id
processTitleTemplate: 'initiator 的 processName - startTime', // 标题模板
processAbstractTemplate: '', // 摘要模板
processEnableAutoDistinct: false, // 开启自动去重
processAutoDistinctType: 'SAMPLE', // 自动去重类型
processEnableRevoke: true, // 开启审批撤销
processEnableCommentRequired: false, // 开启意见必填
// 通知配置
processEnableBackNotice: false, // 开启退回通知
processEnableTodoNotice: false, // 开启待办通知
processEnableCopyNotice: false, // 开启抄送通知
processEnableCompleteNotice: false, // 开启完成通知
// 通知的方式
processBackNoticeChannel: ['MSG'], // 退回通知渠道
processTodoNoticeChannel: ['MSG'], // 待办通知渠道
processCopyNoticeChannel: ['MSG'], // 抄送通知渠道
processCompleteNoticeChannel: ['MSG'], // 完成通知渠道
// 通知配置对应的模板
processBackNoticeTemplate: '您于 startTime 发起的 processName 被退回', // 退回通知模板
processTodoNoticeTemplate: '由 initiator 发起的 processName 需要您审批', // 待办通知模板
processCopyNoticeTemplate: '您收到一条由 initiator 发起的 processName 的抄送', // 抄送通知模板
processCompleteNoticeTemplate: '您于 startTime 发起的 processName 审批通过', // 完成通知模板
// 全局自定义表单
processStartTaskFormUrl: '', // 全局PC申请单
processStartTaskMobileFormUrl: '', // 全局移动端申请单
processUserTaskFormUrl: '', // 全局PC审批单
processUserTaskMobileFormUrl: '' // 全局移动端审批单
},
// 审核节点配置
userTaskConfigInfo: {
userTaskType: 'ARTIFICIAL', // 任务节点类型
userTaskRejectType: 'TO_START', // 审批退回类型
userTaskMulApproveType: 'SEQUENTIAL', // 多人审批时类型
userTaskEmptyApproveType: 'AUTO_COMPLETE', // 审批人为空时类型
userTaskEmptyApproveUser: '', // 审批人为空时转交人id
userTaskEmptyApproveUserArray: [] // 审批人为空时转交人包含name用来前端回显后端不解析
},
// 条件中默认的配置
conditionConfigInfo: {
priorityLevel: 1 // 优先级 默认1
},
// 抄送节点配置
serviceTaseConfigInfo: {}
},
// 按钮相关配置
button: {
// 发起人节点 默认选中按钮
startTaskDefaultButtonkey: ['SAVE', 'SUBMIT'],
// 发起人节点 默认不让选的按钮 // 这个节点屏蔽下面配置的
startTaskNoCheckedButtonkey: ['PASS', 'PRINT', 'REJECT', 'BACK', 'ADD_SIGN', 'TURN', 'JUMP'],
// 审批人节点 默认选中按钮
userTaskDefaultButtonkey: ['PASS', 'REJECT'],
// 审批节点 默认不让选的
userTaskNoCheckedButtonkey: ['SUBMIT', 'SAVE'],
// 抄送人节点 默认选中按钮
serviceTaskDefaultButtonkey: ['SUBMIT']
},
// 字段相关配置
field: {
// 其他节点中字段对象数据模型
fieldModel: {
key: '',
label: '',
value: 'WRITE', // 默认
required: false, // 必填
extJson: '' // 额外扩展,暂无
},
// 审批节点中字段对象数据模型
userTaskFieldModel: {
key: '',
label: '',
value: 'READ', // 默认设为只读
required: false, // 必填
extJson: '' // 额外扩展,暂无
},
// 字段列表中的字典
fieldRadioList: [
{
label: '可编辑',
value: 'WRITE'
},
{
label: '只读',
value: 'READ'
},
{
label: '隐藏',
value: 'HIDE'
}
]
},
// 监听器配置,不同的地方不同的选项
listener: {
// 全局执行监听可以选择的
processExecutionListenerInfo: [
{
key: 'START',
label: '开始',
value: '',
extJson: ''
},
{
key: 'END',
label: '完成',
value: '',
extJson: ''
},
{
key: 'REJECT',
label: '拒绝',
value: '',
extJson: ''
},
{
key: 'CLOSE',
label: '终止',
value: '',
extJson: ''
},
{
key: 'REVOKE',
label: '撤回',
value: '',
extJson: ''
},
{
key: 'DELETE',
label: '删除',
value: '',
extJson: ''
}
],
// 条件执行监听可以选择的
exclusiveGatewayExecutionListenerInfo: [
{
key: 'TAKE',
label: '到达',
value: '',
extJson: ''
}
],
// 开始节点执行监听可选择的
startTaskExecutionListenerInfo: [
{
key: 'START',
label: '开始',
value: '',
extJson: ''
},
{
key: 'END',
label: '结束',
value: '',
extJson: ''
}
],
// 审批节点执行监听可以选择的
userTaskExecutionListenerInfo: [
{
key: 'START',
label: '开始',
value: '',
extJson: ''
},
{
key: 'END',
label: '结束',
value: '',
extJson: ''
}
],
// 抄送节点执行监听可以选择的
serviceTaskExecutionListenerInfo: [
{
key: 'START',
label: '开始',
value: '',
extJson: ''
},
{
key: 'END',
label: '结束',
value: '',
extJson: ''
}
],
// 审批节点任务监听
userTaskTaskListenerInfo: [
{
key: 'CREATE',
label: '创建',
value: '',
extJson: ''
},
{
key: 'ASSIGNMENT',
label: '分配',
value: '',
extJson: ''
},
{
key: 'COMPLETE',
label: '完成',
value: '',
extJson: ''
},
{
key: 'DELETE',
label: '删除',
value: '',
extJson: ''
},
{
key: 'UPDATE',
label: '更新',
value: '',
extJson: ''
},
{
key: 'TIMEOUT',
label: '超时',
value: '',
extJson: ''
}
]
},
// 通知方式字典
noticeInfoList: [
{
label: '短信',
value: 'SMS'
},
{
label: '邮件',
value: 'EMAIL'
},
{
label: '站内信',
value: 'MSG'
}
],
// 模板默认自带字段
templateDefaultFields: [
{
label: '发起人',
value: 'initiator'
},
{
label: '流程名称',
value: 'processName'
},
{
label: '发起时间',
value: 'startTime'
},
{
label: '表单字段',
value: 'disabled' // 这里表示在组件显示的时候就截至了,是下一梭子数组的标题哦
}
],
// 审批节点
userTaskConfig: {
// 审批人员类型
userSelectionTypeList: [
{
label: '机构',
value: 'ORG'
},
{
label: '角色',
value: 'ROLE'
},
{
label: '职位',
value: 'POSITION'
},
{
label: '部门主管',
value: 'ORG_LEADER'
},
{
label: '上级主管',
value: 'SUPERVISOR'
},
{
label: '表单内的人',
value: 'FORM_USER'
},
{
label: '表单内的人上级主管',
value: 'FORM_USER_SUPERVISOR'
},
{
label: '连续多级主管',
value: 'MUL_LEVEL_SUPERVISOR'
},
{
label: '表单内的人连续多级主管',
value: 'FORM_USER_MUL_LEVEL_SUPERVISOR'
},
{
label: '用户',
value: 'USER'
},
{
label: '发起人',
value: 'INITIATOR'
}
],
// 任务节点类型
userTaskTypeList: [
{
label: '人工审批',
value: 'ARTIFICIAL'
},
{
label: '自动通过',
value: 'COMPLETE'
},
{
label: '自动拒绝',
value: 'REJECT'
}
],
// 审批退回类型
userTaskRejectTypeList: [
{
label: '开始节点',
value: 'TO_START'
},
{
label: '自选节点',
value: 'USER_SELECT'
},
{
label: '自动结束',
value: 'AUTO_END'
}
],
// 多人审批时类型
userTaskMulApproveTypeList: [
{
label: '依次审批',
value: 'SEQUENTIAL'
},
{
label: '会签(须所有审批人同意)',
value: 'COUNTERSIGN'
},
{
label: '或签(一名审批人同意或拒绝即可)',
value: 'ORSIGN'
}
],
// 审批人为空时类型
userTaskEmptyApproveTypeList: [
{
label: '自动通过',
value: 'AUTO_COMPLETE'
},
{
label: '自动转交给某个人',
value: 'AUTO_TURN'
}
],
// 主管层级
levelSupervisorList: [
{
label: '最高层主管',
value: '-1'
},
{
label: '直接主管',
value: '1'
},
{
label: '第2级主管',
value: '2'
},
{
label: '第3级主管',
value: '3'
},
{
label: '第4级主管',
value: '4'
},
{
label: '第5级主管',
value: '5'
},
{
label: '第6级主管',
value: '6'
},
{
label: '第7级主管',
value: '7'
},
{
label: '第8级主管',
value: '8'
},
{
label: '第9级主管',
value: '9'
},
{
label: '第10级主管',
value: '10'
}
]
},
// 条件节点
exclusiveGatewayConfig: {
operatorList: [
{
label: '等于',
value: '=='
},
{
label: '不等于',
value: '!='
},
{
label: '大于',
value: '>'
},
{
label: '大于等于',
value: '>='
},
{
label: '小于',
value: '<'
},
{
label: '小于等于',
value: '<='
},
{
label: '包含',
value: 'include'
},
{
label: '不包含',
value: 'notInclude'
}
]
},
// 抄送节点
serviceTaskConfig: {
// 抄送人员类型
userSelectionTypeList: [
{
label: '用户',
value: 'USER'
},
{
label: '表单内的人',
value: 'FORM_USER'
}
]
}
}

View File

@@ -0,0 +1,487 @@
<template>
<div class="branch-wrap">
<div class="branch-box-wrap">
<div class="branch-box">
<a-button class="add-branch" type="primary" shape="round" @click="addTerm"> 添加条件 </a-button>
<div v-for="(item, index) in childNode.conditionNodeList" :key="index" class="col-box">
<div class="condition-node">
<div class="condition-node-box">
<div class="auto-judge" @click="show(index)">
<div v-if="index != 0" class="sort-left" @click.stop="arrTransfer(index, -1)">
<left-outlined />
</div>
<div class="title">
<span class="node-title">{{ item.title }}</span>
<span class="priority-title">优先级{{ item.properties.configInfo.priorityLevel }}</span>
<close-outlined class="close" @click.stop="delTerm(index)" />
</div>
<div class="content">
<span v-if="toText(childNode, index)">{{ toText(childNode, index) }}</span>
<span v-else class="placeholder">请设置条件</span>
</div>
<div
v-if="index !== childNode.conditionNodeList.length - 1"
class="sort-right"
@click.stop="arrTransfer(index)"
>
<right-outlined />
</div>
</div>
<add-node v-model="item.childNode" :node-item="item" />
</div>
</div>
<slot v-if="item.childNode" :node="item" />
<div v-if="index === 0" class="top-left-cover-line" />
<div v-if="index === 0" class="bottom-left-cover-line" />
<div v-if="index === childNode.conditionNodeList.length - 1" class="top-right-cover-line" />
<div v-if="index === childNode.conditionNodeList.length - 1" class="bottom-right-cover-line" />
</div>
</div>
<add-node v-model="childNode.childNode" :parent-data="childNode" />
</div>
<xn-form-container
v-model:visible="drawer"
:destroy-on-close="true"
:width="700"
:body-style="{ 'padding-top': '0px' }"
>
<template #title>
<div class="node-wrap-drawer__title">
<label v-if="!isEditTitle" @click="editTitle">
{{ form.title }}
<edit-outlined class="node-wrap-drawer-title-edit" />
</label>
<a-input
v-if="isEditTitle"
ref="nodeTitle"
v-model:value="form.title"
allow-clear
@blur="saveTitle"
@keyup.enter="saveTitle"
/>
</div>
</template>
<a-layout-content>
<a-tabs v-model:activeKey="activeKey">
<a-tab-pane key="1" tab="条件配置" force-render>
<a-form layout="vertical">
<div v-show="!isNodeLegal(form)" style="margin-bottom: 10px">
<a-alert message="请填写完成所有项!" type="error" />
</div>
<div class="mb-2">
<span class="left-span-label">配置要执行的条件</span>
</div>
<p style="margin-bottom: 2px">
<a-button type="primary" round @click="addDynamicValidateForm" size="small">
<plus-outlined />
增加条件组
</a-button>
</p>
<a-form-item v-for="(domain, index) in dynamicValidateForm" :key="index">
<a-divider style="margin: 10px 0" />
<a-row>
<a-col :span="22">
<a-table :data-source="domain" size="small" :pagination="false">
<a-table-column data-index="field" title="条件字段" width="130">
<template #default="{ record }">
<a-select
v-model:value="record.field"
placeholder="请选择"
v-if="recordData.formType === 'DESIGN'"
>
<a-select-option
v-for="formField in fieldList"
:key="formField.model"
:value="formField.model"
@click="record.label = formField.label"
>{{ formField.label }}</a-select-option
>
</a-select>
<a-input v-model:value="record.field" placeholder="条件" v-else />
</template>
</a-table-column>
<a-table-column data-index="label" title="描述">
<template #default="{ record }">
<a-input v-model:value="record.label" placeholder="描述" />
</template>
</a-table-column>
<a-table-column data-index="operator" width="140">
<template #title>
<a-tooltip>
<template #title
>自定义表单模式下条件选择完全放开中文字段不可以使用大于大于等于小于小于等于</template
>
<question-circle-outlined />
运算符
</a-tooltip>
</template>
<template #default="{ record }">
<a-select v-model:value="record.operator" placeholder="请选择">
<a-select-option value="==">等于</a-select-option>
<a-select-option value="!=">不等于</a-select-option>
<a-select-option value=">" v-if="isSelectOption(record)">大于</a-select-option>
<a-select-option value=">=" v-if="isSelectOption(record)">大于等于</a-select-option>
<a-select-option value="<" v-if="isSelectOption(record)">小于</a-select-option>
<a-select-option value="<=" v-if="isSelectOption(record)">小于等于</a-select-option>
<a-select-option value="include" v-if="!isSelectOption(record, 'include')"
>包含</a-select-option
>
<a-select-option value="notInclude" v-if="!isSelectOption(record, 'notInclude')"
>不包含</a-select-option
>
</a-select>
</template>
</a-table-column>
<a-table-column data-index="value" width="100">
<template #title>
<a-tooltip>
<template #title>中文字段需判断等于值必须加入英文双引号</template>
<question-circle-outlined />
</a-tooltip>
</template>
<template #default="{ record }">
<a-input v-model:value="record.value" placeholder="值" />
</template>
</a-table-column>
<a-table-column data-index="value" title="移除" width="55">
<template #default="{ index }">
<a-button size="small" type="primary" danger ghost @click="deleteConditionList(index, domain)"
>移除</a-button
>
</template>
</a-table-column>
</a-table>
<a-button type="dashed" class="dashedButton" @click="addConditionList(index)">
<PlusOutlined />
增加条件
</a-button>
</a-col>
<a-col :span="2" class="deleteIcon">
<minus-circle-two-tone class="minusCircle" @click="delDomains(index)" />
</a-col>
</a-row>
</a-form-item>
</a-form>
</a-tab-pane>
<a-tab-pane key="2" tab="执行监听" force-render>
<prop-listener-info
ref="propExecutionListenerInfoRef"
:listenerValue="form.properties.executionListenerInfo"
:defaultListenerList="executionListenerInfo"
:listener-value-array="executionListenerArray"
/>
</a-tab-pane>
</a-tabs>
</a-layout-content>
<template #footer>
<a-button type="primary" style="margin-right: 8px" @click="save">保存</a-button>
<a-button @click="drawer = false">取消</a-button>
</template>
</xn-form-container>
</div>
</template>
<script>
import { cloneDeep, isEmpty } from 'lodash-es'
import addNode from './addNode.vue'
import config from '@/components/XnWorkflow/nodes/config/config'
import workFlowUtils from '@/components/XnWorkflow/nodes/utils/index'
import PropListenerInfo from './prop/propListenerInfo.vue'
export default {
components: {
addNode,
PropListenerInfo
},
props: {
modelValue: { type: Object, default: () => {} },
formFieldListValue: { type: Array, default: () => [] },
recordData: { type: Object, default: () => {} },
executionListenerArray: { type: Array, default: () => [] },
taskListenerArray: { type: Array, default: () => [] }
},
data() {
return {
childNode: {},
drawer: false,
isEditTitle: false,
index: 0,
form: {},
dynamicValidateForm: [],
fieldList: [],
activeKey: '1',
executionListenerInfo: cloneDeep(config.listener.exclusiveGatewayExecutionListenerInfo),
operatorList: cloneDeep(config.exclusiveGatewayConfig.operatorList)
}
},
watch: {
modelValue() {
this.childNode = this.modelValue
}
},
mounted() {
this.childNode = this.modelValue
// 把字段给掏出来
this.fieldList = workFlowUtils.getListField(this.formFieldListValue).map((m) => {
let type = m.type //slider rate number
if (type === 'slider' || type === 'rate' || type === 'number') {
m.type = 'number'
}
return {
label: m.label,
model: m.selectTable + '.' + m.model,
type: m.type
}
})
},
methods: {
show(index) {
this.index = index
this.form = {}
this.form = cloneDeep(this.childNode.conditionNodeList[index])
this.drawer = true
this.dynamicValidateForm = this.form.properties.conditionInfo
},
editTitle() {
this.isEditTitle = true
this.$nextTick(() => {
this.$refs.nodeTitle.focus()
})
},
saveTitle() {
this.isEditTitle = false
},
save() {
this.form.properties.conditionInfo = this.dynamicValidateForm
this.form.properties.executionListenerInfo = this.$refs.propExecutionListenerInfoRef.selectedListenerList()
if (this.isNodeLegal(this.form)) {
this.form.dataLegal = true
this.childNode.conditionNodeList[this.index] = this.form
this.setCalibration()
this.$emit('update:modelValue', this.childNode)
this.drawer = false
} else {
this.form.dataLegal = false
}
},
isSelectOption(record, value) {
if (this.recordData.formType === 'DESIGN') {
if (record.field) {
return this.fieldList.find((f) => f.model === record.field).type === 'number'
}
} else {
if (value === 'include' || value === 'notInclude') {
return false
} else {
return true
}
}
},
// 校验此条件是否通过
isNodeLegal(data) {
const priorityLevel = data.properties.configInfo.priorityLevel
const len = this.childNode.conditionNodeList.length
const priorityLevelMax = this.childNode.conditionNodeList[len - 1].properties.configInfo.priorityLevel
// 如果往其他条件的分支中增加,那我们一视同仁
if (priorityLevelMax === priorityLevel) {
if (this.dynamicValidateForm.length > 0) {
for (let i = 0; i < this.dynamicValidateForm.length; i++) {
const obj = this.dynamicValidateForm[i]
if (obj.length > 0) {
return this.isNodeLegalItem()
}
}
} else {
return true
}
} else {
return this.isNodeLegalItem()
}
},
// 设置校验
setCalibration() {
// 在数据返回更新之前,我要顺手吧优先级最后的条件校验设置为 true管他设没设
for (let i = 0; i < this.childNode.conditionNodeList.length; i++) {
let conditionNode = this.childNode.conditionNodeList[i]
// 取到优先级
const priorityLevel = conditionNode.properties.configInfo.priorityLevel
// 如果是最高的
if (priorityLevel === this.childNode.conditionNodeList.length) {
// 给成通过,不管他的条件,本身优先级最后的就是其他条件进入,一般也不设
conditionNode.dataLegal = true
} else {
// 其他地方的,判断是否有条件,无条件的统统给 false
if (conditionNode.properties.conditionInfo.length === 0) {
conditionNode.dataLegal = false
}
}
}
},
isNodeLegalItem() {
let objNum = 0
let successNum = 0
if (this.dynamicValidateForm.length > 0) {
for (let i = 0; i < this.dynamicValidateForm.length; i++) {
const obj = this.dynamicValidateForm[i]
let objNumItem = 0
if (!isEmpty(obj)) {
for (let a = 0; a < obj.length; a++) {
objNumItem++
if (this.isObjLegal(obj[a])) {
successNum++
}
}
objNum = objNum + objNumItem
} else {
objNum++
}
}
}
if (successNum !== 0) {
if (objNum === successNum) {
return true
}
}
return false
},
// 校验对象中是否有空值
isObjLegal(obj) {
let a = 0
for (let b in obj) {
if (!obj[b]) {
a++
}
}
if (a === 1) {
return true
} else {
return false
}
},
// 增加条件组
addDynamicValidateForm() {
this.dynamicValidateForm.push([])
},
// 删除条件组
delDomains(index) {
this.dynamicValidateForm.splice(index, 1)
},
addTerm() {
const len = this.childNode.conditionNodeList.length
const priorityLevel = this.childNode.conditionNodeList[len - 1].properties.configInfo.priorityLevel
// 创建分支节点 n
const condition = cloneDeep(config.nodeModel.node)
condition.id = this.$TOOL.snowyUuid()
condition.type = 'sequenceFlow'
condition.title = `条件${priorityLevel + 1}`
// 创建分支节点2 configInfo
const condition1ConfigInfo = cloneDeep(config.nodeConfigInfo.conditionConfigInfo)
condition1ConfigInfo.priorityLevel = priorityLevel + 1
condition.properties.configInfo = condition1ConfigInfo
this.childNode.conditionNodeList.push(condition)
},
delTerm(index) {
this.childNode.conditionNodeList.splice(index, 1)
if (this.childNode.conditionNodeList.length === 1) {
if (this.childNode.childNode) {
if (JSON.stringify(this.childNode.conditionNodeList[0].childNode) !== '{}') {
this.reData(this.childNode.conditionNodeList[0].childNode, this.childNode.childNode)
} else {
this.childNode.conditionNodeList[0].childNode = this.childNode.childNode
}
}
this.$emit('update:modelValue', this.childNode.conditionNodeList[0].childNode)
}
},
reData(data, addData) {
if (JSON.stringify(data) !== '{}') {
data.childNode = addData
} else {
this.reData(data.childNode, addData)
}
},
arrTransfer(index, type = 1) {
this.childNode.conditionNodeList[index] = this.childNode.conditionNodeList.splice(
index + type,
1,
this.childNode.conditionNodeList[index]
)[0]
this.childNode.conditionNodeList.map((item, index) => {
item.properties.configInfo.priorityLevel = index + 1
})
this.setCalibration()
this.$emit('update:modelValue', this.childNode)
},
addConditionList(index) {
const domainsObj = {
label: '',
key: '',
operator: '==',
value: ''
}
this.dynamicValidateForm[index].push(domainsObj)
},
deleteConditionList(index, domain) {
domain.splice(index, 1)
},
toText(childNode, index) {
const conditionList = childNode.conditionNodeList[index].properties.conditionInfo
const priorityLevel = childNode.conditionNodeList[index].properties.configInfo.priorityLevel
const len = this.childNode.conditionNodeList.length
const priorityLevelMax = this.childNode.conditionNodeList[len - 1].properties.configInfo.priorityLevel
if (JSON.stringify(conditionList) !== undefined && conditionList.length > 0) {
let text = ''
for (let i = 0; i < conditionList.length; i++) {
for (let j = 0; j < conditionList[i].length; j++) {
if (j + 1 !== conditionList[i].length) {
text =
text +
conditionList[i][j].label +
this.getOperatorLabel(conditionList[i][j].operator) +
conditionList[i][j].value +
' 且 '
} else {
text =
text +
conditionList[i][j].label +
this.getOperatorLabel(conditionList[i][j].operator) +
conditionList[i][j].value
}
}
if (i + 1 !== conditionList.length) {
text = text + ' 或 '
}
}
return text
} else if (conditionList.length === 0 && priorityLevel < priorityLevelMax) {
return false
} else {
return '其他条件进入此流程'
}
},
// 通过value 获取界面显示的label汉字
getOperatorLabel(value) {
return this.operatorList.find((item) => item.value === value).label
}
}
}
</script>
<style scoped lang="less">
.deleteIcon {
display: flex;
justify-content: center;
align-items: center;
}
.minusCircle {
font-size: 25px;
}
.dashedButton {
margin-top: 10px;
width: 100%;
}
</style>

View File

@@ -0,0 +1,110 @@
<template>
<div class="branch-wrap">
<div class="branch-box-wrap">
<div class="branch-box">
<a-button class="add-branch" type="primary" shape="round" @click="addTerm">添加并行</a-button>
<div v-for="(item, index) in childNode.conditionNodeList" :key="index" class="col-box">
<div class="condition-node">
<div class="condition-node-box">
<user-task
v-model="childNode.conditionNodeList[index]"
:form-field-list-value="formFieldListValue"
:recordData="recordData"
:processConfigInfo="processConfigInfo"
:execution-listener-array="executionListenerArray"
:task-listener-array="taskListenerArray"
:selector-api-function="selectorApiFunction"
@deleteParalle="delTerm(index)"
/>
</div>
</div>
<slot v-if="item.childNode" :node="item" />
<div v-if="index === 0" class="top-left-cover-line" />
<div v-if="index === 0" class="bottom-left-cover-line" />
<div v-if="index === childNode.conditionNodeList.length - 1" class="top-right-cover-line" />
<div v-if="index === childNode.conditionNodeList.length - 1" class="bottom-right-cover-line" />
</div>
</div>
<add-node v-model="childNode.childNode" :parent-data="childNode" />
</div>
</div>
</template>
<script>
import { cloneDeep } from 'lodash-es'
import addNode from './addNode.vue'
import userTask from './userTask.vue'
import config from '@/components/XnWorkflow/nodes/config/config'
export default {
components: {
addNode,
userTask
},
props: {
modelValue: { type: Object, default: () => {} },
formFieldListValue: { type: Array, default: () => [] },
recordData: { type: Object, default: () => {} },
processConfigInfo: { type: Object, default: () => {} },
executionListenerArray: { type: Array, default: () => [] },
taskListenerArray: { type: Array, default: () => [] },
selectorApiFunction: { type: Object, default: () => {} }
},
data() {
return {
childNode: {},
drawer: false,
isEditTitle: false,
index: 0,
form: {}
}
},
watch: {
modelValue() {
this.childNode = this.modelValue
}
},
mounted() {
this.childNode = this.modelValue
},
methods: {
addTerm() {
const len = this.childNode.conditionNodeList.length + 1
// 创建主节点
const nodeModel = cloneDeep(config.nodeModel.node)
nodeModel.id = this.$TOOL.snowyUuid()
nodeModel.type = 'userTask'
;(nodeModel.title = `审批人${len}`),
(nodeModel.priorityLevel = len),
(nodeModel.conditionNodeList = []),
(nodeModel.childNode = {})
// 创建 configInfo
const configInfo = cloneDeep(config.nodeConfigInfo.userTaskConfigInfo)
nodeModel.properties.configInfo = configInfo
this.childNode.conditionNodeList.push(nodeModel)
},
delTerm(index) {
this.childNode.conditionNodeList.splice(index, 1)
if (this.childNode.conditionNodeList.length == 1) {
if (this.childNode.childNode) {
// 这是{}
if (JSON.stringify(this.childNode.conditionNodeList[0].childNode) !== '{}') {
this.reData(this.childNode.conditionNodeList[0].childNode, this.childNode.childNode)
} else {
this.childNode.conditionNodeList[0].childNode = this.childNode.childNode
}
}
this.$emit('update:modelValue', this.childNode.conditionNodeList[0].childNode)
}
},
reData(data, addData) {
if (JSON.stringify(data) !== '{}') {
data.childNode = addData
} else {
this.reData(data.childNode, addData)
}
}
}
}
</script>

View File

@@ -0,0 +1,107 @@
<template>
<a-modal
v-model:visible="visible"
title="表单人员选择"
:mask-closable="false"
:destroy-on-close="true"
:width="600"
@ok="handleOk"
@cancel="handleCancel"
>
<div class="form-user-table">
<a-table
ref="table"
:columns="columns"
:data-source="dataSource"
:row-key="(record) => record.model"
:expand-row-by-click="true"
:row-selection="{ selectedRowKeys: state.selectedRowKeys, onChange: onSelectChange, type: 'radio' }"
:pagination="false"
size="small"
bordered
/>
</div>
</a-modal>
</template>
<script setup name="formUserSelector">
import workFlowUtils from '@/components/XnWorkflow/nodes/utils/index'
import { ElMessage } from 'element-plus'
const visible = ref(false)
const columns = [
{
title: '字段名',
dataIndex: 'label'
},
{
title: '字段',
dataIndex: 'model'
}
]
// 定义emit事件
const emit = defineEmits({ click: null })
const props = defineProps(['formFieldList'])
const dataSource = ref([])
dataSource.value = workFlowUtils.getListField(props.formFieldList).filter((f) => {
// 判断如果是人员类型的,就让列表显示
if (f.type === 'xnUserSelector') {
return f
}
}).map((m) => {
return {
label: m.label,
model: m.model,
field: m.model,
type: m.type
}
})
const selectedRowKeys = ref([])
// 设置默认选中的
const state = reactive({
selectedRowKeys: selectedRowKeys.value,
loading: false
})
const showFormUserModal = (data = '') => {
if (dataSource.value.length === 0) {
message.warning('表单内无人员可提供选择!')
return
}
if (dataSource.value.length === 1) {
emit('click', dataSource.value[0])
message.success('表单内仅有一个人员选择,默认选中!')
} else {
visible.value = true
state.selectedRowKeys[0] = data
}
}
// 点击复选框回调
const onSelectChange = (selectedRowKeys) => {
state.selectedRowKeys = selectedRowKeys
}
// 确定
const handleOk = () => {
const result = dataSource.value.filter((item) => state.selectedRowKeys[0] === item.model)
emit('click', result[0])
visible.value = false
state.selectedRowKeys = []
}
// 关闭
const handleCancel = () => {
visible.value = false
}
// 抛出方法,让上个界面使用
defineExpose({
showFormUserModal
})
</script>
<style lang="less">
.form-user-table {
overflow: auto;
max-height: 400px;
}
</style>

View File

@@ -0,0 +1,157 @@
<template>
<div class="mb-2">
<span class="left-span-label">参与者可以看见或操作哪些按钮</span>
</div>
<a-table
ref="table"
:columns="columns"
:data-source="dataSource"
:row-key="(record) => record.key"
:expand-row-by-click="true"
:row-selection="{
selectedRowKeys: state.selectedRowKeys,
onChange: onSelectChange,
getCheckboxProps: checkboxProps
}"
:pagination="false"
:show-header="false"
size="small"
>
<template #bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'label'">
<a-popover trigger="hover" placement="left">
<template #content>
<a-button :type="record.type" size="small">
<template #icon>
<component :is="record.icon" />
</template>
{{ record.label }}
</a-button>
<div v-if="record.key === 'SAVE'" style="width: 300px">
”保存“按钮作用为发起节点保存操作,审批节点下无保存操作。
</div>
<div v-if="record.key === 'SUBMIT'" style="width: 300px">
”提交“按钮作用为发起节点填写完申请单,提交流程到下一步。
</div>
<div v-if="record.key === 'REVOKE'" style="width: 300px">”撤回“按钮作用为发起节点发错内容,将其撤回。</div>
<div v-if="record.key === 'PASS'" style="width: 300px">按钮作用为审批节点同意该审核之操作。</div>
<div v-if="record.key === 'REJECT'" style="width: 300px">按钮作用为审批节点进行拒绝之操作。</div>
<div v-if="record.key === 'BACK'" style="width: 300px">按钮作用为审批节点退回操作,可退回至任意节点。</div>
<div v-if="record.key === 'ADD_SIGN'" style="width: 300px">按钮作用为审批节点加签操作。</div>
<div v-if="record.key === 'TURN'" style="width: 300px">按钮作用为审批节点转给他人办理操作。</div>
<div v-if="record.key === 'JUMP'" style="width: 300px">按钮作用为审批节点可跳转至任意节点操作。</div>
<div v-if="record.key === 'PRINT'" style="width: 300px">
按钮配置后,操作人到该节点可以进行管理员配置的全局打印模板,进行打印此流程进展的内容。
</div>
</template>
<a-button size="small" :type="record.type">
<template #icon>
<component :is="record.icon" />
</template>
{{ record.label }}
</a-button>
</a-popover>
</template>
<template v-if="column.dataIndex === 'newLabel'">
<a-input v-model:value="record.newLabel" show-count :maxlength="10" />
</template>
</template>
</a-table>
</template>
<script setup name="propButtonInfo">
import config from '@/components/XnWorkflow/nodes/config/config'
import { cloneDeep } from 'lodash-es'
const columns = [
{
title: '',
dataIndex: 'label',
align: 'center'
},
{
title: '',
dataIndex: 'newLabel'
}
]
const buttonInfo = cloneDeep(config.nodeModel.buttonInfo)
const props = defineProps(['buttonInfo', 'showButton', 'noChecked'])
const dataSource = ref([])
const selectedRowKeys = ref([])
// 将其回传的跟本组件的关联性切断
selectedRowKeys.value = cloneDeep(props.showButton)
// 设置按钮选中及显示的
const buttonInfoData = () => {
buttonInfo.forEach((button) => {
if (props.buttonInfo.length > 0) {
props.buttonInfo.forEach((item) => {
if (button.key === item.key) {
button.newLabel = item.label
button.value = item.value
}
})
} else {
button.newLabel = button.label
}
// 匹配选中的
if (selectedRowKeys.value.length > 0) {
selectedRowKeys.value.forEach((selectedRowKey) => {
if (button.value === 'SHOW') {
// 已有的里面如果不包含这个已经被选中的
if (selectedRowKeys.value.indexOf(button.key) === -1) {
selectedRowKeys.value.push(button.key)
}
}
if (selectedRowKey === button.key) {
button.value = 'SHOW'
}
})
} else if (button.value === 'SHOW') {
if (selectedRowKeys.value.indexOf(button.key) === -1) {
selectedRowKeys.value.push(button.key)
}
}
})
return buttonInfo
}
dataSource.value = buttonInfoData()
// 设置默认选中的
const state = reactive({
selectedRowKeys: selectedRowKeys.value,
loading: false
})
// 点击复选框回调
const onSelectChange = (selectedRowKeys) => {
state.selectedRowKeys = selectedRowKeys
}
// 设置选择框的默认属性配置,将其设置成不可取消,不可选中
const checkboxProps = (record) => ({
disabled:
(record.value === 'SHOW' && props.showButton.indexOf(record.key) > -1) || props.noChecked.indexOf(record.key) > -1
})
// 父界面需要调用获取参数
const selectedButtonKeyList = () => {
let resultData = cloneDeep(dataSource.value)
resultData.forEach((dataItem) => {
if (state.selectedRowKeys.indexOf(dataItem.key) > -1) {
dataItem.value = 'SHOW'
} else {
dataItem.value = 'HIDE'
}
if (dataItem.newLabel !== '') {
dataItem.label = dataItem.newLabel
}
delete dataItem.newLabel
})
return resultData
}
// 抛出方法,让上个界面使用
defineExpose({
selectedButtonKeyList
})
</script>

View File

@@ -0,0 +1,94 @@
propButtonInfo
====
> 工作流中配置按钮选择器
> eg: 发起人、审批人、通知人 这三个节点使用
该组件由 [俞宝山](https://www.xiaonuo.vip) 封装
### 使用方式
```vue
<template>
<div>
<prop-button-info ref="propButtonInfo" :buttonInfo="selectedButtonInfo" :showButton="startTaskDefaultButtonkey"/>
</div>
</template>
<script setup>
// 配置的一些数据
import config from '@/components/XnWorkflow/nodes/config/config';
// 这个数据就是下面说的回显数据选中的数据对象中show值为true
const selectedButtonInfo = ref(
[
{
key: 'SAVE',
label: '保存',
show: false,
},
{
key: 'SUBMIT',
label: '提交',
show: false,
},
{
key: 'REVOKE',
label: '撤回',
show: false,
},
{
key: 'COMPLETE',
label: '同意',
show: false,
},
{
key: 'REJECT',
label: '驳回',
show: false,
},
{
key: 'PRINT',
label: '打印',
show: false,
},
]
)
// 某个节点调用此组件,不管三七二十一,这个是必须选中,而且不能取消的
const startTaskDefaultButtonkey = ref(
// 注意:使用静态配置中的数据,必须切断双向绑定,不能因为不切短改变原始数据
JSON.parse(JSON.stringify(config.button.startTaskDefaultButtonkey))
)
// 可以配置不让配置选择的按钮
const startTaskNoCheckedButtonkey = ref(
// 注意:使用静态配置中的数据,必须切断双向绑定,不能因为不切短改变原始数据
JSON.parse(JSON.stringify(config.button.startTaskNoCheckedButtonkey))
)
// 初始化我们调用这个组件的ref名称Vue3 setup语法糖的写法是这样的哦
const propButtonInfo = ref(null);
// 使用的界面调用这个方法,获得组件中选好的数据
const getButtonInfoData = () => {
console.log('result data :' + JSON.stringify(propButtonInfo.selectedButtonKeyList()))
}
</script>
```
### 事件
| 名称 | 说明 | 类型 | 默认值 |
| --------------------- | ----------------------------------- | ------ | ------ |
| selectedButtonKeyList | 调用界面使用此方法获取组件中选中的数据 | Array | [] |
### 数据
| 名称 | 说明 | 类型 | 默认值 |
| ---------- | ------------------------------------------------------------------------------------ | ------ | ------ |
| buttonInfo | 传送数据为此模型节点中选中的按钮用show=true标识示例`selectedButtonInfo` | Array | [] |
| showButton | 哪个节点调用,哪个节点设置他必选的,不能改变的按钮数据,示例:`startTaskDefaultButtonkey ` | Array | [] |
| noChecked | 默认不让哪个按钮在本节点进行配置,示例:`startTaskNoCheckedButtonkey ` | Array | [] |

View File

@@ -0,0 +1,141 @@
<template>
<div class="mb-2">
<span class="left-span-label">参与者可以看见或操作哪些字段</span>
</div>
<a-table
ref="table"
:columns="columns"
:data-source="dataSource"
:row-key="(record) => record.key"
:expand-row-by-click="true"
:pagination="false"
:show-header="false"
size="small"
>
<template #bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'label'">
<span v-if="record.required" style="color: red">* </span>
<span v-else>&nbsp;</span>
{{ record.label }}
</template>
<template v-if="column.dataIndex === 'value'">
<a-radio-group v-model:value="record.value" :disabled="record.disabled">
<a-radio :key="radio.value" v-for="radio in fieldRadioList" :value="radio.value">{{ radio.label }}</a-radio>
</a-radio-group>
</template>
</template>
</a-table>
</template>
<script setup name="propFieldInfo">
import config from '@/components/XnWorkflow/nodes/config/config'
const columns = [
{
title: '表单字段',
dataIndex: 'label',
width: 150
},
{
title: '',
dataIndex: 'value'
}
]
// 定义一个表单的已拖进去的组件列表,也就是我们要的字段列表
const listField = ref([])
let fieldRadioList = JSON.parse(JSON.stringify(config.field.fieldRadioList))
// 接受到值
const props = defineProps(['formFieldListValue', 'fieldInfo', 'defaultFieldModel'])
const dataSource = ref([])
const getListField = () => {
listField.value = []
// 递归遍历控件树
const traverse = (array) => {
array.forEach((element) => {
if (element.type === 'grid' || element.type === 'tabs') {
// 栅格布局 and 标签页
element.columns.forEach((item) => {
traverse(item.list)
})
} else if (element.type === 'card') {
// 卡片布局 and 动态表格
traverse(element.list)
} else if (element.type === 'table') {
// 表格布局
element.trs.forEach((item) => {
item.tds.forEach((val) => {
traverse(val.list)
})
})
} else {
const type = element.type
// 排除一些
if ((type !== 'alert') & (type !== 'text') & (type !== 'divider') & (type !== 'html')) {
// & (type !== 'batch')
listField.value.push(element)
}
}
})
}
traverse(props.formFieldListValue)
}
if (props.formFieldListValue.length > 0) {
getListField()
// 数据转换至模型中
listField.value.forEach((item) => {
// 创建数据模型
let dataModel = {}
// 如果调用此组件的节点没配置它默认的选中按钮,那么就用通用的
if (props.defaultFieldModel) {
dataModel = JSON.parse(JSON.stringify(props.defaultFieldModel))
} else {
dataModel = JSON.parse(JSON.stringify(config.field.fieldModel))
}
dataModel.key = item.model
// 假如有选中的值,我们将其回显
if (props.fieldInfo.length > 0) {
props.fieldInfo.forEach((field) => {
if (field.key === item.model) {
dataModel.value = field.value
}
})
}
if (item.rules) {
dataModel.required = item.rules[0].required
} else {
dataModel.required = false
}
// 判断有没有从表单那边勾选了隐藏跟禁用
if (item.options.hidden || item.options.disabled) {
dataModel.disabled = true
// 并且设置它选中的选项为隐藏活禁用
if (item.options.disabled) {
dataModel.value = 'READ' // 只读
}
// 如果设置了隐藏跟禁用,我们按隐藏来算
if (item.options.hidden) {
dataModel.value = 'HIDE' // 隐藏
}
} else {
dataModel.disabled = false
}
dataModel.label = item.label
dataSource.value.push(dataModel)
})
}
// 父界面需要调用获取参数
const selectedFieldList = () => {
let resultData = JSON.parse(JSON.stringify(dataSource.value))
return resultData
}
// 抛出方法,让上个界面使用
defineExpose({
selectedFieldList
})
</script>
<style scoped></style>

View File

@@ -0,0 +1,127 @@
propFieldInfo
====
> 工作流中配置字段选择器
> eg: 发起人、审批人这两个节点使用
该组件由 [俞宝山](https://www.xiaonuo.vip) 封装
### 使用方式
```vue
<template>
<div>
<prop-field-info ref="propFieldInfo" :field-info="fieldInfo" :default-field-model="defaultFieldModel" :form-field-list-value="formFieldListValue"/>
</div>
</template>
<script setup>
// 这个数据就是下面说的回显数据选中的数据对象中show值为true
const fieldInfo = ref(
{
"key":"input_1653160767462",
"label":"姓名",
"value":"WRITE",
"required":true,
},
{
"key":"input_1653160770827",
"label":"绰号",
"value":"WRITE",
"required":true,
}
)
// 这个是表单设计器选择后传过来的
const formFieldListValue = ref(
[
{
"type":"input",
"label":"姓名",
"icon":"icon-write",
"options":{
"type":"text",
"width":"100%",
"defaultValue":"",
"placeholder":"请输入",
"clearable":false,
"maxLength":null,
"addonBefore":"",
"addonAfter":"",
"hidden":false,
"disabled":false
},
"model":"input_1653160767462",
"key":"input_1653160767462",
"help":"",
"rules":[
{
"required":true,
"message":"必填项"
}
]
},
{
"type":"input",
"label":"绰号",
"icon":"icon-write",
"options":{
"type":"text",
"width":"100%",
"defaultValue":"",
"placeholder":"请输入",
"clearable":false,
"maxLength":null,
"addonBefore":"",
"addonAfter":"",
"hidden":false,
"disabled":false
},
"model":"input_1653160770827",
"key":"input_1653160770827",
"help":"",
"rules":[
{
"required":false,
"message":"必填项"
}
]
}
],
)
// 默认的节点选中
const defaultFieldModel = ref({
key: '',
label: '',
value: 'READ', // 默认设为只读
required: false, // 必填
extJson: '' // 额外扩展,暂无
})
// 初始化我们调用这个组件的ref名称Vue3 setup语法糖的写法是这样的哦
const propFieldInfo = ref(null);
// 使用的界面调用这个方法,获得组件中选好的数据
const selectedFieldList = () => {
console.log('result data :' + JSON.stringify(propFieldInfo.selectedFieldList()))
}
</script>
```
### 事件
| 名称 | 说明 | 类型 | 默认值 |
| --------------------- | ----------------------------------- | ------ | ------ |
| selectedFieldList | 调用界面使用此方法获取组件中选中的数据 | Array | [] |
### 数据
| 名称 | 说明 | 类型 | 默认值 |
| ---------- | ------------------------------------------------------------------------------------ | ------ | ------ |
| form-field-list-value | 表单设计器中拖拉拽过后的字段数据,示例:`formFieldListValue` | Array | [] |
| field-info | 这个节点已经选好勾好的数据,回显的,示例:`fieldInfo ` | Array | [] |
| default-field-model | 默认这个节点勾选的字段配置,示例:`defaultFieldModel ` | Array | [] |

View File

@@ -0,0 +1,158 @@
<template>
<div class="mb-2">
<span class="left-span-label">配置要执行的监听</span>
</div>
<a-button type="primary" size="small" @click="addEditListener" class="mb-2">
<template #icon>
<plus-outlined />
</template>
新增
</a-button>
<a-table
ref="table"
:columns="columns"
:data-source="dataSource"
:row-key="(record) => record.id"
bordered
:expand-row-by-click="true"
:pagination="false"
size="small"
>
<template #bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'listenerType'">
{{ listenerlabel(record.listenerType) }}
</template>
<template v-if="column.dataIndex === 'action'">
<a-popconfirm title="确定要删除此监听吗" @confirm="deleteListener(record)" placement="topRight">
<a-button type="link" danger size="small">删除</a-button>
</a-popconfirm>
</template>
</template>
</a-table>
<a-modal v-model:visible="modalVisible" title="新增" @ok="handleOk" @cancel="handleCancel">
<a-form ref="formRef" :model="formData" :rules="formRules" layout="vertical">
<a-form-item label="监听类型" name="listenerType">
<a-select v-model:value="formData.listenerType" placeholder="请选择类型" :options="listenerTypeOptions" @change="listenerType" />
</a-form-item>
<a-form-item label="JAVA监听器" name="javaClass">
<a-select v-model:value="formData.javaClass" placeholder="请选择JAVA监听器" :options="listenerValueArray" />
</a-form-item>
</a-form>
</a-modal>
</template>
<script setup name="PropListenerInfo">
import { required } from '@/utils/formRules'
import tool from '@/utils/tool'
import { ref } from 'vue'
const table = ref()
const listenerValueArray = ref([])
const columns = [
{
title: '名称',
dataIndex: 'listenerType',
width: 150
},
{
title: 'JAVA监听器',
dataIndex: 'javaClass',
ellipsis: true
},
{
title: '操作',
dataIndex: 'action',
width: 80
}
]
const dataSource = ref([])
const props = defineProps(['listenerValue', 'defaultListenerList', 'listenerValueArray', 'executionListenerSelectorForCustomEventArray', 'listenerType'])
if (props.listenerValue && props.listenerValue.length > 0) {
// 转换到列表
props.listenerValue.forEach((item) => {
const obj = {
listenerType: item.key,
javaClass: item.value,
id: tool.snowyUuid()
}
dataSource.value.push(obj)
})
}
// 加载
if (props.listenerValueArray) {
listenerValueArray.value = props.listenerValueArray
}
// 新增或编辑
const addEditListener = () => {
modalVisible.value = true
}
// 删除
const deleteListener = (record) => {
// 算了,什么也不说了,留下能用的
dataSource.value = dataSource.value.filter((item) => item.id !== record.id)
}
// 转换监听数据
const selectedListenerList = () => {
return dataSource.value.map((item) => {
return {
key: item.listenerType,
value: item.javaClass,
label: listenerlabel(item.listenerType)
}
})
}
// 通过key获得中文label
const listenerlabel = (key) => {
if (!props.defaultListenerList) {
return '请重新打开'
}
return props.defaultListenerList.find((f) => f.key === key).label
}
// 小对话框
const modalVisible = ref(false)
const formRef = ref()
const formData = ref({})
const formRules = {
listenerType: [required('请选择监听类型')],
javaClass: [required('请选择JAVA监听器')]
}
const listenerTypeOptions = props.defaultListenerList.map((item) => {
return {
label: item.label,
value: item.key
}
})
// 选择器类型监听
const listenerType = (value) => {
// 只有在全局情况下,才会去加载这些自定义的类
if (props.listenerType === 'global') {
formData.value.javaClass = undefined
listenerValueArray.value = []
// 拒绝、终止、撤回、删除 这些类型的时候,加载自定义的到选择列表
if (value === 'REJECT' || value === 'CLOSE' || value === 'REVOKE' || value === 'DELETE') {
if (props.executionListenerSelectorForCustomEventArray) {
listenerValueArray.value = props.executionListenerSelectorForCustomEventArray
}
} else {
if (props.listenerValueArray) {
listenerValueArray.value = props.listenerValueArray
}
}
}
}
const handleOk = () => {
formRef.value.validate().then(() => {
formData.value.id = tool.snowyUuid()
dataSource.value.push(formData.value)
handleCancel()
})
}
const handleCancel = () => {
formData.value = {}
modalVisible.value = false
}
// 抛出方法,让上个界面使用
defineExpose({
selectedListenerList
})
</script>

View File

@@ -0,0 +1,7 @@
<template></template>
<script>
export default {
name: 'PropNoticeInfo'
}
</script>

View File

@@ -0,0 +1,48 @@
<template>
<a-tag v-for="tag in tagList()">{{ tag }}</a-tag
><!-- 花里胡哨的颜色先去掉用的时候拿到旁边就行 :color="randomColor()"-->
</template>
<script setup name="propTag">
// 颜色列表
const colorList = ['pink', 'red', 'orange', 'green', 'cyan', 'blue', 'purple']
// 获取随机颜色
const randomColor = () => {
return colorList[randomNum(0, colorList.length - 1)]
}
// 获取minNum到maxNum内的随机数
const randomNum = (minNum, maxNum) => {
switch (arguments.length) {
case 1:
return parseInt(Math.random() * minNum + 1, 10)
break
case 2:
return parseInt(Math.random() * (maxNum - minNum + 1) + minNum, 10)
break
default:
return 0
break
}
}
const props = defineProps(['tagList'])
// 将配置的参数转换为数组显示tag标签
const tagList = () => {
const tag = props.tagList
if (tag === undefined) {
return []
} else {
if (!tag.label) {
return
}
const str = tag.label
let resultArray = []
if (str.indexOf(',') !== -1) {
resultArray = str.split(',')
} else {
resultArray.push(str)
}
return resultArray
}
}
</script>

View File

@@ -0,0 +1,220 @@
<template>
<div>
<a-textarea
ref="smsInput"
v-model:value="smsContent"
:auto-size="{ minRows: 3, maxRows: 6 }"
placeholder="请输入内容"
@input.native="smsInput"
@blur="inputBlur"
@focus="focusHandler"
@click.native="focusHandler"
@keydown.up.down.left.right.native="focusHandler"
@select.native="selectHandler"
></a-textarea>
<a-dropdown>
<template #overlay>
<a-menu>
<a-menu-item
v-for="fields in fieldInfoLisNew"
:key="fields.value"
:value="fields.value"
:disabled="fields.value === 'disabled'"
@click="insertFields(fields)"
>{{ fields.label }}</a-menu-item
>
</a-menu>
</template>
<a-button size="small" type="primary" style="float: right; margin-top: -35px; margin-right: 10px">
置入字段
<DownOutlined />
</a-button>
</a-dropdown>
</div>
</template>
<script>
import config from '@/components/XnWorkflow/nodes/config/config'
export default {
name: 'TemplateGenerator',
props: {
// 表单数据
fieldInfoLis: { type: Array, default: () => [] },
// v-model数据
inputValue: { type: String, default: () => '' }
},
data() {
return {
smsContent: '',
inputFocus: null,
fieldInfoLisNew: [],
visible: false
}
},
// 这里回显数据,并做转换
mounted() {
this.fieldInfoLisNew = [...JSON.parse(JSON.stringify(config.templateDefaultFields)), ...this.fieldInfoLis]
this.smsContent = this.setInput(this.inputValue)
},
methods: {
// 入数据转换
setInput(value) {
this.fieldInfoLisNew.forEach((field) => {
let temp = ''
if (value.indexOf(field.value) > -1) {
temp = value.replace(new RegExp(field.value, 'g'), '[' + field.label + ']')
}
if (temp !== '') {
value = temp
}
})
return value
},
// 选中置入的
insertFields(fields) {
this.insertStr('[' + fields.label + ']')
},
// 插入元素
insertStr(str) {
let before = this.smsContent.slice(0, this.inputFocus)
let after = this.smsContent.slice(this.inputFocus, this.smsContent.length)
this.inputFocus = this.inputFocus + str.length
this.smsContent = before + str + after
this.$emit('update:inputValue', this.outValue(this.smsContent))
},
// 保存光标位置
inputBlur(e) {
this.inputFocus = e.target.selectionStart
this.visible = false
},
// 删除元素剩余部分
smsInput(e) {
//deleteContentBackward==退格键 deleteContentForward==del键
if (e.inputType === 'deleteContentBackward' || e.inputType === 'deleteContentForward') {
let beforeIndex = 0
let afterIndex = 0
// 光标位置往前
for (let i = e.target.selectionStart - 1; i >= 0; i--) {
if (this.smsContent[i] === '[') {
beforeIndex = i
afterIndex = e.target.selectionStart
break
}
if (this.smsContent[i] === ']') {
break
}
}
// 光标位置往后
for (let i = e.target.selectionStart; i < this.smsContent.length; i++) {
if (this.smsContent[i] === ']') {
afterIndex = i + 1
beforeIndex = e.target.selectionStart
break
}
if (this.smsContent[i] === '[') {
break
}
}
if (beforeIndex === 0 && afterIndex === 0) {
this.$emit('update:inputValue', this.outValue(this.smsContent))
return
}
let beforeStr = this.smsContent.slice(0, beforeIndex)
let afterStr = this.smsContent.slice(afterIndex)
this.smsContent = beforeStr + afterStr
this.inputFocus = beforeStr.length
this.$nextTick(() => {
this.changeFocus(e.target, this.inputFocus, this.inputFocus)
})
}
// this.$emit("smsText", this.smsContent);
this.$emit('update:inputValue', this.outValue(this.smsContent))
},
// 选择元素剩余部分
selectHandler(e) {
// 光标开始位置往前
for (let i = e.target.selectionStart - 1; i >= 0; i--) {
if (this.smsContent[i] === '[') {
this.changeFocus(e.target, i, e.target.selectionEnd)
break
}
if (this.smsContent[i] === ']') {
break
}
}
// 光标结束位置往后
for (let i = e.target.selectionEnd; i < this.smsContent.length; i++) {
if (this.smsContent[i] === ']') {
this.changeFocus(e.target, e.target.selectionStart, i + 1)
break
}
if (this.smsContent[i] === '[') {
break
}
}
},
// 焦点跳出元素内
focusHandler(e) {
setTimeout(() => {
let selStart = e.target.selectionStart
let beforeArrLength = this.smsContent.slice(0, selStart).split('[').length
let afterArrLength = this.smsContent.slice(0, selStart).split(']').length
//根据'['和']'生成两个数组 判断数组长度 是否相等 不相等就不成对就移动光标
if (beforeArrLength !== afterArrLength) {
let pos = this.smsContent.indexOf(']', selStart) + 1
if (beforeArrLength > afterArrLength && e.code == 'ArrowLeft') {
//按下按键左箭头
pos = this.smsContent.lastIndexOf('[', selStart)
}
this.changeFocus(e.target, pos, pos)
}
}, 100)
},
// 修改光标位置
changeFocus(target, start, end) {
let range,
el = target
if (el.setSelectionRange) {
el.setSelectionRange(start, end)
} else {
range = el.createTextRange()
range.collapse(false)
range.select()
}
},
// 出口数据转换
outValue(result) {
let index = result.indexOf('[')
let num = 0
while (index !== -1) {
num++
index = result.indexOf('[', index + 1)
}
if (num > 0) {
for (let i = 1; i <= num; i++) {
let temp = ''
this.fieldInfoLisNew.forEach((field) => {
if (result.indexOf('[' + field.label + ']') > -1) {
temp = result.replace('[' + field.label + ']', field.value)
result = temp
}
})
}
}
return result
}
}
}
</script>
<style scoped>
.insert-list p {
text-align: center;
}
.insert-list div {
margin: 10px 0;
display: flex;
justify-content: space-between;
}
</style>

View File

@@ -0,0 +1,53 @@
templateGenerator
====
> 模板编辑器
> eg: 主要用于工作流中的各种模板,也可作为通知的
该组件由 [俞宝山](https://www.xiaonuo.vip) 封装
### 使用方式
```vue
<template>
<div>
<template-generator ref="templateGenerator" :fieldInfoLis="fieldInfoLis" v-model:inputValue="inputValue"/>
</div>
</template>
<script setup>
// 初始化我们调用这个组件的dom名称Vue3 setup语法糖的写法是这样的哦
const templateGenerator = ref(null)
// 这个数据就是下面说的,置入字段下拉中选择的参数
const fieldInfoLis = ref(
[
{
label: '名称',
value: 'name',
},
{
label: '这有是啥',
value: 'what',
}
]
)
// 组件回显数据
const inputValue = ref('发起人name在几点积分通知了内容为what')
// 使用的界面调用这个方法,获得组件中选好的数据
const selectedFieldList = () => {
console.log('result data :' + JSON.stringify(templateGenerator.getValue()))
}
</script>
```
### 数据
| 名称 | 说明 | 类型 | 默认值 |
| ---------- | ------------------------------------------------------------------------------------ | ------ | ------ |
| fieldInfoLis | 表单数据,下拉框中要选择的,示例:`fieldInfoLis` | Array | [] |
| inputValue | 组件中要回显的数据,示例:`inputValue ` | String | '' |

View File

@@ -0,0 +1,304 @@
<template>
<div class="node-wrap">
<div class="node-wrap-box" @click="show">
<div class="title" style="background: #3296fa">
<send-outlined class="icon" />
<span>{{ childNode.title }}</span>
<close-outlined class="close" @click.stop="delNode()" />
</div>
<div class="content">
<span v-if="toText(childNode)">{{ toText(childNode) }}</span>
<span v-else class="placeholder">请选择人员</span>
</div>
</div>
<add-node v-model="childNode.childNode"></add-node>
<xn-form-container
v-model:visible="drawer"
:destroy-on-close="true"
:width="700"
:body-style="{ 'padding-top': '0px' }"
>
<template #title>
<div class="node-wrap-drawer__title">
<label v-if="!isEditTitle" @click="editTitle"
>{{ form.title }}<edit-outlined class="node-wrap-drawer-title-edit" />
</label>
<a-input
v-if="isEditTitle"
ref="nodeTitle"
v-model:value="form.title"
allow-clear
@blur="saveTitle"
@keyup.enter="saveTitle"
/>
</div>
</template>
<a-layout-content>
<a-tabs v-model:activeKey="activeKey">
<a-tab-pane key="1" tab="抄送配置" force-render>
<a-form layout="vertical" :model="userTypeForm">
<div v-show="nodeLegal" style="margin-bottom: 10px">
<a-alert message="请选择抄送人员!" type="error" />
</div>
<div class="mb-2">
<span class="left-span-label">选择要抄送的人员</span>
</div>
<a-radio-group v-model:value="userSelectionType" style="width: 100%">
<a-row style="padding-bottom: 10px">
<a-col :span="8" v-for="userSelectionType in userSelectionTypeList" :key="userSelectionType.value">
<a-radio :value="userSelectionType.value" @click="selectionClick(userSelectionType.value)">
{{ userSelectionType.label }}
</a-radio>
</a-col>
</a-row>
</a-radio-group>
<a-form-item v-if="userSelectionType === 'USER'">
<a-button class="mt-2" type="primary" size="small" round @click="openSelector">
<template #icon>
<search-outlined />
</template>
选择
</a-button>
<p />
<prop-tag :tag-list="getTagList('USER')" />
</a-form-item>
<a-form-item v-if="recordData.formType === 'DESIGN' && userSelectionType === 'FORM_USER'">
<div class="mt-2">
<a-button type="primary" @click="openSelector" size="small">
<template #icon>
<search-outlined />
</template>
选择
</a-button>
</div>
<div class="mt-2">
<prop-tag
v-if="form.properties.participateInfo.length > 0 && form.properties.participateInfo[0].value !== ''"
:tag-list="form.properties.participateInfo[0]"
/>
</div>
</a-form-item>
<a-form-item
class="mt-2"
v-if="recordData.formType === 'DEFINE' && userSelectionType === 'FORM_USER'"
name="formUser"
:rules="[{ required: true, message: '请输入人员变量' }]"
>
<template #label>
<a-tooltip>
<template #title>单个字段可以采用字段名多个采用字段名1,字段名2,字段名3</template>
<question-circle-outlined />
人员变量
</a-tooltip>
</template>
<a-input
style="width: 50%"
v-model:value="userTypeForm.formUser"
allow-clear
@input="customFormUser"
@keyup.enter="customFormUser"
placeholder="请输入人员变量"
/>
</a-form-item>
</a-form>
</a-tab-pane>
<a-tab-pane key="2" tab="执行监听" force-render>
<prop-listener-info
ref="propExecutionListenerInfoRef"
:listener-value="form.properties.executionListenerInfo"
:default-listener-list="executionListenerInfo"
:listener-value-array="executionListenerArray"
/>
</a-tab-pane>
</a-tabs>
</a-layout-content>
<template #footer>
<a-button type="primary" style="margin-right: 8px" @click="save">保存</a-button>
<a-button @click="drawer = false">取消</a-button>
</template>
</xn-form-container>
<form-user-selector ref="formUserSelectorRef" :form-field-list="formFieldListValue" @click="userCallBack" />
<user-selector-plus
ref="userselectorPlus"
:org-tree-api="selectorApiFunction.orgTreeApi"
:user-page-api="selectorApiFunction.userPageApi"
:checked-user-list-api="selectorApiFunction.checkedUserListApi"
:data-is-converter-flw="true"
@onBack="userCallBack"
/>
</div>
</template>
<script>
import addNode from './addNode.vue'
import userSelectorPlus from '@/components/Selector/userSelectorPlus.vue'
import FormUserSelector from './prop/formUserSelector.vue'
import propTag from './prop/propTag.vue'
import { cloneDeep, isEmpty } from 'lodash-es'
import config from '@/components/XnWorkflow/nodes/config/config'
import PropListenerInfo from './prop/propListenerInfo.vue'
export default {
components: {
FormUserSelector,
addNode,
userSelectorPlus,
propTag,
PropListenerInfo
},
props: {
modelValue: { type: Object, default: () => {} },
recordData: { type: Object, default: () => {} },
executionListenerArray: { type: Array, default: () => [] },
selectorApiFunction: { type: Object, default: () => {} },
formFieldListValue: { type: Array, default: () => [] }
},
data() {
return {
childNode: {},
drawer: false,
isEditTitle: false,
activeKey: '1',
form: {},
nodeLegal: false,
userTypeForm: {},
executionListenerInfo: cloneDeep(config.listener.serviceTaskExecutionListenerInfo),
// 用户选择类型
userSelectionType: '',
userSelectionTypeList: cloneDeep(config.serviceTaskConfig.userSelectionTypeList)
}
},
watch: {
modelValue() {
this.childNode = this.modelValue
}
},
mounted() {
this.childNode = this.modelValue
},
methods: {
show() {
this.form = {}
this.form = cloneDeep(this.childNode)
this.drawer = true
if (!isEmpty(this.form.properties.participateInfo)) {
this.userSelectionType = this.form.properties.participateInfo[0].key
// 如果包含表单内的人
const formUserObj = this.form.properties.participateInfo.find((f) => f.key === 'FORM_USER')
if (formUserObj) {
this.userTypeForm.formUser = formUserObj.value
}
} else {
this.userSelectionType = 'USER'
this.userTypeForm = {}
this.nodeLegal = true
}
},
editTitle() {
this.isEditTitle = true
this.$nextTick(() => {
this.$refs.nodeTitle.focus()
})
},
saveTitle() {
this.isEditTitle = false
},
save() {
if (isEmpty(this.nodeLegal)) {
this.form.properties.executionListenerInfo = this.$refs.propExecutionListenerInfoRef.selectedListenerList()
this.form.dataLegal = true
this.$emit('update:modelValue', this.form)
this.drawer = false
}
},
delNode() {
this.$emit('update:modelValue', this.childNode.childNode)
},
selectHandle(data) {
this.$refs.userselectorPlus.showUserPlusModal(data)
},
// 选择器回调
userCallBack(value) {
if (isEmpty(value.label)) {
this.nodeLegal = true
} else {
this.nodeLegal = false
}
if (this.userSelectionType === 'USER') {
this.form.properties.participateInfo[0] = value
} else if (this.userSelectionType === 'FORM_USER') {
this.form.properties.participateInfo = [
{
key: 'FORM_USER',
label: this.userSelectionTypeList.filter((item) => item.value === 'FORM_USER')[0].label,
value: value.model
}
]
}
},
// 获取tag标签的内容
getTagList(type) {
const participateInfo = this.form.properties.participateInfo
if (participateInfo.length > 0) {
const obj = participateInfo.find((item) => item.key === type)
if (isEmpty(obj.label)) {
return undefined
} else {
return obj
}
} else {
return undefined
}
},
// 选择抄送人员类型
selectionClick(value) {
this.form.properties.participateInfo = []
this.userSelectionType = value
this.nodeLegal = true
},
// 打开选择器
openSelector() {
let data = this.form.properties.participateInfo
if (this.userSelectionType === 'USER') {
this.$refs.userselectorPlus.showUserPlusModal(data)
} else if (this.userSelectionType === 'FORM_USER') {
this.$refs.formUserSelectorRef.showFormUserModal(data[0])
}
},
// 监听自定义表单人员输入
customFormUser() {
if (this.userTypeForm.formUser) {
this.form.properties.participateInfo = [
{
key: 'FORM_USER',
label: '表单内的人',
value: this.userTypeForm.formUser
}
]
this.nodeLegal = false
} else {
this.nodeLegal = true
}
},
toText(childNode) {
if (!isEmpty(childNode)) {
const participateInfo = childNode.properties.participateInfo
if (participateInfo.length > 0) {
let resultArray = []
if (participateInfo[0].label.indexOf(',') !== -1) {
resultArray = participateInfo[0].label.split(',')
} else {
resultArray.push(participateInfo[0].label)
}
return resultArray.toString()
} else {
// return '未选择抄送人';
return false
}
} else {
return false
}
}
}
}
</script>

View File

@@ -0,0 +1,48 @@
<template>
<div class="node-wrap" />
</template>
<script>
export default {
props: {
modelValue: { type: Object, default: () => {} }
},
data() {
return {
childNode: {},
// drawer: false,
isEditTitle: false,
form: {}
}
},
watch: {
modelValue() {
this.childNode = this.modelValue
}
},
mounted() {
this.childNode = this.modelValue
},
methods: {
show() {
this.form = JSON.parse(JSON.stringify(this.childNode))
this.isEditTitle = false
this.drawer = true
},
// editTitle() {
// this.isEditTitle = true;
// this.$nextTick(() => {
// this.$refs.nodeTitle.focus();
// });
// },
// saveTitle() {
// this.isEditTitle = false;
// },
save() {
this.form.id = this.$TOOL.snowyUuid()
this.$emit('update:modelValue', this.form)
this.drawer = false
}
}
}
</script>

View File

@@ -0,0 +1,241 @@
<template>
<div class="node-wrap">
<div class="node-wrap-box start-node" @click="show">
<div class="title" style="background: #576a95">
<user-outlined class="icon" />
<span>{{ childNode.title }}</span>
</div>
<div class="content">
<span v-if="toText(childNode)">{{ toText(childNode) }}</span>
<span v-else class="placeholder">请配置字段与按钮</span>
</div>
</div>
<add-node v-model="childNode.childNode" />
<xn-form-container
v-model:visible="drawer"
:destroy-on-close="true"
:width="700"
:body-style="{ 'padding-top': '0px' }"
>
<template #title>
<div class="node-wrap-drawer__title">
<label v-if="!isEditTitle" @click="editTitle">
{{ form.title }}
<edit-outlined class="node-wrap-drawer-title-edit" />
</label>
<a-input
v-if="isEditTitle"
ref="nodeTitle"
v-model:value="form.title"
allow-clear
@blur="saveTitle"
@keyup.enter="saveTitle"
/>
</div>
</template>
<a-layout-content>
<a-tabs v-model:activeKey="activeKey">
<a-tab-pane key="1" tab="按钮配置" force-render>
<prop-button-info
ref="propButtonInfo"
:button-info="selectedButtonInfo"
:show-button="startTaskDefaultButtonkey"
:no-checked="startTaskNoCheckedButtonkey"
/>
</a-tab-pane>
<a-tab-pane key="2" tab="字段配置" force-render v-if="recordData.formType === 'DESIGN'">
<prop-field-info ref="propFieldInfo" :field-info="fieldInfo" :form-field-list-value="formFieldListValue" />
</a-tab-pane>
<a-tab-pane key="3" tab="表单配置" force-render v-else>
<div class="mb-2">
<span class="left-span-label">参与者可以填写的表单</span>
</div>
<a-form ref="startTaskFormRef" :model="formData" layout="vertical">
<a-form-item
label="节点表单"
name="FORM_URL"
:rules="[{ required: true, message: '请输入节点表单组件地址' }]"
>
<a-input
v-model:value="formData.FORM_URL"
addon-before="src/views/flw/customform/"
addon-after=".vue"
placeholder="请输入节点表单组件地址"
allow-clear
>
<template #suffix>
<a-button
v-if="formData.FORM_URL"
type="primary"
size="small"
@click="$refs.previewCustomFormRef.onOpen(formData.FORM_URL)"
>
预览
</a-button>
</template>
</a-input>
</a-form-item>
<a-form-item
label="移动端节点表单"
name="MOBILE_FORM_URL"
:rules="[{ required: true, message: '请输入移动端节点表单组件地址' }]"
>
<a-input
v-model:value="formData.MOBILE_FORM_URL"
addon-before="pages/flw/customform/"
addon-after=".vue"
placeholder="请输入移动端节点表单组件地址"
allow-clear
/>
</a-form-item>
</a-form>
</a-tab-pane>
<a-tab-pane key="4" tab="执行监听" force-render>
<prop-listener-info
ref="propExecutionListenerInfoRef"
:listener-value="form.properties.executionListenerInfo"
:default-listener-list="executionListenerInfo"
:listener-value-array="executionListenerArray"
/>
</a-tab-pane>
<a-tab-pane key="5" tab="节点监听" force-render>
<prop-listener-info
ref="propTaskListenerInfoRef"
:listener-value="form.properties.taskListenerInfo"
:default-listener-list="taskListenerInfo"
:listener-value-array="taskListenerArray"
/>
</a-tab-pane>
</a-tabs>
</a-layout-content>
<template #footer>
<a-button type="primary" style="margin-right: 8px" @click="save">保存</a-button>
<a-button @click="drawer = false">取消</a-button>
</template>
</xn-form-container>
<preview-custom-form ref="previewCustomFormRef" />
</div>
</template>
<script>
import { cloneDeep } from 'lodash-es'
import config from '@/components/XnWorkflow/nodes/config/config'
import addNode from './addNode.vue'
import propButtonInfo from './prop/propButtonInfo.vue'
import propFieldInfo from './prop/propFieldInfo.vue'
import propListenerInfo from './prop/propListenerInfo.vue'
import PreviewCustomForm from '@/components/XnWorkflow/nodes/common/previewCustomForm.vue'
export default {
components: {
addNode,
propButtonInfo,
propFieldInfo,
propListenerInfo,
PreviewCustomForm
},
props: {
modelValue: { type: Object, default: () => {} },
formFieldListValue: { type: Array, default: () => [] },
recordData: { type: Object, default: () => {} },
processConfigInfo: { type: Object, default: () => {} },
executionListenerArray: { type: Array, default: () => [] },
taskListenerArray: { type: Array, default: () => [] }
},
data() {
return {
childNode: {},
drawer: false,
isEditTitle: false,
form: {},
activeKey: '1',
selectedButtonInfo: [],
executionListenerInfo: cloneDeep(config.listener.startTaskExecutionListenerInfo),
taskListenerInfo: cloneDeep(config.listener.userTaskTaskListenerInfo),
startTaskDefaultButtonkey: cloneDeep(config.button.startTaskDefaultButtonkey),
startTaskNoCheckedButtonkey: cloneDeep(config.button.startTaskNoCheckedButtonkey),
fieldInfo: [],
formData: {}
}
},
watch: {
modelValue() {
this.childNode = this.modelValue
}
},
mounted() {
this.childNode = this.modelValue
},
methods: {
show() {
this.form = cloneDeep(this.childNode)
this.isEditTitle = false
this.drawer = true
this.selectedButtonInfo = this.form.properties.buttonInfo
this.fieldInfo = this.form.properties.fieldInfo
// 初始化自定义表单字段值
this.initFormInfo()
},
initFormInfo() {
const processConfigInfo = cloneDeep(this.processConfigInfo)
if (!this.form.properties.formInfo.find((f) => f.key === 'FORM_URL')) {
this.formData.FORM_URL = processConfigInfo.processStartTaskFormUrl
} else {
this.formData.FORM_URL = this.form.properties.formInfo.find((f) => f.key === 'FORM_URL').value
}
if (!this.form.properties.formInfo.find((f) => f.key === 'MOBILE_FORM_URL')) {
this.formData.MOBILE_FORM_URL = processConfigInfo.processStartTaskMobileFormUrl
} else {
this.formData.MOBILE_FORM_URL = this.form.properties.formInfo.find((f) => f.key === 'MOBILE_FORM_URL').value
}
},
editTitle() {
this.isEditTitle = true
this.$nextTick(() => {
this.$refs.nodeTitle.focus()
})
},
saveTitle() {
this.isEditTitle = false
},
save() {
this.form.id = this.$TOOL.snowyUuid()
this.form.properties.buttonInfo = this.$refs.propButtonInfo.selectedButtonKeyList()
this.form.properties.executionListenerInfo = this.$refs.propExecutionListenerInfoRef.selectedListenerList()
this.form.properties.taskListenerInfo = this.$refs.propTaskListenerInfoRef.selectedListenerList()
if (this.recordData.formType === 'DESIGN') {
this.form.properties.fieldInfo = this.$refs.propFieldInfo.selectedFieldList()
this.form.dataLegal = true
this.$emit('update:modelValue', this.form)
this.drawer = false
} else {
this.$refs.startTaskFormRef
.validate()
.then((values) => {
this.form.dataLegal = true
this.form.properties.formInfo = cloneDeep(config.nodeModel.formInfo)
this.form.properties.formInfo.find((f) => f.key === 'FORM_URL').value = values.FORM_URL
this.form.properties.formInfo.find((f) => f.key === 'MOBILE_FORM_URL').value = values.MOBILE_FORM_URL
this.$emit('update:modelValue', this.form)
this.drawer = false
})
.catch((err) => {})
}
},
// eslint-disable-next-line no-unused-vars
toText(childNode) {
if (childNode.dataLegal) {
return '系统自动配置参与人'
} else {
return false
}
}
}
}
</script>
<style scoped>
.font-span {
padding-bottom: 8px;
}
</style>

View File

@@ -0,0 +1,788 @@
<template>
<div class="node-wrap">
<div class="node-wrap-box" @click="show">
<div class="title" style="background: #ff943e">
<user-outlined class="icon" />
<span>{{ childNode.title }}</span>
<close-outlined class="close" @click.stop="delNode()" />
</div>
<div class="content">
<span v-if="toText(childNode)">{{ toText(childNode) }}</span>
<span v-else class="placeholder">请选择</span>
</div>
</div>
<add-node v-model="childNode.childNode" />
<!-- 抽屉 -->
<xn-form-container
v-model:visible="drawer"
:destroy-on-close="true"
:width="700"
:body-style="{ 'padding-top': '0px' }"
>
<template #title>
<div class="node-wrap-drawer__title">
<label v-if="!isEditTitle" @click="editTitle"
>{{ form.title }}<edit-outlined class="node-wrap-drawer-title-edit"
/></label>
<a-input
v-if="isEditTitle"
ref="nodeTitle"
v-model:value="form.title"
allow-clear
@blur="saveTitle"
@keyup.enter="saveTitle"
/>
</div>
</template>
<a-layout-content>
<a-tabs v-model:activeKey="activeKey">
<a-tab-pane key="1" tab="人员配置" force-render>
<div v-show="!nodeLegal" style="margin-bottom: 10px">
<a-alert message="请配置节点相关人员!" type="error" />
</div>
<a-form layout="vertical" :model="userTypeForm" ref="userTypeFormRef">
<div class="mb-3">
<span class="left-span-label">选择审批人员类型</span>
</div>
<a-radio-group v-model:value="userSelectionType" style="width: 100%">
<a-row style="padding-bottom: 10px">
<a-col :span="8">
<a-radio value="ORG" @click="selectionClick('ORG')">机构</a-radio>
</a-col>
<a-col :span="8">
<a-radio value="ROLE" @click="selectionClick('ROLE')">角色</a-radio>
</a-col>
<a-col :span="8">
<a-radio value="POSITION" @click="selectionClick('POSITION')">职位</a-radio>
</a-col>
</a-row>
<a-row style="padding-bottom: 10px">
<a-col :span="8">
<a-radio value="ORG_LEADER" @click="selectionClick('ORG_LEADER')">部门主管</a-radio>
</a-col>
<a-col :span="8">
<a-radio value="SUPERVISOR" @click="selectionClick('SUPERVISOR')">上级主管</a-radio>
</a-col>
<a-col :span="8">
<a-radio value="MUL_LEVEL_SUPERVISOR" @click="selectionClick('MUL_LEVEL_SUPERVISOR')"
>连续多级主管</a-radio
>
</a-col>
</a-row>
<a-row style="padding-bottom: 10px">
<a-col :span="8">
<a-radio value="USER" @click="selectionClick('USER')">用户</a-radio>
</a-col>
<a-col :span="8">
<a-radio value="FORM_USER" @click="selectionClick('FORM_USER')">表单内的人</a-radio>
</a-col>
<a-col :span="8">
<a-radio value="FORM_USER_SUPERVISOR" @click="selectionClick('FORM_USER_SUPERVISOR')"
>表单内的人上级主管</a-radio
>
</a-col>
</a-row>
<a-row style="padding-bottom: 10px">
<a-col :span="8">
<a-radio
value="FORM_USER_MUL_LEVEL_SUPERVISOR"
@click="selectionClick('FORM_USER_MUL_LEVEL_SUPERVISOR')"
>表单内的人连续多级主管</a-radio
>
</a-col>
<a-col :span="8">
<a-radio value="INITIATOR" @click="selectionClick('INITIATOR')">发起人</a-radio>
</a-col>
<a-col :span="8" />
</a-row>
</a-radio-group>
<a-form-item
class="mt-2"
v-if="
recordData.formType === 'DEFINE' &&
(userSelectionType === 'FORM_USER' ||
userSelectionType === 'FORM_USER_SUPERVISOR' ||
userSelectionType === 'FORM_USER_MUL_LEVEL_SUPERVISOR')
"
name="formUser"
:rules="[{ required: true, message: '请输入人员变量' }]"
>
<template #label>
<a-tooltip>
<template #title>单个字段可以采用字段名多个采用字段名1,字段名2,字段名3</template>
<question-circle-outlined />
人员变量:
</a-tooltip>
</template>
<a-input
style="width: 50%"
v-model:value="userTypeForm.formUser"
allow-clear
@input="customFormUser"
@keyup.enter="customFormUser"
placeholder="请输入人员变量"
/>
</a-form-item>
<div v-if="seleType" class="mt-2">
<a-button type="primary" @click="openSelector" size="small">
<template #icon>
<search-outlined />
</template>
选择
</a-button>
</div>
<div class="mt-2">
<prop-tag
v-if="form.properties.participateInfo.length > 0 && form.properties.participateInfo[0].value !== ''"
:tag-list="form.properties.participateInfo[0]"
/>
</div>
<a-form-item
class="mt-2"
v-if="
userSelectionType === 'SUPERVISOR' ||
userSelectionType === 'FORM_USER_SUPERVISOR' ||
userSelectionType === 'MUL_LEVEL_SUPERVISOR' ||
userSelectionType === 'FORM_USER_MUL_LEVEL_SUPERVISOR'
"
:label="
userSelectionType === 'SUPERVISOR' || userSelectionType === 'FORM_USER_SUPERVISOR'
? '上级主管级别'
: '审批终点主管级别'
"
name="levelSupervisor"
:rules="[{ required: true, message: '请选择主管级别' }]"
>
<a-select
style="width: 50%"
v-model:value="userTypeForm.levelSupervisor"
placeholder="请选择主管级别"
@change="levelSupervisorChange"
:options="levelSupervisorList"
/>
</a-form-item>
</a-form>
</a-tab-pane>
<a-tab-pane key="2" tab="审批配置" force-render>
<span class="left-span-label">配置审批方式</span>
<a-form label-position="top" layout="vertical" class="mt-2">
<div v-show="!nodeLegal" style="margin-bottom: 10px">
<a-alert message="请配置节点相关人员!" type="error" />
</div>
<a-form-item label="任务节点类型">
<a-radio-group v-model:value="form.properties.configInfo.userTaskType">
<a-radio
v-for="userTaskType in userTaskTypeList"
:key="userTaskType.value"
:value="userTaskType.value"
>{{ userTaskType.label }}</a-radio
>
</a-radio-group>
</a-form-item>
<a-form-item label="审批退回类型">
<a-radio-group v-model:value="form.properties.configInfo.userTaskRejectType">
<a-radio
v-for="userTaskRejectType in userTaskRejectTypeList"
:key="userTaskRejectType.value"
:value="userTaskRejectType.value"
>{{ userTaskRejectType.label }}
</a-radio>
<br />
</a-radio-group>
</a-form-item>
<a-form-item label="多人审批类型">
<a-radio-group v-model:value="form.properties.configInfo.userTaskMulApproveType">
<a-radio
v-for="userTaskMulApproveType in userTaskMulApproveTypeList"
:key="userTaskMulApproveType.value"
:value="userTaskMulApproveType.value"
>{{ userTaskMulApproveType.label }}
</a-radio>
<br />
</a-radio-group>
</a-form-item>
<a-form-item label="审批人员为空时">
<a-radio-group v-model:value="form.properties.configInfo.userTaskEmptyApproveType">
<a-radio
v-for="userTaskEmptyApproveType in userTaskEmptyApproveTypeList"
:key="userTaskEmptyApproveType.value"
:value="userTaskEmptyApproveType.value"
@change="userTaskEmptyApproveTypeChange"
>{{ userTaskEmptyApproveType.label }}
</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item
v-if="form.properties.configInfo.userTaskEmptyApproveType === 'AUTO_TURN'"
label="审批人为空时转交人"
>
<p><a-button type="primary" size="small" @click="seleApproveUser">选择人员</a-button></p>
<a-tag
v-if="form.properties.configInfo.userTaskEmptyApproveUserArray.length > 0"
color="orange"
closable
@close="closeApproveUserTag"
>
{{ form.properties.configInfo.userTaskEmptyApproveUserArray[0].name }}</a-tag
>
</a-form-item>
</a-form>
</a-tab-pane>
<a-tab-pane key="3" tab="按钮配置" force-render>
<prop-button-info
ref="propButtonInfo"
:button-info="selectedButtonInfo"
:show-button="defaultButtonkey"
:no-checked="userTaskNoCheckedButtonkey"
/>
</a-tab-pane>
<a-tab-pane key="4" tab="字段配置" force-render v-if="recordData.formType === 'DESIGN'">
<prop-field-info
ref="propFieldInfo"
:field-info="fieldInfo"
:default-field-model="defaultFieldModel"
:form-field-list-value="formFieldListValue"
/>
</a-tab-pane>
<a-tab-pane key="5" tab="表单配置" force-render v-else>
<div class="mb-2">
<span class="left-span-label">参与者可以使用的表单</span>
</div>
<a-form ref="userTaskFormRef" :model="formData" layout="vertical">
<a-form-item
label="节点表单"
name="FORM_URL"
:rules="[{ required: true, message: '请输入节点表单组件地址' }]"
>
<a-input
v-model:value="formData.FORM_URL"
addon-before="src/views/flw/customform/"
addon-after=".vue"
placeholder="请输入节点表单组件地址"
allow-clear
>
<template #suffix>
<a-button
v-if="formData.FORM_URL"
type="primary"
size="small"
@click="$refs.previewCustomFormRef.onOpen(formData.FORM_URL)"
>
预览
</a-button>
</template>
</a-input>
</a-form-item>
<a-form-item
label="移动端节点表单"
name="MOBILE_FORM_URL"
:rules="[{ required: true, message: '请输入移动端节点表单组件地址' }]"
>
<a-input
v-model:value="formData.MOBILE_FORM_URL"
addon-before="pages/flw/customform/"
addon-after=".vue"
placeholder="请输入移动端节点表单组件地址"
allow-clear
/>
</a-form-item>
</a-form>
</a-tab-pane>
<a-tab-pane key="6" tab="执行监听" force-render>
<prop-listener-info
ref="propExecutionListenerInfoRef"
:listener-value="form.properties.executionListenerInfo"
:default-listener-list="executionListenerInfo"
:listener-value-array="executionListenerArray"
/>
</a-tab-pane>
<a-tab-pane key="7" tab="节点监听" force-render>
<prop-listener-info
ref="propTaskListenerInfoRef"
:listener-value="form.properties.taskListenerInfo"
:default-listener-list="taskListenerInfo"
:listener-value-array="taskListenerArray"
/>
</a-tab-pane>
</a-tabs>
</a-layout-content>
<template #footer>
<a-button type="primary" style="margin-right: 8px" @click="save">保存</a-button>
<a-button @click="drawer = false">取消</a-button>
</template>
</xn-form-container>
<role-selector-plus
ref="roleselectorPlus"
:org-tree-api="selectorApiFunction.orgTreeApi"
:role-page-api="selectorApiFunction.rolePageApi"
:checked-role-list-api="selectorApiFunction.checkedRoleListApi"
:data-is-converter-flw="true"
@onBack="callBack"
/>
<user-selector-plus
ref="userselectorPlus"
:org-tree-api="selectorApiFunction.orgTreeApi"
:user-page-api="selectorApiFunction.userPageApi"
:checked-user-list-api="selectorApiFunction.checkedUserListApi"
:data-is-converter-flw="true"
@onBack="callBack"
/>
<pos-selector-plus
ref="posselectorPlus"
:org-tree-api="selectorApiFunction.orgTreeApi"
:pos-page-api="selectorApiFunction.posPageApi"
:checked-pos-list-api="selectorApiFunction.checkedPosListApi"
:data-is-converter-flw="true"
@onBack="callBack"
/>
<org-selector-plus
ref="orgselectorPlus"
:org-tree-api="selectorApiFunction.orgTreeApi"
:org-page-api="selectorApiFunction.orgPageApi"
:checked-org-list-api="selectorApiFunction.checkedOrgListApi"
:data-is-converter-flw="true"
@onBack="callBack"
/>
<form-user-selector ref="formuserselector" :form-field-list="formFieldListValue" @click="formuserClick" />
<user-selector-plus
ref="userselectorApprove"
:org-tree-api="selectorApiFunction.orgTreeApi"
:user-page-api="selectorApiFunction.userPageApi"
:checked-user-list-api="selectorApiFunction.checkedUserListApi"
:radio-model="true"
@onBack="callBackApprove"
/>
<preview-custom-form ref="previewCustomFormRef" />
</div>
</template>
<script>
import { cloneDeep, isEmpty } from 'lodash-es'
import config from '@/components/XnWorkflow/nodes/config/config'
import addNode from './addNode.vue'
import propButtonInfo from './prop/propButtonInfo.vue'
import propFieldInfo from './prop/propFieldInfo.vue'
import propListenerInfo from './prop/propListenerInfo.vue'
import formUserSelector from './prop/formUserSelector.vue'
import roleSelectorPlus from '@/components/Selector/roleSelectorPlus.vue'
import userSelectorPlus from '@/components/Selector/userSelectorPlus.vue'
import posSelectorPlus from '@/components/Selector/posSelectorPlus.vue'
import orgSelectorPlus from '@/components/Selector/orgSelectorPlus.vue'
import propTag from './prop/propTag.vue'
import PreviewCustomForm from '@/components/XnWorkflow/nodes/common/previewCustomForm.vue'
export default {
components: {
addNode,
propButtonInfo,
propFieldInfo,
propListenerInfo,
formUserSelector,
roleSelectorPlus,
userSelectorPlus,
posSelectorPlus,
orgSelectorPlus,
propTag,
PreviewCustomForm
},
// inject: ['select'],
props: {
modelValue: { type: Object, default: () => {} },
formFieldListValue: { type: Array, default: () => [] },
recordData: { type: Object, default: () => {} },
processConfigInfo: { type: Object, default: () => {} },
executionListenerArray: { type: Array, default: () => [] },
taskListenerArray: { type: Array, default: () => [] },
selectorApiFunction: { type: Object, default: () => {} }
},
data() {
return {
nodeLegal: false,
childNode: {},
drawer: false,
isEditTitle: false,
form: {},
selectedButtonInfo: [],
executionListenerInfo: cloneDeep(config.listener.userTaskExecutionListenerInfo),
taskListenerInfo: cloneDeep(config.listener.userTaskTaskListenerInfo),
activeKey: '1',
defaultButtonkey: cloneDeep(config.button.userTaskDefaultButtonkey),
userTaskNoCheckedButtonkey: cloneDeep(config.button.userTaskNoCheckedButtonkey),
defaultFieldModel: cloneDeep(config.field.userTaskFieldModel),
fieldInfo: [],
tagList: [],
seleType: false,
// 新配置
// 用户选择类型
userSelectionType: '',
userSelectionTypeList: cloneDeep(config.userTaskConfig.userSelectionTypeList),
// 主管级别
levelSupervisorList: cloneDeep(config.userTaskConfig.levelSupervisorList),
// 任务节点类型
userTaskTypeList: cloneDeep(config.userTaskConfig.userTaskTypeList),
// 审批退回类型
userTaskRejectTypeList: cloneDeep(config.userTaskConfig.userTaskRejectTypeList),
// 多人审批时类型
userTaskMulApproveTypeList: cloneDeep(config.userTaskConfig.userTaskMulApproveTypeList),
// 审批人为空时类型
userTaskEmptyApproveTypeList: cloneDeep(config.userTaskConfig.userTaskEmptyApproveTypeList),
// 审批人为空时转交人
userTaskEmptyApproveUser: '',
formData: {},
userTypeForm: {
levelSupervisor: '1'
}
}
},
watch: {
modelValue() {
this.childNode = this.modelValue
}
},
mounted() {
this.childNode = this.modelValue
},
methods: {
show() {
this.form = {}
this.form = cloneDeep(this.childNode)
this.selectedButtonInfo = this.form.properties.buttonInfo
this.fieldInfo = this.form.properties.fieldInfo
this.drawer = true
// 设置默认选中的用户类型单选 userSelectionType
if (this.form.properties.participateInfo.length > 0) {
this.userSelectionType = this.form.properties.participateInfo[0].key
// 如果包含表单内的人
const formUserObj = this.form.properties.participateInfo.find((f) => f.key === 'FORM_USER')
if (formUserObj) {
this.userTypeForm.formUser = formUserObj.value
}
// 如果是直属主管或者连续多级审批的
this.form.properties.participateInfo.forEach((item) => {
// 如果只是普通的主管以及级别
if (item.key === 'SUPERVISOR' || item.key === 'MUL_LEVEL_SUPERVISOR') {
this.userTypeForm.levelSupervisor = item.value
}
// 如果是表单内的人的主管以及级别
if (item.key === 'FORM_USER_SUPERVISOR' || item.key === 'FORM_USER_MUL_LEVEL_SUPERVISOR') {
this.userTypeForm.levelSupervisor = item.value
this.userTypeForm.formUser = JSON.parse(item.extJson).value
}
})
} else {
// 设置发起人
this.userSelectionType = 'INITIATOR'
this.form.properties.participateInfo = [{ key: 'INITIATOR', label: '发起人', value: '发起人:${INITIATOR}' }]
}
// 校验状态
this.isNodeLegal()
// 设置第一个选项卡打开选择器的按钮是否展示
if (
this.userSelectionType === 'USER' ||
this.userSelectionType === 'ROLE' ||
this.userSelectionType === 'ORG' ||
this.userSelectionType === 'POSITION' ||
this.userSelectionType === 'FORM_USER' ||
this.userSelectionType === 'FORM_USER_SUPERVISOR' ||
this.userSelectionType === 'FORM_USER_MUL_LEVEL_SUPERVISOR'
) {
this.seleType = true
}
// 初始化自定义表单字段值
this.initFormInfo()
},
initFormInfo() {
const processConfigInfo = cloneDeep(this.processConfigInfo)
if (!this.form.properties.formInfo.find((f) => f.key === 'FORM_URL')) {
this.formData.FORM_URL = processConfigInfo.processUserTaskFormUrl
} else {
this.formData.FORM_URL = this.form.properties.formInfo.find((f) => f.key === 'FORM_URL').value
}
if (!this.form.properties.formInfo.find((f) => f.key === 'MOBILE_FORM_URL')) {
this.formData.MOBILE_FORM_URL = processConfigInfo.processUserTaskMobileFormUrl
} else {
this.formData.MOBILE_FORM_URL = this.form.properties.formInfo.find((f) => f.key === 'MOBILE_FORM_URL').value
}
},
editTitle() {
this.isEditTitle = true
this.$nextTick(() => {
this.$refs.nodeTitle.focus()
})
},
saveTitle() {
this.isEditTitle = false
},
save() {
if (isEmpty(this.form.id)) {
this.form.id = this.$TOOL.snowyUuid()
}
this.form.properties.buttonInfo = this.$refs.propButtonInfo.selectedButtonKeyList()
if (this.recordData.formType === 'DESIGN') {
this.form.properties.fieldInfo = this.$refs.propFieldInfo.selectedFieldList()
}
if (this.isNodeLegal()) {
this.form.properties.executionListenerInfo = this.$refs.propExecutionListenerInfoRef.selectedListenerList()
this.form.properties.taskListenerInfo = this.$refs.propTaskListenerInfoRef.selectedListenerList()
if (this.recordData.formType === 'DESIGN') {
this.form.dataLegal = true
this.$emit('update:modelValue', this.form)
this.drawer = false
} else {
this.$refs.userTaskFormRef
.validate()
.then((values) => {
this.form.dataLegal = true
this.form.properties.formInfo = cloneDeep(config.nodeModel.formInfo)
this.form.properties.formInfo.find((f) => f.key === 'FORM_URL').value = values.FORM_URL
this.form.properties.formInfo.find((f) => f.key === 'MOBILE_FORM_URL').value = values.MOBILE_FORM_URL
this.$emit('update:modelValue', this.form)
this.drawer = false
})
.catch((err) => {})
}
}
},
selectionClick(value) {
const type = value
const result = []
this.form.properties.participateInfo = []
const obj = {}
obj.key = type
obj.label = ''
obj.value = ''
if (type === 'ORG_LEADER' || type === 'INITIATOR') {
// 部门主管 发起人
this.seleType = false
obj.label = this.userSelectionTypeList.filter((item) => item.value === type)[0].label
obj.value = this.userSelectionTypeList.filter((item) => item.value === type)[0].label + ':${' + type + '}'
} else if (type === 'SUPERVISOR' || type === 'MUL_LEVEL_SUPERVISOR') {
this.seleType = false
// 直属主管 连续多级审批 默认选中直接主管
obj.label = this.userSelectionTypeList.filter((item) => item.value === type)[0].label
obj.value = this.userTypeForm.levelSupervisor
} else if (type === 'FORM_USER') {
this.seleType = this.recordData.formType === 'DESIGN'
} else if (type === 'FORM_USER_SUPERVISOR' || type === 'FORM_USER_MUL_LEVEL_SUPERVISOR') {
this.seleType = this.recordData.formType === 'DESIGN'
} else {
this.seleType = true
}
result.push(obj)
this.form.properties.participateInfo = result
// 清空自定义表单内的人员输入框
this.userTypeForm.formUser = ''
this.isNodeLegal()
},
// 打开各种选择器
openSelector() {
let type = this.userSelectionType
let data = this.form.properties.participateInfo
if (type === 'ROLE') {
this.$refs.roleselectorPlus.showRolePlusModal(data)
}
if (type === 'USER') {
this.$refs.userselectorPlus.showUserPlusModal(data)
}
if (type === 'POSITION') {
this.$refs.posselectorPlus.showPosPlusModal(data)
}
if (type === 'ORG') {
this.$refs.orgselectorPlus.showOrgPlusModal(data)
}
if (type === 'FORM_USER' || type === 'FORM_USER_SUPERVISOR' || type === 'FORM_USER_MUL_LEVEL_SUPERVISOR') {
this.$refs.formuserselector.showFormUserModal(data[0])
}
},
delNode() {
// eslint-disable-next-line vue/require-explicit-emits
this.$emit('update:modelValue', this.childNode.childNode)
// eslint-disable-next-line vue/require-explicit-emits
this.$emit('deleteParalle')
},
delUser(index) {
this.form.nodeUserList.splice(index, 1)
},
// 选择转交人
seleApproveUser() {
const data = [this.form.properties.configInfo.userTaskEmptyApproveUser]
this.$refs.userselectorApprove.showUserPlusModal(data)
},
// 选择转交人回调
callBackApprove(value) {
this.form.properties.configInfo.userTaskEmptyApproveUser = value[0].id
this.form.properties.configInfo.userTaskEmptyApproveUserArray = value
},
// 清除转交人
closeApproveUserTag() {
this.form.properties.configInfo.userTaskEmptyApproveUser = ''
this.form.properties.configInfo.userTaskEmptyApproveUserArray = []
},
// 点击自动转交给某人单选时
userTaskEmptyApproveTypeChange(value) {
const type = value.target.value
if (type === 'AUTO_TURN') {
// 赋值默认的管理员
this.form.properties.configInfo.userTaskEmptyApproveUser = JSON.parse(this.recordData.extJson)[0].id
this.form.properties.configInfo.userTaskEmptyApproveUserArray = JSON.parse(this.recordData.extJson)
} else {
// 置为空
this.closeApproveUserTag()
}
},
// 校验节点是否合法
isNodeLegal() {
if (this.form.properties.participateInfo.length > 0) {
if (isEmpty(this.form.properties.participateInfo[0].label)) {
this.nodeLegal = false
return false
} else {
// 再看看默认转交人是否OK
// eslint-disable-next-line no-lonely-if
if (this.form.properties.configInfo.userTaskEmptyApproveType === 'AUTO_TURN') {
if (!isEmpty(this.form.properties.configInfo.userTaskEmptyApproveUser)) {
this.nodeLegal = true
return true
} else {
this.nodeLegal = false
return false
}
} else {
this.nodeLegal = true
return true
}
}
} else {
this.nodeLegal = false
return false
}
},
selectHandle(type, data) {
const value = []
if (data.length > 0) {
data.forEach((item) => {
value.push(item.id)
})
}
if (type === 1) {
this.$refs.userselector.showUserModal(value)
}
if (type === 2) {
this.$refs.roleselector.showRoleModal(value)
}
// this.select(type, data)
},
// 公共回调方法,因为它们返回的数据结构一致
callBack(value) {
if (value.label === []) {
this.nodeLegal = false
} else {
this.form.properties.participateInfo[0] = value
}
this.isNodeLegal()
},
// 表单内的人选择器回调
formuserClick(value) {
// eslint-disable-next-line no-undefined
if (value) {
const participateInfo = this.form.properties.participateInfo[0].key
const result = []
const obj = {}
if (participateInfo === 'FORM_USER_SUPERVISOR' || participateInfo === 'FORM_USER_MUL_LEVEL_SUPERVISOR') {
obj.key = participateInfo
obj.label = this.userSelectionTypeList.filter((item) => item.value === participateInfo)[0].label
obj.value = this.userTypeForm.levelSupervisor
const extJson = {
key: 'FORM_USER',
label: '表单内的人',
value: value.model
}
obj.extJson = JSON.stringify(extJson)
} else {
obj.key = 'FORM_USER'
obj.label = this.userSelectionTypeList.filter((item) => item.value === 'FORM_USER')[0].label
obj.value = value.model
}
result.push(obj)
this.form.properties.participateInfo = result
} else {
this.nodeLegal = false
}
this.isNodeLegal()
},
// 监听自定义表单人员输入
customFormUser() {
if (this.userTypeForm.formUser) {
const participateInfo = this.form.properties.participateInfo[0].key
const result = []
const obj = {}
if (participateInfo === 'FORM_USER_SUPERVISOR' || participateInfo === 'FORM_USER_MUL_LEVEL_SUPERVISOR') {
obj.key = participateInfo
obj.label = this.userSelectionTypeList.filter((item) => item.value === participateInfo)[0].label
obj.value = this.userTypeForm.levelSupervisor
const extJson = {
key: 'FORM_USER',
label: '表单内的人',
value: this.userTypeForm.formUser
}
obj.extJson = JSON.stringify(extJson)
} else {
obj.key = 'FORM_USER'
obj.label = this.userSelectionTypeList.filter((item) => item.value === 'FORM_USER')[0].label
obj.value = this.userTypeForm.formUser
}
result.push(obj)
this.form.properties.participateInfo = result
this.nodeLegal = false
} else {
this.form.properties.participateInfo = []
this.nodeLegal = true
}
this.isNodeLegal()
},
// 选择主管层级
levelSupervisorChange(value) {
this.form.properties.participateInfo[0].value = value
},
toText(childNode) {
if (!isEmpty(childNode)) {
const strArray = this.toTag(childNode.properties.participateInfo[0])
if (strArray.length > 0) {
let value = ''
// eslint-disable-next-line no-plusplus
for (let i = 0; i < strArray.length; i++) {
if (strArray.length === i + 1) {
value = value + strArray[i]
} else {
value = value + strArray[i] + ''
}
}
return value
} else {
return false
}
} else {
return false
}
},
toTag(participateInfo) {
// eslint-disable-next-line no-undefined
if (participateInfo === undefined) {
return []
}
if (isEmpty(participateInfo.label)) {
return []
} else {
let resultArray = []
if (participateInfo.label.indexOf(',') !== -1) {
resultArray = participateInfo.label.split(',')
} else {
resultArray.push(participateInfo.label)
}
return resultArray
}
}
}
}
</script>

View File

@@ -0,0 +1,180 @@
import { ElMessage } from 'element-plus'
// 根据自定义的此节点定义的,转换表单的隐藏、必填、禁用
export default {
// 设置字段显示与否
convSettingsField(formJson, fieldInfo) {
// 递归遍历控件树
const traverse = (array) => {
array.forEach((element) => {
if (element.type === 'grid' || element.type === 'tabs') {
// 栅格布局 and 标签页
element.columns.forEach((item) => {
traverse(item.list)
})
} else if (element.type === 'card') {
// 卡片布局 and 动态表格
traverse(element.list)
} else if (element.type === 'table') {
// 表格布局
element.trs.forEach((item) => {
item.tds.forEach((val) => {
traverse(val.list)
})
})
} else {
const type = element.type
if ((type !== 'alert') & (type !== 'text') & (type !== 'divider') & (type !== 'html')) {
const obj = fieldInfo.find((i) => i.key === element.model)
if (obj) {
element.options.hidden = obj.value === 'HIDE' // ? true : false
element.options.disabled = obj.value === 'READ' // ? true : false
} else {
ElMessage.warning('程序检测到功能字段配置发生了异常,依然能正常使用,请联系管理员进行流程重新配置部署!')
}
}
}
})
}
traverse(formJson.list)
return formJson
},
// 掏出所有字段,返回列表
getListField(data) {
let result = []
// 递归遍历控件树
const traverse = (array) => {
array.forEach((element) => {
if (element.type === 'grid' || element.type === 'tabs') {
// 栅格布局 and 标签页
element.columns.forEach((item) => {
traverse(item.list)
})
} else if (element.type === 'card') {
// 卡片布局 and 动态表格
traverse(element.list)
} else if (element.type === 'table') {
// 表格布局
element.trs.forEach((item) => {
item.tds.forEach((val) => {
traverse(val.list)
})
})
} else {
const type = element.type
// 排除一些
if ((type !== 'alert') & (type !== 'text') & (type !== 'divider') & (type !== 'batch') & (type !== 'html')) {
result.push(element)
}
}
})
}
traverse(data)
return result
},
// 取节点用到按钮权限跟字段并且给节点set一个json也就是我们的审批记录
getChildNode(modelJson, activityId, dataList) {
let result = {}
let traverse = (obj) => {
// obj.properties.commentList = []
if (obj.type === 'exclusiveGateway' || obj.type === 'parallelGateway') {
// 网关下分2步走
if (obj.conditionNodeList) {
obj.conditionNodeList.forEach((item) => {
traverse(item)
})
}
if (obj.childNode) {
traverse(obj.childNode)
}
} else {
if (obj.id === activityId) {
result = obj
} else {
// 这里追加记录
// if (dataList) {
// dataList.forEach((item) => {
// // 给对应的节点
// if (item.activityId === obj.id) {
// obj.properties.commentList.push(item)
// }
// })
// }
// 再穿下去
if (obj.childNode) {
traverse(obj.childNode)
}
}
}
}
// 传入流程的这个
traverse(modelJson)
return result
},
// 遍历表单,将组件设为禁用
convFormComponentsDisabled (formJson) {
// 递归遍历控件树
const traverse = (array) => {
array.forEach((element) => {
if (element.type === 'grid' || element.type === 'tabs') {
// 栅格布局 and 标签页
element.columns.forEach((item) => {
traverse(item.list)
})
} else if (element.type === 'card') {
// 卡片布局 and 动态表格
traverse(element.list)
} else if (element.type === 'table') {
// 表格布局
element.trs.forEach((item) => {
item.tds.forEach((val) => {
traverse(val.list)
})
})
} else {
const type = element.type
if ((type !== 'alert') & (type !== 'text') & (type !== 'divider') & (type !== 'html')) {
element.options.disabled = true
}
}
})
}
traverse(formJson.list)
return formJson
},
// 将渲染图形的信息跟审批记录进行融合每个节点的配置内添加commentList
coalesceDataListChildNode(modelJson, dataList) { // activityId,
// let result = {}
const traverse = (obj) => {
// obj.properties.commentList = []
if (obj.type === 'exclusiveGateway' || obj.type === 'parallelGateway') {
// 网关下分2步走
if (obj.conditionNodeList) {
obj.conditionNodeList.forEach((item) => {
traverse(item)
})
}
if (obj.childNode) {
traverse(obj.childNode)
}
} else {
if (dataList) {
dataList.forEach((item) => {
// 给对应的节点
if (item.activityId === obj.id) {
// 增加多个对象
obj.properties.commentList.push(item)
}
})
}
// 再穿下去
if (obj.childNode) {
traverse(obj.childNode)
}
}
}
// 传入流程的这个
traverse(modelJson)
return modelJson
}
}

View File

@@ -0,0 +1,625 @@
<template>
<xn-form-container
v-model:visible="drawer"
:destroy-on-close="true"
:title="modelTitle"
:width="700"
:bodyStyle="{ 'padding-top': '0px' }"
>
<el-form ref="noticeFormRef" :model="formData" layout="vertical">
<el-tabs v-model:activeKey="activeKey">
<el-tab-pane key="1" tab="人员配置" force-render>
<div v-show="formVerify" style="margin-bottom: 10px">
<el-alert message="请切换标签查看,填写完必填项!" type="error" />
</div>
<div v-show="alertShow" style="margin-bottom: 10px">
<el-alert message="未选择任何类型的人员配置,默认所有人均可参与此流程。" type="warning" />
</div>
<div class="mb-2">
<span class="left-span-label">配置使用该流程的人员</span>
</div>
<el-button type="primary" round @click="selectionParticipants('ORG')" size="small">
<Plus/>
选择机构
</el-button>
<p />
<prop-tag :tagList="getTagList('ORG')" />
<el-divider />
<el-button type="primary" round @click="selectionParticipants('ROLE')" size="small">
<plus-outlined />
选择角色
</el-button>
<p />
<prop-tag :tagList="getTagList('ROLE')" />
<el-divider />
<el-button type="primary" round @click="selectionParticipants('POSITION')" size="small">
<plus-outlined />
选择职位
</el-button>
<p />
<prop-tag :tagList="getTagList('POSITION')" />
<el-divider />
<el-button type="primary" round @click="selectionParticipants('USER')" size="small">
<plus-outlined />
选择用户
</el-button>
<p />
<prop-tag :tagList="getTagList('USER')" />
<el-divider />
</el-tab-pane>
<el-tab-pane key="2" tab="基础配置" force-render>
<div class="mb-2">
<span class="left-span-label">流程基础全局配置</span>
</div>
<el-row :gutter="[10, 0]">
<el-col :span="12">
<el-form-item
label="流水号"
name="processSnTemplateId"
:rules="[{ required: true, message: '请选择流水号' }]"
>
<el-select
v-model:value="form.properties.configInfo.processSnTemplateId"
placeholder="请选择流水号"
:options="snTemplateArray"
>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item
v-if="recordData.formType === 'DESIGN'"
label="打印模板"
name="processPrintTemplateId"
:rules="[{ required: true, message: '请选择打印模板' }]"
>
<el-select
v-model:value="form.properties.configInfo.processPrintTemplateId"
placeholder="请选择打印模板"
:options="printTemplateArray"
/>
</el-form-item>
<span v-else>
<p>打印模板</p>
<p>自定义表单内提供打印方法</p>
</span>
</el-col>
</el-row>
<el-form-item
label="标题模板"
name="processTitleTemplate"
:rules="[{ required: true, message: '请创造标题模板' }]"
>
<template-generator
ref="processTitleGenerator"
v-model:inputValue="form.properties.configInfo.processTitleTemplate"
:fieldInfoLis="fieldInfoLis"
/>
</el-form-item>
<el-form-item
label="摘要模板"
name="processAbstractTemplate"
:rules="[{ required: true, message: '请创造摘要模板' }]"
>
<template-generator
ref="processAbstractGenerator"
v-model:inputValue="form.properties.configInfo.processAbstractTemplate"
:fieldInfoLis="fieldInfoLis"
/>
</el-form-item>
<el-form-item label="开启自动去重" name="processEnableAutoDistinct">
<el-switch v-model:checked="form.properties.configInfo.processEnableAutoDistinct" />
</el-form-item>
<el-form-item v-show="form.properties.configInfo.processEnableAutoDistinct" name="processAutoDistinctType">
<el-radio-group v-model:value="form.properties.configInfo.processAutoDistinctType">
<el-radio
v-for="autoDistinctType in processAutoDistinctTypeList"
:key="autoDistinctType.value"
:value="autoDistinctType.value"
>{{ autoDistinctType.label }}</el-radio
>
</el-radio-group>
</el-form-item>
<el-row :gutter="[10, 0]">
<el-col :span="12">
<el-form-item label="开启审批撤销" name="processEnableRevoke">
<el-switch v-model:checked="form.properties.configInfo.processEnableRevoke" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="开启意见必填" name="processEnableCommentRequired">
<el-switch v-model:checked="form.properties.configInfo.processEnableCommentRequired" />
</el-form-item>
</el-col>
</el-row>
</el-tab-pane>
<el-tab-pane key="3" tab="通知配置" force-render>
<div class="mb-2">
<span class="left-span-label">配置通知事项</span>
</div>
<el-form-item label="开启退回通知" name="processEnableBackNotice">
<el-switch v-model:checked="formData.processEnableBackNotice" />
</el-form-item>
<el-form-item
label="退回通知方式"
v-show="formData.processEnableBackNotice"
name="processBackNoticeChannel"
:rules="[{ required: formData.processEnableBackNotice, message: '请选择通知方式' }]"
>
<el-checkbox-group v-model:value="formData.processBackNoticeChannel" :options="noticeInfoList" />
</el-form-item>
<el-form-item
label="退回通知模板"
v-show="formData.processEnableBackNotice"
name="processBackNoticeTemplate"
:rules="[{ required: formData.processEnableBackNotice, message: '请创造通知模板' }]"
>
<template-generator
ref="enableBackNoticeRef"
v-model:inputValue="formData.processBackNoticeTemplate"
:fieldInfoLis="fieldInfoLis"
/>
</el-form-item>
<el-form-item label="开启待办通知" name="processEnableTodoNotice">
<el-switch v-model:checked="form.properties.configInfo.processEnableTodoNotice" />
</el-form-item>
<el-form-item
label="待办通知方式"
v-show="form.properties.configInfo.processEnableTodoNotice"
name="processTodoNoticeChannel"
:rules="[{ required: formData.processEnableTodoNotice, message: '请选择通知方式' }]"
>
<el-checkbox-group
v-model:value="form.properties.configInfo.processTodoNoticeChannel"
:options="noticeInfoList"
/>
</el-form-item>
<el-form-item
label="待办通知模板"
v-show="form.properties.configInfo.processEnableTodoNotice"
name="processTodoNoticeTemplate"
:rules="[{ required: formData.processEnableTodoNotice, message: '请创造通知模板' }]"
>
<template-generator
ref="todoNoticeChannelRef"
:fieldInfoLis="fieldInfoLis"
v-model:inputValue="form.properties.configInfo.processTodoNoticeTemplate"
/>
</el-form-item>
<el-form-item label="开启抄送通知" name="processEnableCopyNotice">
<el-switch v-model:checked="form.properties.configInfo.processEnableCopyNotice" />
</el-form-item>
<el-form-item
label="抄送通知方式"
v-show="form.properties.configInfo.processEnableCopyNotice"
name="processCopyNoticeChannel"
:rules="[{ required: formData.processEnableCopyNotice, message: '请选择通知方式' }]"
>
<el-checkbox-group
v-model:value="form.properties.configInfo.processCopyNoticeChannel"
:options="noticeInfoList"
/>
</el-form-item>
<el-form-item
label="抄送通知模板"
v-show="form.properties.configInfo.processEnableCopyNotice"
name="processCopyNoticeTemplate"
:rules="[{ required: formData.processEnableCopyNotice, message: '请创造通知模板' }]"
>
<template-generator
ref="enableCopyNoticeRef"
:fieldInfoLis="fieldInfoLis"
v-model:inputValue="form.properties.configInfo.processCopyNoticeTemplate"
/>
</el-form-item>
<el-form-item label="开启完成通知" name="processEnableCompleteNotice">
<el-switch v-model:checked="form.properties.configInfo.processEnableCompleteNotice" />
</el-form-item>
<el-form-item
label="完成通知方式"
v-show="form.properties.configInfo.processEnableCompleteNotice"
name="processCompleteNoticeChannel"
:rules="[{ required: formData.processEnableCompleteNotice, message: '请选择通知方式' }]"
>
<el-checkbox-group
v-model:value="form.properties.configInfo.processCompleteNoticeChannel"
:options="noticeInfoList"
/>
</el-form-item>
<el-form-item
label="完成通知模板"
v-show="form.properties.configInfo.processEnableCompleteNotice"
name="processCompleteNoticeTemplate"
:rules="[{ required: formData.processEnableCompleteNotice, message: '请创造通知模板' }]"
>
<template-generator
ref="enableCompleteNoticeRef"
:fieldInfoLis="fieldInfoLis"
v-model:inputValue="form.properties.configInfo.processCompleteNoticeTemplate"
/>
</el-form-item>
</el-tab-pane>
<el-tab-pane key="4" tab="表单预设" force-render v-if="recordData.formType !== 'DESIGN'">
<div class="mb-2">
<span class="left-span-label">预设全局需要的表单</span>
</div>
<el-form-item
label="开始节点表单"
name="processStartTaskFormUrl"
:rules="[{ required: true, message: '请输入开始节点表单' }]"
>
<el-input
v-model:value="form.properties.configInfo.processStartTaskFormUrl"
addon-before="src/views/flw/customform/"
addon-after=".vue"
placeholder="请输入开始节点表单组件"
allow-clear
>
<template #suffix>
<el-button
v-if="form.properties.configInfo.processStartTaskFormUrl"
type="primary"
size="small"
@click="$refs.previewCustomFormRef.onOpen(form.properties.configInfo.processStartTaskFormUrl)"
>
预览
</el-button>
</template>
</el-input>
</el-form-item>
<el-form-item
label="移动端开始节点表单"
name="processStartTaskMobileFormUrl"
:rules="[{ required: true, message: '请输入移动端开始节点表单' }]"
>
<el-input
v-model:value="form.properties.configInfo.processStartTaskMobileFormUrl"
addon-before="pages/flw/customform/"
addon-after=".vue"
placeholder="请输入移动端开始节点表单组件"
allow-clear
/>
</el-form-item>
<el-form-item
label="人员节点表单"
name="processUserTaskFormUrl"
:rules="[{ required: true, message: '请输入人员节点表单' }]"
>
<el-input
v-model:value="form.properties.configInfo.processUserTaskFormUrl"
addon-before="src/views/flw/customform/"
addon-after=".vue"
placeholder="请输入人员节点表单组件"
allow-clear
>
<template #suffix>
<el-button
v-if="form.properties.configInfo.processUserTaskFormUrl"
type="primary"
size="small"
@click="$refs.previewCustomFormRef.onOpen(form.properties.configInfo.processUserTaskFormUrl)"
>
预览
</el-button>
</template>
</el-input>
</el-form-item>
<el-form-item
label="移动端人员节点表单"
name="processUserTaskMobileFormUrl"
:rules="[{ required: true, message: '请输入移动端人员节点表单' }]"
>
<el-input
v-model:value="form.properties.configInfo.processUserTaskMobileFormUrl"
addon-before="pages/flw/customform/"
addon-after=".vue"
placeholder="请输入移动端人员节点表单组件"
allow-clear
/>
</el-form-item>
</el-tab-pane>
<el-tab-pane key="5" tab="执行监听" force-render>
<prop-listener-info
ref="propListenerInfoRef"
:listenerValue="form.properties.executionListenerInfo"
:defaultListenerList="executionListenerInfo"
:listener-value-array="executionListenerArray"
listener-type="global"
:execution-listener-selector-for-custom-event-array="executionListenerSelectorForCustomEventArray"
/>
</el-tab-pane>
</el-tabs>
</el-form>
<template #footer>
<el-button type="primary" style="margin-right: 8px" @click="onFinish">保存</el-button>
<el-button @click="drawer = false">取消</el-button>
</template>
<role-selector-plus
ref="roleselectorPlus"
:org-tree-api="selectorApiFunction.orgTreeApi"
:role-page-api="selectorApiFunction.rolePageApi"
:checked-role-list-api="selectorApiFunction.checkedRoleListApi"
:data-is-converter-flw="true"
@onBack="callBack"
/>
<user-selector-plus
ref="userselectorPlus"
:org-tree-api="selectorApiFunction.orgTreeApi"
:user-page-api="selectorApiFunction.userPageApi"
:checked-user-list-api="selectorApiFunction.checkedUserListApi"
:data-is-converter-flw="true"
@onBack="callBack"
/>
<pos-selector-plus
ref="posselectorPlus"
:org-tree-api="selectorApiFunction.orgTreeApi"
:pos-page-api="selectorApiFunction.posPageApi"
:checked-pos-list-api="selectorApiFunction.checkedPosListApi"
:datel-is-converter-flw="true"
@onBack="callBack"
/>
<org-selector-plus
ref="orgselectorPlus"
:org-tree-api="selectorApiFunction.orgTreeApi"
:org-page-api="selectorApiFunction.orgPageApi"
:checked-org-list-api="selectorApiFunction.checkedOrgListApi"
:data-is-converter-flw="true"
@onBack="callBack"
/>
<preview-custom-form ref="previewCustomFormRef" />
</xn-form-container>
</template>
<script>
import { cloneDeep } from 'lodash-es'
import templateGenerator from './nodes/prop/templateGenerator.vue'
import config from '@/components/XnWorkflow/nodes/config/config'
import roleSelectorPlus from '@/components/Selector/roleSelectorPlus.vue'
import userSelectorPlus from '@/components/Selector/userSelectorPlus.vue'
import posSelectorPlus from '@/components/Selector/posSelectorPlus.vue'
import orgSelectorPlus from '@/components/Selector/orgSelectorPlus.vue'
import propTag from './nodes/prop/propTag.vue'
import PropListenerInfo from '@/components/XnWorkflow/nodes/prop/propListenerInfo.vue'
import PreviewCustomForm from '@/components/XnWorkflow/nodes/common/previewCustomForm.vue'
export default {
components: {
PropListenerInfo,
templateGenerator,
roleSelectorPlus,
userSelectorPlus,
posSelectorPlus,
orgSelectorPlus,
propTag,
PreviewCustomForm
},
props: {
modelValue: { type: Object, default: () => {} },
formFieldListValue: { type: Array, default: () => [] },
recordData: { type: Object, default: () => {} },
snTemplateArray: { type: Array, default: () => [] },
printTemplateArray: { type: Array, default: () => [] },
executionListenerArray: { type: Array, default: () => [] },
executionListenerSelectorForCustomEventArray: { type: Array, default: () => [] },
taskListenerArray: { type: Array, default: () => [] },
selectorApiFunction: { type: Object, default: () => {} }
},
data() {
return {
noticeInfoList: cloneDeep(config.noticeInfoList),
// 摘要模板,因为要从传来的字段中取
abstractStr: '',
executionListenerInfo: cloneDeep(config.listener.processExecutionListenerInfo),
// 自动去重类型
processAutoDistinctTypeList: [
{
label: '当审批人和发起人是同一个人,审批自动通过',
value: 'SAMPLE'
},
{
label: '当同一审批人在流程中连续多次出现时,自动去重',
value: 'MULTIPLE'
}
],
drawer: false,
modelTitle: '全局属性',
activeKey: '1',
childNode: this.modelValue,
form: {},
formData: {},
fieldInfoLis: [],
formVerify: false
}
},
computed: {
// 监听内部数组,选了人员相关,我们就不提示
alertShow() {
return this.form.properties.participateInfo.length <= 0
}
},
watch: {
modelValue(val) {
this.childNode = val
},
childNode(val) {
this.$emit('update:modelValue', val)
},
formFieldListValue(val) {
// 获取主表名称
const parentTableName = JSON.parse(this.recordData.tableJson).filter((item) => item.tableType === 'parent')[0]
.tableName
// 监听到字段列表后,将其转至定义的变量中
this.fieldInfoLis = []
// 不仅表单中有字段,而且还要必须选择了主表
if (val.length > 0) {
const fildLists = this.getListField(val)
fildLists.forEach((item) => {
const obj = {}
// 判断是否是选择了表,并且选择了字段
if (item.selectTable && item.selectColumn) {
// 判断是否是主表的字段,并且是必填项
const requireds = item.rules[0].required
if ((item.selectTable === parentTableName) & requireds) {
obj.label = item.label
obj.value = item.model
this.fieldInfoLis.push(obj)
}
}
})
}
}
},
methods: {
showDrawer() {
this.form = {}
this.form = cloneDeep(this.childNode)
this.formData = this.form.properties.configInfo
this.drawer = true
// 制作默认的摘要字段
this.makeAbstractTemp()
},
// 制作默认显示的摘要信息
makeAbstractTemp() {
let temp = this.form.properties.configInfo.processAbstractTemplate
if (temp === '') {
if (this.fieldInfoLis.length > 0) {
let fieldInfoTemp = ''
for (let a = 0; a < this.fieldInfoLis.length; a++) {
// 最多3个摘要按顺序而来
if (a < 3) {
const str = this.fieldInfoLis[a].label + ':' + this.fieldInfoLis[a].value
if (a === 3 - 1 || a === this.fieldInfoLis.length - 1) {
fieldInfoTemp = fieldInfoTemp + str
} else {
fieldInfoTemp = fieldInfoTemp + str + ','
}
}
}
temp = fieldInfoTemp
}
}
},
onFinish() {
// 校验表单的正确性
this.$refs.noticeFormRef
.validate()
.then((values) => {
if (this.form.id === '') {
this.form.id = this.$TOOL.snowyUuid()
}
// 获取输入的监听
this.form.properties.executionListenerInfo = this.$refs.propListenerInfoRef.selectedListenerList()
this.form.dataLegal = true
this.form.properties.configInfo = values
this.$emit('update:modelValue', this.form)
this.drawer = false
})
.catch((info) => {
this.formVerify = true
setTimeout((e) => {
this.formVerify = false
}, 2000)
})
},
// 选择参与人
selectionParticipants(type) {
let participateInfo = this.form.properties.participateInfo
let data = []
if (participateInfo.length > 0) {
participateInfo.forEach((item) => {
if (item.key === type) {
data.push(item)
}
})
}
if (type === 'ORG') {
this.$refs.orgselectorPlus.showOrgPlusModal(data)
}
if (type === 'ROLE') {
this.$refs.roleselectorPlus.showRolePlusModal(data)
}
if (type === 'POSITION') {
this.$refs.posselectorPlus.showPosPlusModal(data)
}
if (type === 'USER') {
this.$refs.userselectorPlus.showUserPlusModal(data)
}
},
// 回调函数,这几个选择人员相关的设计器,都是的
callBack(value) {
let participateInfo = this.form.properties.participateInfo
if (participateInfo.length > 0) {
let mark = 0
for (let a = 0; a < participateInfo.length; a++) {
if (value.key === participateInfo[a].key) {
if (value.label === '') {
participateInfo.splice(a, 1)
} else {
participateInfo[a] = value
}
mark = 1
}
}
if (mark === 0) {
participateInfo.push(value)
}
} else {
this.form.properties.participateInfo.push(value)
}
},
// 获取tag标签的内容
getTagList(type) {
const participateInfo = this.form.properties.participateInfo
if (participateInfo.length < 0) {
return undefined
} else {
return participateInfo.find((item) => item.key === type)
}
},
getListField(data) {
let result = []
// 递归遍历控件树
const traverse = (array) => {
array.forEach((element) => {
if (element.type === 'grid' || element.type === 'tabs') {
// 栅格布局 and 标签页
element.columns.forEach((item) => {
traverse(item.list)
})
} else if (element.type === 'card') {
// 卡片布局 and 动态表格
traverse(element.list)
} else if (element.type === 'table') {
// 表格布局
element.trs.forEach((item) => {
item.tds.forEach((val) => {
traverse(val.list)
})
})
} else {
const type = element.type
// 排除一些
if (
(type !== 'alert') &
(type !== 'text') &
(type !== 'divider') &
(type !== 'batch') &
(type !== 'html')
) {
result.push(element)
}
}
})
}
traverse(data)
return result
}
}
}
</script>