19 changed files with 2420 additions and 0 deletions
@ -0,0 +1,3 @@ |
|||
import ProcessDesign from './src/components/ProcessDesign.vue'; |
|||
|
|||
export { ProcessDesign }; |
|||
@ -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> |
|||
@ -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> |
|||
@ -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> |
|||
@ -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,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> |
|||
@ -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 |
|||
}, |
|||
// 快捷键 可提供三种模式 sublime、emacs、vim |
|||
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> |
|||
@ -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> |
|||
@ -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> |
|||
@ -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> |
|||
@ -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> |
|||
@ -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> |
|||
@ -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> |
|||
@ -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> |
|||
@ -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> |
|||
@ -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> |
|||
@ -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 |
|||
} |
|||
@ -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), |
|||
})) |
|||
} |
|||
|
|||
//id映射到map,用来向上遍历 |
|||
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…
Reference in new issue