committed by
GitHub
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