!452 BPM 的草稿 PR(非 BPM 开发成员,不用关注)

Merge pull request !452 from 芋道源码/feature/bpm
pull/559/head
芋道源码 2024-10-04 09:46:09 +00:00 committed by Gitee
commit b94235adf3
No known key found for this signature in database
GPG Key ID: 173E9B9CA92EEF8F
47 changed files with 5699 additions and 2665 deletions

View File

@ -30,7 +30,7 @@ export const getModelPage = async (params) => {
return await request.get({ url: '/bpm/model/page', params })
}
export const getModel = async (id: number) => {
export const getModel = async (id: string) => {
return await request.get({ url: '/bpm/model/get?id=' + id })
}
@ -38,6 +38,10 @@ export const updateModel = async (data: ModelVO) => {
return await request.put({ url: '/bpm/model/update', data: data })
}
export const updateModelBpmn = async (data: ModelVO) => {
return await request.put({ url: '/bpm/model/update-bpmn', data: data })
}
// 任务状态修改
export const updateModelState = async (id: number, state: number) => {
const data = {

View File

@ -1,6 +1,6 @@
import request from '@/config/axios'
import { ProcessDefinitionVO } from '@/api/bpm/model'
import { NodeType } from '@/components/SimpleProcessDesignerV2/src/consts'
export type Task = {
id: string
name: string
@ -22,6 +22,35 @@ export type ProcessInstanceVO = {
processDefinition?: ProcessDefinitionVO
}
// 用户信息
export type User = {
id: number,
nickname: string,
avatar: string
}
// 审批任务信息
export type ApprovalTaskInfo = {
id: number,
ownerUser: User,
assigneeUser: User,
status: number,
reason: string
}
// 审批节点信息
export type ApprovalNodeInfo = {
id : number
name: string
nodeType: NodeType
status: number
startTime?: Date
endTime?: Date
candidateUserList?: User[]
tasks: ApprovalTaskInfo[]
}
export const getProcessInstanceMyPage = async (params: any) => {
return await request.get({ url: '/bpm/process-instance/my-page', params })
}
@ -57,3 +86,14 @@ export const getProcessInstance = async (id: string) => {
export const getProcessInstanceCopyPage = async (params: any) => {
return await request.get({ url: '/bpm/process-instance/copy/page', params })
}
// 获取审批详情
export const getApprovalDetail = async (processInstanceId?:string, processDefinitionId?:string) => {
const param = processInstanceId ? '?processInstanceId='+ processInstanceId : '?processDefinitionId='+ processDefinitionId
return await request.get({ url: 'bpm/process-instance/get-approval-detail'+ param })
}
// 获取表单字段权限
export const getFormFieldsPermission = async (params: any) => {
return await request.get({ url: '/bpm/process-instance/get-form-fields-permission', params })
}

View File

@ -0,0 +1,15 @@
import request from '@/config/axios'
export const updateBpmSimpleModel = async (data) => {
return await request.post({
url: '/bpm/model/simple/update',
data: data
})
}
export const getBpmSimpleModel = async (id) => {
return await request.get({
url: '/bpm/model/simple/get?id=' + id
})
}

View File

@ -1,5 +1,51 @@
import request from '@/config/axios'
/**
*
*/
export enum TaskStatusEnum {
/**
*
*/
NOT_START = -1,
/**
*
*/
WAIT = 0,
/**
*
*/
RUNNING = 1,
/**
*
*/
APPROVE = 2,
/**
*
*/
REJECT = 3,
/**
*
*/
CANCEL = 4,
/**
* 退
*/
RETURN = 5,
/**
*
*/
DELEGATE = 6,
/**
*
*/
APPROVING = 7,
}
export type TaskVO = {
id: number
}

View File

@ -1,237 +0,0 @@
/* stylelint-disable order/properties-order */
<template>
<div class="add-node-btn-box">
<div class="add-node-btn">
<el-popover placement="right-start" v-model="visible" width="auto">
<div class="add-node-popover-body">
<a class="add-node-popover-item approver" @click="addType(1)">
<div class="item-wrapper">
<span class="iconfont"></span>
</div>
<p>审批人</p>
</a>
<a class="add-node-popover-item notifier" @click="addType(2)">
<div class="item-wrapper">
<span class="iconfont"></span>
</div>
<p>抄送人</p>
</a>
<a class="add-node-popover-item condition" @click="addType(4)">
<div class="item-wrapper">
<span class="iconfont"></span>
</div>
<p>条件分支</p>
</a>
</div>
<template #reference>
<button class="btn" type="button">
<span class="iconfont"></span>
</button>
</template>
</el-popover>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
let props = defineProps({
childNodeP: {
type: Object,
default: () => ({})
}
})
let emits = defineEmits(['update:childNodeP'])
let visible = ref(false)
const addType = (type) => {
visible.value = false
if (type != 4) {
var data
if (type == 1) {
data = {
nodeName: '审核人',
error: true,
type: 1,
settype: 1,
selectMode: 0,
selectRange: 0,
directorLevel: 1,
examineMode: 1,
noHanderAction: 1,
examineEndDirectorLevel: 0,
childNode: props.childNodeP,
nodeUserList: []
}
} else if (type == 2) {
data = {
nodeName: '抄送人',
type: 2,
ccSelfSelectFlag: 1,
childNode: props.childNodeP,
nodeUserList: []
}
}
emits('update:childNodeP', data)
} else {
emits('update:childNodeP', {
nodeName: '路由',
type: 4,
childNode: null,
conditionNodes: [
{
nodeName: '条件1',
error: true,
type: 3,
priorityLevel: 1,
conditionList: [],
nodeUserList: [],
childNode: props.childNodeP
},
{
nodeName: '条件2',
type: 3,
priorityLevel: 2,
conditionList: [],
nodeUserList: [],
childNode: null
}
]
})
}
}
</script>
<style scoped lang="scss">
.add-node-btn-box {
width: 240px;
display: inline-flex;
-ms-flex-negative: 0;
flex-shrink: 0;
-webkit-box-flex: 1;
-ms-flex-positive: 1;
position: relative;
&:before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: -1;
margin: auto;
width: 2px;
height: 100%;
background-color: #cacaca;
}
.add-node-btn {
user-select: none;
width: 240px;
padding: 20px 0 32px;
display: flex;
-webkit-box-pack: center;
justify-content: center;
flex-shrink: 0;
-webkit-box-flex: 1;
flex-grow: 1;
.btn {
outline: none;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.1);
width: 30px;
height: 30px;
background: #3296fa;
border-radius: 50%;
position: relative;
border: none;
line-height: 30px;
-webkit-transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
.iconfont {
color: #fff;
font-size: 16px;
}
&:hover {
transform: scale(1.3);
box-shadow: 0 13px 27px 0 rgba(0, 0, 0, 0.1);
}
&:active {
transform: none;
background: #1e83e9;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.1);
}
}
}
}
.add-node-popover-body {
display: flex;
.add-node-popover-item {
margin-right: 10px;
cursor: pointer;
text-align: center;
flex: 1;
color: #191f25 !important;
.item-wrapper {
user-select: none;
display: inline-block;
width: 80px;
height: 80px;
margin-bottom: 5px;
background: #fff;
border: 1px solid #e2e2e2;
border-radius: 50%;
transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
.iconfont {
font-size: 35px;
line-height: 80px;
}
}
&.approver {
.item-wrapper {
color: #ff943e;
}
}
&.notifier {
.item-wrapper {
color: #3296fa;
}
}
&.condition {
.item-wrapper {
color: #15bc83;
}
}
&:hover {
.item-wrapper {
background: #3296fa;
box-shadow: 0 10px 20px 0 rgba(50, 150, 250, 0.4);
}
.iconfont {
color: #fff;
}
}
&:active {
.item-wrapper {
box-shadow: none;
background: #eaeaea;
}
.iconfont {
color: inherit;
}
}
}
}
</style>

View File

@ -1,297 +0,0 @@
<!-- eslint-disable vue/no-mutating-props -->
<!--
* @Date: 2022-09-21 14:41:53
* @LastEditors: StavinLi 495727881@qq.com
* @LastEditTime: 2023-05-24 15:20:24
* @FilePath: /Workflow-Vue3/src/components/nodeWrap.vue
-->
<template>
<div class="node-wrap" v-if="nodeConfig.type < 3">
<div class="node-wrap-box" :class="(nodeConfig.type == 0 ? 'start-node ' : '') +(isTried && nodeConfig.error ? 'active error' : '')">
<div class="title" :style="`background: rgb(${bgColors[nodeConfig.type]});`">
<span v-if="nodeConfig.type == 0">{{ nodeConfig.nodeName }}</span>
<template v-else>
<span class="iconfont">{{nodeConfig.type == 1?'':''}}</span>
<input
v-if="isInput"
type="text"
class="ant-input editable-title-input"
@blur="blurEvent()"
@focus="$event.currentTarget.select()"
v-focus
v-model="nodeConfig.nodeName"
:placeholder="defaultText"
/>
<span v-else class="editable-title" @click="clickEvent()">{{ nodeConfig.nodeName }}</span>
<i class="anticon anticon-close close" @click="delNode"></i>
</template>
</div>
<div class="content" @click="setPerson">
<div class="text">
<span class="placeholder" v-if="!showText">{{defaultText}}</span>
{{showText}}
</div>
<i class="anticon anticon-right arrow"></i>
</div>
<div class="error_tip" v-if="isTried && nodeConfig.error">
<i class="anticon anticon-exclamation-circle"></i>
</div>
</div>
<addNode v-model:childNodeP="nodeConfig.childNode" />
</div>
<div class="branch-wrap" v-if="nodeConfig.type == 4">
<div class="branch-box-wrap">
<div class="branch-box">
<button class="add-branch" @click="addTerm"></button>
<div class="col-box" v-for="(item, index) in nodeConfig.conditionNodes" :key="index">
<div class="condition-node">
<div class="condition-node-box">
<div class="auto-judge" :class="isTried && item.error ? 'error active' : ''">
<div class="sort-left" v-if="index != 0" @click="arrTransfer(index, -1)">&lt;</div>
<div class="title-wrapper">
<input
v-if="isInputList[index]"
type="text"
class="ant-input editable-title-input"
@blur="blurEvent(index)"
@focus="$event.currentTarget.select()"
v-model="item.nodeName"
/>
<span v-else class="editable-title" @click="clickEvent(index)">{{ item.nodeName }}</span>
<span class="priority-title" @click="setPerson(item.priorityLevel)">{{ item.priorityLevel }}</span>
<i class="anticon anticon-close close" @click="delTerm(index)"></i>
</div>
<div class="sort-right" v-if="index != nodeConfig.conditionNodes.length - 1" @click="arrTransfer(index)">&gt;</div>
<div class="content" @click="setPerson(item.priorityLevel)">{{ conditionStr(nodeConfig, index) }}</div>
<div class="error_tip" v-if="isTried && item.error">
<i class="anticon anticon-exclamation-circle"></i>
</div>
</div>
<addNode v-model:childNodeP="item.childNode" />
</div>
</div>
<nodeWrap v-if="item.childNode" v-model:nodeConfig="item.childNode" />
<template v-if="index == 0">
<div class="top-left-cover-line"></div>
<div class="bottom-left-cover-line"></div>
</template>
<template v-if="index == nodeConfig.conditionNodes.length - 1">
<div class="top-right-cover-line"></div>
<div class="bottom-right-cover-line"></div>
</template>
</div>
</div>
<addNode v-model:childNodeP="nodeConfig.childNode" />
</div>
</div>
<nodeWrap v-if="nodeConfig.childNode" v-model:nodeConfig="nodeConfig.childNode" />
</template>
<script setup>
import addNode from './addNode.vue'
import { onMounted, ref, watch, getCurrentInstance, computed } from 'vue'
import {
arrToStr,
conditionStr,
setApproverStr,
copyerStr,
bgColors,
placeholderList
} from './util'
import { useWorkFlowStoreWithOut } from '@/store/modules/simpleWorkflow'
let _uid = getCurrentInstance().uid
let props = defineProps({
nodeConfig: {
type: Object,
default: () => ({})
},
flowPermission: {
type: Object,
// eslint-disable-next-line vue/require-valid-default-prop
default: () => []
}
})
let defaultText = computed(() => {
return placeholderList[props.nodeConfig.type]
})
let showText = computed(() => {
if (props.nodeConfig.type == 0) return arrToStr(props.flowPermission) || '所有人'
if (props.nodeConfig.type == 1) return setApproverStr(props.nodeConfig)
return copyerStr(props.nodeConfig)
})
let isInputList = ref([])
let isInput = ref(false)
const resetConditionNodesErr = () => {
for (var i = 0; i < props.nodeConfig.conditionNodes.length; i++) {
// eslint-disable-next-line vue/no-mutating-props
props.nodeConfig.conditionNodes[i].error =
conditionStr(props.nodeConfig, i) == '请设置条件' &&
i != props.nodeConfig.conditionNodes.length - 1
}
}
onMounted(() => {
if (props.nodeConfig.type == 1) {
// eslint-disable-next-line vue/no-mutating-props
props.nodeConfig.error = !setApproverStr(props.nodeConfig)
} else if (props.nodeConfig.type == 2) {
// eslint-disable-next-line vue/no-mutating-props
props.nodeConfig.error = !copyerStr(props.nodeConfig)
} else if (props.nodeConfig.type == 4) {
resetConditionNodesErr()
}
})
let emits = defineEmits(['update:flowPermission', 'update:nodeConfig'])
let store = useWorkFlowStoreWithOut()
let {
setPromoter,
setApprover,
setCopyer,
setCondition,
setFlowPermission,
setApproverConfig,
setCopyerConfig,
setConditionsConfig
} = store
let isTried = computed(() => store.isTried)
let flowPermission1 = computed(() => store.flowPermission1)
let approverConfig1 = computed(() => store.approverConfig1)
let copyerConfig1 = computed(() => store.copyerConfig1)
let conditionsConfig1 = computed(() => store.conditionsConfig1)
watch(flowPermission1, (flow) => {
if (flow.flag && flow.id === _uid) {
emits('update:flowPermission', flow.value)
}
})
watch(approverConfig1, (approver) => {
if (approver.flag && approver.id === _uid) {
emits('update:nodeConfig', approver.value)
}
})
watch(copyerConfig1, (copyer) => {
if (copyer.flag && copyer.id === _uid) {
emits('update:nodeConfig', copyer.value)
}
})
watch(conditionsConfig1, (condition) => {
if (condition.flag && condition.id === _uid) {
emits('update:nodeConfig', condition.value)
}
})
const clickEvent = (index) => {
if (index || index === 0) {
isInputList.value[index] = true
} else {
isInput.value = true
}
}
const blurEvent = (index) => {
if (index || index === 0) {
isInputList.value[index] = false
// eslint-disable-next-line vue/no-mutating-props
props.nodeConfig.conditionNodes[index].nodeName =
props.nodeConfig.conditionNodes[index].nodeName || '条件'
} else {
isInput.value = false
// eslint-disable-next-line vue/no-mutating-props
props.nodeConfig.nodeName = props.nodeConfig.nodeName || defaultText
}
}
const delNode = () => {
emits('update:nodeConfig', props.nodeConfig.childNode)
}
const addTerm = () => {
let len = props.nodeConfig.conditionNodes.length + 1
// eslint-disable-next-line vue/no-mutating-props
props.nodeConfig.conditionNodes.push({
nodeName: '条件' + len,
type: 3,
priorityLevel: len,
conditionList: [],
nodeUserList: [],
childNode: null
})
resetConditionNodesErr()
emits('update:nodeConfig', props.nodeConfig)
}
const delTerm = (index) => {
// eslint-disable-next-line vue/no-mutating-props
props.nodeConfig.conditionNodes.splice(index, 1)
props.nodeConfig.conditionNodes.map((item, index) => {
item.priorityLevel = index + 1
item.nodeName = `条件${index + 1}`
})
resetConditionNodesErr()
emits('update:nodeConfig', props.nodeConfig)
if (props.nodeConfig.conditionNodes.length == 1) {
if (props.nodeConfig.childNode) {
if (props.nodeConfig.conditionNodes[0].childNode) {
reData(props.nodeConfig.conditionNodes[0].childNode, props.nodeConfig.childNode)
} else {
// eslint-disable-next-line vue/no-mutating-props
props.nodeConfig.conditionNodes[0].childNode = props.nodeConfig.childNode
}
}
emits('update:nodeConfig', props.nodeConfig.conditionNodes[0].childNode)
}
}
const reData = (data, addData) => {
if (!data.childNode) {
data.childNode = addData
} else {
reData(data.childNode, addData)
}
}
const setPerson = (priorityLevel) => {
var { type } = props.nodeConfig
if (type == 0) {
setPromoter(true)
setFlowPermission({
value: props.flowPermission,
flag: false,
id: _uid
})
} else if (type == 1) {
setApprover(true)
setApproverConfig({
value: {
...JSON.parse(JSON.stringify(props.nodeConfig)),
...{ settype: props.nodeConfig.settype ? props.nodeConfig.settype : 1 }
},
flag: false,
id: _uid
})
} else if (type == 2) {
setCopyer(true)
setCopyerConfig({
value: JSON.parse(JSON.stringify(props.nodeConfig)),
flag: false,
id: _uid
})
} else {
setCondition(true)
setConditionsConfig({
value: JSON.parse(JSON.stringify(props.nodeConfig)),
priorityLevel,
flag: false,
id: _uid
})
}
}
const arrTransfer = (index, type = 1) => {
//-1,1
// eslint-disable-next-line vue/no-mutating-props
props.nodeConfig.conditionNodes[index] = props.nodeConfig.conditionNodes.splice(
index + type,
1,
props.nodeConfig.conditionNodes[index]
)[0]
props.nodeConfig.conditionNodes.map((item, index) => {
item.priorityLevel = index + 1
})
resetConditionNodesErr()
emits('update:nodeConfig', props.nodeConfig)
}
</script>

View File

@ -1,165 +0,0 @@
/**
* todo
*/
export const arrToStr = (arr?: [{ name: string }]) => {
if (arr) {
return arr
.map((item) => {
return item.name
})
.toString()
}
}
export const setApproverStr = (nodeConfig: any) => {
if (nodeConfig.settype == 1) {
if (nodeConfig.nodeUserList.length == 1) {
return nodeConfig.nodeUserList[0].name
} else if (nodeConfig.nodeUserList.length > 1) {
if (nodeConfig.examineMode == 1) {
return arrToStr(nodeConfig.nodeUserList)
} else if (nodeConfig.examineMode == 2) {
return nodeConfig.nodeUserList.length + '人会签'
}
}
} else if (nodeConfig.settype == 2) {
const level =
nodeConfig.directorLevel == 1 ? '直接主管' : '第' + nodeConfig.directorLevel + '级主管'
if (nodeConfig.examineMode == 1) {
return level
} else if (nodeConfig.examineMode == 2) {
return level + '会签'
}
} else if (nodeConfig.settype == 4) {
if (nodeConfig.selectRange == 1) {
return '发起人自选'
} else {
if (nodeConfig.nodeUserList.length > 0) {
if (nodeConfig.selectRange == 2) {
return '发起人自选'
} else {
return '发起人从' + nodeConfig.nodeUserList[0].name + '中自选'
}
} else {
return ''
}
}
} else if (nodeConfig.settype == 5) {
return '发起人自己'
} else if (nodeConfig.settype == 7) {
return '从直接主管到通讯录中级别最高的第' + nodeConfig.examineEndDirectorLevel + '个层级主管'
}
}
export const copyerStr = (nodeConfig: any) => {
if (nodeConfig.nodeUserList.length != 0) {
return arrToStr(nodeConfig.nodeUserList)
} else {
if (nodeConfig.ccSelfSelectFlag == 1) {
return '发起人自选'
}
}
}
export const conditionStr = (nodeConfig, index) => {
const { conditionList, nodeUserList } = nodeConfig.conditionNodes[index]
if (conditionList.length == 0) {
return index == nodeConfig.conditionNodes.length - 1 &&
nodeConfig.conditionNodes[0].conditionList.length != 0
? '其他条件进入此流程'
: '请设置条件'
} else {
let str = ''
for (let i = 0; i < conditionList.length; i++) {
const {
columnId,
columnType,
showType,
showName,
optType,
zdy1,
opt1,
zdy2,
opt2,
fixedDownBoxValue
} = conditionList[i]
if (columnId == 0) {
if (nodeUserList.length != 0) {
str += '发起人属于:'
str +=
nodeUserList
.map((item) => {
return item.name
})
.join('或') + ' 并且 '
}
}
if (columnType == 'String' && showType == '3') {
if (zdy1) {
str += showName + '属于:' + dealStr(zdy1, JSON.parse(fixedDownBoxValue)) + ' 并且 '
}
}
if (columnType == 'Double') {
if (optType != 6 && zdy1) {
const optTypeStr = ['', '<', '>', '≤', '=', '≥'][optType]
str += `${showName} ${optTypeStr} ${zdy1} 并且 `
} else if (optType == 6 && zdy1 && zdy2) {
str += `${zdy1} ${opt1} ${showName} ${opt2} ${zdy2} 并且 `
}
}
}
return str ? str.substring(0, str.length - 4) : '请设置条件'
}
}
export const dealStr = (str: string, obj) => {
const arr = []
const list = str.split(',')
for (const elem in obj) {
list.map((item) => {
if (item == elem) {
arr.push(obj[elem].value)
}
})
}
return arr.join('或')
}
export const removeEle = (arr, elem, key = 'id') => {
let includesIndex
arr.map((item, index) => {
if (item[key] == elem[key]) {
includesIndex = index
}
})
arr.splice(includesIndex, 1)
}
export const bgColors = ['87, 106, 149', '255, 148, 62', '50, 150, 250']
export const placeholderList = ['发起人', '审核人', '抄送人']
export const setTypes = [
{ value: 1, label: '指定成员' },
{ value: 2, label: '主管' },
{ value: 4, label: '发起人自选' },
{ value: 5, label: '发起人自己' },
{ value: 7, label: '连续多级主管' }
]
export const selectModes = [
{ value: 1, label: '选一个人' },
{ value: 2, label: '选多个人' }
]
export const selectRanges = [
{ value: 1, label: '全公司' },
{ value: 2, label: '指定成员' },
{ value: 3, label: '指定角色' }
]
export const optTypes = [
{ value: '1', label: '小于' },
{ value: '2', label: '大于' },
{ value: '3', label: '小于等于' },
{ value: '4', label: '等于' },
{ value: '5', label: '大于等于' },
{ value: '6', label: '介于两个数之间' }
]

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,168 @@
<template>
<div class="node-handler-wrapper">
<div class="node-handler" v-if="props.showAdd">
<el-popover
trigger="hover"
v-model:visible="popoverShow"
placement="right-start"
width="auto"
>
<div class="handler-item-wrapper">
<div class="handler-item" @click="addNode(NodeType.USER_TASK_NODE)">
<div class="approve handler-item-icon">
<span class="iconfont icon-approve icon-size"></span>
</div>
<div class="handler-item-text">审批人</div>
</div>
<div class="handler-item" @click="addNode(NodeType.COPY_TASK_NODE)">
<div class="handler-item-icon copy">
<span class="iconfont icon-size icon-copy"></span>
</div>
<div class="handler-item-text">抄送</div>
</div>
<div class="handler-item" @click="addNode(NodeType.CONDITION_BRANCH_NODE)">
<div class="handler-item-icon condition">
<span class="iconfont icon-size icon-exclusive"></span>
</div>
<div class="handler-item-text">条件分支</div>
</div>
<div class="handler-item" @click="addNode(NodeType.PARALLEL_BRANCH_NODE)">
<div class="handler-item-icon condition">
<span class="iconfont icon-size icon-parallel"></span>
</div>
<div class="handler-item-text">并行分支</div>
</div>
</div>
<template #reference>
<div class="add-icon"><Icon icon="ep:plus" /></div>
</template>
</el-popover>
</div>
</div>
</template>
<script setup lang="ts">
import {
ApproveMethodType,
AssignEmptyHandlerType,
AssignStartUserHandlerType,
NODE_DEFAULT_NAME,
NodeType,
RejectHandlerType,
SimpleFlowNode
} from './consts'
import { generateUUID } from '@/utils'
defineOptions({
name: 'NodeHandler'
})
const popoverShow = ref(false)
const props = defineProps({
childNode: {
type: Object as () => SimpleFlowNode,
default: null
},
showAdd: {
//
type: Boolean,
default: true
}
})
const emits = defineEmits(['update:childNode'])
const addNode = (type: number) => {
popoverShow.value = false
if (type === NodeType.USER_TASK_NODE) {
const id = 'Activity_' + generateUUID()
const data: SimpleFlowNode = {
id: id,
name: NODE_DEFAULT_NAME.get(NodeType.USER_TASK_NODE) as string,
showText: '',
type: NodeType.USER_TASK_NODE,
approveMethod: ApproveMethodType.SEQUENTIAL_APPROVE,
//
rejectHandler: {
type: RejectHandlerType.FINISH_PROCESS
},
timeoutHandler: {
enable: false
},
assignEmptyHandler: {
type: AssignEmptyHandlerType.APPROVE
},
assignStartUserHandlerType: AssignStartUserHandlerType.START_USER_AUDIT,
childNode: props.childNode
}
emits('update:childNode', data)
}
if (type === NodeType.COPY_TASK_NODE) {
const data: SimpleFlowNode = {
id: 'Activity_' + generateUUID(),
name: NODE_DEFAULT_NAME.get(NodeType.COPY_TASK_NODE) as string,
showText: '',
type: NodeType.COPY_TASK_NODE,
childNode: props.childNode
}
emits('update:childNode', data)
}
if (type === NodeType.CONDITION_BRANCH_NODE) {
const data: SimpleFlowNode = {
name: '条件分支',
type: NodeType.CONDITION_BRANCH_NODE,
id: 'GateWay_' + generateUUID(),
childNode: props.childNode,
conditionNodes: [
{
id: 'Flow_' + generateUUID(),
name: '条件1',
showText: '',
type: NodeType.CONDITION_NODE,
childNode: undefined,
conditionType: 1,
defaultFlow: false
},
{
id: 'Flow_' + generateUUID(),
name: '其它情况',
showText: '其它情况进入此流程',
type: NodeType.CONDITION_NODE,
childNode: undefined,
conditionType: undefined,
defaultFlow: true
}
]
}
emits('update:childNode', data)
}
if (type === NodeType.PARALLEL_BRANCH_NODE) {
const data: SimpleFlowNode = {
name: '并行分支',
type: NodeType.PARALLEL_BRANCH_NODE,
id: 'GateWay_' + generateUUID(),
childNode: props.childNode,
conditionNodes: [
{
id: 'Flow_' + generateUUID(),
name: '并行1',
showText: '无需配置条件同时执行',
type: NodeType.CONDITION_NODE,
childNode: undefined
},
{
id: 'Flow_' + generateUUID(),
name: '并行2',
showText: '无需配置条件同时执行',
type: NodeType.CONDITION_NODE,
childNode: undefined
}
]
}
emits('update:childNode', data)
}
}
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,107 @@
<template>
<!-- 发起人节点 -->
<StartUserNode
v-if="currentNode && currentNode.type === NodeType.START_USER_NODE"
:flow-node="currentNode"
/>
<!-- 审批节点 -->
<UserTaskNode
v-if="currentNode && currentNode.type === NodeType.USER_TASK_NODE"
:flow-node="currentNode"
@update:flow-node="handleModelValueUpdate"
@find:parent-node="findFromParentNode"
/>
<!-- 抄送节点 -->
<CopyTaskNode
v-if="currentNode && currentNode.type === NodeType.COPY_TASK_NODE"
:flow-node="currentNode"
@update:flow-node="handleModelValueUpdate"
/>
<!-- 条件节点 -->
<ExclusiveNode
v-if="currentNode && currentNode.type === NodeType.CONDITION_BRANCH_NODE"
:flow-node="currentNode"
@update:model-value="handleModelValueUpdate"
@find:parent-node="findFromParentNode"
/>
<!-- 并行节点 -->
<ParallelNode
v-if="currentNode && currentNode.type === NodeType.PARALLEL_BRANCH_NODE"
:flow-node="currentNode"
@update:model-value="handleModelValueUpdate"
@find:parent-node="findFromParentNode"
/>
<!-- 递归显示孩子节点 -->
<ProcessNodeTree
v-if="currentNode && currentNode.childNode"
v-model:flow-node="currentNode.childNode"
:parent-node="currentNode"
@find:recursive-find-parent-node="recursiveFindParentNode"
/>
<!-- 结束节点 -->
<EndEventNode v-if="currentNode && currentNode.type === NodeType.END_EVENT_NODE" />
</template>
<script setup lang="ts">
import StartUserNode from './nodes/StartUserNode.vue'
import EndEventNode from './nodes/EndEventNode.vue'
import UserTaskNode from './nodes/UserTaskNode.vue'
import CopyTaskNode from './nodes/CopyTaskNode.vue'
import ExclusiveNode from './nodes/ExclusiveNode.vue'
import ParallelNode from './nodes/ParallelNode.vue'
import { SimpleFlowNode, NodeType } from './consts'
import { useWatchNode } from './node'
defineOptions({
name: 'ProcessNodeTree'
})
const props = defineProps({
parentNode: {
type: Object as () => SimpleFlowNode,
default: () => null
},
flowNode: {
type: Object as () => SimpleFlowNode,
default: () => null
}
})
const emits = defineEmits<{
'update:flowNode': [node: SimpleFlowNode | undefined]
'find:recursiveFindParentNode': [
nodeList: SimpleFlowNode[],
curentNode: SimpleFlowNode,
nodeType: number
]
}>()
const currentNode = useWatchNode(props)
//
const handleModelValueUpdate = (updateValue) => {
emits('update:flowNode', updateValue)
}
const findFromParentNode = (nodeList: SimpleFlowNode[], nodeType: number) => {
emits('find:recursiveFindParentNode', nodeList, props.parentNode, nodeType)
}
//
const recursiveFindParentNode = (
nodeList: SimpleFlowNode[],
findNode: SimpleFlowNode,
nodeType: number
) => {
if (!findNode) {
return
}
if (findNode.type === NodeType.START_USER_NODE) {
nodeList.push(findNode)
return
}
if (findNode.type === nodeType) {
nodeList.push(findNode)
}
emits('find:recursiveFindParentNode', nodeList, props.parentNode, nodeType)
}
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,215 @@
<template>
<div class="simple-flow-canvas" v-loading="loading">
<div class="simple-flow-container" >
<div class="top-area-container">
<div class="top-actions">
<div class="canvas-control">
<span class="control-scale-group">
<span class="control-scale-button"> <Icon icon="ep:plus" @click="zoomOut()" /></span>
<span class="control-scale-label">{{ scaleValue }}%</span>
<span class="control-scale-button"><Icon icon="ep:minus" @click="zoomIn()" /></span>
</span>
</div>
<el-button type="primary" @click="saveSimpleFlowModel"></el-button>
<!-- <el-button type="primary">全局设置</el-button> -->
</div>
</div>
<div class="scale-container" :style="`transform: scale(${scaleValue / 100});`">
<ProcessNodeTree
v-if="processNodeTree"
v-model:flow-node="processNodeTree"
/>
</div>
</div>
<Dialog v-model="errorDialogVisible" title="保存失败" width="400" :fullscreen="false">
<div class="mb-2">以下节点内容不完善请修改后保存</div>
<div
class="mb-3 b-rounded-1 bg-gray-100 p-2 line-height-normal"
v-for="(item, index) in errorNodes"
:key="index"
>
{{ item.name }} : {{ NODE_DEFAULT_TEXT.get(item.type) }}
</div>
<template #footer>
<el-button type="primary" @click="errorDialogVisible = false">知道了</el-button>
</template>
</Dialog>
</div>
</template>
<script setup lang="ts">
import ProcessNodeTree from './ProcessNodeTree.vue'
import { updateBpmSimpleModel, getBpmSimpleModel } from '@/api/bpm/simple'
import { SimpleFlowNode, NodeType, NodeId, NODE_DEFAULT_TEXT } from './consts'
import { getModel } from '@/api/bpm/model'
import { getForm, FormVO } from '@/api/bpm/form'
import { handleTree } from '@/utils/tree'
import * as RoleApi from '@/api/system/role'
import * as DeptApi from '@/api/system/dept'
import * as PostApi from '@/api/system/post'
import * as UserApi from '@/api/system/user'
import * as UserGroupApi from '@/api/bpm/userGroup'
import { fa } from 'element-plus/es/locale'
defineOptions({
name: 'SimpleProcessDesigner'
})
const router = useRouter() //
const props = defineProps({
modelId: {
type: String,
required: true
}
})
const loading = ref(true)
const formFields = ref<string[]>([])
const formType = ref(20)
const roleOptions = ref<RoleApi.RoleVO[]>([]) //
const postOptions = ref<PostApi.PostVO[]>([]) //
const userOptions = ref<UserApi.UserVO[]>([]) //
const deptOptions = ref<DeptApi.DeptVO[]>([]) //
const deptTreeOptions = ref()
const userGroupOptions = ref<UserGroupApi.UserGroupVO[]>([]) //
provide('formFields', formFields)
provide('formType', formType)
provide('roleList', roleOptions)
provide('postList', postOptions)
provide('userList', userOptions)
provide('deptList', deptOptions)
provide('userGroupList', userGroupOptions)
provide('deptTree', deptTreeOptions)
const message = useMessage() //
const processNodeTree = ref<SimpleFlowNode | undefined>()
const errorDialogVisible = ref(false)
let errorNodes: SimpleFlowNode[] = []
const saveSimpleFlowModel = async () => {
if (!props.modelId) {
message.error('缺少模型 modelId 编号')
return
}
errorNodes = []
validateNode(processNodeTree.value, errorNodes)
if (errorNodes.length > 0) {
errorDialogVisible.value = true
return
}
const data = {
id: props.modelId,
simpleModel: processNodeTree.value
}
const result = await updateBpmSimpleModel(data)
if (result) {
message.success('修改成功')
close()
} else {
message.alert('修改失败')
}
}
// showText
const validateNode = (node: SimpleFlowNode | undefined, errorNodes: SimpleFlowNode[]) => {
if (node) {
const { type, showText, conditionNodes } = node
if (type == NodeType.END_EVENT_NODE) {
return
}
if (type == NodeType.START_USER_NODE) {
validateNode(node.childNode, errorNodes)
}
if (type === NodeType.USER_TASK_NODE) {
if (!showText) {
errorNodes.push(node)
}
validateNode(node.childNode, errorNodes)
}
if (type === NodeType.COPY_TASK_NODE) {
if (!showText) {
errorNodes.push(node)
}
validateNode(node.childNode, errorNodes)
}
if (type === NodeType.CONDITION_NODE) {
if (!showText) {
errorNodes.push(node)
}
validateNode(node.childNode, errorNodes)
}
if (type == NodeType.CONDITION_BRANCH_NODE) {
conditionNodes?.forEach((item) => {
validateNode(item, errorNodes)
})
validateNode(node.childNode, errorNodes)
}
}
}
const close = () => {
router.push({ path: '/bpm/manager/model' })
}
let scaleValue = ref(100)
const MAX_SCALE_VALUE = 200
const MIN_SCALE_VALUE = 50
//
const zoomOut = () => {
if (scaleValue.value == MAX_SCALE_VALUE) {
return
}
scaleValue.value += 10
}
//
const zoomIn = () => {
if (scaleValue.value == MIN_SCALE_VALUE) {
return
}
scaleValue.value -= 10
}
onMounted(async () => {
try {
loading.value = true
//
const bpmnModel = await getModel(props.modelId)
if (bpmnModel) {
formType.value = bpmnModel.formType
if (formType.value === 10) {
const bpmnForm = (await getForm(bpmnModel.formId)) as unknown as FormVO
formFields.value = bpmnForm?.fields
}
}
//
roleOptions.value = await RoleApi.getSimpleRoleList()
//
postOptions.value = await PostApi.getSimplePostList()
//
userOptions.value = await UserApi.getSimpleUserList()
//
deptOptions.value = await DeptApi.getSimpleDeptList()
deptTreeOptions.value = handleTree(deptOptions.value as DeptApi.DeptVO[], 'id')
//
userGroupOptions.value = await UserGroupApi.getUserGroupSimpleList()
// SIMPLE
const result = await getBpmSimpleModel(props.modelId)
if (result) {
processNodeTree.value = result
} else {
//
processNodeTree.value = {
name: '发起人',
type: NodeType.START_USER_NODE,
id: NodeId.START_USER_NODE_ID,
childNode: {
id: NodeId.END_EVENT_NODE_ID,
name: '结束',
type: NodeType.END_EVENT_NODE
}
}
}
} finally {
loading.value = false
}
})
</script>

View File

@ -0,0 +1,544 @@
// @ts-ignore
import { DictDataVO } from '@/api/system/dict/types'
/**
*
*/
export enum NodeType {
/**
*
*/
END_EVENT_NODE = 1,
/**
*
*/
START_USER_NODE = 10,
/**
*
*/
USER_TASK_NODE = 11,
/**
*
*/
COPY_TASK_NODE = 12,
/**
*
*/
CONDITION_NODE = 50,
/**
* ()
*/
CONDITION_BRANCH_NODE = 51,
/**
* ()
*/
PARALLEL_BRANCH_NODE = 52,
/**
* ()
*/
INCLUSIVE_BRANCH_NODE = 53
}
export enum NodeId {
/**
* Id
*/
START_USER_NODE_ID = 'StartUserNode',
/**
* Id
*/
END_EVENT_NODE_ID = 'EndEvent'
}
/**
*
*/
export interface SimpleFlowNode {
id: string
type: NodeType
name: string
showText?: string
// 孩子节点
childNode?: SimpleFlowNode
// 条件节点
conditionNodes?: SimpleFlowNode[]
// 审批类型
approveType?: ApproveType
// 候选人策略
candidateStrategy?: number
// 候选人参数
candidateParam?: string
// 多人审批方式
approveMethod?: ApproveMethodType
//通过比例
approveRatio?: number
// 审批按钮设置
buttonsSetting?: any[]
// 表单权限
fieldsPermission?: Array<Record<string, string>>
// 审批任务超时处理
timeoutHandler?: TimeoutHandler
// 审批任务拒绝处理
rejectHandler?: RejectHandler
// 审批人为空的处理
assignEmptyHandler?: AssignEmptyHandler
// 审批节点的审批人与发起人相同时,对应的处理类型
assignStartUserHandlerType?: number
// 条件类型
conditionType?: ConditionType
// 条件表达式
conditionExpression?: string
// 条件组
conditionGroups?: ConditionGroup
// 是否默认的条件
defaultFlow?: boolean
}
// 候选人策略枚举 用于审批节点。抄送节点 )
export enum CandidateStrategy {
/**
*
*/
ROLE = 10,
/**
*
*/
DEPT_MEMBER = 20,
/**
*
*/
DEPT_LEADER = 21,
/**
*
*/
MULTI_LEVEL_DEPT_LEADER = 23,
/**
*
*/
POST = 22,
/**
*
*/
USER = 30,
/**
*
*/
START_USER_SELECT = 35,
/**
*
*/
START_USER = 36,
/**
*
*/
START_USER_DEPT_LEADER = 37,
/**
*
*/
START_USER_MULTI_LEVEL_DEPT_LEADER = 38,
/**
*
*/
USER_GROUP = 40,
/**
*
*/
EXPRESSION = 60
}
// 多人审批方式类型枚举 用于审批节点
export enum ApproveMethodType {
/**
*
*/
RANDOM_SELECT_ONE_APPROVE = 1,
/**
* ()
*/
APPROVE_BY_RATIO = 2,
/**
* ()
*/
ANY_APPROVE = 3,
/**
*
*/
SEQUENTIAL_APPROVE = 4
}
/**
*
*/
export type RejectHandler = {
// 审批拒绝类型
type: RejectHandlerType
// 回退节点 Id
returnNodeId?: string
}
/**
*
*/
export type TimeoutHandler = {
// 是否开启超时处理
enable: boolean
// 超时执行的动作
type?: number
// 超时时间设置
timeDuration?: string
// 执行动作是自动提醒, 最大提醒次数
maxRemindCount?: number
}
/**
*
*/
export type AssignEmptyHandler = {
// 审批人为空的处理类型
type: AssignEmptyHandlerType
// 指定用户的编号数组
userIds?: number[]
}
// 审批拒绝类型枚举
export enum RejectHandlerType {
/**
*
*/
FINISH_PROCESS = 1,
/**
*
*/
RETURN_USER_TASK = 2
}
// 用户任务超时处理类型枚举
export enum TimeoutHandlerType {
/**
*
*/
REMINDER = 1,
/**
*
*/
APPROVE = 2,
/**
*
*/
REJECT = 3
}
// 用户任务的审批人为空时,处理类型枚举
export enum AssignEmptyHandlerType {
/**
*
*/
APPROVE = 1,
/**
*
*/
REJECT = 2,
/**
*
*/
ASSIGN_USER,
/**
*
*/
ASSIGN_ADMIN = 4
}
// 用户任务的审批人与发起人相同时,处理类型枚举
export enum AssignStartUserHandlerType {
/**
*
*/
START_USER_AUDIT = 1,
/**
* 12
*/
SKIP = 2,
/**
*
*/
ASSIGN_DEPT_LEADER = 3
}
// 用户任务的审批类型。 【参考飞书】
export enum ApproveType {
/**
*
*/
USER = 1,
/**
*
*/
AUTO_APPROVE = 2,
/**
*
*/
AUTO_REJECT = 3
}
// 时间单位枚举
export enum TimeUnitType {
/**
*
*/
MINUTE = 1,
/**
*
*/
HOUR = 2,
/**
*
*/
DAY = 3
}
// 条件配置类型 用于条件节点配置
export enum ConditionType {
/**
*
*/
EXPRESSION = 1,
/**
*
*/
RULE = 2
}
/**
*
*/
export enum FieldPermissionType {
/**
*
*/
READ = '1',
/**
*
*/
WRITE = '2',
/**
*
*/
NONE = '3'
}
/**
*
*/
export type ButtonSetting = {
id: OperationButtonType
displayName: string
enable: boolean
}
// 操作按钮类型枚举 (用于审批节点)
export enum OperationButtonType {
/**
*
*/
APPROVE = 1,
/**
*
*/
REJECT = 2,
/**
*
*/
TRANSFER = 3,
/**
*
*/
DELEGATE = 4,
/**
*
*/
ADD_SIGN = 5,
/**
* 退
*/
RETURN = 6
}
/**
*
*/
export type ConditionRule = {
type: number
opName: string
opCode: string
leftSide: string
rightSide: string
}
/**
*
*/
export type ConditionGroup = {
// 条件组的逻辑关系是否为且
and: boolean
// 条件数组
conditions: Condition[]
}
/**
*
*/
export type Condition = {
// 条件规则的逻辑关系是否为且
and: boolean
rules: ConditionRule[]
}
export const NODE_DEFAULT_TEXT = new Map<number, string>()
NODE_DEFAULT_TEXT.set(NodeType.USER_TASK_NODE, '请配置审批人')
NODE_DEFAULT_TEXT.set(NodeType.COPY_TASK_NODE, '请配置抄送人')
NODE_DEFAULT_TEXT.set(NodeType.CONDITION_NODE, '请设置条件')
NODE_DEFAULT_TEXT.set(NodeType.START_USER_NODE, '请设置发起人')
export const NODE_DEFAULT_NAME = new Map<number, string>()
NODE_DEFAULT_NAME.set(NodeType.USER_TASK_NODE, '审批人')
NODE_DEFAULT_NAME.set(NodeType.COPY_TASK_NODE, '抄送人')
NODE_DEFAULT_NAME.set(NodeType.CONDITION_NODE, '条件')
NODE_DEFAULT_NAME.set(NodeType.START_USER_NODE, '发起人')
// 候选人策略。暂时不从字典中取。 后续可能调整。控制显示顺序
export const CANDIDATE_STRATEGY: DictDataVO[] = [
{ label: '指定成员', value: CandidateStrategy.USER },
{ label: '指定角色', value: CandidateStrategy.ROLE },
{ label: '部门成员', value: CandidateStrategy.DEPT_MEMBER },
{ label: '部门负责人', value: CandidateStrategy.DEPT_LEADER },
{ label: '连续多级部门负责人', value: CandidateStrategy.MULTI_LEVEL_DEPT_LEADER },
{ label: '发起人自选', value: CandidateStrategy.START_USER_SELECT },
{ label: '发起人本人', value: CandidateStrategy.START_USER },
{ label: '发起人部门负责人', value: CandidateStrategy.START_USER_DEPT_LEADER },
{ label: '发起人连续部门负责人', value: CandidateStrategy.START_USER_MULTI_LEVEL_DEPT_LEADER },
{ label: '用户组', value: CandidateStrategy.USER_GROUP },
{ label: '流程表达式', value: CandidateStrategy.EXPRESSION }
]
// 审批节点 的审批类型
export const APPROVE_TYPE: DictDataVO[] = [
{ label: '人工审批', value: ApproveType.USER },
{ label: '自动通过', value: ApproveType.AUTO_APPROVE },
{ label: '自动拒绝', value: ApproveType.AUTO_REJECT }
]
export const APPROVE_METHODS: DictDataVO[] = [
{ label: '按顺序依次审批', value: ApproveMethodType.SEQUENTIAL_APPROVE },
{ label: '会签(可同时审批,至少 % 人必须审批通过)', value: ApproveMethodType.APPROVE_BY_RATIO },
{ label: '或签(可同时审批,有一人通过即可)', value: ApproveMethodType.ANY_APPROVE },
{ label: '随机挑选一人审批', value: ApproveMethodType.RANDOM_SELECT_ONE_APPROVE }
]
export const CONDITION_CONFIG_TYPES: DictDataVO[] = [
{ label: '条件表达式', value: ConditionType.EXPRESSION },
{ label: '条件规则', value: ConditionType.RULE }
]
// 时间单位类型
export const TIME_UNIT_TYPES: DictDataVO[] = [
{ label: '分钟', value: TimeUnitType.MINUTE },
{ label: '小时', value: TimeUnitType.HOUR },
{ label: '天', value: TimeUnitType.DAY }
]
// 超时处理执行动作类型
export const TIMEOUT_HANDLER_TYPES: DictDataVO[] = [
{ label: '自动提醒', value: 1 },
{ label: '自动同意', value: 2 },
{ label: '自动拒绝', value: 3 }
]
export const REJECT_HANDLER_TYPES: DictDataVO[] = [
{ label: '终止流程', value: RejectHandlerType.FINISH_PROCESS },
{ label: '驳回到指定节点', value: RejectHandlerType.RETURN_USER_TASK }
// { label: '结束任务', value: RejectHandlerType.FINISH_TASK }
]
export const ASSIGN_EMPTY_HANDLER_TYPES: DictDataVO[] = [
{ label: '自动通过', value: 1 },
{ label: '自动拒绝', value: 2 },
{ label: '指定成员审批', value: 3 },
{ label: '转交给流程管理员', value: 4 }
]
export const ASSIGN_START_USER_HANDLER_TYPES: DictDataVO[] = [
{ label: '由发起人对自己审批', value: 1 },
{ label: '自动跳过', value: 2 },
{ label: '转交给部门负责人审批', value: 3 }
]
// 比较运算符
export const COMPARISON_OPERATORS: DictDataVO = [
{
value: '==',
label: '等于'
},
{
value: '!=',
label: '不等于'
},
{
value: '>',
label: '大于'
},
{
value: '>=',
label: '大于等于'
},
{
value: '<',
label: '小于'
},
{
value: '<=',
label: '小于等于'
}
]
// 审批操作按钮名称
export const OPERATION_BUTTON_NAME = new Map<number, string>()
OPERATION_BUTTON_NAME.set(OperationButtonType.APPROVE, '通过')
OPERATION_BUTTON_NAME.set(OperationButtonType.REJECT, '拒绝')
OPERATION_BUTTON_NAME.set(OperationButtonType.TRANSFER, '转办')
OPERATION_BUTTON_NAME.set(OperationButtonType.DELEGATE, '委派')
OPERATION_BUTTON_NAME.set(OperationButtonType.ADD_SIGN, '加签')
OPERATION_BUTTON_NAME.set(OperationButtonType.RETURN, '回退')
// 默认的按钮权限设置
export const DEFAULT_BUTTON_SETTING: ButtonSetting[] = [
{ id: OperationButtonType.APPROVE, displayName: '通过', enable: true },
{ id: OperationButtonType.REJECT, displayName: '拒绝', enable: true },
{ id: OperationButtonType.TRANSFER, displayName: '转办', enable: false },
{ id: OperationButtonType.DELEGATE, displayName: '委派', enable: false },
{ id: OperationButtonType.ADD_SIGN, displayName: '加签', enable: false },
{ id: OperationButtonType.RETURN, displayName: '回退', enable: false }
]
// 发起人的按钮权限。暂时定死,不可以编辑
export const START_USER_BUTTON_SETTING: ButtonSetting[] = [
{ id: OperationButtonType.APPROVE, displayName: '提交', enable: true },
{ id: OperationButtonType.REJECT, displayName: '拒绝', enable: false },
{ id: OperationButtonType.TRANSFER, displayName: '转办', enable: false },
{ id: OperationButtonType.DELEGATE, displayName: '委派', enable: false },
{ id: OperationButtonType.ADD_SIGN, displayName: '加签', enable: false },
{ id: OperationButtonType.RETURN, displayName: '回退', enable: false }
]
export const MULTI_LEVEL_DEPT: DictDataVO = [
{ label: '第 1 级部门', 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 },
{ label: '第 11 级部门', value: 11 },
{ label: '第 12 级部门', value: 12 },
{ label: '第 13 级部门', value: 13 },
{ label: '第 14 级部门', value: 14 },
{ label: '第 15 级部门', value: 15 }
]

View File

@ -0,0 +1,4 @@
import SimpleProcessDesigner from './SimpleProcessDesigner.vue'
import '../theme/simple-process-designer.scss'
export { SimpleProcessDesigner }

View File

@ -0,0 +1,478 @@
import { cloneDeep } from 'lodash-es'
import * as RoleApi from '@/api/system/role'
import * as DeptApi from '@/api/system/dept'
import * as PostApi from '@/api/system/post'
import * as UserApi from '@/api/system/user'
import * as UserGroupApi from '@/api/bpm/userGroup'
import {
SimpleFlowNode,
CandidateStrategy,
NodeType,
ApproveMethodType,
RejectHandlerType,
NODE_DEFAULT_NAME,
AssignStartUserHandlerType,
AssignEmptyHandlerType,
FieldPermissionType
} from './consts'
export function useWatchNode(props: { flowNode: SimpleFlowNode }): Ref<SimpleFlowNode> {
const node = ref<SimpleFlowNode>(props.flowNode)
watch(
() => props.flowNode,
(newValue) => {
node.value = newValue
}
)
return node
}
/**
* @description
*/
export function useFormFieldsPermission(defaultPermission: FieldPermissionType) {
// 字段权限配置. 需要有 field, title, permissioin 属性
const fieldsPermissionConfig = ref<Array<Record<string, string>>>([])
const formType = inject<Ref<number>>('formType') // 表单类型
const formFields = inject<Ref<string[]>>('formFields') // 流程表单字段
const getNodeConfigFormFields = (nodeFormFields?: Array<Record<string, string>>) => {
nodeFormFields = toRaw(nodeFormFields)
fieldsPermissionConfig.value =
cloneDeep(nodeFormFields) || getDefaultFieldsPermission(unref(formFields))
}
// 默认的表单权限: 获取表单的所有字段,设置字段默认权限为只读
const getDefaultFieldsPermission = (formFields?: string[]) => {
const defaultFieldsPermission: Array<Record<string, string>> = []
if (formFields) {
formFields.forEach((fieldStr: string) => {
parseFieldsSetDefaultPermission(JSON.parse(fieldStr), defaultFieldsPermission)
})
}
return defaultFieldsPermission
}
// 解析字段。赋给默认权限
const parseFieldsSetDefaultPermission = (
rule: Record<string, any>,
fieldsPermission: Array<Record<string, string>>,
parentTitle: string = ''
) => {
const { /**type,*/ field, title: tempTitle, children } = rule
if (field && tempTitle) {
let title = tempTitle
if (parentTitle) {
title = `${parentTitle}.${tempTitle}`
}
fieldsPermission.push({
field,
title,
permission: defaultPermission
})
// TODO 子表单 需要处理子表单字段
// if (type === 'group' && rule.props?.rule && Array.isArray(rule.props.rule)) {
// // 解析子表单的字段
// rule.props.rule.forEach((item) => {
// parseFieldsSetDefaultPermission(item, fieldsPermission, title)
// })
// }
}
if (children && Array.isArray(children)) {
children.forEach((rule) => {
parseFieldsSetDefaultPermission(rule, fieldsPermission)
})
}
}
return {
formType,
fieldsPermissionConfig,
getNodeConfigFormFields
}
}
/**
* @description
*/
export function useFormFields() {
// 解析后的表单字段
const formFields = inject<Ref<string[]>>('formFields') // 流程表单字段
const parseFormFields = () => {
const parsedFormFields: Array<Record<string, string>> = []
if (formFields) {
formFields.value.forEach((fieldStr: string) => {
parseField(JSON.parse(fieldStr), parsedFormFields)
})
}
return parsedFormFields
}
// 解析字段。
const parseField = (
rule: Record<string, any>,
parsedFormFields: Array<Record<string, string>>,
parentTitle: string = ''
) => {
const { field, title: tempTitle, children, type } = rule
if (field && tempTitle) {
let title = tempTitle
if (parentTitle) {
title = `${parentTitle}.${tempTitle}`
}
parsedFormFields.push({
field,
title,
type
})
// TODO 子表单 需要处理子表单字段
// if (type === 'group' && rule.props?.rule && Array.isArray(rule.props.rule)) {
// // 解析子表单的字段
// rule.props.rule.forEach((item) => {
// parseFieldsSetDefaultPermission(item, fieldsPermission, title)
// })
// }
}
if (children && Array.isArray(children)) {
children.forEach((rule) => {
parseField(rule, parsedFormFields)
})
}
}
return parseFormFields()
}
export type UserTaskFormType = {
//candidateParamArray: any[]
candidateStrategy: CandidateStrategy
approveMethod: ApproveMethodType
roleIds?: number[] // 角色
deptIds?: number[] // 部门
deptLevel?: number // 部门层级
userIds?: number[] // 用户
userGroups?: number[] // 用户组
postIds?: number[] // 岗位
expression?: string // 流程表达式
approveRatio?: number
rejectHandlerType?: RejectHandlerType
returnNodeId?: string
timeoutHandlerEnable?: boolean
timeoutHandlerType?: number
assignEmptyHandlerType?: AssignEmptyHandlerType
assignEmptyHandlerUserIds?: number[]
assignStartUserHandlerType?: AssignStartUserHandlerType
timeDuration?: number
maxRemindCount?: number
buttonsSetting: any[]
}
export type CopyTaskFormType = {
// candidateParamArray: any[]
candidateStrategy: CandidateStrategy
roleIds?: number[] // 角色
deptIds?: number[] // 部门
deptLevel?: number // 部门层级
userIds?: number[] // 用户
userGroups?: number[] // 用户组
postIds?: number[] // 岗位
expression?: string // 流程表达式
}
/**
* @description
*/
export function useNodeForm(nodeType: NodeType) {
const roleOptions = inject<Ref<RoleApi.RoleVO[]>>('roleList') // 角色列表
const postOptions = inject<Ref<PostApi.PostVO[]>>('postList') // 岗位列表
const userOptions = inject<Ref<UserApi.UserVO[]>>('userList') // 用户列表
const deptOptions = inject<Ref<DeptApi.DeptVO[]>>('deptList') // 部门列表
const userGroupOptions = inject<Ref<UserGroupApi.UserGroupVO[]>>('userGroupList') // 用户组列表
const deptTreeOptions = inject('deptTree') // 部门树
const configForm = ref<UserTaskFormType | CopyTaskFormType>()
if (nodeType === NodeType.USER_TASK_NODE) {
configForm.value = {
candidateStrategy: CandidateStrategy.USER,
approveMethod: ApproveMethodType.SEQUENTIAL_APPROVE,
approveRatio: 100,
rejectHandlerType: RejectHandlerType.FINISH_PROCESS,
assignStartUserHandlerType: AssignStartUserHandlerType.START_USER_AUDIT,
returnNodeId: '',
timeoutHandlerEnable: false,
timeoutHandlerType: 1,
timeDuration: 6, // 默认 6小时
maxRemindCount: 1, // 默认 提醒 1次
buttonsSetting: []
}
} else {
configForm.value = {
candidateStrategy: CandidateStrategy.USER
}
}
const getShowText = (): string => {
let showText = ''
// 指定成员
if (configForm.value?.candidateStrategy === CandidateStrategy.USER) {
if (configForm.value?.userIds!.length > 0) {
const candidateNames: string[] = []
userOptions?.value.forEach((item) => {
if (configForm.value?.userIds!.includes(item.id)) {
candidateNames.push(item.nickname)
}
})
showText = `指定成员:${candidateNames.join(',')}`
}
}
// 指定角色
if (configForm.value?.candidateStrategy === CandidateStrategy.ROLE) {
if (configForm.value.roleIds!.length > 0) {
const candidateNames: string[] = []
roleOptions?.value.forEach((item) => {
if (configForm.value?.roleIds!.includes(item.id)) {
candidateNames.push(item.name)
}
})
showText = `指定角色:${candidateNames.join(',')}`
}
}
// 指定部门
if (
configForm.value?.candidateStrategy === CandidateStrategy.DEPT_MEMBER ||
configForm.value?.candidateStrategy === CandidateStrategy.DEPT_LEADER ||
configForm.value?.candidateStrategy === CandidateStrategy.MULTI_LEVEL_DEPT_LEADER
) {
if (configForm.value?.deptIds!.length > 0) {
const candidateNames: string[] = []
deptOptions?.value.forEach((item) => {
if (configForm.value?.deptIds!.includes(item.id!)) {
candidateNames.push(item.name)
}
})
if (configForm.value.candidateStrategy === CandidateStrategy.DEPT_MEMBER) {
showText = `部门成员:${candidateNames.join(',')}`
} else if (configForm.value.candidateStrategy === CandidateStrategy.DEPT_LEADER) {
showText = `部门的负责人:${candidateNames.join(',')}`
} else {
showText = `多级部门的负责人:${candidateNames.join(',')}`
}
}
}
// 指定岗位
if (configForm.value?.candidateStrategy === CandidateStrategy.POST) {
if (configForm.value.postIds!.length > 0) {
const candidateNames: string[] = []
postOptions?.value.forEach((item) => {
if (configForm.value?.postIds!.includes(item.id!)) {
candidateNames.push(item.name)
}
})
showText = `指定岗位: ${candidateNames.join(',')}`
}
}
// 指定用户组
if (configForm.value?.candidateStrategy === CandidateStrategy.USER_GROUP) {
if (configForm.value?.userGroups!.length > 0) {
const candidateNames: string[] = []
userGroupOptions?.value.forEach((item) => {
if (configForm.value?.userGroups!.includes(item.id)) {
candidateNames.push(item.name)
}
})
showText = `指定用户组: ${candidateNames.join(',')}`
}
}
// 发起人自选
if (configForm.value?.candidateStrategy === CandidateStrategy.START_USER_SELECT) {
showText = `发起人自选`
}
// 发起人自己
if (configForm.value?.candidateStrategy === CandidateStrategy.START_USER) {
showText = `发起人自己`
}
// 发起人的部门负责人
if (configForm.value?.candidateStrategy === CandidateStrategy.START_USER_DEPT_LEADER) {
showText = `发起人的部门负责人`
}
// 发起人的部门负责人
if (
configForm.value?.candidateStrategy === CandidateStrategy.START_USER_MULTI_LEVEL_DEPT_LEADER
) {
showText = `发起人连续部门负责人`
}
// 流程表达式
if (configForm.value?.candidateStrategy === CandidateStrategy.EXPRESSION) {
showText = `流程表达式:${configForm.value.expression}`
}
return showText
}
/**
*
*/
const handleCandidateParam = () => {
let candidateParam: undefined | string = undefined
if (!configForm.value) {
return candidateParam
}
switch (configForm.value.candidateStrategy) {
case CandidateStrategy.USER:
candidateParam = configForm.value.userIds!.join(',')
break
case CandidateStrategy.ROLE:
candidateParam = configForm.value.roleIds!.join(',')
break
case CandidateStrategy.POST:
candidateParam = configForm.value.postIds!.join(',')
break
case CandidateStrategy.USER_GROUP:
candidateParam = configForm.value.userGroups!.join(',')
break
case CandidateStrategy.EXPRESSION:
candidateParam = configForm.value.expression!
break
case CandidateStrategy.DEPT_MEMBER:
case CandidateStrategy.DEPT_LEADER:
candidateParam = configForm.value.deptIds!.join(',')
break
// 发起人部门负责人
case CandidateStrategy.START_USER_DEPT_LEADER:
case CandidateStrategy.START_USER_MULTI_LEVEL_DEPT_LEADER:
candidateParam = configForm.value.deptLevel + ''
break
// 指定连续多级部门的负责人
case CandidateStrategy.MULTI_LEVEL_DEPT_LEADER: {
// 候选人参数格式: | 分隔 。左边为部门(多个部门用 , 分隔)。 右边为部门层级
const deptIds = configForm.value.deptIds!.join(',')
candidateParam = deptIds.concat('|' + configForm.value.deptLevel + '')
break
}
default:
break
}
return candidateParam
}
/**
*
*/
const parseCandidateParam = (
candidateStrategy: CandidateStrategy,
candidateParam: string | undefined
) => {
if (!configForm.value || !candidateParam) {
return
}
switch (candidateStrategy) {
case CandidateStrategy.USER: {
configForm.value.userIds = candidateParam.split(',').map((item) => +item)
break
}
case CandidateStrategy.ROLE:
configForm.value.roleIds = candidateParam.split(',').map((item) => +item)
break
case CandidateStrategy.POST:
configForm.value.postIds = candidateParam.split(',').map((item) => +item)
break
case CandidateStrategy.USER_GROUP:
configForm.value.userGroups = candidateParam.split(',').map((item) => +item)
break
case CandidateStrategy.EXPRESSION:
configForm.value.expression = candidateParam
break
case CandidateStrategy.DEPT_MEMBER:
case CandidateStrategy.DEPT_LEADER:
configForm.value.deptIds = candidateParam.split(',').map((item) => +item)
break
// 发起人部门负责人
case CandidateStrategy.START_USER_DEPT_LEADER:
case CandidateStrategy.START_USER_MULTI_LEVEL_DEPT_LEADER:
configForm.value.deptLevel = +candidateParam
break
// 指定连续多级部门的负责人
case CandidateStrategy.MULTI_LEVEL_DEPT_LEADER: {
// 候选人参数格式: | 分隔 。左边为部门(多个部门用 , 分隔)。 右边为部门层级
const paramArray = candidateParam.split('|')
configForm.value.deptIds = paramArray[0].split(',').map((item) => +item)
configForm.value.deptLevel = +paramArray[1]
break
}
default:
break
}
}
return {
configForm,
roleOptions,
postOptions,
userOptions,
userGroupOptions,
deptTreeOptions,
handleCandidateParam,
parseCandidateParam,
getShowText
}
}
/**
* @description
*/
export function useDrawer() {
// 抽屉配置是否可见
const settingVisible = ref(false)
// 关闭配置抽屉
const closeDrawer = () => {
settingVisible.value = false
}
// 打开配置抽屉
const openDrawer = () => {
settingVisible.value = true
}
return {
settingVisible,
closeDrawer,
openDrawer
}
}
/**
* @description
*/
export function useNodeName(nodeType: NodeType) {
// 节点名称
const nodeName = ref<string>()
// 节点名称输入框
const showInput = ref(false)
// 点击节点名称编辑图标
const clickIcon = () => {
showInput.value = true
}
// 节点名称输入框失去焦点
const blurEvent = () => {
showInput.value = false
nodeName.value = nodeName.value || (NODE_DEFAULT_NAME.get(nodeType) as string)
}
return {
nodeName,
showInput,
clickIcon,
blurEvent
}
}
export function useNodeName2(node: Ref<SimpleFlowNode>, nodeType: NodeType) {
// 显示节点名称输入框
const showInput = ref(false)
// 节点名称输入框失去焦点
const blurEvent = () => {
showInput.value = false
node.value.name = node.value.name || (NODE_DEFAULT_NAME.get(nodeType) as string)
}
// 点击节点标题进行输入
const clickTitle = () => {
showInput.value = true
}
return {
showInput,
clickTitle,
blurEvent
}
}

View File

@ -0,0 +1,419 @@
<template>
<el-drawer
:append-to-body="true"
v-model="settingVisible"
:show-close="false"
:size="588"
:before-close="handleClose"
>
<template #header>
<div class="config-header">
<input
v-if="showInput"
type="text"
class="config-editable-input"
@blur="blurEvent()"
v-mountedFocus
v-model="currentNode.name"
:placeholder="currentNode.name"
/>
<div v-else class="node-name"
>{{ currentNode.name }}
<Icon class="ml-1" icon="ep:edit-pen" :size="16" @click="clickIcon()"
/></div>
<div class="divide-line"></div>
</div>
</template>
<div>
<div class="mb-3 font-size-16px" v-if="currentNode.defaultFlow"></div>
<div v-else>
<el-form
ref="formRef"
:model="currentNode"
:rules="formRules"
label-position="top"
>
<el-form-item label="配置方式" prop="conditionType">
<el-radio-group
v-model="currentNode.conditionType"
@change="changeConditionType"
>
<el-radio
v-for="(dict, index) in conditionConfigTypes"
:key="index"
:value="dict.value"
:label="dict.value"
>
{{ dict.label }}
</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item
v-if="currentNode.conditionType === 1"
label="条件表达式"
prop="conditionExpression"
>
<el-input
type="textarea"
v-model="currentNode.conditionExpression"
clearable
style="width: 100%"
/>
</el-form-item>
<el-form-item v-if="currentNode.conditionType === 2" label="条件规则">
<div class="condition-group-tool">
<div class="flex items-center">
<div class="mr-4">条件组关系</div>
<el-switch
v-model="conditionGroups.and"
inline-prompt
active-text="且"
inactive-text="或"
/>
</div>
</div>
<el-space direction="vertical" :spacer="conditionGroups.and ? '且' : '或'">
<el-card
class="condition-group"
style="width: 530px"
v-for="(condition, cIdx) in conditionGroups.conditions"
:key="cIdx"
>
<div class="condition-group-delete" v-if="conditionGroups.conditions.length > 1">
<Icon
color="#0089ff"
icon="ep:circle-close-filled"
:size="18"
@click="deleteConditionGroup(cIdx)"
/>
</div>
<template #header>
<div class="flex items-center justify-between">
<div>条件组</div>
<div class="flex">
<div class="mr-4">规则关系</div>
<el-switch
v-model="condition.and"
inline-prompt
active-text="且"
inactive-text="或"
/>
</div>
</div>
</template>
<div class="flex pt-2" v-for="(rule, rIdx) in condition.rules" :key="rIdx">
<div class="mr-2">
<el-select style="width: 160px" v-model="rule.leftSide">
<el-option
v-for="(item, index) in fieldsInfo"
:key="index"
:label="item.title"
:value="item.field"
/>
</el-select>
</div>
<div class="mr-2">
<el-select v-model="rule.opCode" style="width: 100px">
<el-option
v-for="item in COMPARISON_OPERATORS"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</div>
<div class="mr-2">
<el-input v-model="rule.rightSide" style="width: 160px" />
</div>
<div class="mr-1 flex items-center" v-if="condition.rules.length > 1">
<Icon
icon="ep:delete"
:size="18"
@click="deleteConditionRule(condition, rIdx)"
/>
</div>
<div class="flex items-center">
<Icon icon="ep:plus" :size="18" @click="addConditionRule(condition, rIdx)" />
</div>
</div>
</el-card>
</el-space>
<div title="添加条件组" class="mt-4 cursor-pointer">
<Icon color="#0089ff" icon="ep:plus" :size="24" @click="addConditionGroup" />
</div>
</el-form-item>
</el-form>
</div>
</div>
<template #footer>
<el-divider />
<div>
<el-button type="primary" @click="saveConfig"> </el-button>
<el-button @click="closeDrawer"> </el-button>
</div>
</template>
</el-drawer>
</template>
<script setup lang="ts">
import {
SimpleFlowNode,
CONDITION_CONFIG_TYPES,
ConditionType,
COMPARISON_OPERATORS,
ConditionGroup,
Condition,
ConditionRule
} from '../consts'
import { getDefaultConditionNodeName } from '../utils'
import { useFormFields } from '../node'
const message = useMessage() //
defineOptions({
name: 'ConditionNodeConfig'
})
const formType = inject<Ref<number>>('formType') //
const conditionConfigTypes = computed(() => {
return CONDITION_CONFIG_TYPES.filter((item) => {
//
if (formType?.value !== 10) {
return item.value === ConditionType.RULE
} else {
return true
}
})
})
const props = defineProps({
conditionNode: {
type: Object as () => SimpleFlowNode,
required: true
},
nodeIndex: {
type: Number,
required: true
}
})
const settingVisible = ref(false)
const open = () => {
if (currentNode.value.conditionType === ConditionType.RULE) {
if (currentNode.value.conditionGroups) {
conditionGroups.value = currentNode.value.conditionGroups
}
}
settingVisible.value = true
}
watch(
() => props.conditionNode,
(newValue) => {
currentNode.value = newValue
}
)
//
const showInput = ref(false)
const clickIcon = () => {
showInput.value = true
}
//
const blurEvent = () => {
showInput.value = false
currentNode.value.name =
currentNode.value.name ||
getDefaultConditionNodeName(props.nodeIndex, currentNode.value?.defaultFlow)
}
const currentNode = ref<SimpleFlowNode>(props.conditionNode)
defineExpose({ open }) // open
//
const closeDrawer = () => {
settingVisible.value = false
}
const handleClose = async (done: (cancel?: boolean) => void) => {
const isSuccess = await saveConfig()
if (!isSuccess) {
done(true) // true
} else {
done()
}
}
//
const formRules = reactive({
conditionType: [{ required: true, message: '配置方式不能为空', trigger: 'blur' }],
conditionExpression: [{ required: true, message: '条件表达式不能为空', trigger: 'blur' }]
})
const formRef = ref() // Ref
//
const saveConfig = async () => {
if (!currentNode.value.defaultFlow) {
//
if (!formRef) return false
const valid = await formRef.value.validate()
if (!valid) return false
const showText = getShowText()
if (!showText) {
return false
}
currentNode.value.showText = showText
if (currentNode.value.conditionType === ConditionType.EXPRESSION) {
currentNode.value.conditionGroups = undefined
}
if (currentNode.value.conditionType === ConditionType.RULE) {
currentNode.value.conditionExpression = undefined
currentNode.value.conditionGroups = conditionGroups.value
}
}
settingVisible.value = false
return true
}
const getShowText = (): string => {
let showText = ''
if (currentNode.value.conditionType === ConditionType.EXPRESSION) {
if (currentNode.value.conditionExpression) {
showText = `表达式:${currentNode.value.conditionExpression}`
}
}
if (currentNode.value.conditionType === ConditionType.RULE) {
//
const groupAnd = conditionGroups.value.and
let warningMesg: undefined | string = undefined
const conditionGroup = conditionGroups.value.conditions.map((item) => {
return (
'(' +
item.rules
.map((rule) => {
if (rule.leftSide && rule.rightSide) {
return (
getFieldTitle(rule.leftSide) + ' ' + getOpName(rule.opCode) + ' ' + rule.rightSide
)
} else {
//
warningMesg = '请完善条件规则'
return ''
}
})
.join(item.and ? ' 且 ' : ' 或 ') +
' ) '
)
})
if (warningMesg) {
message.warning(warningMesg)
showText = ''
} else {
showText = conditionGroup.join(groupAnd ? ' 且 ' : ' 或 ')
}
}
return showText
}
//
const changeConditionType = () => {}
const conditionGroups = ref<ConditionGroup>({
and: true,
conditions: [
{
and: true,
rules: [
{
type: 1,
opName: '等于',
opCode: '==',
leftSide: '',
rightSide: ''
}
]
}
]
})
//
const addConditionGroup = () => {
const condition = {
and: true,
rules: [
{
type: 1,
opName: '等于',
opCode: '==',
leftSide: '',
rightSide: ''
}
]
}
conditionGroups.value.conditions.push(condition)
}
//
const deleteConditionGroup = (idx: number) => {
conditionGroups.value.conditions.splice(idx, 1)
}
//
const addConditionRule = (condition: Condition, idx: number) => {
const rule: ConditionRule = {
type: 1,
opName: '等于',
opCode: '==',
leftSide: '',
rightSide: ''
}
condition.rules.splice(idx + 1, 0, rule)
}
const deleteConditionRule = (condition: Condition, idx: number) => {
condition.rules.splice(idx, 1)
}
const fieldsInfo = useFormFields()
const getFieldTitle = (field: string) => {
const item = fieldsInfo.find((item) => item.field === field)
return item?.title
}
const getOpName = (opCode: string): string => {
const opName = COMPARISON_OPERATORS.find((item) => item.value === opCode)
return opName?.label
}
</script>
<style lang="scss" scoped>
.condition-group-tool {
display: flex;
justify-content: space-between;
width: 500px;
margin-bottom: 20px;
}
.condition-group {
position: relative;
&:hover {
border-color: #0089ff;
.condition-group-delete {
opacity: 1;
}
}
.condition-group-delete {
position: absolute;
top: 0;
left: 0;
display: flex;
cursor: pointer;
opacity: 0;
}
}
::v-deep(.el-card__header) {
padding: 8px var(--el-card-padding);
border-bottom: 1px solid var(--el-card-border-color);
box-sizing: border-box;
}
</style>

View File

@ -0,0 +1,307 @@
<template>
<el-drawer
:append-to-body="true"
v-model="settingVisible"
:show-close="false"
:size="550"
:before-close="saveConfig"
>
<template #header>
<div class="config-header">
<input
v-if="showInput"
type="text"
class="config-editable-input"
@blur="blurEvent()"
v-mountedFocus
v-model="nodeName"
:placeholder="nodeName"
/>
<div v-else class="node-name">
{{ nodeName }} <Icon class="ml-1" icon="ep:edit-pen" :size="16" @click="clickIcon()" />
</div>
<div class="divide-line"></div>
</div>
</template>
<el-tabs type="border-card" v-model="activeTabName">
<el-tab-pane label="抄送人" name="user">
<div>
<el-form ref="formRef" :model="configForm" label-position="top" :rules="formRules">
<el-form-item label="抄送人设置" prop="candidateStrategy">
<el-radio-group
v-model="configForm.candidateStrategy"
@change="changeCandidateStrategy"
>
<el-radio
v-for="(dict, index) in copyUserStrategies"
:key="index"
:value="dict.value"
:label="dict.value"
>
{{ dict.label }}
</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item
v-if="configForm.candidateStrategy == CandidateStrategy.ROLE"
label="指定角色"
prop="roleIds"
>
<el-select v-model="configForm.roleIds" clearable multiple style="width: 100%">
<el-option
v-for="item in roleOptions"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item
v-if="
configForm.candidateStrategy == CandidateStrategy.DEPT_MEMBER ||
configForm.candidateStrategy == CandidateStrategy.DEPT_LEADER
"
label="指定部门"
prop="deptIds"
span="24"
>
<el-tree-select
ref="treeRef"
v-model="configForm.deptIds"
:data="deptTreeOptions"
:props="defaultProps"
empty-text="加载中,请稍后"
multiple
node-key="id"
style="width: 100%"
show-checkbox
/>
</el-form-item>
<el-form-item
v-if="configForm.candidateStrategy == CandidateStrategy.POST"
label="指定岗位"
prop="postIds"
span="24"
>
<el-select v-model="configForm.postIds" clearable multiple style="width: 100%">
<el-option
v-for="item in postOptions"
:key="item.id"
:label="item.name"
:value="item.id!"
/>
</el-select>
</el-form-item>
<el-form-item
v-if="configForm.candidateStrategy == CandidateStrategy.USER"
label="指定用户"
prop="userIds"
span="24"
>
<el-select v-model="configForm.userIds" clearable multiple style="width: 100%">
<el-option
v-for="item in userOptions"
:key="item.id"
:label="item.nickname"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item
v-if="configForm.candidateStrategy === CandidateStrategy.USER_GROUP"
label="指定用户组"
prop="userGroups"
>
<el-select v-model="configForm.userGroups" clearable multiple style="width: 100%">
<el-option
v-for="item in userGroupOptions"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item
v-if="configForm.candidateStrategy === CandidateStrategy.EXPRESSION"
label="流程表达式"
prop="expression"
>
<el-input
type="textarea"
v-model="configForm.expression"
clearable
style="width: 100%"
/>
</el-form-item>
</el-form>
</div>
</el-tab-pane>
<el-tab-pane label="表单字段权限" name="fields" v-if="formType === 10">
<div class="field-setting-pane">
<div class="field-setting-desc">字段权限</div>
<div class="field-permit-title">
<div class="setting-title-label first-title"> 字段名称 </div>
<div class="other-titles">
<span class="setting-title-label">只读</span>
<span class="setting-title-label">可编辑</span>
<span class="setting-title-label">隐藏</span>
</div>
</div>
<div
class="field-setting-item"
v-for="(item, index) in fieldsPermissionConfig"
:key="index"
>
<div class="field-setting-item-label"> {{ item.title }} </div>
<el-radio-group class="field-setting-item-group" v-model="item.permission">
<div class="item-radio-wrap">
<el-radio
:value="FieldPermissionType.READ"
size="large"
:label="FieldPermissionType.WRITE"
><span></span
></el-radio>
</div>
<div class="item-radio-wrap">
<el-radio
:value="FieldPermissionType.WRITE"
size="large"
:label="FieldPermissionType.WRITE"
disabled
><span></span
></el-radio>
</div>
<div class="item-radio-wrap">
<el-radio
:value="FieldPermissionType.NONE"
size="large"
:label="FieldPermissionType.NONE"
><span></span
></el-radio>
</div>
</el-radio-group>
</div>
</div>
</el-tab-pane>
</el-tabs>
<template #footer>
<el-divider />
<div>
<el-button type="primary" @click="saveConfig"> </el-button>
<el-button @click="closeDrawer"> </el-button>
</div>
</template>
</el-drawer>
</template>
<script setup lang="ts">
import {
SimpleFlowNode,
CandidateStrategy,
NodeType,
CANDIDATE_STRATEGY,
FieldPermissionType
} from '../consts'
import {
useWatchNode,
useDrawer,
useNodeName,
useFormFieldsPermission,
useNodeForm,
CopyTaskFormType
} from '../node'
import { defaultProps } from '@/utils/tree'
defineOptions({
name: 'CopyTaskNodeConfig'
})
const props = defineProps({
flowNode: {
type: Object as () => SimpleFlowNode,
required: true
}
})
//
const { settingVisible, closeDrawer, openDrawer } = useDrawer()
//
const currentNode = useWatchNode(props)
//
const { nodeName, showInput, clickIcon, blurEvent } = useNodeName(NodeType.COPY_TASK_NODE)
// Tab
const activeTabName = ref('user')
//
const { formType, fieldsPermissionConfig, getNodeConfigFormFields } = useFormFieldsPermission(
FieldPermissionType.READ
)
//
const formRef = ref() // Ref
//
const formRules = reactive({
candidateStrategy: [{ required: true, message: '抄送人设置不能为空', trigger: 'change' }],
userIds: [{ required: true, message: '用户不能为空', trigger: 'change' }],
roleIds: [{ required: true, message: '角色不能为空', trigger: 'change' }],
deptIds: [{ required: true, message: '部门不能为空', trigger: 'change' }],
userGroups: [{ required: true, message: '用户组不能为空', trigger: 'change' }],
postIds: [{ required: true, message: '岗位不能为空', trigger: 'change' }],
expression: [{ required: true, message: '流程表达式不能为空', trigger: 'blur' }]
})
const {
configForm: tempConfigForm,
roleOptions,
postOptions,
userOptions,
userGroupOptions,
deptTreeOptions,
getShowText,
handleCandidateParam,
parseCandidateParam
} = useNodeForm(NodeType.COPY_TASK_NODE)
const configForm = tempConfigForm as Ref<CopyTaskFormType>
//
const copyUserStrategies = computed(() => {
return CANDIDATE_STRATEGY.filter(
(item) =>
item.value !== CandidateStrategy.START_USER_SELECT &&
item.value !== CandidateStrategy.START_USER
)
})
//
const changeCandidateStrategy = () => {
configForm.value.userIds = []
configForm.value.deptIds = []
configForm.value.roleIds = []
configForm.value.postIds = []
configForm.value.userGroups = []
configForm.value.deptLevel = 1
}
//
const saveConfig = async () => {
activeTabName.value = 'user'
if (!formRef) return false
const valid = await formRef.value.validate()
if (!valid) return false
const showText = getShowText()
if (!showText) return false
currentNode.value.name = nodeName.value!
currentNode.value.candidateParam = handleCandidateParam()
currentNode.value.candidateStrategy = configForm.value.candidateStrategy
currentNode.value.showText = showText
currentNode.value.fieldsPermission = fieldsPermissionConfig.value
settingVisible.value = false
return true
}
//
const showCopyTaskNodeConfig = (node: SimpleFlowNode) => {
nodeName.value = node.name
//
configForm.value.candidateStrategy = node.candidateStrategy!
parseCandidateParam(node.candidateStrategy!, node?.candidateParam)
//
getNodeConfigFormFields(node.fieldsPermission)
}
defineExpose({ openDrawer, showCopyTaskNodeConfig }) //
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,136 @@
<template>
<el-drawer
:append-to-body="true"
v-model="settingVisible"
:show-close="false"
:size="550"
:before-close="saveConfig"
>
<template #header>
<div class="config-header">
<input
v-if="showInput"
type="text"
class="config-editable-input"
@blur="blurEvent()"
v-mountedFocus
v-model="nodeName"
:placeholder="nodeName"
/>
<div v-else class="node-name">
{{ nodeName }} <Icon class="ml-1" icon="ep:edit-pen" :size="16" @click="clickIcon()" />
</div>
<div class="divide-line"></div>
</div>
</template>
<el-tabs type="border-card" v-model="activeTabName">
<el-tab-pane label="权限" name="user">
<div> 待实现 </div>
</el-tab-pane>
<el-tab-pane label="表单字段权限" name="fields" v-if="formType === 10">
<div class="field-setting-pane">
<div class="field-setting-desc">字段权限</div>
<div class="field-permit-title">
<div class="setting-title-label first-title"> 字段名称 </div>
<div class="other-titles">
<span class="setting-title-label">只读</span>
<span class="setting-title-label">可编辑</span>
<span class="setting-title-label">隐藏</span>
</div>
</div>
<div
class="field-setting-item"
v-for="(item, index) in fieldsPermissionConfig"
:key="index"
>
<div class="field-setting-item-label"> {{ item.title }} </div>
<el-radio-group class="field-setting-item-group" v-model="item.permission">
<div class="item-radio-wrap">
<el-radio
:value="FieldPermissionType.READ"
size="large"
:label="FieldPermissionType.READ"
><span></span
></el-radio>
</div>
<div class="item-radio-wrap">
<el-radio
:value="FieldPermissionType.WRITE"
size="large"
:label="FieldPermissionType.WRITE"
><span></span
></el-radio>
</div>
<div class="item-radio-wrap">
<el-radio
:value="FieldPermissionType.NONE"
size="large"
:label="FieldPermissionType.NONE"
><span></span
></el-radio>
</div>
</el-radio-group>
</div>
</div>
</el-tab-pane>
</el-tabs>
<template #footer>
<el-divider />
<div>
<el-button type="primary" @click="saveConfig"> </el-button>
<el-button @click="closeDrawer"> </el-button>
</div>
</template>
</el-drawer>
</template>
<script setup lang="ts">
import { SimpleFlowNode, NodeType, FieldPermissionType, START_USER_BUTTON_SETTING } from '../consts'
import { useWatchNode, useDrawer, useNodeName, useFormFieldsPermission } from '../node'
defineOptions({
name: 'StartUserNodeConfig'
})
const props = defineProps({
flowNode: {
type: Object as () => SimpleFlowNode,
required: true
}
})
//
const { settingVisible, closeDrawer, openDrawer } = useDrawer()
//
const currentNode = useWatchNode(props)
//
const { nodeName, showInput, clickIcon, blurEvent } = useNodeName(NodeType.COPY_TASK_NODE)
// Tab
const activeTabName = ref('user')
//
const { formType, fieldsPermissionConfig, getNodeConfigFormFields } = useFormFieldsPermission(
FieldPermissionType.WRITE
)
//
const saveConfig = async () => {
activeTabName.value = 'user'
currentNode.value.name = nodeName.value!
// TODO
currentNode.value.showText = '已设置'
//
currentNode.value.fieldsPermission = fieldsPermissionConfig.value
//
currentNode.value.buttonsSetting = START_USER_BUTTON_SETTING
console.log('currentNode.value.buttonsSetting==>', currentNode.value.buttonsSetting)
settingVisible.value = false
return true
}
//
const showStartUserNodeConfig = (node: SimpleFlowNode) => {
nodeName.value = node.name
//
getNodeConfigFormFields(node.fieldsPermission)
}
defineExpose({ openDrawer, showStartUserNodeConfig }) //
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,901 @@
<template>
<el-drawer
:append-to-body="true"
v-model="settingVisible"
:show-close="false"
:size="550"
:before-close="saveConfig"
class="justify-start"
>
<template #header>
<div class="config-header">
<input
v-if="showInput"
type="text"
class="config-editable-input"
@blur="blurEvent()"
v-mountedFocus
v-model="nodeName"
:placeholder="nodeName"
/>
<div v-else class="node-name">
{{ nodeName }} <Icon class="ml-1" icon="ep:edit-pen" :size="16" @click="clickIcon()" />
</div>
<div class="divide-line"></div>
</div>
</template>
<div class="flex flex-items-center mb-3">
<span class="font-size-16px mr-3">审批类型 :</span>
<el-radio-group v-model="approveType">
<el-radio
v-for="(item, index) in APPROVE_TYPE"
:key="index"
:value="item.value"
:label="item.value"
>
{{ item.label }}
</el-radio>
</el-radio-group>
</div>
<el-tabs type="border-card" v-model="activeTabName" v-if="approveType === ApproveType.USER">
<el-tab-pane label="审批人" name="user">
<div>
<el-form ref="formRef" :model="configForm" label-position="top" :rules="formRules">
<el-form-item label="审批人设置" prop="candidateStrategy">
<el-radio-group
v-model="configForm.candidateStrategy"
@change="changeCandidateStrategy"
>
<el-radio
v-for="(dict, index) in CANDIDATE_STRATEGY"
:key="index"
:value="dict.value"
:label="dict.value"
>
{{ dict.label }}
</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item
v-if="configForm.candidateStrategy == CandidateStrategy.ROLE"
label="指定角色"
prop="roleIds"
>
<el-select v-model="configForm.roleIds" clearable multiple style="width: 100%">
<el-option
v-for="item in roleOptions"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item
v-if="
configForm.candidateStrategy == CandidateStrategy.DEPT_MEMBER ||
configForm.candidateStrategy == CandidateStrategy.DEPT_LEADER ||
configForm.candidateStrategy == CandidateStrategy.MULTI_LEVEL_DEPT_LEADER
"
label="指定部门"
prop="deptIds"
span="24"
>
<el-tree-select
ref="treeRef"
v-model="configForm.deptIds"
:data="deptTreeOptions"
:props="defaultProps"
empty-text="加载中,请稍后"
multiple
node-key="id"
:check-strictly="true"
style="width: 100%"
show-checkbox
/>
</el-form-item>
<el-form-item
v-if="
configForm.candidateStrategy == CandidateStrategy.MULTI_LEVEL_DEPT_LEADER ||
configForm.candidateStrategy == CandidateStrategy.START_USER_DEPT_LEADER ||
configForm.candidateStrategy == CandidateStrategy.START_USER_MULTI_LEVEL_DEPT_LEADER
"
:label="deptLevelLabel!"
prop="deptLevel"
span="24"
>
<el-select v-model="configForm.deptLevel" clearable>
<el-option
v-for="(item, index) in MULTI_LEVEL_DEPT"
:key="index"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item
v-if="configForm.candidateStrategy == CandidateStrategy.POST"
label="指定岗位"
prop="postIds"
span="24"
>
<el-select v-model="configForm.postIds" clearable multiple style="width: 100%">
<el-option
v-for="item in postOptions"
:key="item.id"
:label="item.name"
:value="item.id!"
/>
</el-select>
</el-form-item>
<el-form-item
v-if="configForm.candidateStrategy == CandidateStrategy.USER"
label="指定用户"
prop="userIds"
span="24"
>
<el-select
v-model="configForm.userIds"
clearable
multiple
style="width: 100%"
@change="changedCandidateUsers"
>
<el-option
v-for="item in userOptions"
:key="item.id"
:label="item.nickname"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item
v-if="configForm.candidateStrategy === CandidateStrategy.USER_GROUP"
label="指定用户组"
prop="userGroups"
>
<el-select v-model="configForm.userGroups" clearable multiple style="width: 100%">
<el-option
v-for="item in userGroupOptions"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
<!-- TODO @jason后续要支持选择已经存好的表达式 -->
<el-form-item
v-if="configForm.candidateStrategy === CandidateStrategy.EXPRESSION"
label="流程表达式"
prop="expression"
>
<el-input
type="textarea"
v-model="configForm.expression"
clearable
style="width: 100%"
/>
</el-form-item>
<el-form-item label="多人审批方式" prop="approveMethod">
<el-radio-group v-model="configForm.approveMethod" @change="approveMethodChanged">
<div class="flex-col">
<div
v-for="(item, index) in APPROVE_METHODS"
:key="index"
class="flex items-center"
>
<el-radio
:value="item.value"
:label="item.value"
:disabled="
item.value !== ApproveMethodType.RANDOM_SELECT_ONE_APPROVE &&
notAllowedMultiApprovers
"
>
{{ item.label }}
</el-radio>
<el-form-item prop="approveRatio">
<el-input-number
v-model="configForm.approveRatio"
:min="10"
:max="100"
:step="10"
size="small"
v-if="
item.value === ApproveMethodType.APPROVE_BY_RATIO &&
configForm.approveMethod === ApproveMethodType.APPROVE_BY_RATIO
"
/>
</el-form-item>
</div>
</div>
</el-radio-group>
</el-form-item>
<el-divider content-position="left">审批人拒绝时</el-divider>
<el-form-item prop="rejectHandlerType">
<el-radio-group v-model="configForm.rejectHandlerType">
<div class="flex-col">
<div v-for="(item, index) in REJECT_HANDLER_TYPES" :key="index">
<el-radio :key="item.value" :value="item.value" :label="item.label" />
</div>
</div>
</el-radio-group>
</el-form-item>
<el-form-item
v-if="configForm.rejectHandlerType == RejectHandlerType.RETURN_USER_TASK"
label="驳回节点"
prop="returnNodeId"
>
<el-select v-model="configForm.returnNodeId" clearable style="width: 100%">
<el-option
v-for="item in returnTaskList"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-divider content-position="left">审批人超时未处理时</el-divider>
<el-form-item label="启用开关" prop="timeoutHandlerEnable">
<el-switch
v-model="configForm.timeoutHandlerEnable"
active-text="开启"
inactive-text="关闭"
@change="timeoutHandlerChange"
/>
</el-form-item>
<el-form-item
label="执行动作"
prop="timeoutHandlerType"
v-if="configForm.timeoutHandlerEnable"
>
<el-radio-group
v-model="configForm.timeoutHandlerType"
@change="timeoutHandlerTypeChanged"
>
<el-radio-button
v-for="item in TIMEOUT_HANDLER_TYPES"
:key="item.value"
:value="item.value"
:label="item.label"
/>
</el-radio-group>
</el-form-item>
<el-form-item label="超时时间设置" v-if="configForm.timeoutHandlerEnable">
<span class="mr-2">当超过</span>
<el-form-item prop="timeDuration">
<el-input-number
class="mr-2"
:style="{ width: '100px' }"
v-model="configForm.timeDuration"
:min="1"
controls-position="right"
/>
</el-form-item>
<el-select
v-model="timeUnit"
class="mr-2"
:style="{ width: '100px' }"
@change="timeUnitChange"
>
<el-option
v-for="item in TIME_UNIT_TYPES"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
未处理
</el-form-item>
<el-form-item
label="最大提醒次数"
prop="maxRemindCount"
v-if="configForm.timeoutHandlerEnable && configForm.timeoutHandlerType === 1"
>
<el-input-number v-model="configForm.maxRemindCount" :min="1" :max="10" />
</el-form-item>
<el-divider content-position="left">审批人为空时</el-divider>
<el-form-item prop="assignEmptyHandlerType">
<el-radio-group v-model="configForm.assignEmptyHandlerType">
<div class="flex-col">
<div v-for="(item, index) in ASSIGN_EMPTY_HANDLER_TYPES" :key="index">
<el-radio :key="item.value" :value="item.value" :label="item.label" />
</div>
</div>
</el-radio-group>
</el-form-item>
<el-form-item
v-if="configForm.assignEmptyHandlerType == AssignEmptyHandlerType.ASSIGN_USER"
label="指定用户"
prop="assignEmptyHandlerUserIds"
span="24"
>
<el-select
v-model="configForm.assignEmptyHandlerUserIds"
clearable
multiple
style="width: 100%"
>
<el-option
v-for="item in userOptions"
:key="item.id"
:label="item.nickname"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-divider content-position="left">审批人与提交人为同一人时</el-divider>
<el-form-item prop="assignStartUserHandlerType">
<el-radio-group v-model="configForm.assignStartUserHandlerType">
<div class="flex-col">
<div v-for="(item, index) in ASSIGN_START_USER_HANDLER_TYPES" :key="index">
<el-radio :key="item.value" :value="item.value" :label="item.label" />
</div>
</div>
</el-radio-group>
</el-form-item>
</el-form>
</div>
</el-tab-pane>
<el-tab-pane label="操作按钮设置" name="buttons">
<div class="button-setting-pane">
<div class="button-setting-desc">操作按钮</div>
<div class="button-setting-title">
<div class="button-title-label">操作按钮</div>
<div class="pl-4 button-title-label">显示名称</div>
<div class="button-title-label">启用</div>
</div>
<div class="button-setting-item" v-for="(item, index) in buttonsSetting" :key="index">
<div class="button-setting-item-label"> {{ OPERATION_BUTTON_NAME.get(item.id) }} </div>
<div class="button-setting-item-label">
<input
type="text"
class="editable-title-input"
@blur="btnDisplayNameBlurEvent(index)"
v-mountedFocus
v-model="item.displayName"
:placeholder="item.displayName"
v-if="btnDisplayNameEdit[index]"
/>
<el-button v-else text @click="changeBtnDisplayName(index)"
>{{ item.displayName }} &nbsp;<Icon icon="ep:edit"
/></el-button>
</div>
<div class="button-setting-item-label">
<el-switch v-model="item.enable" />
</div>
</div>
</div>
</el-tab-pane>
<el-tab-pane label="表单字段权限" name="fields" v-if="formType === 10">
<div class="field-setting-pane">
<div class="field-setting-desc">字段权限</div>
<div class="field-permit-title">
<div class="setting-title-label first-title"> 字段名称 </div>
<div class="other-titles">
<span class="setting-title-label">只读</span>
<span class="setting-title-label">可编辑</span>
<span class="setting-title-label">隐藏</span>
</div>
</div>
<div
class="field-setting-item"
v-for="(item, index) in fieldsPermissionConfig"
:key="index"
>
<div class="field-setting-item-label"> {{ item.title }} </div>
<el-radio-group class="field-setting-item-group" v-model="item.permission">
<div class="item-radio-wrap">
<el-radio
:value="FieldPermissionType.READ"
size="large"
:label="FieldPermissionType.READ"
><span></span
></el-radio>
</div>
<div class="item-radio-wrap">
<el-radio
:value="FieldPermissionType.WRITE"
size="large"
:label="FieldPermissionType.WRITE"
><span></span
></el-radio>
</div>
<div class="item-radio-wrap">
<el-radio
:value="FieldPermissionType.NONE"
size="large"
:label="FieldPermissionType.NONE"
><span></span
></el-radio>
</div>
</el-radio-group>
</div>
</div>
</el-tab-pane>
</el-tabs>
<template #footer>
<el-divider />
<div>
<el-button type="primary" @click="saveConfig"> </el-button>
<el-button @click="closeDrawer"> </el-button>
</div>
</template>
</el-drawer>
</template>
<script setup lang="ts">
import {
SimpleFlowNode,
APPROVE_TYPE,
ApproveType,
APPROVE_METHODS,
CandidateStrategy,
NodeType,
ApproveMethodType,
TimeUnitType,
RejectHandlerType,
TIMEOUT_HANDLER_TYPES,
TIME_UNIT_TYPES,
REJECT_HANDLER_TYPES,
DEFAULT_BUTTON_SETTING,
OPERATION_BUTTON_NAME,
ButtonSetting,
MULTI_LEVEL_DEPT,
CANDIDATE_STRATEGY,
ASSIGN_START_USER_HANDLER_TYPES,
TimeoutHandlerType,
ASSIGN_EMPTY_HANDLER_TYPES,
AssignEmptyHandlerType,
FieldPermissionType
} from '../consts'
import {
useWatchNode,
useNodeName,
useFormFieldsPermission,
useNodeForm,
UserTaskFormType,
useDrawer
} from '../node'
import { defaultProps } from '@/utils/tree'
import { cloneDeep } from 'lodash-es'
import { convertTimeUnit, getApproveTypeText } from '../utils'
defineOptions({
name: 'UserTaskNodeConfig'
})
const props = defineProps({
flowNode: {
type: Object as () => SimpleFlowNode,
required: true
}
})
const emits = defineEmits<{
'find:returnTaskNodes': [nodeList: SimpleFlowNode[]]
}>()
const deptLevelLabel = computed(() => {
let label = '部门负责人来源'
if (configForm.value.candidateStrategy == CandidateStrategy.MULTI_LEVEL_DEPT_LEADER) {
label = label + '(指定部门向上)'
} else {
label = label + '(发起人部门向上)'
}
return label
})
//
const currentNode = useWatchNode(props)
//
const { settingVisible, closeDrawer, openDrawer } = useDrawer()
//
const { nodeName, showInput, clickIcon, blurEvent } = useNodeName(NodeType.USER_TASK_NODE)
// Tab
const activeTabName = ref('user')
//
const { formType, fieldsPermissionConfig, getNodeConfigFormFields } = useFormFieldsPermission(
FieldPermissionType.READ
)
//
const { buttonsSetting, btnDisplayNameEdit, changeBtnDisplayName, btnDisplayNameBlurEvent } =
useButtonsSetting()
const approveType = ref(ApproveType.USER)
//
const formRef = ref() // Ref
//
const formRules = reactive({
candidateStrategy: [{ required: true, message: '审批人设置不能为空', trigger: 'change' }],
userIds: [{ required: true, message: '用户不能为空', trigger: 'change' }],
roleIds: [{ required: true, message: '角色不能为空', trigger: 'change' }],
deptIds: [{ required: true, message: '部门不能为空', trigger: 'change' }],
userGroups: [{ required: true, message: '用户组不能为空', trigger: 'change' }],
postIds: [{ required: true, message: '岗位不能为空', trigger: 'change' }],
expression: [{ required: true, message: '流程表达式不能为空', trigger: 'blur' }],
approveMethod: [{ required: true, message: '多人审批方式不能为空', trigger: 'change' }],
approveRatio: [{ required: true, message: '通过比例不能为空', trigger: 'blur' }],
returnNodeId: [{ required: true, message: '驳回节点不能为空', trigger: 'change' }],
timeoutHandlerEnable: [{ required: true }],
timeoutHandlerType: [{ required: true }],
timeDuration: [{ required: true, message: '超时时间不能为空', trigger: 'blur' }],
maxRemindCount: [{ required: true, message: '提醒次数不能为空', trigger: 'blur' }],
assignEmptyHandlerType: [{ required: true }],
assignEmptyHandlerUserIds: [{ required: true, message: '用户不能为空', trigger: 'change' }],
assignStartUserHandlerType: [{ required: true }]
})
const {
configForm: tempConfigForm,
roleOptions,
postOptions,
userOptions,
userGroupOptions,
deptTreeOptions,
handleCandidateParam,
parseCandidateParam,
getShowText
} = useNodeForm(NodeType.USER_TASK_NODE)
const configForm = tempConfigForm as Ref<UserTaskFormType>
//
const notAllowedMultiApprovers = ref(false)
//
const changeCandidateStrategy = () => {
configForm.value.userIds = []
configForm.value.deptIds = []
configForm.value.roleIds = []
configForm.value.postIds = []
configForm.value.userGroups = []
configForm.value.deptLevel = 1
configForm.value.approveMethod = ApproveMethodType.SEQUENTIAL_APPROVE
if (
configForm.value.candidateStrategy === CandidateStrategy.START_USER ||
configForm.value.candidateStrategy === CandidateStrategy.USER
) {
notAllowedMultiApprovers.value = true
} else {
notAllowedMultiApprovers.value = false
}
}
//
const changedCandidateUsers = () => {
if (
configForm.value.userIds &&
configForm.value.userIds?.length <= 1 &&
configForm.value.candidateStrategy === CandidateStrategy.USER
) {
configForm.value.approveMethod = ApproveMethodType.RANDOM_SELECT_ONE_APPROVE
configForm.value.rejectHandlerType = RejectHandlerType.FINISH_PROCESS
notAllowedMultiApprovers.value = true
} else {
notAllowedMultiApprovers.value = false
}
}
//
const approveMethodChanged = () => {
configForm.value.rejectHandlerType = RejectHandlerType.FINISH_PROCESS
if (configForm.value.approveMethod === ApproveMethodType.APPROVE_BY_RATIO) {
configForm.value.approveRatio = 100
}
formRef.value.clearValidate('approveRatio')
}
// 退
const returnTaskList = ref<SimpleFlowNode[]>([])
//
const {
timeoutHandlerChange,
cTimeoutType,
timeoutHandlerTypeChanged,
timeUnit,
timeUnitChange,
isoTimeDuration,
cTimeoutMaxRemindCount
} = useTimeoutHandler()
//
const saveConfig = async () => {
activeTabName.value = 'user'
//
currentNode.value.name = nodeName.value!
//
currentNode.value.approveType = approveType.value
//
if (approveType.value !== ApproveType.USER) {
currentNode.value.showText = getApproveTypeText(approveType.value)
settingVisible.value = false
return true
}
if (!formRef) return false
const valid = await formRef.value.validate()
if (!valid) return false
const showText = getShowText()
if (!showText) return false
currentNode.value.candidateStrategy = configForm.value.candidateStrategy
// candidateParam
currentNode.value.candidateParam = handleCandidateParam()
//
currentNode.value.approveMethod = configForm.value.approveMethod
if (configForm.value.approveMethod === ApproveMethodType.APPROVE_BY_RATIO) {
currentNode.value.approveRatio = configForm.value.approveRatio
}
//
currentNode.value.rejectHandler = {
type: configForm.value.rejectHandlerType!,
returnNodeId: configForm.value.returnNodeId
}
//
currentNode.value.timeoutHandler = {
enable: configForm.value.timeoutHandlerEnable!,
type: cTimeoutType.value,
timeDuration: isoTimeDuration.value,
maxRemindCount: cTimeoutMaxRemindCount.value
}
//
currentNode.value.assignEmptyHandler = {
type: configForm.value.assignEmptyHandlerType!,
userIds:
configForm.value.assignEmptyHandlerType === AssignEmptyHandlerType.ASSIGN_USER
? configForm.value.assignEmptyHandlerUserIds
: undefined
}
//
currentNode.value.assignStartUserHandlerType = configForm.value.assignStartUserHandlerType
//
currentNode.value.fieldsPermission = fieldsPermissionConfig.value
//
currentNode.value.buttonsSetting = buttonsSetting.value
currentNode.value.showText = showText
settingVisible.value = false
return true
}
//
const showUserTaskNodeConfig = (node: SimpleFlowNode) => {
nodeName.value = node.name
// 1
approveType.value = node.approveType ? node.approveType : ApproveType.USER
//
if (approveType.value !== ApproveType.USER) {
return
}
//2.1
configForm.value.candidateStrategy = node.candidateStrategy!
//
parseCandidateParam(node.candidateStrategy!, node?.candidateParam)
if (configForm.value.userIds && configForm.value.userIds.length > 1) {
notAllowedMultiApprovers.value = true
} else {
notAllowedMultiApprovers.value = false
}
// 2.2
configForm.value.approveMethod = node.approveMethod!
if (node.approveMethod == ApproveMethodType.APPROVE_BY_RATIO) {
configForm.value.approveRatio = node.approveRatio!
}
// 2.3
configForm.value.rejectHandlerType = node.rejectHandler!.type
configForm.value.returnNodeId = node.rejectHandler?.returnNodeId
const matchNodeList = []
emits('find:returnTaskNodes', matchNodeList)
returnTaskList.value = matchNodeList
// 2.4
configForm.value.timeoutHandlerEnable = node.timeoutHandler!.enable
if (node.timeoutHandler?.enable && node.timeoutHandler?.timeDuration) {
const strTimeDuration = node.timeoutHandler.timeDuration
let parseTime = strTimeDuration.slice(2, strTimeDuration.length - 1)
let parseTimeUnit = strTimeDuration.slice(strTimeDuration.length - 1)
configForm.value.timeDuration = parseInt(parseTime)
timeUnit.value = convertTimeUnit(parseTimeUnit)
}
configForm.value.timeoutHandlerType = node.timeoutHandler?.type
configForm.value.maxRemindCount = node.timeoutHandler?.maxRemindCount
// 2.5
configForm.value.assignEmptyHandlerType = node.assignEmptyHandler?.type
configForm.value.assignEmptyHandlerUserIds = node.assignEmptyHandler?.userIds
// 2.6
configForm.value.assignStartUserHandlerType = node.assignStartUserHandlerType
// 3.
buttonsSetting.value = cloneDeep(node.buttonsSetting) || DEFAULT_BUTTON_SETTING
// 4.
getNodeConfigFormFields(node.fieldsPermission)
}
defineExpose({ openDrawer, showUserTaskNodeConfig }) //
/**
* @description 操作按钮设置
*/
function useButtonsSetting() {
const buttonsSetting = ref<ButtonSetting[]>()
//
const btnDisplayNameEdit = ref<boolean[]>([])
const changeBtnDisplayName = (index: number) => {
btnDisplayNameEdit.value[index] = true
}
const btnDisplayNameBlurEvent = (index: number) => {
btnDisplayNameEdit.value[index] = false
const buttonItem = buttonsSetting.value![index]
buttonItem.displayName = buttonItem.displayName || OPERATION_BUTTON_NAME.get(buttonItem.id)!
}
return {
buttonsSetting,
btnDisplayNameEdit,
changeBtnDisplayName,
btnDisplayNameBlurEvent
}
}
/**
* @description 审批人超时未处理配置
*/
function useTimeoutHandler() {
//
const timeUnit = ref(TimeUnitType.HOUR)
//
const timeoutHandlerChange = () => {
if (configForm.value.timeoutHandlerEnable) {
timeUnit.value = 2
configForm.value.timeDuration = 6
configForm.value.timeoutHandlerType = 1
configForm.value.maxRemindCount = 1
}
}
//
const cTimeoutType = computed(() => {
if (!configForm.value.timeoutHandlerEnable) {
return undefined
}
return configForm.value.timeoutHandlerType
})
//
const timeoutHandlerTypeChanged = () => {
if (configForm.value.timeoutHandlerType === TimeoutHandlerType.REMINDER) {
configForm.value.maxRemindCount = 1 // 1
}
}
//
const timeUnitChange = () => {
// 60
if (timeUnit.value === TimeUnitType.MINUTE) {
configForm.value.timeDuration = 60
}
// 6
if (timeUnit.value === TimeUnitType.HOUR) {
configForm.value.timeDuration = 6
}
// 1
if (timeUnit.value === TimeUnitType.DAY) {
configForm.value.timeDuration = 1
}
}
// ISO
const isoTimeDuration = computed(() => {
if (!configForm.value.timeoutHandlerEnable) {
return undefined
}
let strTimeDuration = 'PT'
if (timeUnit.value === TimeUnitType.MINUTE) {
strTimeDuration += configForm.value.timeDuration + 'M'
}
if (timeUnit.value === TimeUnitType.HOUR) {
strTimeDuration += configForm.value.timeDuration + 'H'
}
if (timeUnit.value === TimeUnitType.DAY) {
strTimeDuration += configForm.value.timeDuration + 'D'
}
return strTimeDuration
})
//
const cTimeoutMaxRemindCount = computed(() => {
if (!configForm.value.timeoutHandlerEnable) {
return undefined
}
if (configForm.value.timeoutHandlerType !== TimeoutHandlerType.REMINDER) {
return undefined
}
return configForm.value.maxRemindCount
})
return {
timeoutHandlerChange,
cTimeoutType,
timeoutHandlerTypeChanged,
timeUnit,
timeUnitChange,
isoTimeDuration,
cTimeoutMaxRemindCount
}
}
</script>
<style lang="scss" scoped>
.button-setting-pane {
display: flex;
flex-direction: column;
font-size: 14px;
.button-setting-desc {
padding-right: 8px;
margin-bottom: 16px;
font-size: 16px;
font-weight: 700;
}
.button-setting-title {
display: flex;
justify-content: space-between;
align-items: center;
height: 45px;
padding-left: 12px;
background-color: #f8fafc0a;
border: 1px solid #1f38581a;
& > :first-child {
width: 100px !important;
text-align: left !important;
}
& > :last-child {
text-align: center !important;
}
.button-title-label {
width: 150px;
font-size: 13px;
font-weight: 700;
color: #000;
text-align: left;
}
}
.button-setting-item {
align-items: center;
display: flex;
justify-content: space-between;
height: 38px;
padding-left: 12px;
border: 1px solid #1f38581a;
border-top: 0;
& > :first-child {
width: 100px !important;
}
& > :last-child {
text-align: center !important;
}
.button-setting-item-label {
width: 150px;
overflow: hidden;
text-align: left;
text-overflow: ellipsis;
white-space: nowrap;
}
.editable-title-input {
height: 24px;
max-width: 130px;
margin-left: 4px;
line-height: 24px;
border: 1px solid #d9d9d9;
border-radius: 4px;
transition: all 0.3s;
&:focus {
border-color: #40a9ff;
outline: 0;
box-shadow: 0 0 0 2px rgb(24 144 255 / 20%);
}
}
}
}
</style>

View File

@ -0,0 +1,79 @@
<template>
<div class="node-wrapper">
<div class="node-container">
<div class="node-box" :class="{ 'node-config-error': !currentNode.showText }">
<div class="node-title-container">
<div class="node-title-icon copy-task"><span class="iconfont icon-copy"></span></div>
<input
v-if="showInput"
type="text"
class="editable-title-input"
@blur="blurEvent()"
v-mountedFocus
v-model="currentNode.name"
:placeholder="currentNode.name"
/>
<div v-else class="node-title" @click="clickTitle">
{{ currentNode.name }}
</div>
</div>
<div class="node-content" @click="openNodeConfig">
<div class="node-text" :title="currentNode.showText" v-if="currentNode.showText">
{{ currentNode.showText }}
</div>
<div class="node-text" v-else>
{{ NODE_DEFAULT_TEXT.get(NodeType.COPY_TASK_NODE) }}
</div>
<Icon icon="ep:arrow-right-bold" />
</div>
<div class="node-toolbar">
<div class="toolbar-icon"
><Icon color="#0089ff" icon="ep:circle-close-filled" :size="18" @click="deleteNode"
/></div>
</div>
</div>
<!-- 传递子节点给添加节点组件会在子节点前面添加节点 -->
<NodeHandler v-if="currentNode" v-model:child-node="currentNode.childNode" />
</div>
<CopyTaskNodeConfig v-if="currentNode" ref="nodeSetting" :flow-node="currentNode" />
</div>
</template>
<script setup lang="ts">
import { SimpleFlowNode, NodeType, NODE_DEFAULT_TEXT } from '../consts'
import NodeHandler from '../NodeHandler.vue'
import { useNodeName2, useWatchNode } from '../node'
import CopyTaskNodeConfig from '../nodes-config/CopyTaskNodeConfig.vue'
defineOptions({
name: 'CopyTaskNode'
})
const props = defineProps({
flowNode: {
type: Object as () => SimpleFlowNode,
required: true
}
})
//
const emits = defineEmits<{
'update:flowNode': [node: SimpleFlowNode | undefined]
}>()
//
const currentNode = useWatchNode(props)
//
const { showInput, blurEvent, clickTitle } = useNodeName2(currentNode, NodeType.COPY_TASK_NODE)
const nodeSetting = ref()
//
const openNodeConfig = () => {
nodeSetting.value.showCopyTaskNodeConfig(currentNode.value)
nodeSetting.value.openDrawer()
}
//
const deleteNode = () => {
emits('update:flowNode', currentNode.value.childNode)
}
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,13 @@
<template>
<div class="end-node-wrapper">
<div class="end-node-box">
<span class="node-fixed-name" title="结束">结束</span>
</div>
</div>
</template>
<script setup lang="ts">
defineOptions({
name: 'EndEventNode'
})
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,207 @@
<template>
<div class="branch-node-wrapper">
<div class="branch-node-container">
<div class="branch-node-add" @click="addCondition"></div>
<div
class="branch-node-item"
v-for="(item, index) in currentNode.conditionNodes"
:key="index"
>
<template v-if="index == 0">
<div class="branch-line-first-top"> </div>
<div class="branch-line-first-bottom"></div>
</template>
<template v-if="index + 1 == currentNode.conditionNodes?.length">
<div class="branch-line-last-top"></div>
<div class="branch-line-last-bottom"></div>
</template>
<div class="node-wrapper">
<div class="node-container">
<div class="node-box" :class="{ 'node-config-error': !item.showText }">
<div class="branch-node-title-container">
<div v-if="showInputs[index]">
<input
type="text"
class="input-max-width editable-title-input"
@blur="blurEvent(index)"
v-mountedFocus
v-model="item.name"
/>
</div>
<div v-else class="branch-title" @click="clickEvent(index)"> {{ item.name }} </div>
<div class="branch-priority"> 优先级{{ index + 1 }} </div>
</div>
<div class="branch-node-content" @click="conditionNodeConfig(item.id)">
<div class="branch-node-text" :title="item.showText" v-if="item.showText">
{{ item.showText }}
</div>
<div class="branch-node-text" v-else>
{{ NODE_DEFAULT_TEXT.get(NodeType.CONDITION_NODE) }}
</div>
</div>
<div class="node-toolbar" v-if="index + 1 !== currentNode.conditionNodes?.length">
<div class="toolbar-icon">
<Icon
color="#0089ff"
icon="ep:circle-close-filled"
:size="18"
@click="deleteCondition(index)"
/>
</div>
</div>
<div
class="branch-node-move move-node-left"
v-if="index != 0 && index + 1 !== currentNode.conditionNodes?.length"
@click="moveNode(index, -1)"
>
<Icon icon="ep:arrow-left" />
</div>
<div
class="branch-node-move move-node-right"
v-if="currentNode.conditionNodes && index < currentNode.conditionNodes.length - 2"
@click="moveNode(index, 1)"
>
<Icon icon="ep:arrow-right" />
</div>
</div>
<NodeHandler v-model:child-node="item.childNode" />
</div>
</div>
<ConditionNodeConfig :node-index="index" :condition-node="item" :ref="item.id" />
<!-- 递归显示子节点 -->
<ProcessNodeTree
v-if="item && item.childNode"
:parent-node="item"
v-model:flow-node="item.childNode"
@find:recursive-find-parent-node="recursiveFindParentNode"
/>
</div>
</div>
<NodeHandler v-if="currentNode" v-model:child-node="currentNode.childNode" />
</div>
</template>
<script setup lang="ts">
import NodeHandler from '../NodeHandler.vue'
import ProcessNodeTree from '../ProcessNodeTree.vue'
import { SimpleFlowNode, NodeType, NODE_DEFAULT_TEXT } from '../consts'
import { getDefaultConditionNodeName } from '../utils'
import { generateUUID } from '@/utils'
import ConditionNodeConfig from '../nodes-config/ConditionNodeConfig.vue'
const { proxy } = getCurrentInstance() as any
defineOptions({
name: 'ExclusiveNode'
})
const props = defineProps({
// parentNode : {
// type: Object as () => SimpleFlowNode,
// required: true
// },
flowNode: {
type: Object as () => SimpleFlowNode,
required: true
}
})
//
const emits = defineEmits<{
'update:modelValue': [node: SimpleFlowNode | undefined]
'find:parentNode': [nodeList: SimpleFlowNode[], nodeType: number]
'find:recursiveFindParentNode': [
nodeList: SimpleFlowNode[],
curentNode: SimpleFlowNode,
nodeType: number
]
}>()
const currentNode = ref<SimpleFlowNode>(props.flowNode)
// const conditionNodes = computed(() => currentNode.value.conditionNodes);
watch(
() => props.flowNode,
(newValue) => {
currentNode.value = newValue
}
)
const showInputs = ref<boolean[]>([])
//
const blurEvent = (index: number) => {
showInputs.value[index] = false
const conditionNode = currentNode.value.conditionNodes?.at(index) as SimpleFlowNode
conditionNode.name =
conditionNode.name || getDefaultConditionNodeName(index, conditionNode.defaultFlow)
}
//
const clickEvent = (index: number) => {
showInputs.value[index] = true
}
const conditionNodeConfig = (nodeId: string) => {
const conditionNode = proxy.$refs[nodeId][0]
conditionNode.open()
}
//
const addCondition = () => {
const conditionNodes = currentNode.value.conditionNodes
if (conditionNodes) {
const len = conditionNodes.length
let lastIndex = len - 1
const conditionData: SimpleFlowNode = {
id: 'Flow_' + generateUUID(),
name: '条件' + len,
showText: '',
type: NodeType.CONDITION_NODE,
childNode: undefined,
conditionNodes: [],
conditionType: 1,
defaultFlow: false
}
conditionNodes.splice(lastIndex, 0, conditionData)
}
}
//
const deleteCondition = (index: number) => {
const conditionNodes = currentNode.value.conditionNodes
if (conditionNodes) {
conditionNodes.splice(index, 1)
if (conditionNodes.length == 1) {
const childNode = currentNode.value.childNode
//
emits('update:modelValue', childNode)
}
}
}
//
const moveNode = (index: number, to: number) => {
// -1 1
if (currentNode.value.conditionNodes) {
currentNode.value.conditionNodes[index] = currentNode.value.conditionNodes.splice(
index + to,
1,
currentNode.value.conditionNodes[index]
)[0]
}
}
//
const recursiveFindParentNode = (
nodeList: SimpleFlowNode[],
node: SimpleFlowNode,
nodeType: number
) => {
if (!node || node.type === NodeType.START_EVENT_NODE) {
return
}
if (node.type === nodeType) {
nodeList.push(node)
}
// (NodeType.CONDITION_NODE) NodeType.EXCLUSIVE_NODE)
emits('find:parentNode', nodeList, nodeType)
}
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,181 @@
<template>
<div class="branch-node-wrapper">
<div class="branch-node-container">
<div class="branch-node-add" @click="addCondition"></div>
<div
class="branch-node-item"
v-for="(item, index) in currentNode.conditionNodes"
:key="index"
>
<template v-if="index == 0">
<div class="branch-line-first-top"></div>
<div class="branch-line-first-bottom"></div>
</template>
<template v-if="index + 1 == currentNode.conditionNodes?.length">
<div class="branch-line-last-top"></div>
<div class="branch-line-last-bottom"></div>
</template>
<div class="node-wrapper">
<div class="node-container">
<div class="node-box">
<div class="branch-node-title-container">
<div v-if="showInputs[index]">
<input
type="text"
class="input-max-width editable-title-input"
@blur="blurEvent(index)"
v-mountedFocus
v-model="item.name"
/>
</div>
<div v-else class="branch-title" @click="clickEvent(index)"> {{ item.name }} </div>
<div class="branch-priority">无优先级</div>
</div>
<div class="branch-node-content" @click="conditionNodeConfig(item.id)">
<div class="branch-node-text" :title="item.showText" v-if="item.showText">
{{ item.showText }}
</div>
<div class="branch-node-text" v-else>
{{ NODE_DEFAULT_TEXT.get(NodeType.CONDITION_NODE) }}
</div>
</div>
<div class="node-toolbar">
<div class="toolbar-icon">
<Icon
color="#0089ff"
icon="ep:circle-close-filled"
:size="18"
@click="deleteCondition(index)"
/>
</div>
</div>
<!-- <div
class="branch-node-move move-node-left"
v-if="index != 0 && index + 1 !== currentNode.conditionNodes?.length" @click="moveNode(index, -1)">
<Icon icon="ep:arrow-left" />
</div> -->
<!-- <div
class="branch-node-move move-node-right"
v-if="currentNode.conditionNodes && index < currentNode.conditionNodes.length - 2"
@click="moveNode(index, 1)">
<Icon icon="ep:arrow-right" />
</div> -->
</div>
<NodeHandler v-model:child-node="item.childNode" />
</div>
</div>
<!-- 递归显示子节点 -->
<ProcessNodeTree
v-if="item && item.childNode"
:parent-node="item"
v-model:flow-node="item.childNode"
@find:recursive-find-parent-node="recursiveFindParentNode"
/>
</div>
</div>
<NodeHandler v-if="currentNode" v-model:child-node="currentNode.childNode" />
</div>
</template>
<script setup lang="ts">
import NodeHandler from '../NodeHandler.vue'
import ProcessNodeTree from '../ProcessNodeTree.vue'
import { SimpleFlowNode, NodeType, NODE_DEFAULT_TEXT } from '../consts'
import { generateUUID } from '@/utils'
const { proxy } = getCurrentInstance() as any
defineOptions({
name: 'ParallelNode'
})
const props = defineProps({
flowNode: {
type: Object as () => SimpleFlowNode,
required: true
}
})
//
const emits = defineEmits<{
'update:modelValue': [node: SimpleFlowNode | undefined]
'find:parentNode': [nodeList: SimpleFlowNode[], nodeType: number]
'find:recursiveFindParentNode': [
nodeList: SimpleFlowNode[],
curentNode: SimpleFlowNode,
nodeType: number
]
}>()
const currentNode = ref<SimpleFlowNode>(props.flowNode)
watch(
() => props.flowNode,
(newValue) => {
currentNode.value = newValue
}
)
const showInputs = ref<boolean[]>([])
//
const blurEvent = (index: number) => {
showInputs.value[index] = false
const conditionNode = currentNode.value.conditionNodes?.at(index) as SimpleFlowNode
conditionNode.name = conditionNode.name || `并行${index + 1}`
}
//
const clickEvent = (index: number) => {
showInputs.value[index] = true
}
const conditionNodeConfig = (nodeId: string) => {
const conditionNode = proxy.$refs[nodeId][0]
conditionNode.open()
}
//
const addCondition = () => {
const conditionNodes = currentNode.value.conditionNodes
if (conditionNodes) {
const len = conditionNodes.length
let lastIndex = len - 1
const conditionData: SimpleFlowNode = {
id: 'Flow_' + generateUUID(),
name: '并行' + len,
showText: '无需配置条件同时执行',
type: NodeType.CONDITION_NODE,
childNode: undefined,
conditionNodes: []
}
conditionNodes.splice(lastIndex, 0, conditionData)
}
}
//
const deleteCondition = (index: number) => {
const conditionNodes = currentNode.value.conditionNodes
if (conditionNodes) {
conditionNodes.splice(index, 1)
if (conditionNodes.length == 1) {
const childNode = currentNode.value.childNode
//
emits('update:modelValue', childNode)
}
}
}
//
const recursiveFindParentNode = (
nodeList: SimpleFlowNode[],
node: SimpleFlowNode,
nodeType: number
) => {
if (!node || node.type === NodeType.START_EVENT_NODE) {
return
}
if (node.type === nodeType) {
nodeList.push(node)
}
// (NodeType.CONDITION_NODE) NodeType.PARALLEL_NODE)
emits('find:parentNode', nodeList, nodeType)
}
</script>

View File

@ -0,0 +1,69 @@
<template>
<div class="node-wrapper">
<div class="node-container">
<div class="node-box" :class="{ 'node-config-error': !currentNode.showText }">
<div class="node-title-container">
<div class="node-title-icon start-user"
><span class="iconfont icon-start-user"></span
></div>
<input
v-if="showInput"
type="text"
class="editable-title-input"
@blur="blurEvent()"
v-mountedFocus
v-model="currentNode.name"
:placeholder="currentNode.name"
/>
<div v-else class="node-title" @click="clickTitle">
{{ currentNode.name }}
</div>
</div>
<div class="node-content" @click="openNodeConfig">
<div class="node-text" :title="currentNode.showText" v-if="currentNode.showText">
{{ currentNode.showText }}
</div>
<div class="node-text" v-else>
{{ NODE_DEFAULT_TEXT.get(NodeType.START_USER_NODE) }}
</div>
<Icon icon="ep:arrow-right-bold" />
</div>
</div>
<!-- 传递子节点给添加节点组件会在子节点前面添加节点 -->
<NodeHandler v-if="currentNode" v-model:child-node="currentNode.childNode" />
</div>
</div>
<StartUserNodeConfig v-if="currentNode" ref="nodeSetting" :flow-node="currentNode" />
</template>
<script setup lang="ts">
import NodeHandler from '../NodeHandler.vue'
import { useWatchNode, useNodeName2 } from '../node'
import { SimpleFlowNode, NODE_DEFAULT_TEXT, NodeType } from '../consts'
import StartUserNodeConfig from '../nodes-config/StartUserNodeConfig.vue'
defineOptions({
name: 'StartEventNode'
})
const props = defineProps({
flowNode: {
type: Object as () => SimpleFlowNode,
default: () => null
}
})
//
const emits = defineEmits<{
'update:modelValue': [node: SimpleFlowNode | undefined]
}>()
//
const currentNode = useWatchNode(props)
//
const { showInput, blurEvent, clickTitle } = useNodeName2(currentNode, NodeType.START_USER_NODE)
const nodeSetting = ref()
//
const openNodeConfig = () => {
//
nodeSetting.value.showStartUserNodeConfig(currentNode.value)
nodeSetting.value.openDrawer()
}
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,88 @@
<template>
<div class="node-wrapper">
<div class="node-container">
<div class="node-box" :class="{ 'node-config-error': !currentNode.showText }">
<div class="node-title-container">
<div class="node-title-icon user-task"><span class="iconfont icon-approve"></span></div>
<input
v-if="showInput"
type="text"
class="editable-title-input"
@blur="blurEvent()"
v-mountedFocus
v-model="currentNode.name"
:placeholder="currentNode.name"
/>
<div v-else class="node-title" @click="clickTitle">
{{ currentNode.name }}
</div>
</div>
<div class="node-content" @click="openNodeConfig">
<div class="node-text" :title="currentNode.showText" v-if="currentNode.showText">
{{ currentNode.showText }}
</div>
<div class="node-text" v-else>
{{ NODE_DEFAULT_TEXT.get(NodeType.USER_TASK_NODE) }}
</div>
<Icon icon="ep:arrow-right-bold" />
</div>
<div class="node-toolbar">
<div class="toolbar-icon"
><Icon color="#0089ff" icon="ep:circle-close-filled" :size="18" @click="deleteNode"
/></div>
</div>
</div>
<!-- 传递子节点给添加节点组件会在子节点前面添加节点 -->
<NodeHandler v-if="currentNode" v-model:child-node="currentNode.childNode" />
</div>
</div>
<UserTaskNodeConfig
v-if="currentNode"
ref="nodeSetting"
:flow-node="currentNode"
@find:return-task-nodes="findReturnTaskNodes"
/>
</template>
<script setup lang="ts">
import { SimpleFlowNode, NodeType, NODE_DEFAULT_TEXT } from '../consts'
import { useWatchNode, useNodeName2 } from '../node'
import NodeHandler from '../NodeHandler.vue'
import UserTaskNodeConfig from '../nodes-config/UserTaskNodeConfig.vue'
defineOptions({
name: 'UserTaskNode'
})
const props = defineProps({
flowNode: {
type: Object as () => SimpleFlowNode,
required: true
}
})
const emits = defineEmits<{
'update:flowNode': [node: SimpleFlowNode | undefined]
'find:parentNode': [nodeList: SimpleFlowNode[], nodeType: NodeType]
}>()
//
const currentNode = useWatchNode(props)
//
const { showInput, blurEvent, clickTitle } = useNodeName2(currentNode, NodeType.START_USER_NODE)
const nodeSetting = ref()
//
const openNodeConfig = () => {
//
nodeSetting.value.showUserTaskNodeConfig(currentNode.value)
nodeSetting.value.openDrawer()
}
const deleteNode = () => {
emits('update:flowNode', currentNode.value.childNode)
}
//
const findReturnTaskNodes = (
matchNodeList: SimpleFlowNode[] //
) => {
//
emits('find:parentNode', matchNodeList, NodeType.USER_TASK_NODE)
}
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,33 @@
import { TimeUnitType, ApproveType, APPROVE_TYPE } from './consts'
// 获取条件节点默认的名称
export const getDefaultConditionNodeName = (index: number, defaultFlow: boolean | undefined): string => {
if (defaultFlow) {
return '其它情况'
}
return '条件' + (index + 1)
}
export const convertTimeUnit = (strTimeUnit: string) => {
if (strTimeUnit === 'M') {
return TimeUnitType.MINUTE
}
if (strTimeUnit === 'H') {
return TimeUnitType.HOUR
}
if (strTimeUnit === 'D') {
return TimeUnitType.DAY
}
return TimeUnitType.HOUR
}
export const getApproveTypeText = (approveType: ApproveType): string => {
let approveTypeText = ''
APPROVE_TYPE.forEach((item) => {
if (item.value === approveType) {
approveTypeText = item.label
return
}
})
return approveTypeText
}

View File

@ -0,0 +1,714 @@
.simple-flow-canvas {
position: absolute;
inset: 0;
z-index: 1;
overflow: auto;
background-color: #fafafa;
user-select: none;
.simple-flow-container {
position: relative;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
.top-area-container {
position: sticky;
inset: 0;
display: flex;
width: 100%;
height: 42px;
z-index: 1;
// padding: 4px 0;
background-color: #fff;
justify-content: flex-end;
align-items: center;
.top-actions {
display: flex;
margin: 4px;
margin-right: 8px;
align-items: center;
.canvas-control {
font-size: 16px;
.control-scale-group {
display: inline-flex;
align-items: center;
margin-right: 8px;
.control-scale-button {
display: inline-flex;
width: 28px;
height: 28px;
padding: 2px;
text-align: center;
cursor: pointer;
justify-content: center;
align-items: center;
}
.control-scale-label {
margin: 0 4px;
font-size: 14px;
}
}
}
}
}
.scale-container {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
margin-top: 16px;
background-color: #fafafa;
transform-origin: 50% 0 0;
transform: scale(1);
transition: transform 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
//
.node-container {
width: 200px;
}
//
.node-box {
position: relative;
display: flex;
min-height: 70px;
padding: 5px 10px 8px;
cursor: pointer;
background-color: #fff;
flex-direction: column;
border: 2px solid transparent;
// border-color: #0089ff;
border-radius: 8px;
// border-color: #0089ff;
box-shadow: 0 1px 4px 0 rgba(10, 30, 65, 0.16);
transition: all 0.1s cubic-bezier(0.645, 0.045, 0.355, 1);
&:hover {
border-color: #0089ff;
.node-toolbar {
opacity: 1;
}
.branch-node-move {
display: flex;
}
}
//
.node-title-container {
display: flex;
padding: 4px;
cursor: pointer;
border-radius: 4px 4px 0 0;
align-items: center;
.node-title-icon {
display: flex;
align-items: center;
&.user-task {
color: #ff943e;
}
&.copy-task {
color: #3296fa;
}
&.start-user {
color: #676565;
}
}
.node-title {
margin-left: 4px;
font-size: 14px;
font-weight: 600;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
color: #1f1f1f;
line-height: 18px;
&:hover {
border-bottom: 1px dashed #f60;
}
}
}
//
.branch-node-title-container {
display: flex;
padding: 4px 0;
cursor: pointer;
border-radius: 4px 4px 0 0;
align-items: center;
justify-content: space-between;
.input-max-width {
max-width: 115px !important;
}
.branch-title {
font-size: 13px;
font-weight: 600;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
color: #f60;
&:hover {
border-bottom: 1px dashed #000;
}
}
.branch-priority {
min-width: 50px;
font-size: 13px;
}
}
.node-content {
display: flex;
min-height: 32px;
padding: 4px 8px;
margin-top: 4px;
line-height: 32px;
justify-content: space-between;
align-items: center;
color: #111f2c;
background: rgba(0, 0, 0, 0.03);
border-radius: 4px;
.node-text {
display: -webkit-box;
overflow: hidden;
font-size: 14px;
line-height: 24px;
text-overflow: ellipsis;
word-break: break-all;
-webkit-line-clamp: 2; /* 这将限制文本显示为两行 */
-webkit-box-orient: vertical;
}
}
//
.branch-node-content {
display: flex;
min-height: 32px;
padding: 4px 8px;
margin-top: 4px;
line-height: 32px;
align-items: center;
color: #111f2c;
border-radius: 4px;
.branch-node-text {
overflow: hidden;
font-size: 14px;
line-height: 24px;
text-overflow: ellipsis;
word-break: break-all;
-webkit-line-clamp: 2; /* 这将限制文本显示为两行 */
-webkit-box-orient: vertical;
}
}
//
.node-toolbar {
opacity: 0;
position: absolute;
top: -20px;
right: 0px;
display: flex;
.toolbar-icon {
text-align: center;
vertical-align: middle;
}
}
//
.branch-node-move {
position: absolute;
width: 10px;
cursor: pointer;
display: none;
align-items: center;
height: 100%;
justify-content: center;
}
.move-node-left {
left: -2px;
top: 0px;
background: rgba(126, 134, 142, 0.08);
border-top-left-radius: 8px;
border-bottom-left-radius: 8px;
}
.move-node-right {
right: -2px;
top: 0px;
background: rgba(126, 134, 142, 0.08);
border-top-right-radius: 6px;
border-bottom-right-radius: 6px;
}
}
.node-config-error {
border-color: #ff5219 !important;
}
//
.node-wrapper {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
// 线
.node-handler-wrapper {
position: relative;
display: flex;
height: 70px;
align-items: center;
user-select: none;
justify-content: center;
flex-direction: column;
&::before {
position: absolute;
top: 0;
right: 0;
left: 0;
// bottom: 5px;
bottom: 0px;
z-index: 0;
width: 2px;
height: 100%;
// height: calc(100% - 5px);
margin: auto;
background-color: #dedede;
content: '';
}
.node-handler {
.add-icon {
position: relative;
top: -5px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
width: 25px;
height: 25px;
color: #fff;
background-color: #0089ff;
border-radius: 50%;
&:hover {
transform: scale(1.1);
}
}
}
.node-handler-arrow {
position: absolute;
bottom: 0;
left: 50%;
display: flex;
transform: translateX(-50%);
}
}
//
.branch-node-wrapper {
position: relative;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
margin-top: 16px;
.branch-node-container {
position: relative;
display: flex;
&::before {
position: absolute;
height: 100%;
width: 4px;
background-color: #fafafa;
content: '';
left: 50%;
transform: translate(-50%);
}
.branch-node-add {
position: absolute;
top: -18px;
left: 50%;
z-index: 1;
height: 36px;
padding: 0 10px;
font-size: 12px;
line-height: 36px;
color: #222;
cursor: pointer;
background: #fff;
border: 2px solid #dedede;
border-radius: 18px;
transform: translateX(-50%);
transform-origin: center center;
transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
}
.branch-node-item {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
min-width: 280px;
padding: 40px 40px 0;
background: transparent;
border-top: 2px solid #dedede;
border-bottom: 2px solid #dedede;
&::before {
position: absolute;
width: 2px;
height: 100%;
margin: auto;
inset: 0;
background-color: #dedede;
content: '';
}
}
// 线
.branch-line-first-top {
position: absolute;
top: -5px;
left: -1px;
width: 50%;
height: 7px;
background-color: #fafafa;
content: '';
}
// 线
.branch-line-first-bottom {
position: absolute;
bottom: -5px;
left: -1px;
width: 50%;
height: 7px;
background-color: #fafafa;
content: '';
}
// 线
.branch-line-last-top {
position: absolute;
top: -5px;
right: -1px;
width: 50%;
height: 7px;
background-color: #fafafa;
content: '';
}
// 线
.branch-line-last-bottom {
position: absolute;
right: -1px;
bottom: -5px;
width: 50%;
height: 7px;
background-color: #fafafa;
content: '';
}
}
}
.node-fixed-name {
display: inline-block;
width: auto;
padding: 0 4px;
overflow: hidden;
text-align: center;
text-overflow: ellipsis;
white-space: nowrap;
}
//
.start-node-wrapper {
position: relative;
margin-top: 16px;
.start-node-container {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
.start-node-box {
display: flex;
justify-content: center;
align-items: center;
width: 90px;
height: 36px;
padding: 3px 4px;
color: #212121;
cursor: pointer;
// background: #2c2c2c;
background: #fafafa;
border-radius: 30px;
box-shadow: 0 1px 5px 0 rgba(10, 30, 65, 0.08);
box-sizing: border-box;
}
}
}
//
.end-node-wrapper {
margin-bottom: 16px;
.end-node-box {
display: flex;
justify-content: center;
align-items: center;
width: 80px;
height: 36px;
color: #212121;
// background: #6e6e6e;
background: #fafafa;
border-radius: 30px;
box-shadow: 0 1px 5px 0 rgba(10, 30, 65, 0.08);
box-sizing: border-box;
}
}
// title
.editable-title-input {
height: 20px;
max-width: 145px;
line-height: 20px;
font-size: 12px;
margin-left: 4px;
border: 1px solid #d9d9d9;
border-radius: 4px;
transition: all 0.3s;
&:focus {
border-color: #40a9ff;
outline: 0;
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
}
}
}
}
}
//
.config-header {
display: flex;
flex-direction: column;
.node-name {
display: flex;
height: 24px;
line-height: 24px;
font-size: 16px;
cursor: pointer;
align-items: center;
}
.divide-line {
width: 100%;
height: 1px;
margin-top: 16px;
background: #eee;
}
.config-editable-input {
height: 24px;
max-width: 510px;
font-size: 16px;
line-height: 24px;
border: 1px solid #d9d9d9;
border-radius: 4px;
transition: all 0.3s;
&:focus {
border-color: #40a9ff;
outline: 0;
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
}
}
}
//
.field-setting-pane {
display: flex;
flex-direction: column;
font-size: 14px;
.field-setting-desc {
padding-right: 8px;
margin-bottom: 16px;
font-size: 16px;
font-weight: 700;
}
.field-permit-title {
display: flex;
justify-content: space-between;
align-items: center;
height: 45px;
padding-left: 12px;
line-height: 45px;
background-color: #f8fafc0a;
border: 1px solid #1f38581a;
.first-title {
text-align: left !important;
}
.other-titles {
display: flex;
justify-content: space-between;
}
.setting-title-label {
display: inline-block;
width: 110px;
padding: 5px 0;
font-size: 13px;
font-weight: 700;
color: #000;
text-align: center;
}
}
.field-setting-item {
align-items: center;
display: flex;
justify-content: space-between;
height: 38px;
padding-left: 12px;
border: 1px solid #1f38581a;
border-top: 0;
.field-setting-item-label {
display: inline-block;
width: 110px;
min-height: 16px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
cursor: text;
}
.field-setting-item-group {
display: flex;
justify-content: space-between;
.item-radio-wrap {
display: inline-block;
width: 110px;
text-align: center;
}
}
}
}
// 线
.handler-item-wrapper {
display: flex;
cursor: pointer;
.handler-item {
margin-right: 8px;
}
.handler-item-icon {
width: 80px;
height: 80px;
background: #fff;
border: 1px solid #e2e2e2;
border-radius: 50%;
transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
user-select: none;
text-align: center;
&:hover {
background: #e2e2e2;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.1);
}
.icon-size {
font-size: 35px;
line-height: 80px;
}
}
.approve {
color: #ff943e;
}
.copy {
color: #3296fa;
}
.condition {
color: #15bc83;
}
.handler-item-text {
margin-top: 4px;
width: 80px;
text-align: center;
}
}
// iconfont
@font-face {
font-family: 'iconfont'; /* Project id 4495938 */
src:
url('iconfont.woff2?t=1724339470412') format('woff2'),
url('iconfont.woff?t=1724339470412') format('woff'),
url('iconfont.ttf?t=1724339470412') format('truetype');
}
.iconfont {
font-family: 'iconfont' !important;
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.icon-start-user:before {
content: '\e679';
}
.icon-inclusive:before {
content: '\e602';
}
.icon-copy:before {
content: '\e7eb';
}
.icon-handle:before {
content: '\e61c';
}
.icon-exclusive:before {
content: '\e717';
}
.icon-approve:before {
content: '\e715';
}
.icon-parallel:before {
content: '\e688';
}

View File

@ -11,3 +11,14 @@ export const setupAuth = (app: App<Element>) => {
hasRole(app)
hasPermi(app)
}
/**
* v-mountedFocus
*/
export const setupMountedFocus = (app: App<Element>) => {
app.directive('mountedFocus', {
mounted(el) {
el.focus()
}
})
}

View File

@ -28,8 +28,8 @@ import '@/plugins/animate.css'
// 路由
import router, { setupRouter } from '@/router'
// 权限
import { setupAuth } from '@/directives'
// 指令
import { setupAuth, setupMountedFocus } from '@/directives'
import { createApp } from 'vue'
@ -58,7 +58,9 @@ const setupAll = async () => {
setupRouter(app)
// directives 指令
setupAuth(app)
setupMountedFocus(app)
await router.isReady()

View File

@ -292,7 +292,7 @@ const remainingRouter: AppRouteRecordRaw[] = [
},
{
path: 'process-instance/detail',
component: () => import('@/views/bpm/processInstance/detail/index.vue'),
component: () => import('@/views/bpm/processInstance/detail/index_new.vue'),
name: 'BpmProcessInstanceDetail',
meta: {
noCache: true,
@ -300,7 +300,14 @@ const remainingRouter: AppRouteRecordRaw[] = [
canTo: true,
title: '流程详情',
activeMenu: '/bpm/task/my'
}
},
props: route => (
{
id: route.query.id,
taskId: route.query.taskId,
activityId: route.query.activityId
}
)
},
{
path: 'oa/leave/create',

View File

@ -1,4 +1,4 @@
import { store } from '../index'
import { store } from '../../index'
import { defineStore } from 'pinia'
export const useWorkFlowStore = defineStore('simpleWorkflow', {
@ -6,15 +6,15 @@ export const useWorkFlowStore = defineStore('simpleWorkflow', {
tableId: '',
isTried: false,
promoterDrawer: false,
flowPermission1: {},
approverDrawer: false,
approverConfig1: {},
copyerDrawer: false,
copyerConfig1: {},
copyerConfig: {},
conditionDrawer: false,
conditionsConfig1: {
conditionNodes: []
}
},
userTaskConfig: {}
}),
actions: {
setTableId(payload) {
@ -26,26 +26,26 @@ export const useWorkFlowStore = defineStore('simpleWorkflow', {
setPromoter(payload) {
this.promoterDrawer = payload
},
setFlowPermission(payload) {
this.flowPermission1 = payload
},
setApprover(payload) {
setApproverDrawer(payload) {
this.approverDrawer = payload
},
setApproverConfig(payload) {
this.approverConfig1 = payload
},
setCopyer(payload) {
setCopyerDrawer(payload) {
this.copyerDrawer = payload
},
setCopyerConfig(payload) {
this.copyerConfig1 = payload
this.copyerConfig = payload
},
setCondition(payload) {
this.conditionDrawer = payload
},
setConditionsConfig(payload) {
this.conditionsConfig1 = payload
},
setUserTaskConfig(payload) {
this.userTaskConfig = payload
}
}
})

View File

@ -437,3 +437,15 @@ export const ErpBizType = {
SALE_OUT: 21,
SALE_RETURN: 22
}
// ========== BPM 模块 ==========
export const BpmModelType = {
BPMN: 10, // BPMN 设计器
SIMPLE: 20 // 简易设计器
}
export const BpmModelFormType = {
NORMAL: 10, // 流程表单
CUSTOM: 20 // 业务表单
}

View File

@ -143,6 +143,7 @@ export enum DICT_TYPE {
INFRA_OPERATE_TYPE = 'infra_operate_type',
// ========== BPM 模块 ==========
BPM_MODEL_TYPE = 'bpm_model_type',
BPM_MODEL_FORM_TYPE = 'bpm_model_form_type',
BPM_TASK_CANDIDATE_STRATEGY = 'bpm_task_candidate_strategy',
BPM_PROCESS_INSTANCE_STATUS = 'bpm_process_instance_status',

View File

@ -8,12 +8,7 @@
label-width="110px"
>
<el-form-item label="流程标识" prop="key">
<el-input
v-model="formData.key"
:disabled="!!formData.id"
placeholder="请输入流标标识"
style="width: 330px"
/>
<el-input v-model="formData.key" :disabled="!!formData.id" placeholder="请输入流标标识" />
<el-tooltip
v-if="!formData.id"
class="item"
@ -35,7 +30,7 @@
placeholder="请输入流程名称"
/>
</el-form-item>
<el-form-item v-if="formData.id" label="流程分类" prop="category">
<el-form-item label="流程分类" prop="category">
<el-select
v-model="formData.category"
clearable
@ -50,73 +45,108 @@
/>
</el-select>
</el-form-item>
<el-form-item v-if="formData.id" label="流程图标" prop="icon">
<UploadImg v-model="formData.icon" :limit="1" height="128px" width="128px" />
<el-form-item label="流程图标" prop="icon">
<UploadImg v-model="formData.icon" :limit="1" height="64px" width="64px" />
</el-form-item>
<el-form-item label="流程描述" prop="description">
<el-input v-model="formData.description" clearable type="textarea" />
</el-form-item>
<div v-if="formData.id">
<el-form-item label="表单类型" prop="formType">
<el-radio-group v-model="formData.formType">
<el-radio
v-for="dict in getIntDictOptions(DICT_TYPE.BPM_MODEL_FORM_TYPE)"
:key="dict.value"
:value="dict.value"
>
{{ dict.label }}
</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item v-if="formData.formType === 10" label="流程表单" prop="formId">
<el-select v-model="formData.formId" clearable style="width: 100%">
<el-option
v-for="form in formList"
:key="form.id"
:label="form.name"
:value="form.id"
/>
</el-select>
</el-form-item>
<el-form-item
v-if="formData.formType === 20"
label="表单提交路由"
prop="formCustomCreatePath"
>
<el-input
v-model="formData.formCustomCreatePath"
placeholder="请输入表单提交路由"
style="width: 330px"
/>
<el-tooltip
class="item"
content="自定义表单的提交路径,使用 Vue 的路由地址例如说bpm/oa/leave/create"
effect="light"
placement="top"
<el-form-item label="流程类型" prop="type">
<el-radio-group v-model="formData.type">
<el-radio
v-for="dict in getIntDictOptions(DICT_TYPE.BPM_MODEL_TYPE)"
:key="dict.value"
:label="dict.value"
>
<i class="el-icon-question" style="padding-left: 5px"></i>
</el-tooltip>
</el-form-item>
<el-form-item
v-if="formData.formType === 20"
label="表单查看地址"
prop="formCustomViewPath"
>
<el-input
v-model="formData.formCustomViewPath"
placeholder="请输入表单查看的组件地址"
style="width: 330px"
/>
<el-tooltip
class="item"
content="自定义表单的查看组件地址,使用 Vue 的组件地址例如说bpm/oa/leave/detail"
effect="light"
placement="top"
{{ dict.label }}
</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="表单类型" prop="formType">
<el-radio-group v-model="formData.formType">
<el-radio
v-for="dict in getIntDictOptions(DICT_TYPE.BPM_MODEL_FORM_TYPE)"
:key="dict.value"
:label="dict.value"
>
<i class="el-icon-question" style="padding-left: 5px"></i>
</el-tooltip>
</el-form-item>
</div>
{{ dict.label }}
</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item v-if="formData.formType === 10" label="流程表单" prop="formId">
<el-select v-model="formData.formId" clearable style="width: 100%">
<el-option v-for="form in formList" :key="form.id" :label="form.name" :value="form.id" />
</el-select>
</el-form-item>
<el-form-item
v-if="formData.formType === 20"
label="表单提交路由"
prop="formCustomCreatePath"
>
<el-input
v-model="formData.formCustomCreatePath"
placeholder="请输入表单提交路由"
style="width: 330px"
/>
<el-tooltip
class="item"
content="自定义表单的提交路径,使用 Vue 的路由地址例如说bpm/oa/leave/create.vue"
effect="light"
placement="top"
>
<i class="el-icon-question" style="padding-left: 5px"></i>
</el-tooltip>
</el-form-item>
<el-form-item v-if="formData.formType === 20" label="表单查看地址" prop="formCustomViewPath">
<el-input
v-model="formData.formCustomViewPath"
placeholder="请输入表单查看的组件地址"
style="width: 330px"
/>
<el-tooltip
class="item"
content="自定义表单的查看组件地址,使用 Vue 的组件地址例如说bpm/oa/leave/detail.vue"
effect="light"
placement="top"
>
<i class="el-icon-question" style="padding-left: 5px"></i>
</el-tooltip>
</el-form-item>
<el-form-item label="是否可见" prop="visible">
<el-radio-group v-model="formData.visible">
<el-radio
v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
:key="dict.value as string"
:label="dict.value"
>
{{ dict.label }}
</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="谁可以发起" prop="startUserIds">
<el-select
v-model="formData.startUserIds"
multiple
placeholder="请选择可发起人,默认(不选择)则所有人都可以发起"
>
<el-option
v-for="user in userList"
:key="user.id"
:label="user.nickname"
:value="user.id"
/>
</el-select>
</el-form-item>
<el-form-item label="流程管理员" prop="managerUserIds">
<el-select v-model="formData.managerUserIds" multiple placeholder="请选择流程管理员">
<el-option
v-for="user in userList"
:key="user.id"
:label="user.nickname"
:value="user.id"
/>
</el-select>
</el-form-item>
</el-form>
<template #footer>
<el-button :disabled="formLoading" type="primary" @click="submitForm"> </el-button>
@ -125,45 +155,62 @@
</Dialog>
</template>
<script lang="ts" setup>
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { DICT_TYPE, getBoolDictOptions, getIntDictOptions } from '@/utils/dict'
import { ElMessageBox } from 'element-plus'
import * as ModelApi from '@/api/bpm/model'
import * as FormApi from '@/api/bpm/form'
import { CategoryApi } from '@/api/bpm/category'
import { BpmModelFormType, BpmModelType } from '@/utils/constants'
import { UserVO } from '@/api/system/user'
import * as UserApi from '@/api/system/user'
import { useUserStoreWithOut } from '@/store/modules/user'
defineOptions({ name: 'ModelForm' })
const { t } = useI18n() //
const message = useMessage() //
const userStore = useUserStoreWithOut() //
const dialogVisible = ref(false) //
const dialogTitle = ref('') //
const formLoading = ref(false) // 12
const formType = ref('') // create - update -
const formData = ref({
formType: 10,
id: undefined,
name: '',
key: '',
category: undefined,
icon: undefined,
description: '',
type: BpmModelType.BPMN,
formType: BpmModelFormType.NORMAL,
formId: '',
formCustomCreatePath: '',
formCustomViewPath: ''
formCustomViewPath: '',
visible: true,
startUserIds: [],
managerUserIds: []
})
const formRules = reactive({
name: [{ required: true, message: '参数名称不能为空', trigger: 'blur' }],
key: [{ required: true, message: '参数键名不能为空', trigger: 'blur' }],
category: [{ required: true, message: '参数分类不能为空', trigger: 'blur' }],
icon: [{ required: true, message: '参数图标不能为空', trigger: 'blur' }],
value: [{ required: true, message: '参数键值不能为空', trigger: 'blur' }],
visible: [{ required: true, message: '是否可见不能为空', trigger: 'blur' }]
name: [{ required: true, message: '流程名称不能为空', trigger: 'blur' }],
key: [{ required: true, message: '流程标识不能为空', trigger: 'blur' }],
category: [{ required: true, message: '流程分类不能为空', trigger: 'blur' }],
icon: [{ required: true, message: '流程图标不能为空', trigger: 'blur' }],
type: [{ required: true, message: '是否可见不能为空', trigger: 'blur' }],
formType: [{ required: true, message: '是否可见不能为空', trigger: 'blur' }],
formId: [{ required: true, message: '流程表单不能为空', trigger: 'blur' }],
formCustomCreatePath: [{ required: true, message: '表单提交路由不能为空', trigger: 'blur' }],
formCustomViewPath: [{ required: true, message: '表单查看地址不能为空', trigger: 'blur' }],
visible: [{ required: true, message: '是否可见不能为空', trigger: 'blur' }],
managerUserIds: [{ required: true, message: '流程管理员不能为空', trigger: 'blur' }]
})
const formRef = ref() // Ref
const formList = ref([]) //
const categoryList = ref([]) //
const userList = ref<UserVO[]>([]) //
/** 打开弹窗 */
const open = async (type: string, id?: number) => {
const open = async (type: string, id?: string) => {
dialogVisible.value = true
dialogTitle.value = t('action.' + type)
formType.value = type
@ -176,11 +223,15 @@ const open = async (type: string, id?: number) => {
} finally {
formLoading.value = false
}
} else {
formData.value.managerUserIds.push(userStore.getUser.id)
}
//
formList.value = await FormApi.getFormSimpleList()
//
categoryList.value = await CategoryApi.getCategorySimpleList()
//
userList.value = await UserApi.getSimpleUserList()
}
defineExpose({ open }) // open
@ -199,10 +250,9 @@ const submitForm = async () => {
await ModelApi.createModel(data)
//
await ElMessageBox.alert(
'<strong>新建模型成功!</strong>后续需要执行如下 3 个步骤:' +
'<div>1. 点击【修改流程】按钮,配置流程的分类、表单信息</div>' +
'<div>2. 点击【设计流程】按钮,绘制流程图</div>' +
'<div>3. 点击【发布流程】按钮,完成流程的最终发布</div>' +
'<strong>新建模型成功!</strong>后续需要执行如下 2 个步骤:' +
'<div>1. 点击【设计流程】按钮,绘制流程图</div>' +
'<div>2. 点击【发布流程】按钮,完成流程的最终发布</div>' +
'另外,每次流程修改后,都需要点击【发布流程】按钮,才能正式生效!!!',
'重要提示',
{
@ -225,14 +275,20 @@ const submitForm = async () => {
/** 重置表单 */
const resetForm = () => {
formData.value = {
formType: 10,
id: undefined,
name: '',
key: '',
category: undefined,
icon: '',
icon: undefined,
description: '',
type: BpmModelType.BPMN,
formType: BpmModelFormType.NORMAL,
formId: '',
formCustomCreatePath: '',
formCustomViewPath: ''
formCustomViewPath: '',
visible: true,
startUserIds: [],
managerUserIds: []
}
formRef.value?.resetFields()
}

View File

@ -1,141 +0,0 @@
<template>
<Dialog v-model="dialogVisible" title="导入流程" width="400">
<div>
<el-upload
ref="uploadRef"
v-model:file-list="fileList"
:action="importUrl"
:auto-upload="false"
:data="formData"
:disabled="formLoading"
:headers="uploadHeaders"
:limit="1"
:on-error="submitFormError"
:on-exceed="handleExceed"
:on-success="submitFormSuccess"
accept=".bpmn, .xml"
drag
name="bpmnFile"
>
<Icon class="el-icon--upload" icon="ep:upload-filled" />
<div class="el-upload__text"> 将文件拖到此处 <em>点击上传</em></div>
<template #tip>
<div class="el-upload__tip" style="color: red">
提示仅允许导入bpmxml格式文件
</div>
<div>
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="120px">
<el-form-item label="流程标识" prop="key">
<el-input
v-model="formData.key"
placeholder="请输入流标标识"
style="width: 250px"
/>
</el-form-item>
<el-form-item label="流程名称" prop="name">
<el-input v-model="formData.name" clearable placeholder="请输入流程名称" />
</el-form-item>
<el-form-item label="流程描述" prop="description">
<el-input v-model="formData.description" clearable type="textarea" />
</el-form-item>
</el-form>
</div>
</template>
</el-upload>
</div>
<template #footer>
<el-button :disabled="formLoading" type="primary" @click="submitForm"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
</template>
<script lang="ts" setup>
import { getAccessToken, getTenantId } from '@/utils/auth'
defineOptions({ name: 'ModelImportForm' })
const message = useMessage() //
const dialogVisible = ref(false) //
const formLoading = ref(false) //
const formData = ref({
key: '',
name: '',
description: ''
})
const formRules = reactive({
key: [{ required: true, message: '流程标识不能为空', trigger: 'blur' }],
name: [{ required: true, message: '流程名称不能为空', trigger: 'blur' }]
})
const formRef = ref() // Ref
const uploadRef = ref() // Ref
const importUrl = import.meta.env.VITE_BASE_URL + import.meta.env.VITE_API_URL + '/bpm/model/import'
const uploadHeaders = ref() // Header
const fileList = ref([]) //
/** 打开弹窗 */
const open = async () => {
dialogVisible.value = true
resetForm()
}
defineExpose({ open }) // open
/** 提交表单 */
const submitForm = async () => {
//
if (!formRef) return
const valid = await formRef.value.validate()
if (!valid) return
if (fileList.value.length == 0) {
message.error('请上传文件')
return
}
//
uploadHeaders.value = {
Authorization: 'Bearer ' + getAccessToken(),
'tenant-id': getTenantId()
}
formLoading.value = true
uploadRef.value!.submit()
}
/** 文件上传成功 */
const emit = defineEmits(['success']) // success
const submitFormSuccess = async (response: any) => {
if (response.code !== 0) {
message.error(response.msg)
formLoading.value = false
return
}
//
message.success('导入流程成功!请点击【设计流程】按钮,进行编辑保存后,才可以进行【发布流程】')
dialogVisible.value = false
//
emit('success')
}
/** 上传错误提示 */
const submitFormError = (): void => {
message.error('导入流程失败,请您重新上传!')
formLoading.value = false
}
/** 重置表单 */
const resetForm = () => {
//
formLoading.value = false
uploadRef.value?.clearFiles()
//
formData.value = {
key: '',
name: '',
description: ''
}
formRef.value?.resetFields()
}
/** 文件数超出提示 */
const handleExceed = (): void => {
message.error('最多只能上传一个文件!')
}
</script>

View File

@ -58,17 +58,17 @@ const initModeler = (item) => {
}
/** 添加/修改模型 */
const save = async (bpmnXml) => {
const save = async (bpmnXml: string) => {
const data = {
...model.value,
bpmnXml: bpmnXml // bpmnXml
} as unknown as ModelApi.ModelVO
//
if (data.id) {
await ModelApi.updateModel(data)
await ModelApi.updateModelBpmn(data)
message.success('修改成功')
} else {
await ModelApi.createModel(data)
await ModelApi.updateModelBpmn(data)
message.success('新增成功')
}
//

View File

@ -58,10 +58,7 @@
@click="openForm('create')"
v-hasPermi="['bpm:model:create']"
>
<Icon icon="ep:plus" class="mr-5px" /> 新建流程
</el-button>
<el-button type="success" plain @click="openImportForm" v-hasPermi="['bpm:model:import']">
<Icon icon="ep:upload" class="mr-5px" /> 导入流程
<Icon icon="ep:plus" class="mr-5px" /> 新建
</el-button>
</el-form-item>
</el-form>
@ -70,21 +67,34 @@
<!-- 列表 -->
<ContentWrap>
<el-table v-loading="loading" :data="list">
<el-table-column label="流程标识" align="center" prop="key" width="200" />
<el-table-column label="流程名称" align="center" prop="name" width="200">
<el-table-column label="流程名称" align="center" prop="name" min-width="200" />
<el-table-column label="流程图标" align="center" prop="icon" min-width="100">
<template #default="scope">
<el-button type="primary" link @click="handleBpmnDetail(scope.row)">
<span>{{ scope.row.name }}</span>
</el-button>
<el-image :src="scope.row.icon" class="h-32px w-32px" />
</template>
</el-table-column>
<el-table-column label="流程图标" align="center" prop="icon" width="100">
<el-table-column label="可见范围" align="center" prop="startUserIds" min-width="100">
<template #default="scope">
<el-image :src="scope.row.icon" class="w-32px h-32px" />
<el-text v-if="!scope.row.startUsers || scope.row.startUsers.length === 0">
全部可见
</el-text>
<el-text v-else-if="scope.row.startUsers.length == 1">
{{ scope.row.startUsers[0].nickname }}
</el-text>
<el-text v-else>
<el-tooltip
class="box-item"
effect="dark"
placement="top"
:content="scope.row.startUsers.map((user: any) => user.nickname).join('、')"
>
{{ scope.row.startUsers[0].nickname }} {{ scope.row.startUsers.length }} 人可见
</el-tooltip>
</el-text>
</template>
</el-table-column>
<el-table-column label="流程分类" align="center" prop="categoryName" width="100" />
<el-table-column label="表单信息" align="center" prop="formType" width="200">
<el-table-column label="流程分类" align="center" prop="categoryName" min-width="100" />
<el-table-column label="表单信息" align="center" prop="formType" min-width="200">
<template #default="scope">
<el-button
v-if="scope.row.formType === 10"
@ -105,101 +115,87 @@
<label v-else></label>
</template>
</el-table-column>
<el-table-column
label="创建时间"
align="center"
prop="createTime"
width="180"
:formatter="dateFormatter"
/>
<el-table-column label="最新部署的流程定义" align="center">
<el-table-column
label="流程版本"
align="center"
prop="processDefinition.version"
width="100"
>
<template #default="scope">
<el-tag v-if="scope.row.processDefinition">
v{{ scope.row.processDefinition.version }}
</el-tag>
<el-tag v-else type="warning">未部署</el-tag>
</template>
</el-table-column>
<el-table-column
label="激活状态"
align="center"
prop="processDefinition.version"
width="85"
>
<template #default="scope">
<el-switch
v-if="scope.row.processDefinition"
v-model="scope.row.processDefinition.suspensionState"
:active-value="1"
:inactive-value="2"
@change="handleChangeState(scope.row)"
/>
</template>
</el-table-column>
<el-table-column label="部署时间" align="center" prop="deploymentTime" width="180">
<template #default="scope">
<span v-if="scope.row.processDefinition">
{{ formatDate(scope.row.processDefinition.deploymentTime) }}
</span>
</template>
</el-table-column>
<el-table-column label="最后发布" align="center" prop="deploymentTime" min-width="250">
<template #default="scope">
<span v-if="scope.row.processDefinition">
{{ formatDate(scope.row.processDefinition.deploymentTime) }}
</span>
<el-tag v-if="scope.row.processDefinition" class="ml-10px">
v{{ scope.row.processDefinition.version }}
</el-tag>
<el-tag v-else type="warning">未部署</el-tag>
<el-tag
v-if="scope.row.processDefinition?.suspensionState === 2"
type="warning"
class="ml-10px"
>
已停用
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" align="center" min-width="240" fixed="right">
<el-table-column label="操作" align="center" width="200" fixed="right">
<template #default="scope">
<el-button
link
type="primary"
@click="openForm('update', scope.row.id)"
v-hasPermi="['bpm:model:update']"
:disabled="!isManagerUser(scope.row)"
>
修改流程
修改
</el-button>
<el-button
link
class="!ml-5px"
type="primary"
@click="handleDesign(scope.row)"
v-hasPermi="['bpm:model:update']"
:disabled="!isManagerUser(scope.row)"
>
设计流程
</el-button>
<el-button
link
type="primary"
@click="handleSimpleDesign(scope.row.id)"
v-hasPermi="['bpm:model:update']"
>
仿钉钉设计流程
设计
</el-button>
<el-button
link
class="!ml-5px"
type="primary"
@click="handleDeploy(scope.row)"
v-hasPermi="['bpm:model:deploy']"
:disabled="!isManagerUser(scope.row)"
>
发布流程
发布
</el-button>
<el-button
link
type="primary"
v-hasPermi="['bpm:process-definition:query']"
@click="handleDefinitionList(scope.row)"
<el-dropdown
class="!align-middle ml-5px"
@command="(command) => handleCommand(command, scope.row)"
v-hasPermi="['bpm:process-definition:query', 'bpm:model:update', 'bpm:model:delete']"
>
流程定义
</el-button>
<el-button
link
type="danger"
@click="handleDelete(scope.row.id)"
v-hasPermi="['bpm:model:delete']"
>
删除
</el-button>
<el-button type="primary" link>更多</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item
command="handleDefinitionList"
v-if="checkPermi(['bpm:process-definition:query'])"
>
历史
</el-dropdown-item>
<el-dropdown-item
command="handleChangeState"
v-if="checkPermi(['bpm:model:update']) && scope.row.processDefinition"
:disabled="!isManagerUser(scope.row)"
>
{{ scope.row.processDefinition.suspensionState === 1 ? '停用' : '启用' }}
</el-dropdown-item>
<el-dropdown-item
type="danger"
command="handleDelete"
v-if="checkPermi(['bpm:model:delete'])"
:disabled="!isManagerUser(scope.row)"
>
删除
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
</el-table-column>
</el-table>
@ -215,41 +211,29 @@
<!-- 表单弹窗添加/修改流程 -->
<ModelForm ref="formRef" @success="getList" />
<!-- 表单弹窗导入流程 -->
<ModelImportForm ref="importFormRef" @success="getList" />
<!-- 弹窗表单详情 -->
<Dialog title="表单详情" v-model="formDetailVisible" width="800">
<form-create :rule="formDetailPreview.rule" :option="formDetailPreview.option" />
</Dialog>
<!-- 弹窗流程模型图的预览 -->
<Dialog title="流程图" v-model="bpmnDetailVisible" width="800">
<MyProcessViewer
key="designer"
v-model="bpmnXML"
:value="bpmnXML as any"
v-bind="bpmnControlForm"
:prefix="bpmnControlForm.prefix"
/>
</Dialog>
</template>
<script lang="ts" setup>
import { dateFormatter, formatDate } from '@/utils/formatTime'
import { MyProcessViewer } from '@/components/bpmnProcessDesigner/package'
import { formatDate } from '@/utils/formatTime'
import * as ModelApi from '@/api/bpm/model'
import * as FormApi from '@/api/bpm/form'
import ModelForm from './ModelForm.vue'
import ModelImportForm from '@/views/bpm/model/ModelImportForm.vue'
import { setConfAndFields2 } from '@/utils/formCreate'
import { CategoryApi } from '@/api/bpm/category'
import { BpmModelType } from '@/utils/constants'
import { checkPermi } from '@/utils/permission'
import { useUserStoreWithOut } from '@/store/modules/user'
defineOptions({ name: 'BpmModel' })
const message = useMessage() //
const { t } = useI18n() //
const { push } = useRouter() //
const userStore = useUserStoreWithOut() //
const loading = ref(true) //
const total = ref(0) //
@ -288,25 +272,36 @@ const resetQuery = () => {
handleQuery()
}
/** '更多'操作按钮 */
const handleCommand = (command: string, row: any) => {
switch (command) {
case 'handleDefinitionList':
handleDefinitionList(row)
break
case 'handleDelete':
handleDelete(row)
break
case 'handleChangeState':
handleChangeState(row)
break
default:
break
}
}
/** 添加/修改操作 */
const formRef = ref()
const openForm = (type: string, id?: number) => {
formRef.value.open(type, id)
}
/** 添加/修改操作 */
const importFormRef = ref()
const openImportForm = () => {
importFormRef.value.open()
}
/** 删除按钮操作 */
const handleDelete = async (id: number) => {
const handleDelete = async (row: any) => {
try {
//
await message.delConfirm()
//
await ModelApi.deleteModel(id)
await ModelApi.deleteModel(row.id)
message.success(t('common.delSuccess'))
//
await getList()
@ -314,45 +309,45 @@ const handleDelete = async (id: number) => {
}
/** 更新状态操作 */
const handleChangeState = async (row) => {
const handleChangeState = async (row: any) => {
const state = row.processDefinition.suspensionState
const newState = state === 1 ? 2 : 1
try {
//
const id = row.id
const statusState = state === 1 ? '激活' : '挂起'
debugger
const statusState = state === 1 ? '停用' : '启用'
const content = '是否确认' + statusState + '流程名字为"' + row.name + '"的数据项?'
await message.confirm(content)
//
await ModelApi.updateModelState(id, state)
await ModelApi.updateModelState(id, newState)
message.success(statusState + '成功')
//
await getList()
} catch {
//
row.processDefinition.suspensionState = state === 1 ? 2 : 1
}
} catch {}
}
/** 设计流程 */
const handleDesign = (row) => {
push({
name: 'BpmModelEditor',
query: {
modelId: row.id
}
})
}
const handleSimpleDesign = (row) => {
push({
name: 'SimpleWorkflowDesignEditor',
query: {
modelId: row.id
}
})
const handleDesign = (row: any) => {
if (row.type == BpmModelType.BPMN) {
push({
name: 'BpmModelEditor',
query: {
modelId: row.id
}
})
} else {
push({
name: 'SimpleWorkflowDesignEditor',
query: {
modelId: row.id
}
})
}
}
/** 发布流程 */
const handleDeploy = async (row) => {
const handleDeploy = async (row: any) => {
try {
//
await message.confirm('是否部署该流程!!')
@ -380,7 +375,7 @@ const formDetailPreview = ref({
rule: [],
option: {}
})
const handleFormDetail = async (row) => {
const handleFormDetail = async (row: any) => {
if (row.formType == 10) {
//
const data = await FormApi.getForm(row.formId)
@ -394,16 +389,10 @@ const handleFormDetail = async (row) => {
}
}
/** 流程图的详情按钮操作 */
const bpmnDetailVisible = ref(false)
const bpmnXML = ref(null)
const bpmnControlForm = ref({
prefix: 'flowable'
})
const handleBpmnDetail = async (row) => {
const data = await ModelApi.getModel(row.id)
bpmnXML.value = data.bpmnXml || ''
bpmnDetailVisible.value = true
/** 判断是否可以操作 */
const isManagerUser = (row: any) => {
const userId = userStore.getUser.id
return row.managerUserIds && row.managerUserIds.includes(userId)
}
/** 初始化 **/

View File

@ -1,13 +1,22 @@
<template>
<div
class="h-50px position-fixed bottom-10 text-14px flex items-center color-#32373c dark:color-#fff font-bold btn-container"
class="h-50px bottom-10 text-14px flex items-center color-#32373c dark:color-#fff font-bold btn-container"
v-if="runningTask.id"
>
<el-popover :visible="passVisible" placement="top-end" :width="500" trigger="click">
<!-- 通过按钮 -->
<el-popover
:visible="passVisible"
placement="top-end"
:width="500"
trigger="click"
v-if="isShowButton(OperationButtonType.APPROVE)"
>
<template #reference>
<el-button plain type="success" @click="openPopover('1')">
<Icon icon="ep:select" />&nbsp; 通过
<Icon icon="ep:select" />&nbsp; {{ getButtonDisplayName(OperationButtonType.APPROVE) }}
</el-button>
</template>
<!-- 审批表单 -->
<div class="flex flex-col flex-1 pt-20px px-20px" v-loading="formLoading">
<el-form
label-position="top"
@ -50,19 +59,28 @@
<el-form-item>
<el-button :disabled="formLoading" type="success" @click="handleAudit(true)">
通过
{{ getButtonDisplayName(OperationButtonType.APPROVE) }}
</el-button>
<el-button @click="passVisible = false"> 取消 </el-button>
</el-form-item>
</el-form>
</div>
</el-popover>
<el-popover :visible="rejectVisible" placement="top-end" :width="500" trigger="click">
<!-- 拒绝按钮 -->
<el-popover
:visible="rejectVisible"
placement="top-end"
:width="500"
trigger="click"
v-if="isShowButton(OperationButtonType.REJECT)"
>
<template #reference>
<el-button class="mr-20px" plain type="danger" @click="openPopover('2')">
<Icon icon="ep:close" />&nbsp; 拒绝
<Icon icon="ep:close" />&nbsp; {{ getButtonDisplayName(OperationButtonType.REJECT) }}
</el-button>
</template>
<!-- 审批表单 -->
<div class="flex flex-col flex-1 pt-20px px-20px" v-loading="formLoading">
<el-form
label-position="top"
@ -105,21 +123,46 @@
<el-form-item>
<el-button :disabled="formLoading" type="danger" @click="handleAudit(false)">
拒绝
{{ getButtonDisplayName(OperationButtonType.REJECT) }}
</el-button>
<el-button @click="rejectVisible = false"> 取消 </el-button>
</el-form-item>
</el-form>
</div>
</el-popover>
<!-- 抄送按钮 -->
<div @click="handleSend"> <Icon :size="14" icon="svg-icon:send" />&nbsp;抄送 </div>
<div @click="openTaskUpdateAssigneeForm">
<Icon :size="14" icon="fa:share-square-o" />&nbsp;转交
<!-- 转交按钮 -->
<div @click="openTaskUpdateAssigneeForm" v-if="isShowButton(OperationButtonType.TRANSFER)">
<Icon :size="14" icon="fa:share-square-o" />&nbsp;
{{ getButtonDisplayName(OperationButtonType.TRANSFER) }}
</div>
<div @click="handleDelegate"> <Icon :size="14" icon="ep:position" />&nbsp;委派 </div>
<div @click="handleSign"> <Icon :size="14" icon="ep:plus" />&nbsp;加签 </div>
<div @click="handleBack"> <Icon :size="14" icon="fa:mail-reply" />&nbsp;退回 </div>
<!-- 委托按钮 -->
<div @click="handleDelegate" v-if="isShowButton(OperationButtonType.DELEGATE)">
<Icon :size="14" icon="ep:position" />&nbsp;
{{ getButtonDisplayName(OperationButtonType.DELEGATE) }}
</div>
<!-- 加签 -->
<div @click="handleSign" v-if="isShowButton(OperationButtonType.ADD_SIGN)">
<Icon :size="14" icon="ep:plus" />&nbsp;
{{ getButtonDisplayName(OperationButtonType.ADD_SIGN) }}
</div>
<!-- TODO @jason减签 -->
<!-- 退回按钮 -->
<div @click="handleBack" v-if="isShowButton(OperationButtonType.RETURN)">
<Icon :size="14" icon="fa:mail-reply" />&nbsp;
{{ getButtonDisplayName(OperationButtonType.RETURN) }}
</div>
<!--TODO @jason撤回 -->
<!--TODO @jason再次发起 -->
</div>
<!-- 弹窗转派审批人 -->
<TaskTransferForm ref="taskTransferFormRef" @success="getDetail" />
<!-- 弹窗回退节点 -->
@ -129,7 +172,6 @@
<!-- 弹窗加签当前任务审批人为A向前加签选了一个C则需要C先审批然后再是A审批向后加签BA审批完需要B再审批完才算完成这个任务节点 -->
<TaskSignCreateForm ref="taskSignCreateFormRef" @success="getDetail" />
</template>
<script lang="ts" setup>
import { setConfAndFields2 } from '@/utils/formCreate'
import { useUserStore } from '@/store/modules/user'
@ -140,7 +182,10 @@ import TaskDelegateForm from './dialog/TaskDelegateForm.vue'
import TaskTransferForm from './dialog/TaskTransferForm.vue'
import TaskSignCreateForm from './dialog/TaskSignCreateForm.vue'
import { isEmpty } from '@/utils/is'
import {
OperationButtonType,
OPERATION_BUTTON_NAME
} from '@/components/SimpleProcessDesignerV2/src/consts'
defineOptions({ name: 'ProcessInstanceBtnConatiner' })
const userId = useUserStore().getUser.id //
@ -175,15 +220,17 @@ watch(
deep: true
}
)
// TODO @jaosn
/**
* 设置 runningTasks 中的任务
*/
const loadRunningTask = (tasks) => {
const loadRunningTask = (tasks: any[]) => {
runningTask.value = {}
auditForm.value = {}
approveForm.value = {}
approveFormFApi.value = {}
tasks.forEach((task) => {
tasks.forEach((task: any) => {
if (!isEmpty(task.children)) {
loadRunningTask(task.children)
}
@ -214,7 +261,7 @@ const loadRunningTask = (tasks) => {
}
/** 处理审批通过和不通过的操作 */
const handleAudit = async (pass) => {
const handleAudit = async (pass: any) => {
formLoading.value = true
try {
const auditFormRef = proxy.$refs['formRef']
@ -254,6 +301,7 @@ const handleAudit = async (pass) => {
/* 抄送 TODO */
const handleSend = () => {}
// TODO flag approve: boolean flag 1 2
const openPopover = (flag) => {
passVisible.value = false
rejectVisible.value = false
@ -289,6 +337,24 @@ const getDetail = () => {
emit('success')
}
/** 是否显示按钮 */
const isShowButton = (btnType: OperationButtonType): boolean => {
let isShow = true
if (runningTask.value.buttonsSetting && runningTask.value.buttonsSetting[btnType]) {
isShow = runningTask.value.buttonsSetting[btnType].enable
}
return isShow
}
/** 获取按钮的显示名称 */
const getButtonDisplayName = (btnType: OperationButtonType) => {
let displayName = OPERATION_BUTTON_NAME.get(btnType)
if (runningTask.value.buttonsSetting && runningTask.value.buttonsSetting[btnType]) {
displayName = runningTask.value.buttonsSetting[btnType].displayName
}
return displayName
}
defineExpose({ loadRunningTask })
</script>
@ -299,10 +365,11 @@ defineExpose({ loadRunningTask })
.btn-container {
> div {
display: flex;
margin: 0 15px;
cursor: pointer;
display: flex;
align-items: center;
&:hover {
color: #6db5ff;
}

View File

@ -1,9 +1,111 @@
<!-- 审批详情的右侧审批流 -->
<template>
<el-timeline class="pt-20px">
<el-timeline-item v-for="(activity, index) in mockData" :key="index" size="large">
<!-- 遍历每个审批节点 -->
<el-timeline-item
v-for="(activity, index) in approveNodes"
:key="index"
size="large"
:icon="getApprovalNodeIcon(activity.status, activity.nodeType)"
:color="getApprovalNodeColor(activity.status)"
>
<div class="flex flex-col items-start">
<div class="font-bold"> {{ activity.name }}</div>
<div class="color-#a1a6ae text-12px mb-10px"> {{ activity.assigneeUser.nickname }}</div>
<div class="flex items-center mt-1">
<!-- 情况一遍历每个审批节点下的进行中task 任务 -->
<div v-for="(task, idx) in activity.tasks" :key="idx" class="flex items-center">
<div class="flex items-center flex-col pr-2">
<div class="position-relative" v-if="task.assigneeUser || task.ownerUser">
<!-- 信息头像 -->
<el-avatar
:size="36"
v-if="task.assigneeUser && task.assigneeUser.avatar"
:src="task.assigneeUser.avatar"
/>
<el-avatar v-else-if="task.assigneeUser && task.assigneeUser.nickname">
{{ task.assigneeUser.nickname.substring(0, 1) }}
</el-avatar>
<el-avatar
v-else-if="task.ownerUser && task.ownerUser.avatar"
:src="task.ownerUser.avatar"
/>
<el-avatar v-else-if="task.ownerUser && task.ownerUser.nickname">
{{ task.ownerUser.nickname.substring(0, 1) }}
</el-avatar>
<!-- 信息任务 ICON -->
<div
class="position-absolute top-26px left-26px bg-#fff rounded-full flex items-center p-2px"
>
<Icon
:size="12"
:icon="statusIconMap2[task.status]?.icon"
:color="statusIconMap2[task.status]?.color"
/>
</div>
</div>
<div class="flex flex-col mt-1">
<!-- 信息昵称 -->
<div
v-if="task.assigneeUser && task.assigneeUser.nickname"
class="text-10px text-align-center"
>
{{ task.assigneeUser.nickname }}
</div>
<div
v-else-if="task.ownerUser && task.ownerUser.nickname"
class="text-10px text-align-center"
>
{{ task.ownerUser.nickname }}
</div>
<!-- TODO @jason审批意见要展示哈 -->
<!-- <div v-if="task.reason" :title="task.reason" class="text-13px text-truncate w-150px mt-1"> : {{ task.reason }}</div> -->
</div>
</div>
</div>
<!-- 情况二遍历每个审批节点下的候选的task 任务例如说1依次审批2未来的审批任务等 -->
<div
v-for="(user, idx1) in activity.candidateUserList"
:key="idx1"
class="flex items-center"
>
<div class="flex items-center flex-col pr-2">
<div class="position-relative">
<!-- 信息头像 -->
<el-avatar :size="36" v-if="user.avatar" :src="user.avatar" />
<el-avatar v-else-if="user.nickname && user.nickname">
{{ user.nickname.substring(0, 1) }}
</el-avatar>
<!-- 信息任务 ICON -->
<div
class="position-absolute top-26px left-26px bg-#fff rounded-full flex items-center p-2px"
>
<Icon
:size="12"
:icon="statusIconMap2['-1']?.icon"
:color="statusIconMap2['-1']?.color"
/>
</div>
</div>
<div class="flex flex-col mt-1">
<!-- 信息昵称 -->
<div v-if="user.nickname" class="text-10px text-align-center">
{{ user.nickname }}
</div>
<!-- <div v-if="task.reason" :title="task.reason" class="text-13px text-truncate w-150px mt-1"> : {{ task.reason }}</div> -->
</div>
</div>
</div>
</div>
<!-- 信息时间 -->
<div
v-if="activity.status !== TaskStatusEnum.NOT_START"
class="text-#a5a5a5 text-13px mt-1"
>
{{ getApprovalNodeTime(activity) }}
</div>
<!-- TODO @jason审批意见要展示哈 -->
<!-- <div class="color-#a1a6ae text-12px mb-10px"> {{ activity.assigneeUser.nickname }}</div>
<div v-if="activity.opinion" class="text-#a5a5a5 text-12px w-100%">
<div class="mb-5px">审批意见</div>
<div
@ -14,148 +116,118 @@
</div>
<div v-if="activity.createTime" class="text-#a5a5a5 text-13px">
{{ formatDate(activity.createTime) }}
</div>
</div> -->
</div>
<!-- 该节点用户的头像 -->
<template #dot>
<div class="w-35px h-35px position-relative">
<img
src="@/assets/imgs/avatar.jpg"
class="rounded-full w-full h-full position-absolute bottom-6px right-12px"
alt=""
/>
<div
class="position-absolute top-16px left-8px bg-#fff rounded-full flex items-center content-center p-2px"
>
<Icon
:size="12"
:icon="optIconMap[activity.status]?.icon"
:color="optIconMap[activity.status]?.color"
/>
</div>
</div>
</template>
</el-timeline-item>
</el-timeline>
</template>
<script lang="ts" setup>
import { formatDate } from '@/utils/formatTime'
import { propTypes } from '@/utils/propTypes'
import * as ProcessInstanceApi from '@/api/bpm/processInstance'
import { TaskStatusEnum } from '@/api/bpm/task'
import { NodeType } from '@/components/SimpleProcessDesignerV2/src/consts'
import { Check, Close, Loading, Clock, Minus, Delete } from '@element-plus/icons-vue'
defineOptions({ name: 'BpmProcessInstanceTimeline' })
defineProps({
tasks: propTypes.array //
const props = defineProps({
//
processInstanceId: {
type: String,
required: false,
default: ''
},
//
processDefinitionId: {
type: String,
required: false,
default: ''
}
})
const optIconMap = {
//
const approveNodes = ref<ProcessInstanceApi.ApprovalNodeInfo[]>([])
const statusIconMap2 = {
//
'-1': { color: '#e5e7ec', icon: 'ep-clock' },
//
'0': { color: '#e5e7ec', icon: 'ep:loading' },
//
'1': {
color: '#00b32a',
icon: 'fa-solid:clock'
},
'1': { color: '#448ef7', icon: 'ep:loading' },
//
'2': { color: '#00b32a', icon: 'fa-solid:check-circle' },
'2': { color: '#00b32a', icon: 'ep:circle-check-filled' },
//
'3': { color: '#f46b6c', icon: 'fa-solid:times-circle' }
'3': { color: '#f46b6c', icon: 'fa-solid:times-circle' },
//
'4': { color: '#cccccc', icon: 'ep:delete-filled' },
// 退
'5': { color: '#f46b6c', icon: 'ep:remove-filled' },
//
'6': { color: '#448ef7', icon: 'ep:loading' },
//
'7': { color: '#00b32a', icon: 'ep:circle-check-filled' }
}
const mockData: any = [
{
id: 'fe1190ee-68c3-11ef-9c7d-00a6181404fd',
name: '发起人',
createTime: 1725237646192,
endTime: null,
durationInMillis: null,
status: 1,
reason: null,
ownerUser: null,
assigneeUser: {
id: 104,
nickname: '测试号',
deptId: 107,
deptName: '运维部门'
},
taskDefinitionKey: 'task-01',
processInstanceId: 'fe0c60c6-68c3-11ef-9c7d-00a6181404fd',
processInstance: {
id: 'fe0c60c6-68c3-11ef-9c7d-00a6181404fd',
name: 'oa_leave',
createTime: null,
processDefinitionId: 'oa_leave:1:6e5ac269-5f87-11ef-bdb6-00a6181404fd',
startUser: null
},
parentTaskId: null,
children: null,
formId: null,
formName: null,
formConf: null,
formFields: null,
formVariables: null
},
{
id: 'fe1190ee-68c3-11ef-9c7d-00a6181404fd',
name: '领导审批',
createTime: 1725237646192,
endTime: null,
durationInMillis: null,
status: 2,
reason: null,
ownerUser: null,
assigneeUser: {
id: 104,
nickname: '领导',
deptId: 107,
deptName: '运维部门'
},
taskDefinitionKey: 'task-01',
processInstanceId: 'fe0c60c6-68c3-11ef-9c7d-00a6181404fd',
processInstance: {
id: 'fe0c60c6-68c3-11ef-9c7d-00a6181404fd',
name: 'oa_leave',
createTime: null,
processDefinitionId: 'oa_leave:1:6e5ac269-5f87-11ef-bdb6-00a6181404fd',
startUser: null
},
parentTaskId: null,
children: null,
formId: null,
formName: null,
formConf: null,
formFields: null,
formVariables: null
},
{
id: 'fe1190ee-68c3-11ef-9c7d-00a6181404fd',
name: '财务总监审核',
createTime: 1725237646192,
endTime: null,
durationInMillis: null,
status: 3,
reason: null,
ownerUser: null,
assigneeUser: {
id: 104,
nickname: '财务总监',
deptId: 107,
deptName: '运维部门'
},
taskDefinitionKey: 'task-01',
processInstanceId: 'fe0c60c6-68c3-11ef-9c7d-00a6181404fd',
processInstance: {
id: 'fe0c60c6-68c3-11ef-9c7d-00a6181404fd',
name: 'oa_leave',
createTime: null,
processDefinitionId: 'oa_leave:1:6e5ac269-5f87-11ef-bdb6-00a6181404fd',
startUser: null
},
parentTaskId: null,
children: null,
formId: null,
formName: null,
formConf: null,
formFields: null,
formVariables: null
const statusIconMap = {
//
'-1': { color: '#e5e7ec', icon: Clock },
'0': { color: '#e5e7ec', icon: Clock },
//
'1': { color: '#448ef7', icon: Loading },
//
'2': { color: '#00b32a', icon: Check },
//
'3': { color: '#f46b6c', icon: Close },
//
'4': { color: '#cccccc', icon: Delete },
// 退
'5': { color: '#f46b6c', icon: Minus },
//
'6': { color: '#448ef7', icon: Loading },
//
'7': { color: '#00b32a', icon: Check }
}
/** 获得审批详情 */
const getApprovalDetail = async () => {
const data = await ProcessInstanceApi.getApprovalDetail(
props.processInstanceId,
props.processDefinitionId
)
approveNodes.value = data.approveNodes
}
const getApprovalNodeIcon = (taskStatus: number, nodeType: NodeType) => {
if (taskStatus == TaskStatusEnum.NOT_START) {
return statusIconMap[taskStatus]?.icon
}
]
if (nodeType === NodeType.START_USER_NODE || nodeType === NodeType.USER_TASK_NODE) {
return statusIconMap[taskStatus]?.icon
}
}
const getApprovalNodeColor = (taskStatus: number) => {
return statusIconMap[taskStatus]?.color
}
const getApprovalNodeTime = (node: ProcessInstanceApi.ApprovalNodeInfo) => {
if (node.endTime) {
return `结束时间:${formatDate(node.endTime)}`
}
if (node.startTime) {
return `创建时间:${formatDate(node.startTime)}`
}
}
/** 重新刷新审批详情 */
const refresh = () => {
getApprovalDetail()
}
defineExpose({ refresh })
onMounted(async () => {
await getApprovalDetail()
})
</script>

View File

@ -56,29 +56,73 @@
</el-form-item>
</el-form>
<div style="margin-bottom: 20px; margin-left: 10%; font-size: 14px">
<el-button type="success" @click="handleAudit(item, true)">
<!-- TODO @jason建议搞个 if 来判断替代现有的 !item.buttonsSetting || item.buttonsSetting[OpsButtonType.APPROVE]?.enable -->
<el-button
type="success"
v-if="!item.buttonsSetting || item.buttonsSetting[OperationButtonType.APPROVE]?.enable"
@click="handleAudit(item, true)"
>
<Icon icon="ep:select" />
通过
<!-- TODO @jason这个也是类似哈搞个方法来生成名字 -->
{{
item.buttonsSetting?.[OperationButtonType.APPROVE]?.displayName ||
OPERATION_BUTTON_NAME.get(OperationButtonType.APPROVE)
}}
</el-button>
<el-button type="danger" @click="handleAudit(item, false)">
<el-button
v-if="!item.buttonsSetting || item.buttonsSetting[OperationButtonType.REJECT]?.enable"
type="danger"
@click="handleAudit(item, false)"
>
<Icon icon="ep:close" />
不通过
{{
item.buttonsSetting?.[OperationButtonType.REJECT].displayName ||
OPERATION_BUTTON_NAME.get(OperationButtonType.REJECT)
}}
</el-button>
<el-button type="primary" @click="openTaskUpdateAssigneeForm(item.id)">
<el-button
v-if="!item.buttonsSetting || item.buttonsSetting[OperationButtonType.TRANSFER]?.enable"
type="primary"
@click="openTaskUpdateAssigneeForm(item.id)"
>
<Icon icon="ep:edit" />
转办
{{
item.buttonsSetting?.[OperationButtonType.TRANSFER]?.displayName ||
OPERATION_BUTTON_NAME.get(OperationButtonType.TRANSFER)
}}
</el-button>
<el-button type="primary" @click="handleDelegate(item)">
<el-button
v-if="!item.buttonsSetting || item.buttonsSetting[OperationButtonType.DELEGATE]?.enable"
type="primary"
@click="handleDelegate(item)"
>
<Icon icon="ep:position" />
委派
{{
item.buttonsSetting?.[OperationButtonType.DELEGATE]?.displayName ||
OPERATION_BUTTON_NAME.get(OperationButtonType.DELEGATE)
}}
</el-button>
<el-button type="primary" @click="handleSign(item)">
<el-button
v-if="!item.buttonsSetting || item.buttonsSetting[OperationButtonType.ADD_SIGN]?.enable"
type="primary"
@click="handleSign(item)"
>
<Icon icon="ep:plus" />
加签
{{
item.buttonsSetting?.[OperationButtonType.ADD_SIGN]?.displayName ||
OPERATION_BUTTON_NAME.get(OperationButtonType.ADD_SIGN)
}}
</el-button>
<el-button type="warning" @click="handleBack(item)">
<el-button
v-if="!item.buttonsSetting || item.buttonsSetting[OperationButtonType.RETURN]?.enable"
type="warning"
@click="handleBack(item)"
>
<Icon icon="ep:back" />
回退
{{
item.buttonsSetting?.[OperationButtonType.RETURN]?.displayName ||
OPERATION_BUTTON_NAME.get(OperationButtonType.RETURN)
}}
</el-button>
</div>
</el-col>
@ -147,6 +191,10 @@ import TaskSignCreateForm from './dialog/TaskSignCreateForm.vue'
import { registerComponent } from '@/utils/routerHelper'
import { isEmpty } from '@/utils/is'
import * as UserApi from '@/api/system/user'
import {
OperationButtonType,
OPERATION_BUTTON_NAME
} from '@/components/SimpleProcessDesignerV2/src/consts'
defineOptions({ name: 'BpmProcessInstanceDetail' })
@ -200,7 +248,11 @@ const handleAudit = async (task, pass) => {
// 1.2
const elForm = unref(auditFormRef)
if (!elForm) return
const valid = await elForm.validate()
let valid = await elForm.validate()
if (!valid) return
//
if (!fApi.value) return
valid = await fApi.value.validate()
if (!valid) return
// 2.1
@ -216,6 +268,9 @@ const handleAudit = async (task, pass) => {
await formCreateApi.validate()
data.variables = approveForms.value[index].value
}
//
data.variables = getWritableValueOfForm(task.fieldsPermission)
await TaskApi.approveTask(data)
message.success('审批通过成功')
} else {
@ -251,11 +306,11 @@ const handleSign = async (task: any) => {
}
/** 获得详情 */
const getDetail = () => {
// 1.
const getDetail = async () => {
// 1.
await getTaskList()
// 2.
getProcessInstance()
// 2.
getTaskList()
}
/** 加载流程实例 */
@ -273,16 +328,29 @@ const getProcessInstance = async () => {
//
const processDefinition = data.processDefinition
if (processDefinition.formType === 10) {
setConfAndFields2(
detailForm,
processDefinition.formConf,
processDefinition.formFields,
data.formVariables
)
if (detailForm.value.rule.length > 0) {
detailForm.value.value = data.formVariables
} else {
setConfAndFields2(
detailForm,
processDefinition.formConf,
processDefinition.formFields,
data.formVariables
)
}
nextTick().then(() => {
fApi.value?.btn.show(false)
fApi.value?.resetBtn.show(false)
fApi.value?.disabled(true)
//
if (runningTasks.value.length > 0) {
const task = runningTasks.value.at(0)
if (task.fieldsPermission) {
Object.keys(task.fieldsPermission).forEach((item) => {
setFieldPermission(item, task.fieldsPermission[item])
})
}
}
})
} else {
// data.processDefinition.formCustomViewPath /crm/contract/detail/index.vue
@ -353,6 +421,7 @@ const loadRunningTask = (tasks) => {
if (!task.assigneeUser || task.assigneeUser.id !== userId) {
return
}
// 2.3
runningTasks.value.push({ ...task })
auditForms.value.push({
@ -371,6 +440,35 @@ const loadRunningTask = (tasks) => {
})
}
/**
* 设置表单权限
*/
const setFieldPermission = (field: string, permission: string) => {
if (permission === '1') {
fApi.value?.disabled(true, field)
}
if (permission === '2') {
fApi.value?.disabled(false, field)
}
if (permission === '3') {
fApi.value?.hidden(true, field)
}
}
/**
* 获取可以编辑字段的值
*/
const getWritableValueOfForm = (fieldsPermission: Object) => {
const fieldsValue = {}
if (fieldsPermission && fApi.value) {
Object.keys(fieldsPermission).forEach((item) => {
if (fieldsPermission[item] === '2') {
fieldsValue[item] = fApi.value.getValue(item)
}
})
}
return fieldsValue
}
/** 初始化 */
const userOptions = ref<UserApi.UserVO[]>([]) //
onMounted(async () => {

View File

@ -1,88 +1,106 @@
<template>
<ContentWrap :bodyStyle="{ padding: '10px 20px' }" class="position-relative">
<img
class="position-absolute right-20px"
width="150"
:src="auditIcons[processInstance.status]"
alt=""
/>
<div class="text-#878c93">编号{{ id }}</div>
<el-divider class="!my-8px" />
<div class="flex items-center gap-5 mb-10px">
<div class="text-26px font-bold mb-5px">{{ processInstance.name }}</div>
<dict-tag :type="DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS" :value="processInstance.status" />
</div>
<div class="processInstance-wrap-main">
<el-scrollbar>
<img
class="position-absolute right-20px"
width="150"
:src="auditIcons[processInstance.status]"
alt=""
/>
<div class="text-#878c93 h-15px">编号{{ id }}</div>
<el-divider class="!my-8px" />
<div class="flex items-center gap-5 mb-10px h-40px">
<div class="text-26px font-bold mb-5px">{{ processInstance.name }}</div>
<dict-tag :type="DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS" :value="processInstance.status" />
</div>
<div class="flex items-center gap-5 mb-10px text-13px">
<div class="bg-gray-100 h-35px rounded-3xl flex items-center p-8px gap-2 dark:color-gray-600">
<img class="rounded-full h-28px" src="@/assets/imgs/avatar.jpg" alt="" />
{{ processInstance?.startUser?.nickname }}
</div>
<div class="text-#878c93"> {{ formatDate(processInstance.startTime) }} 提交 </div>
</div>
<div class="flex items-center gap-5 mb-10px text-13px h-35px">
<div
class="bg-gray-100 h-35px rounded-3xl flex items-center p-8px gap-2 dark:color-gray-600"
>
<img class="rounded-full h-28px" src="@/assets/imgs/avatar.jpg" alt="" />
{{ processInstance?.startUser?.nickname }}
</div>
<div class="text-#878c93"> {{ formatDate(processInstance.startTime) }} 提交 </div>
</div>
<el-tabs>
<!-- 表单信息 -->
<el-tab-pane label="表单信息">
<el-row :gutter="10">
<el-col :span="18" class="!flex !flex-col formCol">
<!-- 表单信息 -->
<div v-loading="processInstanceLoading" class="form-box flex flex-col mb-30px flex-1">
<!-- 情况一流程表单 -->
<el-col
v-if="processInstance?.processDefinition?.formType === 10"
:offset="6"
:span="16"
>
<form-create
v-model="detailForm.value"
v-model:api="fApi"
:option="detailForm.option"
:rule="detailForm.rule"
/>
</el-col>
<!-- 情况二业务表单 -->
<div v-if="processInstance?.processDefinition?.formType === 20">
<BusinessFormComponent :id="processInstance.businessKey" />
</div>
<el-tabs v-model="activeTab">
<!-- 表单信息 -->
<el-tab-pane label="审批详情" name="form">
<div class="form-scroll-area">
<el-scrollbar>
<el-row :gutter="10">
<el-col :span="18" class="!flex !flex-col formCol">
<!-- 表单信息 -->
<div
v-loading="processInstanceLoading"
class="form-box flex flex-col mb-30px flex-1"
>
<!-- 情况一流程表单 -->
<el-col
v-if="processInstance?.processDefinition?.formType === 10"
:offset="6"
:span="16"
>
<form-create
v-model="detailForm.value"
v-model:api="fApi"
:option="detailForm.option"
:rule="detailForm.rule"
/>
</el-col>
<!-- 情况二业务表单 -->
<div v-if="processInstance?.processDefinition?.formType === 20">
<BusinessFormComponent :id="processInstance.businessKey" />
</div>
</div>
</el-col>
<el-col :span="6">
<!-- 审批记录时间线 -->
<ProcessInstanceTimeline ref="timelineRef" :process-instance-id="id" />
</el-col>
</el-row>
</el-scrollbar>
</div>
<!-- 操作栏按钮 -->
<ProcessInstanceOperationButton
ref="operationButtonRef"
:processInstance="processInstance"
:userOptions="userOptions"
@success="getDetail"
</el-tab-pane>
<!-- 流程图 -->
<el-tab-pane label="流程图" name="diagram">
<ProcessInstanceBpmnViewer
:id="`${id}`"
:bpmn-xml="bpmnXml"
:loading="processInstanceLoading"
:process-instance="processInstance"
:tasks="tasks"
/>
</el-col>
<el-col :span="6">
<!-- 审批记录时间线 -->
<ProcessInstanceTimeline :process-instance="processInstance" :tasks="tasks" />
</el-col>
</el-row>
</el-tab-pane>
<!-- 流程图 -->
<el-tab-pane label="流程图">
<ProcessInstanceBpmnViewer
:id="`${id}`"
:bpmn-xml="bpmnXml"
:loading="processInstanceLoading"
:process-instance="processInstance"
:tasks="tasks"
/>
</el-tab-pane>
<!-- 流转记录 -->
<el-tab-pane label="流转记录">
<ProcessInstanceTaskList
:loading="tasksLoad"
:process-instance="processInstance"
:tasks="tasks"
@refresh="getTaskList"
/>
</el-tab-pane>
<!-- 流转评论 -->
<el-tab-pane label="流转评论"> 流转评论 </el-tab-pane>
</el-tabs>
</el-tab-pane>
<!-- 流转记录 -->
<el-tab-pane label="流转记录" name="record">
<ProcessInstanceTaskList
:loading="tasksLoad"
:process-instance="processInstance"
:tasks="tasks"
@refresh="getTaskList"
/>
</el-tab-pane>
<!-- 流转评论 TODO 待开发 -->
<el-tab-pane label="流转评论" name="comment"> 流转评论 </el-tab-pane>
</el-tabs>
<div
class="b-t-solid border-t-1px border-[var(--el-border-color)]"
v-if="activeTab === 'form'"
>
<!-- 操作栏按钮 -->
<ProcessInstanceOperationButton
ref="operationButtonRef"
:processInstance="processInstance"
:userOptions="userOptions"
@success="refresh"
/>
</div>
</el-scrollbar>
</div>
</ContentWrap>
</template>
<script lang="ts" setup>
@ -99,18 +117,22 @@ import ProcessInstanceOperationButton from './ProcessInstanceOperationButton.vue
import ProcessInstanceTimeline from './ProcessInstanceTimeline.vue'
import { registerComponent } from '@/utils/routerHelper'
import * as UserApi from '@/api/system/user'
import { FieldPermissionType } from '@/components/SimpleProcessDesignerV2/src/consts'
import audit1 from '@/assets/svgs/bpm/audit1.svg'
import audit2 from '@/assets/svgs/bpm/audit2.svg'
import audit3 from '@/assets/svgs/bpm/audit3.svg'
defineOptions({ name: 'BpmProcessInstanceDetail' })
const { query } = useRoute() //
const props = defineProps<{
id: string //
taskId?: string //
activityId?: string //
}>()
const message = useMessage() //
const id = query.id as unknown as string //
const processInstanceLoading = ref(false) //
const processInstance = ref<any>({}) //
const operationButtonRef = ref()
const timelineRef = ref()
const bpmnXml = ref('') // BPMN XML
const tasksLoad = ref(true) //
const tasks = ref<any[]>([]) //
@ -141,7 +163,7 @@ const BusinessFormComponent = ref<any>(null) // 异步组件
const getProcessInstance = async () => {
try {
processInstanceLoading.value = true
const data = await ProcessInstanceApi.getProcessInstance(id)
const data = await ProcessInstanceApi.getProcessInstance(props.id)
if (!data) {
message.error('查询不到流程信息!')
return
@ -151,6 +173,15 @@ const getProcessInstance = async () => {
//
const processDefinition = data.processDefinition
if (processDefinition.formType === 10) {
//
let fieldsPermission = undefined
if (props.taskId || props.activityId) {
fieldsPermission = await ProcessInstanceApi.getFormFieldsPermission({
processInstanceId: props.id,
taskId: props.taskId,
activityId: props.activityId
})
}
setConfAndFields2(
detailForm,
processDefinition.formConf,
@ -161,6 +192,11 @@ const getProcessInstance = async () => {
fApi.value?.btn.show(false)
fApi.value?.resetBtn.show(false)
fApi.value?.disabled(true)
if (fieldsPermission) {
Object.keys(fieldsPermission).forEach((item) => {
setFieldPermission(item, fieldsPermission[item])
})
}
})
} else {
// data.processDefinition.formCustomViewPath /crm/contract/detail/index.vue
@ -174,15 +210,30 @@ const getProcessInstance = async () => {
}
}
/**
* 设置表单权限
*/
const setFieldPermission = (field: string, permission: string) => {
if (permission === FieldPermissionType.READ) {
fApi.value?.disabled(true, field)
}
if (permission === FieldPermissionType.WRITE) {
fApi.value?.disabled(false, field)
}
if (permission === FieldPermissionType.NONE) {
fApi.value?.hidden(true, field)
}
}
/** 加载任务列表 */
const getTaskList = async () => {
try {
//
tasksLoad.value = true
const data = await TaskApi.getTaskListByProcessInstanceId(id)
const data = await TaskApi.getTaskListByProcessInstanceId(props.id)
tasks.value = []
// 1.1
data.forEach((task) => {
data.forEach((task: any) => {
if (task.status !== 4) {
tasks.value.push(task)
}
@ -209,6 +260,19 @@ const getTaskList = async () => {
}
}
/**
* 操作成功后刷新
*/
const refresh = () => {
//
getDetail()
// Timeline
timelineRef.value?.refresh()
}
/** 当前的Tab */
const activeTab = ref('form')
/** 初始化 */
const userOptions = ref<UserApi.UserVO[]>([]) //
onMounted(async () => {
@ -219,6 +283,33 @@ onMounted(async () => {
</script>
<style lang="scss" scoped>
$wrap-padding-height: 30px;
$wrap-margin-height: 15px;
$button-height: 51px;
$process-header-height: 194px;
.processInstance-wrap-main {
height: calc(
100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 45px
);
max-height: calc(
100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 45px
);
overflow: auto;
.form-scroll-area {
height: calc(
100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 45px -
$process-header-height - 40px
);
max-height: calc(
100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 45px -
$process-header-height - 40px
);
overflow: auto;
}
}
.form-box {
:deep(.el-card) {
border: none;

View File

@ -1,28 +1,13 @@
<template>
<div>
<section class="dingflow-design">
<div class="box-scale">
<nodeWrap v-model:nodeConfig="nodeConfig" />
<div class="end-node">
<div class="end-node-circle"></div>
<div class="end-node-text">流程结束</div>
</div>
</div>
</section>
</div>
<SimpleProcessDesigner :model-id="modelId" />
</template>
<script lang="ts" setup>
import nodeWrap from '@/components/SimpleProcessDesigner/src/nodeWrap.vue'
defineOptions({ name: 'SimpleWorkflowDesignEditor' })
let nodeConfig = ref({
nodeName: '发起人',
type: 0,
id: 'root',
formPerms: {},
nodeUserList: [],
childNode: {}
<script setup lang="ts">
import { SimpleProcessDesigner } from '@/components/SimpleProcessDesignerV2/src/'
defineOptions({
name: 'SimpleWorkflowDesignEditor'
})
const { query } = useRoute() //
const modelId = query.modelId as string
</script>
<style>
@import url('@/components/SimpleProcessDesigner/theme/workflow.css');
</style>
<style lang="scss" scoped></style>

View File

@ -111,11 +111,16 @@ const getList = async () => {
/** 处理审批按钮 */
const handleAudit = (row: any) => {
const query = {
id: row.processInstanceId,
activityId: undefined
}
if (row.activityId) {
query.activityId = row.activityId
}
push({
name: 'BpmProcessInstanceDetail',
query: {
id: row.processInstanceId
}
query: query
})
}

View File

@ -158,7 +158,8 @@ const handleAudit = (row: any) => {
push({
name: 'BpmProcessInstanceDetail',
query: {
id: row.processInstance.id
id: row.processInstance.id,
taskId: row.id
}
})
}

View File

@ -140,7 +140,8 @@ const handleAudit = (row: any) => {
push({
name: 'BpmProcessInstanceDetail',
query: {
id: row.processInstance.id
id: row.processInstance.id,
taskId: row.id
}
})
}