仿钉钉流程设计器- 优化节点配置

This commit is contained in:
jason 2024-05-07 23:44:01 +08:00
parent e8193a0aa0
commit 22af2dc2d9
8 changed files with 157 additions and 65 deletions

View File

@ -7,9 +7,19 @@
:before-close="saveConfig" :before-close="saveConfig"
> >
<template #header> <template #header>
<div class="w-full flex flex-col"> <div class="config-header">
<div class="mb-2 text-size-2xl">{{ currentNode.name }}</div> <input
<el-divider /> 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> </div>
</template> </template>
<div> <div>
@ -65,6 +75,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { SimpleFlowNode, CONDITION_CONFIG_TYPES } from '../consts' import { SimpleFlowNode, CONDITION_CONFIG_TYPES } from '../consts'
import { getDefaultConditionNodeName } from '../utils';
defineOptions({ defineOptions({
name: 'ConditionNode' name: 'ConditionNode'
}) })
@ -72,6 +83,10 @@ const props = defineProps({
conditionNode: { conditionNode: {
type: Object as () => SimpleFlowNode, type: Object as () => SimpleFlowNode,
required: true required: true
},
nodeIndex : {
type: Number,
required: true
} }
}) })
const settingVisible = ref(false) const settingVisible = ref(false)
@ -82,10 +97,21 @@ const open = () => {
watch(() => props.conditionNode, (newValue) => { watch(() => props.conditionNode, (newValue) => {
currentNode.value = 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.attributes?.defaultFlow)
}
const currentNode = ref<SimpleFlowNode>(props.conditionNode) const currentNode = ref<SimpleFlowNode>(props.conditionNode)
// TODO nodeInfo
defineExpose({ open, nodeInfo: currentNode }) // open defineExpose({ open }) // open
// //
const closeDrawer = () => { const closeDrawer = () => {
@ -117,10 +143,5 @@ const changeConditionType = () => {
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
::v-deep(.el-divider--horizontal) {
display: block;
height: 1px;
margin: 0;
border-top: 1px var(--el-border-color) var(--el-border-style);
}
</style> </style>

View File

@ -7,9 +7,19 @@
:before-close="saveConfig" :before-close="saveConfig"
> >
<template #header> <template #header>
<div class="w-full flex flex-col"> <div class="config-header">
<div class="mb-2 text-size-2xl">{{ currentNode.name }}</div> <input
<el-divider /> 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> </div>
</template> </template>
<el-tabs type="border-card"> <el-tabs type="border-card">
@ -144,7 +154,7 @@
</el-drawer> </el-drawer>
</template> </template>
<script setup lang='ts'> <script setup lang='ts'>
import { SimpleFlowNode, CandidateStrategy } from '../consts' import { SimpleFlowNode, CandidateStrategy,NodeType, NODE_DEFAULT_NAME } from '../consts'
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import * as RoleApi from '@/api/system/role' import * as RoleApi from '@/api/system/role'
import * as DeptApi from '@/api/system/dept' import * as DeptApi from '@/api/system/dept'
@ -282,6 +292,17 @@ const getShowText = () : string => {
} }
return showText return showText
} }
//
const showInput = ref(false)
const clickIcon = () => {
showInput.value = true;
}
//
const blurEvent = () => {
showInput.value = false
currentNode.value.name = currentNode.value.name || NODE_DEFAULT_NAME.get(NodeType.COPY_TASK_NODE) as string
}
onMounted(async () => { onMounted(async () => {
console.log('candidateParam', currentNode.value.attributes?.candidateParam) console.log('candidateParam', currentNode.value.attributes?.candidateParam)
@ -291,10 +312,10 @@ onMounted(async () => {
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
::v-deep(.el-divider--horizontal) { // ::v-deep(.el-divider--horizontal) {
display: block; // display: block;
height: 1px; // height: 1px;
margin: 0; // margin: 0;
border-top: 1px var(--el-border-color) var(--el-border-style); // border-top: 1px var(--el-border-color) var(--el-border-style);
} // }
</style> </style>

View File

@ -8,9 +8,19 @@
class="justify-start" class="justify-start"
> >
<template #header> <template #header>
<div class="w-full flex flex-col"> <div class="config-header">
<div class="mb-2 text-size-2xl">{{ currentNode.name }}</div> <input
<el-divider /> 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> </div>
</template> </template>
<el-tabs type="border-card"> <el-tabs type="border-card">
@ -162,7 +172,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { SimpleFlowNode, APPROVE_METHODS,CandidateStrategy } from '../consts' import { SimpleFlowNode, APPROVE_METHODS, CandidateStrategy, NodeType, NODE_DEFAULT_NAME } from '../consts'
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { defaultProps } from '@/utils/tree' import { defaultProps } from '@/utils/tree'
import * as RoleApi from '@/api/system/role' import * as RoleApi from '@/api/system/role'
@ -309,18 +319,18 @@ const changedCandidateUsers = () => {
notAllowedMultiApprovers.value = false notAllowedMultiApprovers.value = false
} }
} }
//
const showInput = ref(false)
const clickIcon = () => {
showInput.value = true;
}
//
const blurEvent = () => {
showInput.value = false
currentNode.value.name = currentNode.value.name || NODE_DEFAULT_NAME.get(NodeType.USER_TASK_NODE) as string
}
onMounted(async () => { onMounted(async () => {
//
// roleOptions.value = await RoleApi.getSimpleRoleList()
// postOptions.value = await PostApi.getSimplePostList()
// //
// userOptions.value = await UserApi.getSimpleUserList()
// //
// deptOptions = await DeptApi.getSimpleDeptList()
// deptTreeOptions.value = handleTree(deptOptions, 'id')
// //
// userGroupOptions.value = await UserGroupApi.getUserGroupSimpleList()
console.log('candidateParam', currentNode.value.attributes?.candidateParam)
candidateParamArray.value = currentNode.value.attributes?.candidateParam?.split(',').map(item=> +item) candidateParamArray.value = currentNode.value.attributes?.candidateParam?.split(',').map(item=> +item)
console.log('candidateParamArray.value', candidateParamArray.value) console.log('candidateParamArray.value', candidateParamArray.value)
if (currentNode.value.attributes?.candidateStrategy === CandidateStrategy.USER && candidateParamArray.value?.length <= 1) { if (currentNode.value.attributes?.candidateStrategy === CandidateStrategy.USER && candidateParamArray.value?.length <= 1) {
@ -332,10 +342,4 @@ onMounted(async () => {
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
::v-deep(.el-divider--horizontal) {
display: block;
height: 1px;
margin: 0;
border-top: 1px var(--el-border-color) var(--el-border-style);
}
</style> </style>

View File

@ -27,8 +27,7 @@
<Icon icon="ep:arrow-right-bold" /> <Icon icon="ep:arrow-right-bold" />
</div> </div>
<div class="node-toolbar"> <div class="node-toolbar">
<!-- <div class="toolbar-icon"><Icon icon="ep:document-copy" @click="copyNode" /></div> --> <div class="toolbar-icon"><Icon color="#0089ff" icon="ep:circle-close-filled" :size="18" @click="deleteNode" /></div>
<div class="toolbar-icon"><Icon icon="ep:delete" :size="16" @click="deleteNode" /></div>
</div> </div>
</div> </div>

View File

@ -15,10 +15,13 @@
<div class="node-container"> <div class="node-container">
<div class="node-box" :class="{ 'node-config-error': !item.showText }"> <div class="node-box" :class="{ 'node-config-error': !item.showText }">
<div class="branch-node-title-container"> <div class="branch-node-title-container">
<div class="branch-title" v-if="showInputs[index]"> <div v-if="showInputs[index]">
<input <input
type="text" class="input-max-width editable-title-input" @blur="blurEvent(index)" type="text"
v-mountedFocus v-model="item.name" /> class="input-max-width editable-title-input"
@blur="blurEvent(index)"
v-mountedFocus
v-model="item.name" />
</div> </div>
<div v-else class="branch-title" @click="clickEvent(index)"> {{ item.name }} </div> <div v-else class="branch-title" @click="clickEvent(index)"> {{ item.name }} </div>
<div class="branch-priority"> 优先级{{ index + 1 }} </div> <div class="branch-priority"> 优先级{{ index + 1 }} </div>
@ -33,7 +36,7 @@ type="text" class="input-max-width editable-title-input" @blur="blurEvent(index)
</div> </div>
<div class="node-toolbar" v-if="index + 1 !== currentNode.conditionNodes?.length"> <div class="node-toolbar" v-if="index + 1 !== currentNode.conditionNodes?.length">
<div class="toolbar-icon"> <div class="toolbar-icon">
<Icon icon="ep:delete" :size="16" @click="deleteCondition(index)" /> <Icon color="#0089ff" icon="ep:circle-close-filled" :size="18" @click="deleteCondition(index)" />
</div> </div>
</div> </div>
<div <div
@ -52,7 +55,7 @@ type="text" class="input-max-width editable-title-input" @blur="blurEvent(index)
<NodeHandler v-model:child-node="item.childNode" /> <NodeHandler v-model:child-node="item.childNode" />
</div> </div>
</div> </div>
<ConditionNodeConfig :condition-node="item" :ref="item.id" /> <ConditionNodeConfig :node-index="index" :condition-node="item" :ref="item.id" />
<!-- 递归显示子节点 --> <!-- 递归显示子节点 -->
<ProcessNodeTree v-if="item && item.childNode" v-model:flow-node="item.childNode" /> <ProcessNodeTree v-if="item && item.childNode" v-model:flow-node="item.childNode" />
</div> </div>
@ -65,6 +68,7 @@ type="text" class="input-max-width editable-title-input" @blur="blurEvent(index)
import NodeHandler from '../NodeHandler.vue' import NodeHandler from '../NodeHandler.vue'
import ProcessNodeTree from '../ProcessNodeTree.vue' import ProcessNodeTree from '../ProcessNodeTree.vue'
import { SimpleFlowNode, NodeType, NODE_DEFAULT_TEXT } from '../consts' import { SimpleFlowNode, NodeType, NODE_DEFAULT_TEXT } from '../consts'
import { getDefaultConditionNodeName } from '../utils'
import { generateUUID } from '@/utils' import { generateUUID } from '@/utils'
import ConditionNodeConfig from '../nodes-config/ConditionNodeConfig.vue' import ConditionNodeConfig from '../nodes-config/ConditionNodeConfig.vue'
const { proxy } = getCurrentInstance() as any const { proxy } = getCurrentInstance() as any
@ -88,29 +92,24 @@ const currentNode = ref<SimpleFlowNode>(props.flowNode)
watch(() => props.flowNode, (newValue) => { watch(() => props.flowNode, (newValue) => {
currentNode.value = newValue; currentNode.value = newValue;
}); });
// TODO
// watch(() => conditionNodes, (newValue) => {
// console.log('new conditionNodes is ', newValue);
// },{ deep: true });
const showInputs = ref<boolean[]>([]) const showInputs = ref<boolean[]>([])
// //
const blurEvent = (index: number) => { const blurEvent = (index: number) => {
showInputs.value[index] = false showInputs.value[index] = false
const conditionNode = currentNode.value.conditionNodes?.at(index) as SimpleFlowNode; const conditionNode = currentNode.value.conditionNodes?.at(index) as SimpleFlowNode;
conditionNode.name = conditionNode.name || '条件' + index conditionNode.name = conditionNode.name || getDefaultConditionNodeName(index, conditionNode.attributes?.defaultFlow)
} }
// //
const clickEvent = (index: number) => { const clickEvent = (index: number) => {
showInputs.value[index] = true showInputs.value[index] = true
} }
const conditionNodeConfig = (nodeId: string) => { const conditionNodeConfig = (nodeId: string) => {
console.log('nodeId', nodeId);
console.log("proxy.$refs", proxy.$refs); console.log("proxy.$refs", proxy.$refs);
// TODO
const conditionNode = proxy.$refs[nodeId][0]; const conditionNode = proxy.$refs[nodeId][0];
console.log("node inf is ", conditionNode.nodeInfo);
conditionNode.open() conditionNode.open()
} }

View File

@ -27,8 +27,7 @@
<Icon icon="ep:arrow-right-bold" /> <Icon icon="ep:arrow-right-bold" />
</div> </div>
<div class="node-toolbar"> <div class="node-toolbar">
<!-- <div class="toolbar-icon"><Icon icon="ep:document-copy" @click="copyNode" /></div> --> <div class="toolbar-icon"><Icon color="#0089ff" icon="ep:circle-close-filled" :size="18" @click="deleteNode" /></div>
<div class="toolbar-icon"><Icon icon="ep:delete" :size="18" @click="deleteNode" /></div>
</div> </div>
</div> </div>
<!-- 传递子节点给添加节点组件会在子节点前面添加节点 --> <!-- 传递子节点给添加节点组件会在子节点前面添加节点 -->

View File

@ -0,0 +1,7 @@
// 获取条件节点默认的名称
export const getDefaultConditionNodeName = (index:number, defaultFlow: boolean) : string => {
if ( defaultFlow ){
return "其它情况"
}
return '条件' + (index+1)
}

View File

@ -91,7 +91,7 @@
transition: all 0.1s cubic-bezier(0.645, 0.045, 0.355, 1); transition: all 0.1s cubic-bezier(0.645, 0.045, 0.355, 1);
&:hover { &:hover {
// border-color: #0089ff; border-color: #0089ff;
.node-toolbar { .node-toolbar {
opacity: 1; opacity: 1;
} }
@ -130,6 +130,9 @@
text-overflow: ellipsis; text-overflow: ellipsis;
color: #1f1f1f; color: #1f1f1f;
line-height: 18px; line-height: 18px;
&:hover {
border-bottom: 1px dashed #f60;
}
} }
} }
@ -153,6 +156,9 @@
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
color: #f60; color: #f60;
&:hover {
border-bottom: 1px dashed #000;
}
} }
.branch-priority { .branch-priority {
@ -211,14 +217,13 @@
.node-toolbar { .node-toolbar {
opacity: 0; opacity: 0;
position: absolute; position: absolute;
top: -25px; top: -20px;
right: 0px; right: 0px;
display: flex; display: flex;
.toolbar-icon { .toolbar-icon {
text-align: center; text-align: center;
vertical-align: middle; vertical-align: middle;
color: #000;
} }
} }
@ -501,10 +506,47 @@
} }
} }
} }
} }
// 配置节点头部
.config-header {
display: flex;
flex-direction: column;
.node-name {
display: flex;
height: 24px;
line-height: 24px;
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, .2)
}
}
}
// 节点连线气泡卡片样式 // 节点连线气泡卡片样式
.handler-item-wrapper { .handler-item-wrapper {
display: flex; display: flex;