仿钉钉流程设计器- 新增发起人节点,去掉开始节点

This commit is contained in:
jason 2024-08-21 21:00:38 +08:00
parent eb7e9397f5
commit eb79ee1b77
10 changed files with 355 additions and 105 deletions

View File

@ -20,13 +20,13 @@
</div>
<div class="handler-item-text">抄送</div>
</div>
<div class="handler-item" @click="addNode(NodeType.EXCLUSIVE_NODE)">
<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_NODE_FORK)">
<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>
@ -107,10 +107,10 @@ const addNode = (type: number) => {
}
emits('update:childNode', data)
}
if (type === NodeType.EXCLUSIVE_NODE) {
if (type === NodeType.CONDITION_BRANCH_NODE) {
const data: SimpleFlowNode = {
name: '条件分支',
type: NodeType.EXCLUSIVE_NODE,
type: NodeType.CONDITION_BRANCH_NODE,
id: 'GateWay_' + generateUUID(),
childNode: props.childNode,
conditionNodes: [
@ -140,10 +140,10 @@ const addNode = (type: number) => {
}
emits('update:childNode', data)
}
if (type === NodeType.PARALLEL_NODE_FORK) {
if (type === NodeType.PARALLEL_BRANCH_NODE) {
const data: SimpleFlowNode = {
name: '并行分支',
type: NodeType.PARALLEL_NODE_FORK,
type: NodeType.PARALLEL_BRANCH_NODE,
id: 'GateWay_' + generateUUID(),
childNode: props.childNode,
conditionNodes: [

View File

@ -1,7 +1,7 @@
<template>
<!-- 开始节点 -->
<StartEventNode
v-if="currentNode && currentNode.type === NodeType.START_EVENT_NODE"
<!-- 发起人节点 -->
<StartUserNode
v-if="currentNode && currentNode.type === NodeType.START_USER_NODE"
:flow-node="currentNode"
/>
<!-- 审批节点 -->
@ -19,14 +19,14 @@
/>
<!-- 条件节点 -->
<ExclusiveNode
v-if="currentNode && currentNode.type === NodeType.EXCLUSIVE_NODE"
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_NODE_FORK"
v-if="currentNode && currentNode.type === NodeType.PARALLEL_BRANCH_NODE"
:flow-node="currentNode"
@update:model-value="handleModelValueUpdate"
@find:parent-node="findFromParentNode"
@ -43,7 +43,7 @@
<EndEventNode v-if="currentNode && currentNode.type === NodeType.END_EVENT_NODE" />
</template>
<script setup lang="ts">
import StartEventNode from './nodes/StartEventNode.vue'
import StartUserNode from './nodes/StartUserNode.vue'
import EndEventNode from './nodes/EndEventNode.vue'
import UserTaskNode from './nodes/UserTaskNode.vue'
import CopyTaskNode from './nodes/CopyTaskNode.vue'
@ -90,7 +90,11 @@ const recursiveFindParentNode = (
findNode: SimpleFlowNode,
nodeType: number
) => {
if (!findNode || findNode.type === NodeType.START_EVENT_NODE) {
if (!findNode) {
return
}
if (findNode.type === NodeType.START_USER_NODE) {
nodeList.push(findNode)
return
}

View File

@ -37,7 +37,7 @@
<script setup lang="ts">
import ProcessNodeTree from './ProcessNodeTree.vue'
import { updateBpmSimpleModel, getBpmSimpleModel } from '@/api/bpm/simple'
import { SimpleFlowNode, NodeType, NODE_DEFAULT_TEXT } from './consts'
import { SimpleFlowNode, NodeType, NodeId, NODE_DEFAULT_TEXT } from './consts'
defineOptions({
name: 'SimpleProcessDesigner'
@ -83,7 +83,7 @@ const validateNode = (node: SimpleFlowNode | undefined, errorNodes: SimpleFlowNo
if (type == NodeType.END_EVENT_NODE) {
return
}
if (type == NodeType.START_EVENT_NODE) {
if (type == NodeType.START_USER_NODE) {
validateNode(node.childNode, errorNodes)
}
@ -106,7 +106,7 @@ const validateNode = (node: SimpleFlowNode | undefined, errorNodes: SimpleFlowNo
validateNode(node.childNode, errorNodes)
}
if (type == NodeType.EXCLUSIVE_NODE) {
if (type == NodeType.CONDITION_BRANCH_NODE) {
conditionNodes?.forEach((item) => {
validateNode(item, errorNodes)
})
@ -138,17 +138,16 @@ const zoomIn = () => {
onMounted(async () => {
const result = await getBpmSimpleModel(props.modelId)
console.log('the result is :', result)
if (result) {
processNodeTree.value = result
} else {
//
processNodeTree.value = {
name: '开始',
type: NodeType.START_EVENT_NODE,
id: 'StartEvent',
name: '发起人',
type: NodeType.START_USER_NODE,
id: NodeId.START_USER_NODE_ID,
childNode: {
id: 'EndEvent',
id: NodeId.END_EVENT_NODE_ID,
name: '结束',
type: NodeType.END_EVENT_NODE
}

View File

@ -5,50 +5,55 @@ import { DictDataVO } from '@/api/system/dict/types'
*
*/
export enum NodeType {
/**
*
*/
START_EVENT_NODE = 0,
/**
*
*/
END_EVENT_NODE = -2,
END_EVENT_NODE = 1,
/**
*
*/
START_USER_NODE = 10,
/**
*
*/
USER_TASK_NODE = 1,
USER_TASK_NODE = 11,
/**
*
*/
COPY_TASK_NODE = 2,
COPY_TASK_NODE = 12,
/**
*
*/
CONDITION_NODE = 3,
CONDITION_NODE = 50,
/**
*
* ()
*/
EXCLUSIVE_NODE = 4,
CONDITION_BRANCH_NODE = 51,
/**
*
* ()
*/
PARALLEL_NODE_FORK = 5,
PARALLEL_BRANCH_NODE = 52,
/**
*
* ()
*/
PARALLEL_NODE_JOIN = 6,
/**
*
*/
INCLUSIVE_NODE_FORK = 7,
/**
*
*/
INCLUSIVE_NODE_JOIN = 8
INCLUSIVE_BRANCH_NODE = 53
}
export enum NodeId {
/**
* Id
*/
START_USER_NODE_ID = 'StartUserNode',
/**
* Id
*/
END_EVENT_NODE_ID = 'EndEvent'
}
/**
*
*/
@ -298,7 +303,23 @@ export enum ConditionConfigType {
*/
RULE = 2
}
/**
*
*/
export enum FieldPermissionType {
/**
*
*/
READ = '1',
/**
*
*/
WRITE = '2',
/**
*
*/
NONE = '3'
}
/**
*
*/
@ -335,6 +356,7 @@ export enum OperationButtonType {
*/
RETURN = 6
}
/**
*
*/
@ -369,11 +391,13 @@ 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[] = [
@ -483,6 +507,16 @@ export const DEFAULT_BUTTON_SETTING: ButtonSetting[] = [
{ 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 },

View File

@ -12,7 +12,8 @@ import {
RejectHandlerType,
NODE_DEFAULT_NAME,
AssignStartUserHandlerType,
AssignEmptyHandlerType
AssignEmptyHandlerType,
FieldPermissionType
} from './consts'
export function useWatchNode(props: { flowNode: SimpleFlowNode }): Ref<SimpleFlowNode> {
const node = ref<SimpleFlowNode>(props.flowNode)
@ -26,9 +27,9 @@ export function useWatchNode(props: { flowNode: SimpleFlowNode }): Ref<SimpleFlo
}
/**
* @description
* @description
*/
export function useFormFieldsPermission() {
export function useFormFieldsPermission(defaultPermission: FieldPermissionType) {
// 字段权限配置. 需要有 field, title, permissioin 属性
const fieldsPermissionConfig = ref<Array<Record<string, string>>>([])
@ -66,7 +67,7 @@ export function useFormFieldsPermission() {
fieldsPermission.push({
field,
title,
permission: '1' // 只读
permission: defaultPermission
})
// TODO 子表单 需要处理子表单字段
// if (type === 'group' && rule.props?.rule && Array.isArray(rule.props.rule)) {
@ -139,7 +140,6 @@ export function useNodeForm(nodeType: NodeType) {
const configForm = ref<UserTaskFormType | CopyTaskFormType>()
if (nodeType === NodeType.USER_TASK_NODE) {
configForm.value = {
//candidateParamArray: [],
candidateStrategy: CandidateStrategy.USER,
approveMethod: ApproveMethodType.RRANDOM_SELECT_ONE_APPROVE,
approveRatio: 100,
@ -154,7 +154,6 @@ export function useNodeForm(nodeType: NodeType) {
}
} else {
configForm.value = {
//candidateParamArray: [],
candidateStrategy: CandidateStrategy.USER
}
}

View File

@ -157,13 +157,29 @@
<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="1" size="large" label="1"><span></span></el-radio>
<el-radio
:value="FieldPermissionType.READ"
size="large"
:label="FieldPermissionType.WRITE"
><span></span
></el-radio>
</div>
<div class="item-radio-wrap">
<el-radio value="2" size="large" label="2" disabled><span></span></el-radio>
<el-radio
:value="FieldPermissionType.WRITE"
size="large"
:label="FieldPermissionType.WRITE"
disabled
><span></span
></el-radio>
</div>
<div class="item-radio-wrap">
<el-radio value="3" size="large" label="3"><span></span></el-radio>
<el-radio
:value="FieldPermissionType.NONE"
size="large"
:label="FieldPermissionType.NONE"
><span></span
></el-radio>
</div>
</el-radio-group>
</div>
@ -180,7 +196,13 @@
</el-drawer>
</template>
<script setup lang="ts">
import { SimpleFlowNode, CandidateStrategy, NodeType, CANDIDATE_STRATEGY } from '../consts'
import {
SimpleFlowNode,
CandidateStrategy,
NodeType,
CANDIDATE_STRATEGY,
FieldPermissionType
} from '../consts'
import {
useWatchNode,
useDrawer,
@ -208,7 +230,9 @@ const { nodeName, showInput, clickIcon, blurEvent } = useNodeName(NodeType.COPY_
// Tab
const activeTabName = ref('user')
//
const { formType, fieldsPermissionConfig, getNodeConfigFormFields } = useFormFieldsPermission()
const { formType, fieldsPermissionConfig, getNodeConfigFormFields } = useFormFieldsPermission(
FieldPermissionType.READ
)
//
const formRef = ref() // Ref
//

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

@ -25,7 +25,7 @@
</div>
</template>
<div class="flex flex-items-center mb-3">
<span class="font-size-4 mr-3">审批类型 :</span>
<span class="font-size-16px mr-3">审批类型 :</span>
<el-radio-group v-model="approveType">
<el-radio
v-for="(item, index) in APPROVE_TYPE"
@ -390,13 +390,28 @@
<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="1" size="large" label="1"><span></span></el-radio>
<el-radio
:value="FieldPermissionType.READ"
size="large"
:label="FieldPermissionType.READ"
><span></span
></el-radio>
</div>
<div class="item-radio-wrap">
<el-radio value="2" size="large" label="2"><span></span></el-radio>
<el-radio
:value="FieldPermissionType.WRITE"
size="large"
:label="FieldPermissionType.WRITE"
><span></span
></el-radio>
</div>
<div class="item-radio-wrap">
<el-radio value="3" size="large" label="3"><span></span></el-radio>
<el-radio
:value="FieldPermissionType.NONE"
size="large"
:label="FieldPermissionType.NONE"
><span></span
></el-radio>
</div>
</el-radio-group>
</div>
@ -435,7 +450,8 @@ import {
ASSIGN_START_USER_HANDLER_TYPES,
TimeoutHandlerType,
ASSIGN_EMPTY_HANDLER_TYPES,
AssignEmptyHandlerType
AssignEmptyHandlerType,
FieldPermissionType
} from '../consts'
import {
@ -479,7 +495,9 @@ const { nodeName, showInput, clickIcon, blurEvent } = useNodeName(NodeType.USER_
// Tab
const activeTabName = ref('user')
//
const { formType, fieldsPermissionConfig, getNodeConfigFormFields } = useFormFieldsPermission()
const { formType, fieldsPermissionConfig, getNodeConfigFormFields } = useFormFieldsPermission(
FieldPermissionType.READ
)
//
const { buttonsSetting, btnDisplayNameEdit, changeBtnDisplayName, btnDisplayNameBlurEvent } =
useButtonsSetting()

View File

@ -1,43 +0,0 @@
<template>
<div class="start-node-wrapper">
<div class="start-node-container">
<div class="start-node-box">
<Icon icon="ep:avatar" />
<span class="node-fixed-name" :title="currentNode.name">{{currentNode.name}}</span>
<Icon icon="ep:arrow-right-bold" />
</div>
<!-- 传递子节点给添加节点组件会在子节点后面添加节点 -->
<NodeHandler v-if="currentNode" v-model:child-node="currentNode.childNode" />
</div>
</div>
</template>
<script setup lang="ts">
import NodeHandler from '../NodeHandler.vue'
import { SimpleFlowNode } from '../consts';
defineOptions({
name: 'StartEventNode'
})
const props = defineProps({
flowNode : {
type: Object as () => SimpleFlowNode,
default: () => null
}
});
//
const emits = defineEmits<{
'update:modelValue': [node: SimpleFlowNode | undefined]
}>()
const currentNode = ref<SimpleFlowNode>(props.flowNode);
watch(
() => props.flowNode,
(newValue) => {
currentNode.value = newValue
}
)
</script>
<style lang="scss" scoped>
</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 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="clickEvent">
{{ 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 } from '../node'
import { SimpleFlowNode, NODE_DEFAULT_NAME, 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 = ref(false)
//
const blurEvent = () => {
showInput.value = false
currentNode.value.name =
currentNode.value.name || (NODE_DEFAULT_NAME.get(NodeType.START_USER_NODE) as string)
}
//
const clickEvent = () => {
showInput.value = true
}
const nodeSetting = ref()
//
const openNodeConfig = () => {
//
nodeSetting.value.showStartUserNodeConfig(currentNode.value)
nodeSetting.value.openDrawer()
}
</script>
<style lang="scss" scoped></style>