Browse Source

Merge pull request #837 from colinin/add-component-flow-design

feat(components): add flow design components.
pull/840/head
yx lin 3 years ago
committed by GitHub
parent
commit
d9e766127d
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      apps/vue/src/components/FlowDesign/index.ts
  2. 45
      apps/vue/src/components/FlowDesign/src/components/Ellipsis.vue
  3. 115
      apps/vue/src/components/FlowDesign/src/components/InsertButton.vue
  4. 45
      apps/vue/src/components/FlowDesign/src/components/OrgItems.vue
  5. 141
      apps/vue/src/components/FlowDesign/src/components/ProcessDesign.vue
  6. 0
      apps/vue/src/components/FlowDesign/src/components/config/NodeConfig.vue
  7. 45
      apps/vue/src/components/FlowDesign/src/components/config/RootNodeConfig.vue
  8. 187
      apps/vue/src/components/FlowDesign/src/components/config/TriggerNodeConfig.vue
  9. 161
      apps/vue/src/components/FlowDesign/src/components/nodes/ApprovalNode.vue
  10. 76
      apps/vue/src/components/FlowDesign/src/components/nodes/CcNode.vue
  11. 184
      apps/vue/src/components/FlowDesign/src/components/nodes/ConcurrentNode.vue
  12. 330
      apps/vue/src/components/FlowDesign/src/components/nodes/ConditionNode.vue
  13. 94
      apps/vue/src/components/FlowDesign/src/components/nodes/DelayNode.vue
  14. 19
      apps/vue/src/components/FlowDesign/src/components/nodes/EmptyNode.vue
  15. 191
      apps/vue/src/components/FlowDesign/src/components/nodes/Node.vue
  16. 52
      apps/vue/src/components/FlowDesign/src/components/nodes/RootNode.vue
  17. 79
      apps/vue/src/components/FlowDesign/src/components/nodes/TriggerNode.vue
  18. 112
      apps/vue/src/components/FlowDesign/src/components/process/DefaultNodeProps.ts
  19. 541
      apps/vue/src/components/FlowDesign/src/components/process/ProcessTree.vue

3
apps/vue/src/components/FlowDesign/index.ts

@ -0,0 +1,3 @@
import ProcessDesign from './src/components/ProcessDesign.vue';
export { ProcessDesign };

45
apps/vue/src/components/FlowDesign/src/components/Ellipsis.vue

@ -0,0 +1,45 @@
<template>
<div
:class="{'line': row === 1, 'lines': row > 1}"
:title="hoverTip ? content: ''"
:style="{'--row':row}"
>
<slot name="pre"></slot>
{{ content }}
</div>
</template>
<script setup lang="ts">
defineProps({
row: {
type: Number,
default: 1
},
hoverTip:{
type: Boolean,
default: false
},
content:{
type: String,
default: ''
}
});
</script>
<style lang="less" scoped>
.line{
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.lines{
display: -webkit-box;
word-break: break-all;
overflow: hidden;
text-overflow: ellipsis;
-webkit-line-clamp: var(--row);
-webkit-box-orient: vertical;
}
</style>

115
apps/vue/src/components/FlowDesign/src/components/InsertButton.vue

@ -0,0 +1,115 @@
<template>
<Popover placement="bottomLeft" title="添加流程节点" width="350" trigger="click">
<template #content>
<div class="node-select">
<div @click="addApprovalNode">
<CheckOutlined class="icon" style="color:rgb(255, 148, 62);" />
<span>审批人</span>
</div>
<div @click="addCcNode">
<SendOutlined class="icon" style="color:rgb(50, 150, 250);" />
<span>抄送人</span>
</div>
<div @click="addConditionsNode">
<ShareAltOutlined class="icon" style="color:rgb(21, 188, 131);" />
<span>条件分支</span>
</div>
<div @click="addConcurrentsNode">
<ThunderboltOutlined class="icon" style="color:#718dff;" />
<span>并行分支</span>
</div>
<div @click="addDelayNode">
<ClockCircleOutlined class="icon" style="color:#f25643;" />
<span>延迟等待</span>
</div>
<div @click="addTriggerNode">
<SettingOutlined class="icon" style="color:#15BC83;" />
<span>触发器</span>
</div>
</div>
</template>
<Button type="primary" size="small" shape="circle">
<template #icon>
<PlusOutlined />
</template>
</Button>
</Popover>
</template>
<script lang="ts" setup>
//import { computed } from 'vue';
import { Button, Popover } from 'ant-design-vue';
import {
CheckOutlined,
SendOutlined,
ShareAltOutlined,
ThunderboltOutlined,
ClockCircleOutlined,
SettingOutlined,
PlusOutlined,
} from '@ant-design/icons-vue';
//import { useFlowStoreWithOut } from '/@/store/modules/flow';
const emits = defineEmits(['insertNode']);
//const flowStore = useFlowStoreWithOut();
// const selectedNode = computed(() => {
// return flowStore.selectedNode;
// });
function addApprovalNode() {
emits('insertNode', "APPROVAL");
}
function addCcNode() {
emits('insertNode', "CC");
}
function addDelayNode() {
emits('insertNode', "DELAY");
}
function addConditionsNode() {
emits('insertNode', "CONDITIONS");
}
function addConcurrentsNode() {
emits('insertNode', "CONCURRENTS");
}
function addTriggerNode() {
emits('insertNode', "TRIGGER");
}
</script>
<style lang="less" scoped>
.node-select{
div{
display: inline-block;
margin: 5px 5px;
cursor: pointer;
padding: 10px 15px;
border: 1px solid #F8F9F9;
background-color: #F8F9F9;
border-radius: 10px;
width: 130px;
position: relative;
span{
position: absolute;
left: 65px;
top: 18px;
}
&:hover{
background-color: #fff;
box-shadow: 0 0 8px 2px #d6d6d6;
}
icon{
font-size: 25px;
padding: 5px;
border: 1px solid #dedfdf;
border-radius: 14px;
}
}
}
</style>

45
apps/vue/src/components/FlowDesign/src/components/OrgItems.vue

@ -0,0 +1,45 @@
<template>
<div style="margin-top: 10px">
<template v-for="(org, index) in _value" >
<Tag class="org-item" closable @close="removeOrgItem(index)">
<template #icon>
<InfoOutlined v-if="org.type !== 'dept'" />
</template>
{{ org.name }}
</Tag>
</template>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import { Tag } from 'ant-design-vue';
import { InfoOutlined } from '@ant-design/icons-vue';
const emits = defineEmits(['input']);
const props = defineProps({
value: {
type: Array as PropType<any[]>,
default: () => {
return []
}
},
});
const _value = computed({
get: () => props.value,
set: (val) => {
emits('input', val);
},
});
function removeOrgItem(index) {
_value.value.splice(index, 1)
}
</script>
<style scoped>
.org-item{
margin: 5px;
}
</style>

141
apps/vue/src/components/FlowDesign/src/components/ProcessDesign.vue

@ -0,0 +1,141 @@
<template>
<Layout class="main">
<div class="scale">
<Button class="button" size="small" @click="state.scale += 10" :disabled="state.scale >= 150" shape="circle">
<template #icon>
<PlusOutlined />
</template>
</Button>
<span>{{ state.scale }}%</span>
<Button class="button" size="small" @click="state.scale -= 10" :disabled="state.scale <= 40" shape="circle">
<template #icon>
<MinusOutlined />
</template>
</Button>
<!-- <Button @click="validate">校验流程</Button> -->
</div>
<div class="design" :style="'transform: scale('+ state.scale / 100 +');'">
<ProcessTree ref="processTreeRef" @selectedNode="nodeSelected"/>
</div>
<Drawer
:title="selectedNode.name"
:visible="state.showConfig"
destroy-on-close
@close="() => state.showConfig = false"
>
<div slot="title">
<Input
v-model="selectedNode.name"
size="small"
v-show="state.showInput"
style="width: 300px"
@blur="state.showInput = false"
>
</Input>
</div>
<div class="node-config-content">
</div>
</Drawer>
</Layout>
</template>
<script setup lang="ts">
import { computed, reactive, ref, unref, onMounted } from 'vue';
import { Button, Drawer, Input, Layout } from 'ant-design-vue';
import { PlusOutlined, MinusOutlined } from '@ant-design/icons-vue';
import { useFlowStoreWithOut } from '/@/store/modules/flow';
import ProcessTree from './process/ProcessTree.vue';
const flowStore = useFlowStoreWithOut();
const state = reactive({
scale: 100,
selected: {} as any,
showInput: false,
showConfig: false,
});
const processTreeRef = ref<any>();
const selectedNode = computed(() => flowStore.selectedNode);
onMounted(loadInitFrom);
function loadInitFrom() {
flowStore.loadForm({
formId: null,
formName: "未命名表单",
logo: {
icon: "home",
background: "#1e90ff"
},
settings: {
commiter: [],
admin: [],
sign: false,
notify: {
types: ["APP"],
title: "消息通知标题"
}
},
groupId: undefined,
formItems:[],
process: {
id: "root",
parentId: null,
type: "ROOT",
name: "发起人",
desc: "任何人",
props: {
assignedUser: [],
formPerms: []
},
children: {}
},
remark: "备注说明"
});
}
function validate(){
const processTree = unref(processTreeRef);
return processTree?.validateProcess()
}
function nodeSelected(node){
console.log('配置节点', node)
state.showConfig = true
}
</script>
<style lang="less" scoped>
.main {
background: white;
height: 100%;
}
.design {
margin-top: 100px;
display: flex;
transform-origin: 50% 0px 0px;
}
.scale {
z-index: 999;
position: fixed;
top: 80px;
right: 40px;
.button {
margin: 0 10px;
}
span {
font-size: 15px;
color: #7a7a7a;
}
}
.node-config-content{
padding: 0 20px 20px;
}
:deep(.el-drawer__body){
overflow-y: auto;
}
</style>

0
apps/vue/src/components/FlowDesign/src/components/config/NodeConfig.vue

45
apps/vue/src/components/FlowDesign/src/components/config/RootNodeConfig.vue

@ -0,0 +1,45 @@
<template>
<div>
<p class="desc">选择能发起该审批的人员/部门不选则默认开放给所有人</p>
<Button size="small" @click="selectOrg" type="primary" round>请选择</Button>
<OrgItems v-model="select"/>
</div>
</template>
<script setup lang="ts">
import { computed, reactive } from 'vue';
import { Button } from 'ant-design-vue';
import OrgItems from "../OrgItems.vue";
const props = defineProps({
config:{
type: Object,
default: ()=>{
return {}
}
},
});
const select = computed(() => {
return props.config.assignedUser;
});
const state = reactive({
showOrgSelect: false,
});
function selectOrg() {
console.log(selectOrg);
}
function selected(sel: any) {
select.value.length = 0;
select.value.forEach(val => sel.push(val));
}
function removeOrgItem(index: number){
select.value.splice(index, 1);
}
</script>
<style scoped>
</style>

187
apps/vue/src/components/FlowDesign/src/components/config/TriggerNodeConfig.vue

@ -0,0 +1,187 @@
<template>
<div>
<Form label-position="top" label-width="90px">
<FormItem label="选择触发的动作" prop="text" class="user-type">
<RadioGroup v-model="config.type">
<Radio label="WEBHOOK">发送网络请求</Radio>
<Radio label="EMAIL">发送邮件</Radio>
</RadioGroup>
</FormItem>
<div v-if="config.type === 'WEBHOOK'">
<FormItem label="请求地址" prop="text">
<Input placeholder="请输入URL地址" size="middle" v-model="config.http.url" >
<Select v-model="config.http.method" style="width: 85px;" slot="prepend" placeholder="URL">
<Option label="GET" value="GET"></Option>
<Option label="POST" value="POST"></Option>
<Option label="PUT" value="PUT"></Option>
<Option label="DELETE" value="DELETE"></Option>
</Select>
</Input>
</FormItem>
<FormItem label="Header请求头" prop="text">
<div slot="label">
<span style="margin-right: 10px">Header请求头</span>
<Button type="text" @click="addItem(config.http.headers)"> + 添加</Button>
</div>
<div v-for="(header, index) in config.http.headers" :key="header.name">
- <Input placeholder="参数名" size="small" style="width: 100px;" v-model="header.name" />
<RadioGroup size="small" style="margin: 0 5px;" v-model="header.isField">
<RadioButton :label="true">表单</RadioButton>
<RadioButton :label="false">固定</RadioButton>
</RadioGroup>
<Select v-if="header.isField" style="width: 180px;" v-model="header.value" size="small" placeholder="请选择表单字段">
<Option v-for="form in forms" :key="form.id" :label="form.title" :value="form.title"></Option>
</Select>
<Input v-else placeholder="请设置字段值" size="small" v-model="header.value" style="width: 180px;"/>
<DeleteOutlined @click="delItem(config.http.headers, index)" style="margin-left: 5px; color: #c75450; cursor: pointer"/>
</div>
</FormItem>
<FormItem label="Header请求参数" prop="text">
<div slot="label">
<span style="margin-right: 10px">Header请求参数 </span>
<Button style="margin-right: 20px" type="text" @click="addItem(config.http.params)"> + 添加</Button>
<span>参数类型 - </span>
<RadioGroup size="small" style="margin: 0 5px;" v-model="config.http.contentType">
<RadioButton label="JSON">json</RadioButton>
<RadioButton label="FORM">form</RadioButton>
</RadioGroup>
</div>
<div v-for="(param, index) in config.http.params" :key="param.name">
- <Input placeholder="参数名" size="small" style="width: 100px;" v-model="param.name" />
<RadioGroup size="small" style="margin: 0 5px;" v-model="param.isField">
<RadioButton :label="true">表单</RadioButton>
<RadioButton :label="false">固定</RadioButton>
</RadioGroup>
<Select v-if="param.isField" style="width: 180px;" v-model="param.value" size="small" placeholder="请选择表单字段">
<Option v-for="form in forms" :key="form.id" :label="form.title" :value="form.title"></Option>
</Select>
<Input v-else placeholder="请设置字段值" size="small" v-model="param.value" style="width: 180px;"/>
<DeleteOutlined @click="delItem(config.http.params, index)" style="margin-left: 5px; color: #c75450; cursor: pointer"/>
</div>
<div>
</div>
</FormItem>
<FormItem label="请求结果处理" prop="text">
<div slot="label">
<span>请求结果处理</span>
<span style="margin-left: 20px">自定义脚本: </span>
<Switch v-model="config.http.handlerByScript"></Switch>
</div>
<span class="item-desc" v-if="config.http.handlerByScript">
👉 返回值为 ture 则流程通过 false 则流程将被驳回
<div>支持函数
<span style="color: dodgerblue">setFormByName(
<span style="color: #939494">'表单字段名', '表单字段值'</span>
)</span>
可改表单数据
</div>
</span>
<span class="item-desc" v-else>👉 无论请求结果如何均通过</span>
<div v-if="config.http.handlerByScript">
<div>
<span>请求成功😀</span>
<Textarea v-model="config.http.success" :rows="3"></Textarea>
</div>
<div>
<span>请求失败😥</span>
<Textarea v-model="config.http.fail" :rows="3"></Textarea>
</div>
</div>
</FormItem>
</div>
<div v-else-if="config.type === 'EMAIL'">
<FormItem label="邮件主题" prop="text">
<Input placeholder="请输入邮件主题" size="middle" v-model="config.email.subject" />
</FormItem>
<FormItem label="收件方" prop="text">
<Select size="small" style="width: 100%;" v-model="config.email.to" filterable multiple allow-create default-first-option placeholder="请输入收件人">
<Option v-for="item in config.email.to" :key="item" :label="item" :value="item"></Option>
</Select>
</FormItem>
<FormItem label="邮件正文" prop="text">
<Textarea v-model="config.email.content" :rows="4" placeholder="邮件内容,支持变量提取表单数据 ${表单字段名} "></Textarea>
</FormItem>
</div>
</Form>
</div>
</template>
<script setup lang="ts">
import { computed, reactive, toRefs } from 'vue';
import { Button, Form, Input, Select, Switch, Textarea, Radio } from 'ant-design-vue';
import { DeleteOutlined } from '@ant-design/icons-vue';
import { useMessage } from '/@/hooks/web/useMessage';
import { useFlowStoreWithOut } from '/@/store/modules/flow';
const FormItem = Form.Item;
const Option = Select.Option;
const RadioGroup = Radio.Group;
const RadioButton = Radio.Button;
const flowStore = useFlowStoreWithOut();
defineProps({
config:{
type: Object,
default: ()=>{
return {}
}
},
});
const forms = computed(() => {
return flowStore.design.formItems || [];
});
const state = reactive({
cmOptions:{
tabSize: 4, // tab
indentUnit: 4,
styleActiveLine: true, //
lineNumbers: true, //
styleSelectedText: true,
line: true,
foldGutter: true, //
gutters: ['CodeMirror-linenumbers', "lock", "warn"],
highlightSelectionMatches: { showToken: /w/, annotateScrollbar: true }, //
mode:'javascript',
// hint.js options
hintOptions: {
//
completeSingle: false
},
// sublimeemacsvim
keyMap: 'sublime',
matchBrackets: true,
showCursorWhenSelecting: false,
// scrollbarStyle:null,
// readOnly:true, //
theme: 'material', // material
extraKeys: { 'Ctrl': 'autocomplete' }, // keyMap
lastLineBefore:0
}
});
const { createMessage } = useMessage();
function addItem(items: any){
if (items.length > 0 && (items[items.length - 1].name.trim() === ''
|| items[items.length - 1].value.trim() === '')) {
createMessage.warning("请完善之前项后在添加");
return;
}
items.push({name: '', value: '', isField: true});
}
function delItem(items: any, index: number){
items.splice(index, 1);
}
defineExpose({
...toRefs(state),
});
</script>
<style lang="less" scoped>
.item-desc{
color: #939494;
}
</style>

161
apps/vue/src/components/FlowDesign/src/components/nodes/ApprovalNode.vue

@ -0,0 +1,161 @@
<template>
<Node
:title="config.name"
:show-error="state.showError"
:content="content"
:error-info="state.errorInfo"
@selected="$emit('selected')"
@delNode="$emit('delNode')"
@insertNode="type => $emit('insertNode', type)"
placeholder="请设置审批人"
header-bgc="#ff943e"
>
<template #headerIcon>
<UserOutlined />
</template>
</Node>
</template>
<script setup lang="ts">
import { computed, reactive } from 'vue';
import { UserOutlined } from '@ant-design/icons-vue';
import { useFlowStoreWithOut } from '/@/store/modules/flow';
import Node from './Node.vue';
defineEmits(['delNode', 'insertNode', 'selected']);
const props = defineProps({
config:{
type: Object,
default: () => {
return {}
}
},
});
const content = computed(() => {
console.log('props', props);
const config = props.config.props;
switch (config.assignedType) {
case "ASSIGN_USER":
if (config.assignedUser.length > 0) {
let texts: string[] = [];
config.assignedUser.forEach(org => texts.push(org.name));
return String(texts).replaceAll(',', '、');
} else {
return '请指定审批人';
}
case "SELF":
return '发起人自己';
case "SELF_SELECT":
return config.selfSelect.multiple ? '发起人自选多人':'发起人自选一人';
case "LEADER_TOP":
return '多级主管依次审批';
case "LEADER":
return config.leader.level > 1 ? '发起人的第 ' + config.leader.level + ' 级主管' : '发起人的直接主管';
case "FORM_USER":
if (!config.formUser || config.formUser === '') {
return '表单内联系人(未选择)';
} else {
let text = getFormItemById(config.formUser);
if (text && text.title) {
return `表单(${text.title})内的人员`;
}else {
return '该表单已被移除😥';
}
}
case "ROLE":
if (config.role.length > 0) {
return String(config.role).replaceAll(',', '、');
} else {
return '指定角色(未设置)';
}
default: return '未知设置项😥';
}
});
const state = reactive({
showError: false,
errorInfo: '',
validate: {} as any,
});
const flowStore = useFlowStoreWithOut();
function getFormItemById(id: any) {
return flowStore.design.formItems.find(item => item.id === id);
}
function validate(err: string[]) {
try {
return state.showError = !state.validate[`validate_${props.config.props.assignedType}`](err);
} catch (e) {
return true;
}
}
function validate_ASSIGN_USER(err: string[]) {
if (props.config.props.assignedUser.length > 0) {
return true;
} else {
state.errorInfo = '请指定审批人员';
err.push(`${props.config.name} 未指定审批人员`);
return false;
}
}
function validate_SELF_SELECT(err: string[]) {
console.log(err);
return true;
}
function validate_LEADER_TOP(err: string[]) {
console.log(err);
return true;
}
function validate_LEADER(err: string[]) {
console.log(err);
return true;
}
function validate_ROLE(err: string[]) {
if (props.config.props.role.length <= 0) {
state.errorInfo = '请指定负责审批的系统角色';
err.push(`${props.config.name} 未指定审批角色`);
return false;
}
return true;
}
function validate_SELF(err: string[]) {
console.log(err);
return true;
}
function validate_FORM_USER(err: string[]) {
if (props.config.props.formUser === '') {
state.errorInfo = '请指定表单中的人员组件';
err.push(`${props.config.name} 审批人为表单中人员,但未指定`);
return false;
}
return true;
}
function validate_REFUSE(err: string[]){
console.log(err);
return true;
}
defineExpose({
validate,
validate_ASSIGN_USER,
validate_SELF_SELECT,
validate_LEADER_TOP,
validate_LEADER,
validate_ROLE,
validate_SELF,
validate_FORM_USER,
validate_REFUSE,
});
</script>
<style scoped>
</style>

76
apps/vue/src/components/FlowDesign/src/components/nodes/CcNode.vue

@ -0,0 +1,76 @@
<template>
<Node
:title="config.name"
:show-error="state.showError"
:content="content"
:error-info="state.errorInfo"
@selected="$emit('selected')"
@delNode="$emit('delNode')"
@insertNode="type => $emit('insertNode', type)"
placeholder="请设置抄送人"
header-bgc="#3296fa"
>
<template #headerIcon>
<SettingOutlined />
</template>
</Node>
</template>
<script lang="ts">
export default {
name: 'CC',
};
</script>
<script setup lang="ts">
import { computed, reactive } from 'vue';
import { SettingOutlined } from '@ant-design/icons-vue';
import Node from './Node.vue';
defineEmits(['delNode', 'insertNode', 'selected']);
const props = defineProps({
config:{
type: Object,
default: () => {
return {}
}
},
});
const content = computed(() => {
if (props.config.props.shouldAdd){
return '由发起人指定';
}else if (props.config.props.assignedUser.length > 0) {
let texts: string[] = [];
props.config.props.assignedUser.forEach(org => texts.push(org.name));
return String(texts).replaceAll(',', '、');
} else {
return undefined;
}
});
const state = reactive({
showError: false,
errorInfo: '',
});
//
function validate(err: string[]){
state.showError = false;
if (props.config.props.shouldAdd) {
state.showError = false;
} else if(props.config.props.assignedUser.length === 0) {
state.showError = true;
state.errorInfo = '请选择需要抄送的人员';
}
if (state.showError) {
err.push(`抄送节点 ${props.config.name} 未设置抄送人`);
}
return !state.showError;
}
defineExpose({
validate,
});
</script>
<style scoped>
</style>

184
apps/vue/src/components/FlowDesign/src/components/nodes/ConcurrentNode.vue

@ -0,0 +1,184 @@
<template>
<div class="node">
<div class="node-body" @click="$emit('selected')">
<div class="node-body-left" @click.stop="$emit('leftMove')" v-if="level > 1">
<LeftOutlined />
</div>
<div class="node-body-main">
<div class="node-body-main-header">
<span class="title">
<SettingOutlined />
<Ellipsis class="name" hover-tip :content="config.name ? config.name:('并行任务' + level)"/>
</span>
<span class="option">
<Tooltip effect="dark" content="复制分支" placement="top">
<CopyOutlined @click="$emit('copy')" />
</Tooltip>
<CloseOutlined @click.stop="$emit('delNode')" />
</span>
</div>
<div class="node-body-main-content">
<span>并行任务同时进行</span>
</div>
</div>
<div class="node-body-right" @click.stop="$emit('rightMove')" v-if="level < size">
<RightOutlined />
</div>
</div>
<div class="node-footer">
<div class="btn">
<InsertButton @insertNode="type => $emit('insertNode', type)"></InsertButton>
</div>
</div>
</div>
</template>
<script lang="ts">
export default {
name: 'Concurrent',
};
</script>
<script setup lang="ts">
import { Tooltip } from 'ant-design-vue';
import { LeftOutlined, RightOutlined, CloseOutlined, CopyOutlined, SettingOutlined } from '@ant-design/icons-vue';
import Ellipsis from '../Ellipsis.vue';
import InsertButton from '../InsertButton.vue'
defineEmits(['delNode', 'insertNode', 'leftMove', 'rightMove', 'selected']);
defineProps({
config:{
type: Object,
default: () => {
return {}
}
},
level:{
type: Number,
default: 1
},
//
size:{
type: Number,
default: 0
},
});
</script>
<style lang="less" scoped>
.node{
padding: 30px 55px 0;
width: 330px;
.node-body{
overflow: hidden;
cursor: pointer;
min-height: 80px;
max-height: 120px;
position: relative;
border-radius: 5px;
background-color: white;
box-shadow: 0px 0px 5px 0px #d8d8d8;
&:hover{
.node-body-left, .node-body-right{
i{
display: block !important;
}
}
.node-body-main {
.option{
display: inline-block !important;
}
}
box-shadow: 0px 0px 3px 0px @primary-color;
}
.node-body-left, .node-body-right{
display: flex;
align-items: center;
position: absolute;
height: 100%;
i{
display: none;
}
&:hover{
background-color: #ececec;
}
}
.node-body-left{
left: 0;
}
.node-body-right{
right: 0;
}
.node-body-main {
position: absolute;
width: 188px;
left: 17px;
display: inline-block;
.node-body-main-header{
padding: 10px 0px 5px;
font-size: xx-small;
position: relative;
.title{
color: #718dff;
.name{
display: inline-block;
height: 14px;
width: 130px;
margin-left: 2px;
}
}
.option{
position: absolute;
right: 0;
display: none;
font-size: medium;
i{
color: #888888;
padding: 0 3px;
}
}
}
.node-body-main-content {
padding: 6px;
color: #656363;
font-size: 14px;
i {
position: absolute;
top: 55%;
right: 10px;
font-size: medium;
}
}
}
}
.node-footer{
position: relative;
.btn{
width: 100%;
display: flex;
height: 70px;
padding: 20px 0 32px;
justify-content: center;
}
:deep(.a-button) {
height: 32px;
}
&::before{
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: -1;
margin: auto;
width: 2px;
height: 100%;
background-color: #CACACA;
}
}
}
</style>

330
apps/vue/src/components/FlowDesign/src/components/nodes/ConditionNode.vue

@ -0,0 +1,330 @@
<template>
<div :class="{'node': true, 'node-error-state': state.showError}">
<div :class="{'node-body': true, 'error': state.showError}">
<div class="node-body-left" @click="$emit('leftMove')" v-if="level > 1">
<LeftOutlined />
</div>
<div class="node-body-main" @click="$emit('selected')">
<div class="node-body-main-header">
<Ellipsis class="title" hover-tip :content="config.name ? config.name : ('条件' + level)"/>
<span class="level">优先级{{ level }}</span>
<span class="option">
<Tooltip effect="dark" content="复制条件" placement="top">
<CopyOutlined @click.stop="$emit('copy')" />
</Tooltip>
<CloseOutlined class="icon-close" @click.stop="$emit('delNode')" />
</span>
</div>
<div class="node-body-main-content">
<span class="placeholder" v-if="(content || '').trim() === ''">{{ state.placeholder }}</span>
<Ellipsis hoverTip :row="4" :content="content" v-else/>
</div>
</div>
<div class="node-body-right" @click="$emit('rightMove')" v-if="level < size">
<RightOutlined />
</div>
<div class="node-error" v-if="state.showError">
<Tooltip effect="dark" :content="state.errorInfo" placement="top">
<WarningOutlined />
</Tooltip>
</div>
</div>
<div class="node-footer">
<div class="btn">
<InsertButton @insertNode="type => $emit('insertNode', type)"></InsertButton>
</div>
</div>
</div>
</template>
<script lang="ts">
export default {
name: 'Condition',
};
</script>
<script setup lang="ts">
import { computed, reactive } from 'vue';
import { Tooltip } from 'ant-design-vue';
import { RightOutlined, CloseOutlined, CopyOutlined, LeftOutlined, WarningOutlined } from '@ant-design/icons-vue';
import Ellipsis from '../Ellipsis.vue';
import InsertButton from '../InsertButton.vue'
defineEmits(['delNode', 'insertNode', 'leftMove', 'rightMove', 'selected']);
const props = defineProps({
config: {
type: Object,
default: () => {
return {}
}
},
//
level: {
type: Number,
default: 1
},
//
size: {
type: Number,
default: 0
},
});
const content = computed(() => {
console.log('props', props);
const groups = props.config.props.groups;
let confitions: string[] = [];
groups.forEach(group => {
let subConditions: string[] = [];
group.conditions.forEach(subCondition => {
let subConditionStr = '';
switch (subCondition.valueType) {
case 'dept':
case 'user':
subConditionStr = `${subCondition.title}属于[${String(subCondition.value.map(u => u.name)).replaceAll(',', '. ')}]之一`;
break;
case 'number':
case 'string':
subConditionStr = getOrdinaryConditionContent(subCondition);
break;
}
subConditions.push(subConditionStr);
})
//
let subConditionsStr = String(subConditions)
.replaceAll(',', subConditions.length > 1 ?
(group.groupType === 'AND' ? ') 且 (' : ') 或 (') :
(group.groupType === 'AND' ? ' 且 ' : ' 或 '))
confitions.push(subConditions.length > 1 ? `(${subConditionsStr})` : subConditionsStr);
})
//
return String(confitions).replaceAll(',', (props.config.props.groupsType === 'AND' ? ' 且 ' : ' 或 '));
});
const state = reactive({
groupNames: [] as string[],
placeholder: '请设置条件',
errorInfo: '',
showError: false
});
function getDefault(val, df) {
return val && val !== '' ? val : df;
}
function getOrdinaryConditionContent(subCondition: any) {
switch (subCondition.compare) {
case 'IN':
return `${subCondition.title}为[${String(subCondition.value).replaceAll(',', '、')}]中之一`;
case 'B':
return `${subCondition.value[0]} < ${subCondition.title} < ${subCondition.value[1]}`;
case 'AB':
return `${subCondition.value[0]}${subCondition.title} < ${subCondition.value[1]}`;
case 'BA':
return `${subCondition.value[0]} < ${subCondition.title}${subCondition.value[1]}`;
case 'ABA':
return `${subCondition.value[0]}${subCondition.title}${subCondition.value[1]}`;
case '<=':
return `${subCondition.title}${getDefault(subCondition.value[0], ' ?')}`;
case '>=':
return `${subCondition.title}${getDefault(subCondition.value[0], ' ?')}`;
default:
return `${subCondition.title}${subCondition.compare}${getDefault(subCondition.value[0], ' ?')}`;
}
}
function validate(err: string[]) {
const thatProps = props.config.props;
if (thatProps.groups.length <= 0) {
state.showError = true;
state.errorInfo = '请设置分支条件';
err.push(`${props.config.name} 未设置条件`);
}else {
for (let i = 0; i < thatProps.groups.length; i++) {
if (thatProps.groups[i].cids.length === 0) {
state.showError = true;
state.errorInfo = `请设置条件组${thatProps.groupNames[i]}内的条件`;
err.push(`条件 ${props.config.name} 条件组${thatProps.groupNames[i]}内未设置条件`);
break;
}else {
let conditions = thatProps.groups[i].conditions;
for (let ci = 0; ci < conditions.length; ci++) {
let subc = conditions[ci];
if (subc.value.length === 0) {
state.showError = true;
} else {
state.showError = false;
}
if (state.showError) {
state.errorInfo = `请完善条件组${thatProps.groupNames[i]}内的${subc.title}条件`;
err.push(`条件 ${props.config.name} 条件组${thatProps.groupNames[i]}${subc.title}条件未完善`);
return false;
}
}
}
}
}
return !state.showError;
}
defineExpose({
validate,
});
</script>
<style lang="less" scoped>
.node-error-state {
.node-body {
box-shadow: 0px 0px 5px 0px #F56C6C !important;
}
}
.node {
padding: 30px 55px 0;
width: 330px;
.node-body {
cursor: pointer;
min-height: 80px;
max-height: 120px;
position: relative;
border-radius: 5px;
background-color: white;
box-shadow: 0px 0px 5px 0px #d8d8d8;
&:hover {
.node-body-left, .node-body-right {
i {
display: block !important;
}
}
.node-body-main {
.level {
display: none !important;
}
.option {
display: inline-block !important;
}
}
box-shadow: 0px 0px 3px 0px @primary-color;
}
.node-body-left, .node-body-right {
display: flex;
align-items: center;
position: absolute;
height: 100%;
i {
display: none;
}
&:hover {
background-color: #ececec;
}
}
.node-body-left {
left: 0;
}
.node-body-right {
right: 0;
top: 0;
}
.node-body-main {
position: absolute;
width: 188px;
margin-left: 17px;
display: inline-block;
.node-body-main-header {
padding: 10px 0px 5px;
font-size: xx-small;
position: relative;
.title {
color: #15bca3;
display: inline-block;
height: 14px;
width: 125px;
}
.level {
position: absolute;
right: 15px;
color: #888888;
}
.option {
position: absolute;
right: 0;
display: none;
font-size: medium;
i {
color: #888888;
padding: 0 3px;
}
}
}
.node-body-main-content {
padding: 6px;
color: #656363;
font-size: 14px;
i {
position: absolute;
top: 55%;
right: 10px;
font-size: medium;
}
.placeholder {
color: #8c8c8c;
}
}
}
.node-error {
position: absolute;
right: -40px;
top: 20px;
font-size: 25px;
color: #F56C6C;
}
}
.node-footer {
position: relative;
.btn {
width: 100%;
display: flex;
height: 70px;
padding: 20px 0 32px;
justify-content: center;
}
:deep(.a-button) {
height: 32px;
}
&::before {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: -1;
margin: auto;
width: 2px;
height: 100%;
background-color: #CACACA;
}
}
}
</style>

94
apps/vue/src/components/FlowDesign/src/components/nodes/DelayNode.vue

@ -0,0 +1,94 @@
<template>
<Node
:title="config.name"
:show-error="state.showError"
:content="content"
:error-info="state.errorInfo"
@selected="$emit('selected')"
@delNode="$emit('delNode')"
@insertNode="type => $emit('insertNode', type)"
placeholder="请设置延时时间"
header-bgc="#f25643"
>
<template #headerIcon>
<ClockCircleOutlined />
</template>
</Node>
</template>
<script lang="ts">
export default {
name: 'Delay',
};
</script>
<script setup lang="ts">
import { computed, reactive} from 'vue';
import { ClockCircleOutlined } from '@ant-design/icons-vue';
import Node from './Node.vue';
defineEmits(['delNode', 'insertNode', 'selected']);
const props = defineProps({
config:{
type: Object,
default: () => {
return {}
}
},
});
const state = reactive({
showError: false,
errorInfo: '',
});
const content = computed(() => {
if (props.config.props.type === 'FIXED') {
return `等待 ${props.config.props.time} ${getName(props.config.props.unit)}`
} else if (props.config.props.type === 'AUTO') {
return `至当天 ${props.config.props.dateTime}`
} else {
return undefined;
}
});
function getName(unit: string){
switch (unit){
case 'D': return '天';
case 'H': return '小时';
case 'M': return '分钟';
default: return '未知';
}
}
function validate(err: string[]){
state.showError = false;
try {
if (props.config.props.type === "AUTO") {
if ((props.config.props.dateTime || "") === "") {
state.showError = true;
state.errorInfo = "请选择时间点";
}
} else {
if (props.config.props.time <= 0) {
state.showError = true;
state.errorInfo = "请设置延时时长";
}
}
} catch (e) {
state.showError = true;
state.errorInfo = "配置出现问题";
}
if (state.showError) {
err.push(`${props.config.name} 未设置延时规则`);
}
return !state.showError;
}
defineExpose({
validate,
});
</script>
<style scoped>
</style>

19
apps/vue/src/components/FlowDesign/src/components/nodes/EmptyNode.vue

@ -0,0 +1,19 @@
<template>
<Node :show="false" @insertNode="type => $emit('insertNode', type)"/>
</template>
<script lang="ts">
export default {
name: 'Empty',
};
</script>
<script setup lang="ts">
import Node from './Node.vue';
defineEmits(['insertNode']);
</script>
<style scoped>
</style>

191
apps/vue/src/components/FlowDesign/src/components/nodes/Node.vue

@ -0,0 +1,191 @@
<template>
<div :class="{'node': true, 'root': isRoot || !show, 'node-error-state': showError}">
<div v-if="show" @click="$emit('selected')" :class="{'node-body': true, 'error': showError}" >
<div>
<div class="node-body-header" :style="{'background-color': headerBgc}">
<div style="margin-right: 5px">
<slot name="headerIcon"></slot>
</div>
<Ellipsis class="name" hover-tip :content="title"/>
<CloseOutlined v-if="!isRoot" class="icon-close" style="float:right;" @click="$emit('delNode')" />
</div>
<div class="node-body-content">
<slot name="leftIcon"></slot>
<span class="placeholder" v-if="(content || '').trim() === ''">{{placeholder}}</span>
<Ellipsis :row="3" :content="content" v-else/>
<RightOutlined class="i" />
</div>
<div class="node-error" v-if="showError">
<Tooltip effect="dark" :content="errorInfo" placement="top">
<WarningOutlined />
</Tooltip>
</div>
</div>
</div>
<div class="node-footer">
<div class="btn">
<InsertButton @insertNode="type => $emit('insertNode', type)"></InsertButton>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { Tooltip } from 'ant-design-vue';
import { RightOutlined, CloseOutlined, WarningOutlined } from '@ant-design/icons-vue';
import Ellipsis from '../Ellipsis.vue';
import InsertButton from '../InsertButton.vue';
defineEmits(['delNode', 'insertNode', 'selected']);
defineProps({
//
isRoot: {
type: Boolean,
default: false
},
//
show: {
type: Boolean,
default: true
},
//
content: {
type: String,
default: '',
},
title:{
type: String,
default: '标题'
},
placeholder:{
type: String,
default: '请设置'
},
//
headerBgc:{
type: String,
default: '#576a95'
},
//
showError:{
type: Boolean,
default: false
},
errorInfo:{
type: String,
default: '无信息'
},
});
</script>
<style lang="less" scoped>
.root{
&:before{
display: none !important;
}
}
.node-error-state{
.node-body{
box-shadow: 0px 0px 5px 0px #F56C6C !important;
}
}
.node{
padding: 0 10px;
//width: 220px;
position: relative;
&:before{
content: '';
position: absolute;
top: -12px;
left: 50%;
-webkit-transform: translateX(-50%);
transform: translateX(-50%);
width: 0;
border-style: solid;
border-width: 8px 6px 4px;
border-color: #CACACA transparent transparent;
background: #F5F5F7;
}
.node-body{
cursor: pointer;
max-height: 120px;
position: relative;
border-radius: 5px;
background-color: white;
box-shadow: 0px 0px 5px 0px #d8d8d8;
&:hover{
box-shadow: 0px 0px 3px 0px @primary-color;
.node-body-header {
.icon-close {
display: inline;
font-size: medium;
}
}
}
.node-body-header{
display: flex;
border-top-left-radius: 5px;
border-top-right-radius: 5px;
padding: 5px 15px;
color: white;
font-size: xx-small;
.icon-close{
display: none;
}
.name{
height: 14px;
width: 150px;
display: inline-block
}
}
.node-body-content{
padding: 18px;
color: #656363;
font-size: 14px;
.i{
position: absolute;
top: 55%;
right: 5px;
font-size: medium;
}
.placeholder{
color: #8c8c8c;
}
}
.node-error{
position: absolute;
right: -40px;
top: 20px;
font-size: 25px;
color: #F56C6C;
}
}
.node-footer{
position: relative;
.btn{
width: 100%;
display: flex;
padding: 20px 0 32px;
justify-content: center;
}
:deep(.a-button) {
height: 32px;
}
&::before{
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: -1;
margin: auto;
width: 2px;
height: 100%;
background-color: #CACACA;
}
}
}
</style>

52
apps/vue/src/components/FlowDesign/src/components/nodes/RootNode.vue

@ -0,0 +1,52 @@
<template>
<Node
title="发起人"
:is-root="true"
:content="content"
@selected="$emit('selected')"
@insertNode="type => $emit('insertNode', type)"
placeholder="所有人"
header-bgc="#576a95"
header-icon="el-icon-user-solid"
>
<template #headerIcon>
<HomeOutlined />
</template>
</Node>
</template>
<script lang="ts">
export default {
name: 'root',
};
</script>
<script setup lang="ts">
import { computed } from 'vue';
import { HomeOutlined } from '@ant-design/icons-vue';
import Node from './Node.vue';
defineEmits(['insertNode', 'selected']);
const props = defineProps({
config:{
type: Object,
default: () => {
return {}
}
}
});
const content = computed(() => {
if (props.config.props?.assignedUser?.length > 0) {
let texts: any[] = [];
props.config.props.assignedUser.forEach(org => texts.push(org.name));
return String(texts).replaceAll(',', '、');
} else {
return '所有人';
}
});
</script>
<style scoped>
</style>

79
apps/vue/src/components/FlowDesign/src/components/nodes/TriggerNode.vue

@ -0,0 +1,79 @@
<template>
<Node
:title="config.name"
:show-error="state.showError"
:error-info="state.errorInfo"
@selected="$emit('selected')"
@delNode="$emit('delNode')"
@insertNode="type => $emit('insertNode', type)"
placeholder="请设置触发器"
header-bgc="#47bc82"
>
<template #headerIcon>
<SettingOutlined />
</template>
</Node>
</template>
<script lang="ts">
export default {
name: 'Trigger',
};
</script>
<script setup lang="ts">
import { computed, reactive } from 'vue';
import { SettingOutlined } from '@ant-design/icons-vue';
import { isNullOrWhiteSpace } from '/@/utils/strings';
import Node from './Node.vue';
defineEmits(['delNode', 'insertNode', 'selected']);
const props = defineProps({
config:{
type: Object,
default: () => {
return {}
}
},
});
computed(() => props.config);
const state = reactive({
showError: false,
errorInfo: '',
});
function validate(err: string[]) {
state.showError = false;
if (props.config.props.type === 'WEBHOOK') {
if(!isNullOrWhiteSpace(props.config.props.http.url)) {
state.showError = false;
} else {
state.showError = true;
state.errorInfo = '请设置WEBHOOK的URL地址';
}
} else if(props.config.props.type === 'EMAIL') {
if (isNullOrWhiteSpace(props.config.props.email.subject)
|| props.config.props.email.to.length === 0
|| isNullOrWhiteSpace(props.config.props.email.content)) {
state.showError = true;
state.errorInfo = '请设置邮件发送配置';
} else {
state.showError = false;
}
}
if (state.showError){
err.push(`${props.config.name} 触发动作未设置完善`);
}
return !state.showError;
}
defineExpose({
validate,
});
</script>
<style scoped>
</style>

112
apps/vue/src/components/FlowDesign/src/components/process/DefaultNodeProps.ts

@ -0,0 +1,112 @@
//审批节点默认属性
export const APPROVAL_PROPS = {
assignedType: "ASSIGN_USER",
mode: "AND",
sign: false,
nobody: {
handler: "TO_PASS",
assignedUser:[]
},
timeLimit:{
timeout:{
unit: "H",
value: 0
},
handler:{
type: "REFUSE",
notify:{
once: true,
hour: 1
}
}
},
assignedUser:[],
formPerms:[],
selfSelect: {
multiple: false
},
leaderTop: {
endCondition: "TOP",
endLevel: 1,
},
leader:{
level: 1
},
role:[],
refuse: {
type: 'TO_END', //驳回规则 TO_END TO_NODE TO_BEFORE
target: '' //驳回到指定ID的节点
},
formUser: ''
}
//根节点默认属性
export const ROOT_PROPS = {
assignedUser: [],
formPerms:[]
}
//条件节点默认属性
export const CONDITION_PROPS = {
groupsType:"OR", //条件组逻辑关系 OR、AND
groups:[
{
groupType:"AND", //条件组内条件关系 OR、AND
cids:[], //条件ID集合
conditions:[] //组内子条件
}
],
expression: "" //自定义表达式,灵活构建逻辑关系
}
//抄送节点默认属性
export const CC_PROPS = {
shouldAdd: false,
assignedUser: [],
formPerms:[]
}
//触发器节点默认属性
export const TRIGGER_PROPS = {
type: 'WEBHOOK',
http:{
method: 'GET', //请求方法 支持GET/POST
url: '', //URL地址,可以直接带参数
headers: [ //http header
{
name: '',
isField: true,
value: '' //支持表达式 ${xxx} xxx为表单字段名称
}
],
contentType: 'FORM', //请求参数类型
params:[ //请求参数
{
name: '',
isField: true, //是表单字段还是自定义
value: '' //支持表达式 ${xxx} xxx为表单字段名称
}
],
retry: 1,
handlerByScript: false,
success: 'function handlerOk(res) {\n return true;\n}',
fail: 'function handlerFail(res) {\n return true;\n}'
},
email:{
subject: '',
to: [],
content: ''
}
}
//延时节点默认属性
export const DELAY_PROPS = {
type: "FIXED", //延时类型 FIXED:到达当前节点后延时固定时长 、AUTO:延时到 dateTime设置的时间
time: 0, //延时时间
unit: "M", //时间单位 D天 H小时 M分钟
dateTime: "" //如果当天没有超过设置的此时间点,就延时到这个指定的时间,到了就直接跳过不延时
}
export default {
APPROVAL_PROPS, CC_PROPS, DELAY_PROPS, CONDITION_PROPS, ROOT_PROPS, TRIGGER_PROPS
}

541
apps/vue/src/components/FlowDesign/src/components/process/ProcessTree.vue

@ -0,0 +1,541 @@
<script lang="ts">
import { computed, defineComponent, reactive, ref, unref, h, resolveComponent, getCurrentInstance } from 'vue';
import { Button } from 'ant-design-vue';
import { useMessage } from '/@/hooks/web/useMessage';
import { useFlowStoreWithOut } from '/@/store/modules/flow';
//
import Approval from '../nodes/ApprovalNode.vue';
import Cc from '../nodes/CcNode.vue';
import Concurrent from '../nodes/ConcurrentNode.vue';
import Condition from '../nodes/ConditionNode.vue';
import Trigger from '../nodes/TriggerNode.vue';
import Delay from '../nodes/DelayNode.vue';
import Empty from '../nodes/EmptyNode.vue';
import Root from '../nodes/RootNode.vue';
import Node from '../nodes/Node.vue';
import DefaultProps from "./DefaultNodeProps";
import { cloneDeep } from 'lodash-es';
export default defineComponent({
name: 'ProcessTree',
components: {
Approval,
Cc,
Concurrent,
Condition,
Trigger,
Delay,
Empty,
Root,
Node,
},
emits: ['selectedNode'],
setup: (_, { emit, expose }) => {
const instance = getCurrentInstance();
const { createMessage } = useMessage();
const flowStore = useFlowStoreWithOut();
const nodeMap = computed(() => {
return flowStore.nodeMap;
});
const dom = computed(() => {
return flowStore.design.process;
});
const state = reactive({
valid: true,
});
const rootRef = ref<any>();
function getDomTree(node) {
toMapping(node);
if (isPrimaryNode(node)){
//
let childDoms = getDomTree(node.children)
decodeAppendDom(node, childDoms)
return [h('div', {'class':{'primary-node': true}}, { default: () => childDoms })];
} else if (isBranchNode(node)){
let index = 0;
//
let branchItems = node.branchs.map(branchNode => {
//
toMapping(branchNode);
let childDoms = getDomTree(branchNode.children)
decodeAppendDom(branchNode, childDoms, {level: index + 1, size: node.branchs.length})
//4线线
insertCoverLine(index, childDoms, node.branchs)
//
index++;
return h('div', {'class':{'branch-node-item': true}}, { default: () => childDoms });
})
///
branchItems.unshift(
h(
'div',
{'class':{'add-branch-btn': true}},
{ default: () => [
h(
Button,
{
'class':{'add-branch-btn-el': true},
size: 'small',
shape : 'round',
onClick: () => addBranchNode(node),
innerHTML: `添加${isConditionNode(node)?'条件':'分支'}`,
},
{ default: () => [] })
]}));
let bchDom = [h('div', {'class':{'branch-node': true}}, { default: () => branchItems })]
//
let afterChildDoms = getDomTree(node.children)
return [h('div', {}, { default: () => [bchDom, afterChildDoms] })]
}else if (isEmptyNode(node)){
//
let childDoms = getDomTree(node.children)
decodeAppendDom(node, childDoms)
return [h('div', {'class':{'empty-node': true}}, { default: () => childDoms })];
}else {
//
return [];
}
}
//dom
function decodeAppendDom(node, dom, props = {} as any){
console.log('decodeAppendDom', node);
props.config = node
const component = resolveComponent(node.type.toLowerCase());
dom?.unshift(h(component, {
...props,
ref: node.id,
key: node.id,
///
onInsertNode: (type) => insertNode(type, node),
onDelNode: () => delNode(node),
onSelected: () => selectNode(node),
onCopy: () => copyBranch(node),
onLeftMove: () => branchMove(node, -1),
onRightMove: () => branchMove(node, 1),
}))
}
//idmap
function toMapping(node){
if (node && node.id){
//console.log("node=> " + node.id + " name:" + node.name + " type:" + node.type)
nodeMap.value.set(node.id, node)
}
}
function insertCoverLine(index, doms, branchs){
if (index === 0){
//
doms.unshift(h('div', {'class':{'line-top-left': true}}, { default: () => [] }))
doms.unshift(h('div', {'class':{'line-bot-left': true}}, { default: () => [] }))
} else if (index === branchs.length - 1){
//
doms.unshift(h('div', {'class':{'line-top-right': true}}, { default: () => [] }))
doms.unshift(h('div', {'class':{'line-bot-right': true}}, { default: () => [] }))
}
}
function copyBranch(node) {
let parentNode = nodeMap.value.get(node.parentId);
let branchNode = cloneDeep(node);
branchNode.name = branchNode.name + '-copy';
forEachNode(parentNode, branchNode, (parent, node) => {
let id = getRandomId()
console.log(node, '新id =>'+ id, '老nodeId:' + node.id )
node.id = id
node.parentId = parent.id
})
parentNode.branchs.splice(parentNode.branchs.indexOf(node), 0, branchNode)
instance?.proxy?.$forceUpdate();
}
function branchMove(node, offset){
let parentNode = nodeMap.value.get(node.parentId)
let index = parentNode.branchs.indexOf(node)
let branch = parentNode.branchs[index + offset]
parentNode.branchs[index + offset] = parentNode.branchs[index]
parentNode.branchs[index] = branch
instance?.proxy?.$forceUpdate();
}
//
function isPrimaryNode(node){
return node &&
(node.type === 'ROOT' || node.type === 'APPROVAL'
|| node.type === 'CC' || node.type === 'DELAY'
|| node.type === 'TRIGGER');
}
function isBranchNode(node){
return node && (node.type === 'CONDITIONS' || node.type === 'CONCURRENTS');
}
function isEmptyNode(node){
return node && (node.type === 'EMPTY')
}
//
function isConditionNode(node){
return node.type === 'CONDITIONS';
}
//
function isBranchSubNode(node){
return node && (node.type === 'CONDITION' || node.type === 'CONCURRENT');
}
function isConcurrentNode(node){
return node.type === 'CONCURRENTS'
}
function getRandomId(){
return `node_${new Date().getTime().toString().substring(5)}${Math.round(Math.random()*9000+1000)}`
}
//
function selectNode(node){
flowStore.selectNode(node);
emit('selectedNode', node)
}
//
function insertNode(type, parentNode){
console.log('insertNode', type, parentNode);
const rootEl = unref(rootRef);
rootEl?.click()
//
let afterNode = parentNode.children
//
parentNode.children = {
id: getRandomId(),
parentId: parentNode.id,
props: {},
type: type,
}
switch (type){
case 'APPROVAL': insertApprovalNode(parentNode); break;
case 'CC': insertCcNode(parentNode); break;
case 'DELAY': insertDelayNode(parentNode); break;
case 'TRIGGER': insertTriggerNode(parentNode); break;
case 'CONDITIONS': insertConditionsNode(parentNode); break;
case 'CONCURRENTS': insertConcurrentsNode(parentNode); break;
default: break;
}
//
if (isBranchNode({type: type})){
if (afterNode && afterNode.id){
afterNode.parentId = parentNode.children.children.id
}
parentNode.children.children.children = afterNode;
} else {
if (afterNode && afterNode.id){
afterNode.parentId = parentNode.children.id
}
parentNode.children.children = afterNode;
}
instance?.proxy?.$forceUpdate();
}
function insertApprovalNode(parentNode){
parentNode.children.name = '审批人';
parentNode.children.props = cloneDeep(DefaultProps.APPROVAL_PROPS);
console.log('parentNode', parentNode);
}
function insertCcNode(parentNode){
parentNode.children.name = '抄送人';
parentNode.children.props = cloneDeep(DefaultProps.CC_PROPS);
}
function insertDelayNode(parentNode){
parentNode.children.name = '延时处理';
parentNode.children.props = cloneDeep(DefaultProps.DELAY_PROPS);
}
function insertTriggerNode(parentNode){
parentNode.children.name = '触发器';
parentNode.children.props = cloneDeep(DefaultProps.TRIGGER_PROPS);
}
function insertConditionsNode(parentNode){
parentNode.children.name = '条件分支';
parentNode.children.children = {
id: getRandomId(),
parentId: parentNode.children.id,
type: "EMPTY"
};
parentNode.children.branchs = [
{
id: getRandomId(),
parentId: parentNode.children.id,
type: "CONDITION",
props: cloneDeep(DefaultProps.CONDITION_PROPS),
name: "条件1",
children:{}
},{
id: getRandomId(),
parentId: parentNode.children.id,
type: "CONDITION",
props: cloneDeep(DefaultProps.CONDITION_PROPS),
name: "条件2",
children:{}
}
];
}
function insertConcurrentsNode(parentNode){
parentNode.children.name = '并行分支';
parentNode.children.children = {
id: getRandomId(),
parentId: parentNode.children.id,
type: "EMPTY"
};
parentNode.children.branchs = [
{
id: getRandomId(),
name: "分支1",
parentId: parentNode.children.id,
type: "CONCURRENT",
props: {},
children:{}
},{
id: getRandomId(),
name: "分支2",
parentId: parentNode.children.id,
type: "CONCURRENT",
props: {},
children:{}
}
];
}
function getBranchEndNode(conditionNode){
if (!conditionNode.children || !conditionNode.children.id){
return conditionNode;
}
return getBranchEndNode(conditionNode.children);
}
function addBranchNode(node){
if (node.branchs.length < 8){
node.branchs.push({
id: getRandomId(),
parentId: node.id,
name: (isConditionNode(node) ? '条件':'分支') + (node.branchs.length + 1),
props: isConditionNode(node) ? cloneDeep(DefaultProps.CONDITION_PROPS):{},
type: isConditionNode(node) ? "CONDITION":"CONCURRENT",
children:{}
})
} else {
createMessage.warning("最多只能添加 8 项😥");
}
}
//
function delNode(node){
console.log("删除节点", node);
//
let parentNode = nodeMap.value.get(node.parentId)
if (parentNode){
//
if (isBranchNode(parentNode)){
//
parentNode.branchs.splice(parentNode.branchs.indexOf(node), 1)
//1
if (parentNode.branchs.length < 2){
//
let ppNode = nodeMap.value.get(parentNode.parentId)
//
if (parentNode.branchs[0].children && parentNode.branchs[0].children.id){
//
ppNode.children = parentNode.branchs[0].children
ppNode.children.parentId = ppNode.id
//
let endNode = getBranchEndNode(parentNode.branchs[0])
//, EMPTY
endNode.children = parentNode.children.children
if (endNode.children && endNode.children.id){
endNode.children.parentId = endNode.id
}
}else {
//EMPTY
ppNode.children = parentNode.children.children
if (ppNode.children && ppNode.children.id){
ppNode.children.parentId = ppNode.id
}
}
}
}else {
//
if (node.children && node.children.id) {
node.children.parentId = parentNode.id
}
parentNode.children = node.children
}
instance?.proxy?.$forceUpdate();
} else {
createMessage.warning("出现错误,找不到上级节点😥")
}
}
function validateProcess(){
state.valid = true
let err = []
validate(err, dom.value)
return err
}
function validateNode(err, node){
const nodeRef = instance?.refs[node.id] as unknown as any;
if (nodeRef?.validate){
state.valid = nodeRef.validate(err)
}
}
//dom
function nodeDomUpdate(node){
const nodeRef = instance?.refs[node.id] as unknown as any;
nodeRef?.$forceUpdate()
}
//
function forEachNode(parent, node, callback){
if (isBranchNode(node)){
callback(parent, node)
forEachNode(node, node.children, callback)
node.branchs.map(branchNode => {
callback(node, branchNode)
forEachNode(branchNode, branchNode.children, callback)
})
} else if (isPrimaryNode(node) || isEmptyNode(node) || isBranchSubNode(node)){
callback(parent, node)
forEachNode(node, node.children, callback)
}
}
//
function validate(err, node){
if (isPrimaryNode(node)){
validateNode(err, node)
validate(err, node.children)
} else if (isBranchNode(node)){
//
node.branchs.map(branchNode => {
//
validateNode(err, branchNode)
//
validate(err, branchNode.children)
})
validate(err, node.children)
} else if (isEmptyNode(node)){
validate(err, node.children)
}
}
expose({
validateProcess,
});
return () => {
nodeMap.value.clear();
let processTrees = getDomTree(dom.value);
//
processTrees.push(
h('div',
{style:{'text-align': 'center'}},
{ default: () => [h('div', {class:{'process-end': true}, innerHTML:'流程结束'})]}
));
return h('div', {class:{'_root': true}, ref: rootRef}, { default: () => processTrees });
}
},
});
</script>
<style lang="less" scoped>
._root{
margin: 0 auto;
}
.process-end{
width: 100px;
margin: 0 auto;
margin-bottom: 20px;
border-radius: 15px;
padding: 5px 10px;
font-size: small;
color: #747474;
background-color: #f2f2f2;
box-shadow: 0 0 10px 0 #bcbcbc;
}
.primary-node{
display: flex;
align-items: center;
flex-direction: column;
}
.branch-node{
display: flex;
justify-content: center;
/*border-top: 2px solid #cccccc;
border-bottom: 2px solid #cccccc;*/
}
.branch-node-item{
position: relative;
display: flex;
background: white;
flex-direction: column;
align-items: center;
border-top: 2px solid #cccccc;
border-bottom: 2px solid #cccccc;
&:before{
content: "";
position: absolute;
top: 0;
left: calc(50% - 1px);
margin: auto;
width: 2px;
height: 100%;
background-color: #CACACA;
}
.line-top-left, .line-top-right, .line-bot-left, .line-bot-right{
position: absolute;
width: 50%;
height: 4px;
background-color: white;
}
.line-top-left{
top: -2px;
left: -1px;
}
.line-top-right{
top: -2px;
right: -1px;
}
.line-bot-left{
bottom: -2px;
left: -1px;
}
.line-bot-right{
bottom: -2px;
right: -1px;
}
}
.add-branch-btn{
position: absolute;
width: 80px;
.add-branch-btn-el{
z-index: 999;
position: absolute;
top: -15px;
}
}
.empty-node{
display: flex;
justify-content: center;
flex-direction: column;
align-items: center;
}
</style>
Loading…
Cancel
Save