仿钉钉流程设计器- 审批节点,抄送节点增加表单字段权限配置

This commit is contained in:
jason 2024-05-08 20:04:25 +08:00
parent 22af2dc2d9
commit 672db903f3
11 changed files with 237 additions and 64 deletions

View File

@ -52,11 +52,13 @@ const props = defineProps({
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: 'Activity_'+ generateUUID(),
id: id,
name: NODE_DEFAULT_NAME.get(NodeType.USER_TASK_NODE) as string,
showText: '',
type: NodeType.USER_TASK_NODE,
@ -64,11 +66,12 @@ const addNode = (type: number) => {
attributes: {
approveMethod: 1,
candidateStrategy: CandidateStrategy.USER,
candidateParam: undefined
candidateParam: undefined,
fieldsPermission: undefined,
},
childNode: props.childNode
}
emits('update:childNode', data)
emits('update:childNode', data);
}
if (type === NodeType.COPY_TASK_NODE) {
const data: SimpleFlowNode = {
@ -79,7 +82,8 @@ const addNode = (type: number) => {
//
attributes: {
candidateStrategy: CandidateStrategy.USER,
candidateParam: undefined
candidateParam: undefined,
fieldsPermission: undefined
},
childNode: props.childNode
}

View File

@ -1,6 +1,9 @@
<template>
<!-- 开始节点 -->
<StartEventNode v-if="currentNode && currentNode.type === NodeType.START_EVENT_NODE" :flow-node ="currentNode" />
<StartEventNode
v-if="currentNode && currentNode.type === NodeType.START_EVENT_NODE"
:flow-node ="currentNode"
@update:model-value="handleModelValueUpdate" />
<!-- 审批节点 -->
<UserTaskNode
v-if="currentNode && currentNode.type === NodeType.USER_TASK_NODE"
@ -39,11 +42,11 @@ const emits = defineEmits(['update:flowNode'])
const currentNode = ref<SimpleFlowNode>(props.flowNode);
// .
// .
watch(() => props.flowNode, (newValue) => {
console.log("Flow Nodes changed", newValue);
currentNode.value = newValue;
});
}
);
const handleModelValueUpdate = (updateValue) => {
console.log('Process Node Tree handleModelValueUpdate', updateValue)

View File

@ -65,7 +65,6 @@ const saveSimpleFlowModel = async () => {
}
errorNodes = []
validateNode(processNodeTree.value, errorNodes)
console.log('errorNodes is ', errorNodes)
if (errorNodes.length > 0) {
errorDialogVisible.value = true
return;
@ -76,7 +75,6 @@ const saveSimpleFlowModel = async () => {
}
const result = await saveBpmSimpleModel(data)
console.log('save the result is ', result)
if (result) {
message.success('修改成功')
close()
@ -148,7 +146,7 @@ const zoomIn = () => {
onMounted(async () => {
const result = await getBpmSimpleModel(props.modelId)
if (result) {
console.log('get the result is ', result)
console.log('the result is ', result)
processNodeTree.value = result
}
})

View File

@ -143,6 +143,39 @@
</el-form>
</div>
</el-tab-pane>
<el-tab-pane label="表单字段权限" 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 currentNode.attributes.fieldsPermission"
: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="1" size="large" label="1" disabled><span></span></el-radio>
</div>
<div class="item-radio-wrap">
<el-radio value="2" size="large" label="2"><span></span></el-radio>
</div>
<div class="item-radio-wrap">
<el-radio value="3" size="large" label="3"><span></span></el-radio>
</div>
</el-radio-group>
</div>
</div>
</el-tab-pane>
</el-tabs>
<template #footer>
<el-divider />
@ -155,6 +188,7 @@
</template>
<script setup lang='ts'>
import { SimpleFlowNode, CandidateStrategy,NodeType, NODE_DEFAULT_NAME } from '../consts'
import { getDefaultFieldsPermission } from '../utils'
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import * as RoleApi from '@/api/system/role'
import * as DeptApi from '@/api/system/dept'
@ -171,13 +205,9 @@ const props = defineProps({
required: true
}
})
//
const emits = defineEmits<{
'update:modelValue': [node: SimpleFlowNode]
}>()
//
const settingVisible = ref(false)
//
//
const currentNode = ref<SimpleFlowNode>(props.flowNode)
const roleOptions = inject<Ref<RoleApi.RoleVO[]>>('roleList') //
const postOptions = inject<Ref<PostApi.PostVO[]>>('postList') //
@ -185,6 +215,8 @@ 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 formType = inject('formType') //
const formFields = inject<Ref<string[]>>('formFields')
//
const copyUserStrategies = computed( ()=> {
@ -203,14 +235,24 @@ const closeDrawer = () => {
const saveConfig = () => {
currentNode.value.attributes.candidateParam = candidateParamArray.value?.join(',')
currentNode.value.showText = getShowText()
emits('update:modelValue', currentNode.value)
settingVisible.value = false
}
const open = () => {
settingVisible.value = true
}
defineExpose({ open }) // open
//
const setCurrentNode = (node:SimpleFlowNode) => {
currentNode.value = node;
currentNode.value.attributes.fieldsPermission = node.attributes.fieldsPermission || getDefaultFieldsPermission(formFields?.value)
const strCandidateParam = node.attributes?.candidateParam
if(strCandidateParam) {
candidateParamArray.value = strCandidateParam.split(',').map(item=> +item)
}
}
defineExpose({ open, setCurrentNode }) //
const changeCandidateStrategy = () => {
candidateParamArray.value = []
@ -227,7 +269,6 @@ const getShowText = () : string => {
candidateNames.push(item.nickname)
}
})
console.log("candidateNames is ", candidateNames)
showText = `指定成员:${candidateNames.join(',')}`
}
}
@ -304,11 +345,6 @@ const blurEvent = () => {
currentNode.value.name = currentNode.value.name || NODE_DEFAULT_NAME.get(NodeType.COPY_TASK_NODE) as string
}
onMounted(async () => {
console.log('candidateParam', currentNode.value.attributes?.candidateParam)
candidateParamArray.value = currentNode.value.attributes?.candidateParam?.split(',').map(item=> +item)
console.log('candidateParamArray.value', candidateParamArray.value)
})
</script>
<style lang="scss" scoped>

View File

@ -160,6 +160,39 @@
</el-form>
</div>
</el-tab-pane>
<el-tab-pane label="表单字段权限" 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 currentNode.attributes.fieldsPermission"
: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="1" size="large" label="1"><span></span></el-radio>
</div>
<div class="item-radio-wrap">
<el-radio value="2" size="large" label="2"><span></span></el-radio>
</div>
<div class="item-radio-wrap">
<el-radio value="3" size="large" label="3"><span></span></el-radio>
</div>
</el-radio-group>
</div>
</div>
</el-tab-pane>
</el-tabs>
<template #footer>
<el-divider />
@ -174,6 +207,7 @@
<script setup lang="ts">
import { SimpleFlowNode, APPROVE_METHODS, CandidateStrategy, NodeType, NODE_DEFAULT_NAME } from '../consts'
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { getDefaultFieldsPermission } from '../utils'
import { defaultProps } from '@/utils/tree'
import * as RoleApi from '@/api/system/role'
import * as DeptApi from '@/api/system/dept'
@ -190,11 +224,9 @@ const props = defineProps({
required: true
}
})
const emits = defineEmits<{
'update:modelValue': [node: SimpleFlowNode]
}>()
const notAllowedMultiApprovers = ref(false)
const currentNode = ref<SimpleFlowNode>(props.flowNode)
const currentNode = ref<SimpleFlowNode>(props.flowNode);
const settingVisible = ref(false)
const roleOptions = inject<Ref<RoleApi.RoleVO[]>>('roleList') //
const postOptions = inject<Ref<PostApi.PostVO[]>>('postList') //
@ -202,6 +234,8 @@ 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 formType = inject('formType') //
const formFields = inject<Ref<string[]>>('formFields')
const candidateParamArray = ref<any[]>([])
const closeDrawer = () => {
@ -210,8 +244,6 @@ const closeDrawer = () => {
const saveConfig = () => {
currentNode.value.attributes.candidateParam = candidateParamArray.value?.join(',')
currentNode.value.showText = getShowText()
console.log('currentNode value is ', currentNode.value)
emits('update:modelValue', currentNode.value)
settingVisible.value = false
}
const getShowText = () : string => {
@ -225,7 +257,6 @@ const getShowText = () : string => {
candidateNames.push(item.nickname)
}
})
console.log("candidateNames is ", candidateNames)
showText = `指定成员:${candidateNames.join(',')}`
}
}
@ -300,7 +331,23 @@ const getShowText = () : string => {
const open = () => {
settingVisible.value = true
}
defineExpose({ open }) // open
//
const setCurrentNode = (node:SimpleFlowNode) => {
currentNode.value = node;
currentNode.value.attributes.fieldsPermission = node.attributes.fieldsPermission || getDefaultFieldsPermission(formFields?.value)
const strCandidateParam = node.attributes?.candidateParam
if(strCandidateParam) {
candidateParamArray.value = strCandidateParam.split(',').map(item=> +item)
}
if (currentNode.value.attributes?.candidateStrategy === CandidateStrategy.USER && candidateParamArray.value?.length <= 1) {
notAllowedMultiApprovers.value = true
} else {
notAllowedMultiApprovers.value = false
}
}
defineExpose({ open, setCurrentNode }) //
const changeCandidateStrategy = () => {
candidateParamArray.value = []
currentNode.value.attributes.approveMethod = 1
@ -330,15 +377,6 @@ const blurEvent = () => {
showInput.value = false
currentNode.value.name = currentNode.value.name || NODE_DEFAULT_NAME.get(NodeType.USER_TASK_NODE) as string
}
onMounted(async () => {
candidateParamArray.value = currentNode.value.attributes?.candidateParam?.split(',').map(item=> +item)
console.log('candidateParamArray.value', candidateParamArray.value)
if (currentNode.value.attributes?.candidateStrategy === CandidateStrategy.USER && candidateParamArray.value?.length <= 1) {
notAllowedMultiApprovers.value = true
} else {
notAllowedMultiApprovers.value = false
}
})
</script>
<style lang="scss" scoped>

View File

@ -8,7 +8,7 @@
v-if="showInput"
type="text"
class="editable-title-input"
@blur="blurEvent($event)"
@blur="blurEvent()"
v-mountedFocus
v-model="currentNode.name"
:placeholder="currentNode.name"
@ -34,11 +34,11 @@
<!-- 传递子节点给添加节点组件会在子节点前面添加节点 -->
<NodeHandler v-if="currentNode" v-model:child-node="currentNode.childNode" />
</div>
<!-- 其实只需要一个全局抄送节点配置就行, 不需要多个点击配置的时候传值. TODO 后面优化 -->
<CopyTaskNodeConfig
v-if="currentNode"
ref="nodeSetting"
:flow-node="currentNode"
@update:model-value="handleModelValueUpdate"
/>
</div>
</template>
@ -72,7 +72,7 @@ watch(
//
const showInput = ref(false)
//
const blurEvent = (event) => {
const blurEvent = () => {
showInput.value = false
currentNode.value.name = currentNode.value.name || NODE_DEFAULT_NAME.get(NodeType.USER_TASK_NODE) as string
}
@ -83,6 +83,7 @@ const clickEvent = () => {
const nodeSetting = ref()
//
const openNodeConfig = () => {
nodeSetting.value.setCurrentNode(currentNode.value);
nodeSetting.value.open()
}
@ -107,11 +108,6 @@ const copyNode = () => {
currentNode.value = newCopyNode
emits('update:modelValue', currentNode.value)
}
//
const handleModelValueUpdate = (updateValue) => {
emits('update:modelValue', updateValue)
}
</script>
<style lang='scss' scoped>

View File

@ -108,7 +108,6 @@ const clickEvent = (index: number) => {
}
const conditionNodeConfig = (nodeId: string) => {
console.log("proxy.$refs", proxy.$refs);
const conditionNode = proxy.$refs[nodeId][0];
conditionNode.open()
}

View File

@ -7,7 +7,7 @@
<Icon icon="ep:arrow-right-bold" />
</div>
<!-- 传递子节点给添加节点组件会在子节点后面添加节点 -->
<node-handler v-if="currentNode" v-model:child-node="currentNode.childNode" />
<NodeHandler v-if="currentNode" v-model:child-node="currentNode.childNode" />
</div>
</div>
</template>
@ -23,12 +23,20 @@ const props = defineProps({
default: () => null
}
});
//
const emits = defineEmits<{
'update:modelValue': [node: SimpleFlowNode | undefined]
}>()
const currentNode = ref<SimpleFlowNode>(props.flowNode);
watch(() => props.flowNode, (newValue) => {
console.log('start node value changed=================>', newValue);
currentNode.value = newValue;
});
watch(
() => props.flowNode,
(newValue) => {
currentNode.value = newValue
}
)
</script>
<style lang="scss" scoped>

View File

@ -8,7 +8,7 @@
v-if="showInput"
type="text"
class="editable-title-input"
@blur="blurEvent($event)"
@blur="blurEvent()"
v-mountedFocus
v-model="currentNode.name"
:placeholder="currentNode.name"
@ -34,11 +34,11 @@
<NodeHandler v-if="currentNode" v-model:child-node="currentNode.childNode" />
</div>
</div>
<!-- 其实只需要一个全局审批节点配置就行, 不需要多个点击配置的时候传值. TODO 后面优化 -->
<UserTaskNodeConfig
v-if="currentNode"
ref="nodeSetting"
:flow-node="currentNode"
@update:model-value="handleModelValueUpdate"
/>
</template>
<script setup lang="ts">
@ -63,8 +63,11 @@ const currentNode = ref<SimpleFlowNode>(props.flowNode)
const nodeSetting = ref()
//
const openNodeConfig = () => {
//
nodeSetting.value.setCurrentNode(currentNode.value);
nodeSetting.value.open()
}
//
watch(
() => props.flowNode,
(newValue) => {
@ -74,7 +77,7 @@ watch(
//
const showInput = ref(false)
//
const blurEvent = (event) => {
const blurEvent = () => {
showInput.value = false
currentNode.value.name = currentNode.value.name || NODE_DEFAULT_NAME.get(NodeType.USER_TASK_NODE) as string
}
@ -82,16 +85,12 @@ const blurEvent = (event) => {
const clickEvent = () => {
showInput.value = true
}
const handleModelValueUpdate = (updateValue) => {
emits('update:modelValue', updateValue)
console.log('user task node handleModelValueUpdate', updateValue)
}
const deleteNode = () => {
console.log('the child node is ', currentNode.value.childNode)
emits('update:modelValue', currentNode.value.childNode)
}
const copyNode = () => {
// const oldChildNode = currentNode.value.childNode
const newCopyNode: SimpleFlowNode = {
id: generateUUID(),
name: currentNode.value.name,

View File

@ -5,3 +5,19 @@ export const getDefaultConditionNodeName = (index:number, defaultFlow: boolean)
}
return '条件' + (index+1)
}
// 获得默认的表单字段权限.
export const getDefaultFieldsPermission = (formFields: string[] | undefined) =>{
const defaultFieldsPermission : any[] = [];
if(formFields){
formFields.forEach((fieldStr: string) => {
const { field, title } = JSON.parse(fieldStr)
defaultFieldsPermission.push({
field,
title,
permission: '2' // 只读
})
})
}
return defaultFieldsPermission;
}

View File

@ -517,6 +517,7 @@
display: flex;
height: 24px;
line-height: 24px;
font-size: 16px;
cursor: pointer;
align-items: center;
}
@ -545,7 +546,82 @@
}
}
// 表单字段权限
.field-setting-pane {
display: flex;
flex-direction: column;
padding: 16px 0;
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 {