Browse Source

feat(flow): add flow node config.

pull/839/head
colin 3 years ago
parent
commit
3c72d690d8
  1. 9
      apps/vue/src/components/FlowDesign/src/components/InsertButton.vue
  2. 573
      apps/vue/src/components/FlowDesign/src/components/OrgPicker.vue
  3. 11
      apps/vue/src/components/FlowDesign/src/components/ProcessDesign.vue
  4. 349
      apps/vue/src/components/FlowDesign/src/components/config/ApprovalNodeConfig.vue
  5. 86
      apps/vue/src/components/FlowDesign/src/components/config/HttpEndPointNodeConfig.vue
  6. 58
      apps/vue/src/components/FlowDesign/src/components/config/NodeConfig.vue
  7. 21
      apps/vue/src/components/FlowDesign/src/components/config/RootNodeConfig.vue
  8. 108
      apps/vue/src/components/FlowDesign/src/components/config/TriggerNodeConfig.vue
  9. 8
      apps/vue/src/components/FlowDesign/src/components/nodes/ApprovalNode.vue
  10. 73
      apps/vue/src/components/FlowDesign/src/components/nodes/HttpEndPointNode.vue
  11. 5
      apps/vue/src/components/FlowDesign/src/components/nodes/Node.vue
  12. 28
      apps/vue/src/components/FlowDesign/src/components/process/DefaultNodeProps.ts
  13. 244
      apps/vue/src/components/FlowDesign/src/components/process/ProcessTree.vue
  14. 2
      apps/vue/src/components/FormDesign/src/components/VFormDesign/components/ComponentProps.vue
  15. 18
      apps/vue/src/components/FormDesign/src/components/VFormDesign/config/componentPropsConfig.ts

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

@ -26,6 +26,10 @@
<SettingOutlined class="icon" style="color:#15BC83;" />
<span>触发器</span>
</div>
<div @click="addHttpEndPointNode">
<GlobalOutlined class="icon" style="color:#2f65c3;" />
<span>HttpEndPoint</span>
</div>
</div>
</template>
<Button type="primary" size="small" shape="circle">
@ -47,6 +51,7 @@
ClockCircleOutlined,
SettingOutlined,
PlusOutlined,
GlobalOutlined,
} from '@ant-design/icons-vue';
//import { useFlowStoreWithOut } from '/@/store/modules/flow';
@ -81,6 +86,10 @@
function addTriggerNode() {
emits('insertNode', "TRIGGER");
}
function addHttpEndPointNode() {
emits('insertNode', "HTTPENDPOINT");
}
</script>
<style lang="less" scoped>

573
apps/vue/src/components/FlowDesign/src/components/OrgPicker.vue

@ -0,0 +1,573 @@
<template>
<Modal
:width="width"
:title="title"
:mask-closable="clickClose"
:destroy-on-close="closeFree"
v-model:visible="state.visible"
@ok="selectOk"
>
<div class="picker">
<div class="candidate" v-loading="state.loading">
<div v-if="type !== 'role'">
<InputSearch
v-model="state.search"
@search="searchUser"
style="width: 95%;"
size="small"
allow-clear
placeholder="搜索人员,支持拼音、姓名"
/>
<div v-show="!showUsers">
<Ellipsis hoverTip style="height: 18px; color: #8c8c8c; padding: 5px 0 0" :row="1" :content="deptStackStr">
<!-- <i slot="pre" class="el-icon-office-building"></i> -->
<BuildOutlined>
<slot name="pre"></slot>
</BuildOutlined>
</Ellipsis>
<div style="margin-top: 5px">
<Checkbox v-model="state.checkAll" @change="handleCheckAllChange" :disabled="!multiple">全选</Checkbox>
<span v-show="state.deptStack.length > 0" class="top-dept" @click="beforeNode">上一级</span>
</div>
</div>
</div>
<div class="role-header" v-else>
<div>系统角色</div>
</div>
<div class="org-items" :style="type === 'role' ? 'height: 350px':''">
<Empty :image-size="100" description="似乎没有数据" v-show="orgs.length === 0"/>
<div v-for="(org, index) in orgs" :key="index" :class="orgItemClass(org)" @click="selectChange(org)">
<Checkbox v-model="org.selected" :disabled="disableDept(org)"></Checkbox>
<div v-if="org.type === 'dept'">
<!-- <i class="el-icon-folder-opened"></i> -->
<FolderOpenOutlined />
<span class="name">{{ org.name }}</span>
<span @click.stop="nextNode(org)" :class="`next-dept${org.selected ? '-disable':''}`">
<!-- <i class="iconfont icon-map-site"></i>下级 -->
<ApartmentOutlined />
下级
</span>
</div>
<div v-else-if="org.type === 'user'" style="display: flex; align-items: center">
<Avatar :size="35" :src="org.avatar" v-if="!isNullOrWhiteSpace(org.avatar)"/>
<span v-else class="avatar">{{getShortName(org.name)}}</span>
<span class="name">{{ org.name }}</span>
</div>
<div style="display: inline-block" v-else>
<!-- <i class="iconfont icon-bumen"></i> -->
<TeamOutlined />
<span class="name">{{ org.name }}</span>
</div>
</div>
</div>
</div>
<div class="selected">
<div class="count">
<span>已选 {{ state.select.length }} </span>
<span @click="clearSelected">清空</span>
</div>
<div class="org-items" style="height: 350px;">
<Empty :image-size="100" description="请点击左侧列表选择数据" v-show="state.select.length === 0"/>
<div v-for="(org, index) in state.select" :key="index" :class="orgItemClass(org)" >
<div v-if="org.type === 'dept'">
<!-- <i class="el-icon-folder-opened"></i> -->
<FolderOpenOutlined />
<span style="position: static" class="name">{{ org.name }}</span>
</div>
<div v-else-if="org.type === 'user'" style="display: flex; align-items: center">
<Avatar :size="35" :src="org.avatar" v-if="!isNullOrWhiteSpace(org.avatar)"/>
<span v-else class="avatar">{{getShortName(org.name)}}</span>
<span class="name">{{ org.name }}</span>
</div>
<div v-else>
<!-- <i class="iconfont icon-bumen"></i> -->
<TeamOutlined />
<span class="name">{{ org.name }}</span>
</div>
<!-- <i class="el-icon-close" @click="noSelected(index)"></i> -->
<CloseOutlined @click="noSelected(index)" />
</div>
</div>
</div>
</div>
</Modal>
</template>
<script setup lang="ts">
import { computed, reactive } from 'vue';
import { Avatar, Checkbox, Empty, Modal, Input } from 'ant-design-vue';
import { BuildOutlined, CloseOutlined, FolderOpenOutlined, ApartmentOutlined, TeamOutlined } from '@ant-design/icons-vue';
import { useMessage } from '/@/hooks/web/useMessage';
import { isNullOrWhiteSpace } from '/@/utils/strings';
import { findByUserName } from '/@/api/identity/userLookup';
import { getList as getUsers } from '/@/api/identity/user';
import { getList as getRoles } from '/@/api/identity/role';
import { getList as getOrganizationUnits } from '/@/api/identity/organization-units';
import Ellipsis from './Ellipsis.vue';
const InputSearch = Input.Search;
const emits = defineEmits(['ok', 'close']);
const props = defineProps({
width: {
type: String,
default: '600px'
},
clickClose: {
type: Boolean,
default: false
},
closeFree: {
type: Boolean,
default: false
},
title: {
default: '请选择',
type: String
},
type: {
default: 'dept', //org/ user- dept- role-
type: String
},
multiple: { //
default: false,
type: Boolean
},
selected: {
default: () => {
return []
},
type: Array
},
});
const deptStackStr = computed(() => {
return String(state.deptStack.map(v => v.name)).replaceAll(',', ' > ');
});
const orgs = computed(() => {
return !state.search || state.search.trim() === '' ? state.nodes : state.searchUsers;
});
const showUsers = computed(() => {
return state.search || state.search.trim() !== '';
});
const state = reactive({
visible: false,
loading: false,
checkAll: false,
nowDeptId: null,
isIndeterminate: false,
searchUsers: [] as string[],
nodes: [] as any[],
select: [] as any[],
search: '',
deptStack: [] as any[],
});
const { createMessage, createConfirm } = useMessage();
function show() {
state.visible = true;
init();
initOrgList(props.type);
}
function orgItemClass(org) {
return {
'org-item': true,
'org-dept-item': org.type === 'dept',
'org-user-item': org.type === 'user',
'org-role-item': org.type === 'role',
};
}
function disableDept(node) {
return props.type === 'user' && 'dept' === node.type;
}
function initOrgList(type) {
switch (type) {
case 'dept':
getOrgList();
break;
case 'role':
getRoleList();
break;
case 'user':
getUserList();
break;
}
}
function getUserList() {
state.loading = true;
getUsers({}).then(rsp => {
state.loading = false;
state.nodes = rsp.items.map(item => {
return {
...item,
name: item.userName,
};
});
selectToLeft();
}).catch(err => {
state.loading = false;
createMessage.error(err.response.data);
});
}
function getRoleList() {
state.loading = true;
getRoles({}).then(rsp => {
state.loading = false;
state.nodes = rsp.items;
selectToLeft();
}).catch(err => {
state.loading = false;
createMessage.error(err.response.data);
});
}
function getOrgList() {
state.loading = true;
getOrganizationUnits({}).then(rsp => {
state.loading = false;
state.nodes = rsp.items.map(item => {
return {
...item,
name: item.displayName,
};
});
selectToLeft();
}).catch(err => {
state.loading = false;
createMessage.error(err.response.data);
});
}
function getShortName(name) {
if (name) {
return name.length > 2 ? name.substring(1, 3) : name;
}
return '**';
}
function searchUser() {
let userName = state.search.trim();
state.searchUsers = [];
state.loading = true;
findByUserName(userName).then(rsp => {
state.loading = false;
state.searchUsers = [rsp.userName];
selectToLeft();
}).catch(() => {
state.loading = false;
createMessage.error("接口异常");
});
}
function selectToLeft() {
let nodes = state.search.trim() === '' ? state.nodes : state.searchUsers;
nodes.forEach(node => {
for (let i = 0; i < state.select.length; i++) {
if (state.select[i].id === node.id) {
node.selected = true;
break;
} else {
node.selected = false;
}
}
});
}
function selectChange(node) {
if (node.selected) {
state.checkAll = false;
for (let i = 0; i < state.select.length; i++) {
if (state.select[i].id === node.id) {
state.select.splice(i, 1);
break;
}
}
node.selected = false;
} else if (!disableDept(node)) {
node.selected = true;
let nodes = state.search.trim() === '' ? state.nodes : state.searchUsers;
if (!props.multiple) {
nodes.forEach(nd => {
if (node.id !== nd.id) {
nd.selected = false;
}
});
}
if (node.type === 'dept') {
if (!props.multiple) {
state.select = [node];
} else {
state.select.unshift(node);
}
} else {
if (!props.multiple) {
state.select = [node];
} else {
state.select.push(node);
}
}
}
}
function noSelected(index) {
let nodes = state.nodes;
for (let f = 0; f < 2; f++) {
for (let i = 0; i < nodes.length; i++) {
if (nodes[i].id === state.select[index].id) {
nodes[i].selected = false;
state.checkAll = false;
break;
}
}
nodes = state.searchUsers;
}
state.select.splice(index, 1)
}
function handleCheckAllChange() {
state.nodes.forEach(node => {
if (state.checkAll) {
if (!node.selected && !disableDept(node)) {
node.selected = true
state.select.push(node)
}
} else {
node.selected = false;
for (let i = 0; i < state.select.length; i++) {
if (state.select[i].id === node.id) {
state.select.splice(i, 1);
break;
}
}
}
});
}
function nextNode(node) {
state.nowDeptId = node.id;
state.deptStack.push(node);
getOrgList();
}
function beforeNode() {
if (state.deptStack.length === 0) {
return;
}
if (state.deptStack.length < 2) {
state.nowDeptId = null;
} else {
state.nowDeptId = state.deptStack[state.deptStack.length - 2].id;
}
state.deptStack.splice(state.deptStack.length - 1, 1);
getOrgList();
}
function recover() {
state.select = [];
state.nodes.forEach(nd => nd.selected = false);
}
function selectOk() {
emits('ok', Object.assign([], state.select.map(v => {
v.avatar = undefined;
return v;
})));
state.visible = false;
recover();
}
function clearSelected(){
createConfirm({
title: '提示',
content: '您确定要清空已选中的项?',
iconType: 'warning',
onOk: () => {
recover();
},
});
}
function close() {
emits('close');
recover();
state.visible = false;
}
function init() {
state.checkAll = false;
state.nowDeptId = null;
state.deptStack = [];
state.nodes = [];
state.select = Object.assign([], props.selected);
selectToLeft();
}
defineExpose({
show,
close,
});
</script>
<style lang="less" scoped>
@containWidth: 278px;
.candidate, .selected {
position: absolute;
display: inline-block;
width: @containWidth;
height: 400px;
border: 1px solid #e8e8e8;
}
.picker {
height: 402px;
position: relative;
text-align: left;
.candidate {
left: 0;
top: 0;
.role-header{
padding: 10px !important;
margin-bottom: 5px;
border-bottom: 1px solid #e8e8e8;
}
.top-dept{
margin-left: 20px;
cursor: pointer;
color:#38adff;
}
.next-dept {
float: right;
color: @primary-color;
cursor: pointer;
}
.next-dept-disable {
float: right;
color: #8c8c8c;
cursor: not-allowed;
}
& > div:first-child {
padding: 5px 10px;
}
}
.selected {
right: 0;
top: 0;
}
.org-items {
overflow-y: auto;
height: 310px;
.anticon-close {
position: absolute;
right: 5px;
cursor: pointer;
font-size: larger;
}
.org-dept-item {
padding: 10px 5px;
& > div {
display: inline-block;
& > span:last-child {
position: absolute;
right: 5px;
}
}
}
.org-role-item {
display: flex;
align-items: center;
padding: 10px 5px;
}
:deep(.org-user-item) {
display: flex;
align-items: center;
padding: 5px;
& > div {
display: inline-block;
}
.avatar {
width: 35px;
text-align: center;
line-height: 35px;
background: @primary-color;
color: white;
border-radius: 50%;
}
}
:deep(.org-item) {
margin: 0 5px;
border-radius: 5px;
position: relative;
display: flex;
.ant-checkbox {
margin-right: 10px;
}
.name {
margin-left: 5px;
}
&:hover {
background: #f1f1f1;
}
}
}
}
.selected {
border-left: none;
.count {
width: calc(@containWidth - 20px);
padding: 10px;
display: inline-block;
border-bottom: 1px solid #e8e8e8;
margin-bottom: 5px;
& > span:nth-child(2) {
float: right;
color: #c75450;
cursor: pointer;
}
}
}
:deep(.ant-modal-body) {
padding: 10px 20px;
}
.disabled{
cursor: not-allowed !important;
color: #8c8c8c !important;
}
::-webkit-scrollbar {
float: right;
width: 4px;
height: 4px;
background-color: white;
}
::-webkit-scrollbar-thumb {
border-radius: 16px;
background-color: #efefef;
}
</style>

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

@ -12,12 +12,13 @@
<MinusOutlined />
</template>
</Button>
<!-- <Button @click="validate">校验流程</Button> -->
<Button @click="validate">校验流程</Button>
</div>
<div class="design" :style="'transform: scale('+ state.scale / 100 +');'">
<ProcessTree ref="processTreeRef" @selectedNode="nodeSelected"/>
</div>
<Drawer
:width="500"
:title="selectedNode.name"
:visible="state.showConfig"
destroy-on-close
@ -34,6 +35,7 @@
</Input>
</div>
<div class="node-config-content">
<NodeConfig />
</div>
</Drawer>
</Layout>
@ -45,6 +47,7 @@
import { PlusOutlined, MinusOutlined } from '@ant-design/icons-vue';
import { useFlowStoreWithOut } from '/@/store/modules/flow';
import ProcessTree from './process/ProcessTree.vue';
import NodeConfig from './config/NodeConfig.vue'
const flowStore = useFlowStoreWithOut();
const state = reactive({
@ -102,6 +105,10 @@
console.log('配置节点', node)
state.showConfig = true
}
defineExpose({
validate,
});
</script>
<style lang="less" scoped>
@ -135,7 +142,7 @@
padding: 0 20px 20px;
}
:deep(.el-drawer__body){
:deep(.ant-drawer-body){
overflow-y: auto;
}
</style>

349
apps/vue/src/components/FlowDesign/src/components/config/ApprovalNodeConfig.vue

@ -0,0 +1,349 @@
<template>
<div>
<Form layout="vertical">
<FormItem label="⚙ 选择审批对象" name="text" class="user-type">
<RadioGroup v-model:value="nodeProps.assignedType">
<Radio v-for="t in state.approvalTypes" :value="t.type" :key="t.type">{{ t.name }}</Radio>
</RadioGroup>
<div v-if="nodeProps.assignedType === 'ASSIGN_USER'">
<Button size="small" type="primary" @click="handleSelectUser" round>
<template #icon>
<PlusOutlined />
</template>
选择人员
</Button>
<OrgItems v-model:value="nodeProps.assignedUser"/>
</div>
<div v-else-if="nodeProps.assignedType === 'SELF_SELECT'">
<RadioGroup size="small" v-model:value="nodeProps.selfSelect.multiple">
<RadioButton :value="false">自选一个人</RadioButton>
<RadioButton :value="true">自选多个人</RadioButton>
</RadioGroup>
</div>
<div v-else-if="nodeProps.assignedType === 'ROLE'">
<Button size="small" type="primary" @click="handleSelectRole" round>
<template #icon>
<PlusOutlined />
</template>
选择系统角色
</Button>
<OrgItems v-model:value="nodeProps.role" />
</div>
<div v-else-if="nodeProps.assignedType === 'FORM_USER'">
<FormItem label="选择表单联系人项" name="text" class="approve-end">
<Select style="width: 80%;" size="small" v-model:value="nodeProps.formUser" placeholder="请选择包含联系人的表单项">
<SelectOption v-for="op in forms" :value="op.id">{{ op.title }}</SelectOption>
</Select>
</FormItem>
</div>
<div v-else>
<span class="item-desc">发起人自己作为审批人进行审批</span>
</div>
</FormItem>
<Divider></Divider>
<FormItem label="👤 审批人为空时" name="text" class="line-mode">
<RadioGroup v-model:value="nodeProps.nobody.handler">
<Radio value="TO_PASS">自动通过</Radio>
<Radio value="TO_REFUSE">自动驳回</Radio>
<Radio value="TO_ADMIN">转交审批管理员</Radio>
<Radio value="TO_USER">转交到指定人员</Radio>
</RadioGroup>
<div style="margin-top: 10px" v-if="nodeProps.nobody.handler === 'TO_USER'">
<Button size="small" type="primary" @click="handleSelectNoSetUser" round>
<template #icon>
<PlusOutlined />
</template>
选择人员</Button>
<OrgItems v-model:value="nodeProps.assignedUser"/>
</div>
</FormItem>
<div v-if="showMode">
<Divider />
<FormItem label="👩‍👦‍👦 多人审批时审批方式" name="text" class="approve-mode">
<RadioGroup v-model:value="nodeProps.mode">
<Radio value="NEXT">会签 按选择顺序审批每个人必须同意</Radio>
<Radio value="AND">会签可同时审批每个人必须同意</Radio>
<Radio value="OR">或签有一人同意即可</Radio>
</RadioGroup>
</FormItem>
</div>
<Divider>高级设置</Divider>
<FormItem label="✍ 审批同意时是否需要签字" name="text">
<Switch checked-children="需要" un-checked-children="不用" v-model:checked="nodeProps.sign"></Switch>
<Tooltip class="item" effect="dark" content="如果全局设置了需要签字,则此处不生效" placement="top">
<QuestionOutlined style="margin-left: 10px; font-size: medium; color: #b0b0b1" />
</Tooltip>
</FormItem>
<FormItem label="⏱ 审批期限(为 0 则不生效)" name="timeLimit">
<InputGroup style="width: 180px;" compact>
<Input
style="width: 100px;"
placeholder="时长"
size="small"
type="number"
v-model:value="nodeProps.timeLimit.timeout.value"
/>
<Select style="width: 75px;" v-model:value="nodeProps.timeLimit.timeout.unit" size="small" placeholder="请选择">
<SelectOption value="D"></SelectOption>
<SelectOption value="H">小时</SelectOption>
<SelectOption value="M">分钟</SelectOption>
</Select>
</InputGroup>
</FormItem>
<FormItem label="审批期限超时后执行" name="level" v-if="nodeProps.timeLimit.timeout.value > 0">
<RadioGroup v-model:value="nodeProps.timeLimit.handler.type">
<Radio value="PASS">自动通过</Radio>
<Radio value="REFUSE">自动驳回</Radio>
<Radio value="NOTIFY">发送提醒</Radio>
</RadioGroup>
<div v-if="nodeProps.timeLimit.handler.type === 'NOTIFY'">
<div style="color:#409EEF; font-size: small">默认提醒当前审批人</div>
<Switch checked-children="一次" un-checked-children="循环" v-model:checked="nodeProps.timeLimit.handler.notify.once"></Switch>
<span style="margin-left: 20px" v-if="!nodeProps.timeLimit.handler.notify.once">
每隔
<InputNumber
:min="0"
:max="10000"
:step="1"
size="small"
v-model:value="nodeProps.timeLimit.handler.notify.hour"
/>
小时提醒一次
</span>
</div>
</FormItem>
<FormItem label="🙅‍ 如果审批被驳回 👇">
<RadioGroup v-model:value="nodeProps.refuse.type">
<Radio value="TO_END">直接结束流程</Radio>
<Radio value="TO_BEFORE">驳回到上级审批节点</Radio>
<Radio value="TO_NODE">驳回到指定节点</Radio>
</RadioGroup>
<div v-if="nodeProps.refuse.type === 'TO_NODE'">
<span>指定节点:</span>
<Select style="margin-left: 10px; width: 150px;" placeholder="选择跳转步骤" size="small" v-model:value="nodeProps.refuse.target">
<SelectOption v-for="(node, i) in nodeOptions" :key="i" :value="node.id">{{ node.name }}</SelectOption>
</Select>
</div>
</FormItem>
</Form>
<OrgPicker
multiple
:title="pickerTitle"
:type="state.orgPickerType"
ref="orgPickerRef"
:selected="state.orgPickerSelected"
@ok="selected"
/>
</div>
</template>
<script setup lang="ts">
import { computed, nextTick, reactive, ref, unref } from 'vue';
import { Button, Divider, Form, Radio, Input, InputNumber, Select, Switch, Tooltip } from 'ant-design-vue';
import { PlusOutlined, QuestionOutlined } from '@ant-design/icons-vue';
import { useFlowStoreWithOut } from '/@/store/modules/flow';
import OrgPicker from "../OrgPicker.vue";
import OrgItems from "../OrgItems.vue";
const FormItem = Form.Item;
const InputGroup = Input.Group;
const RadioGroup = Radio.Group;
const RadioButton = Radio.Button;
const SelectOption = Select.Option;
const props = defineProps({
config: {
type: Object,
default: () => {
return {}
},
},
});
const orgPickerRef = ref<any>();
const flowStore = useFlowStoreWithOut();
const nodeProps = computed(() => {
return flowStore.selectedNode.props;
});
const selectUser = computed(() => {
return props.config.assignedUser || [];
});
const selectRole = computed(() => {
return props.config.role || [];
});
const forms = computed(() => {
return flowStore.design.formItems.filter(f => {
return f.name === 'UserPicker';
});
});
const pickerTitle = computed(() => {
switch (state.orgPickerType) {
case 'user':
return '请选择人员';
case 'role':
return '请选择系统角色';
default:
return undefined;
}
});
const nodeOptions = computed(() => {
let values: any[] = [];
const excType = ['ROOT', 'EMPTY', "CONDITION", "CONDITIONS", "CONCURRENT", "CONCURRENTS"];
flowStore.nodeMap.forEach((v) => {
if (excType.indexOf(v.type) === -1) {
values.push({id: v.id, name: v.name});
}
});
return values;
});
const showMode = computed(() => {
const node = unref(nodeProps);
switch (node.assignedType) {
case "ASSIGN_USER":
return node.assignedUser.length > 0;
case "SELF_SELECT":
return node.selfSelect.multiple;
case "FORM_USER":
return true;
case "ROLE":
return true;
default:
return false;
};
});
const state = reactive({
showOrgSelect: false,
orgPickerSelected: [] as any[],
orgPickerType: 'user',
approvalTypes: [
{name: '指定人员', type: 'ASSIGN_USER'},
{name: '发起人自选', type: 'SELF_SELECT'},
{name: '角色', type: 'ROLE'},
{name: '发起人自己', type: 'SELF'},
{name: '表单内联系人', type: 'FORM_USER'},
]
});
function handleSelectUser() {
state.orgPickerSelected = unref(selectUser);
state.orgPickerType = 'user';
nextTick(() => {
const orgPicker = unref(orgPickerRef);
orgPicker?.show();
});
}
function handleSelectNoSetUser() {
state.orgPickerSelected = props.config.nobody.assignedUser;
state.orgPickerType = 'user';
nextTick(() => {
const orgPicker = unref(orgPickerRef);
orgPicker?.show();
});
}
function handleSelectRole() {
state.orgPickerSelected = unref(selectRole);
state.orgPickerType = 'role';
nextTick(() => {
const orgPicker = unref(orgPickerRef);
orgPicker?.show();
});
}
function selected(select) {
switch (state.orgPickerType) {
case 'user':
nodeProps.value.role = [];
nodeProps.value.assignedUser = select;
state.orgPickerSelected.length = 0;
select.forEach(val => state.orgPickerSelected.push(val));
break;
case 'role':
nodeProps.value.assignedUser = [];
nodeProps.value.role = select;
state.orgPickerSelected.length = 0;
select.forEach(val => state.orgPickerSelected.push(val));
break;
}
}
function removeUserItem(index) {
selectUser.value.splice(index, 1);
}
function removeRoleItem(index) {
selectRole.value.splice(index, 1);
}
defineExpose({
removeUserItem,
removeRoleItem,
});
</script>
<style lang="less" scoped>
.user-type {
:deep(.a-radio) {
width: 110px;
margin-top: 10px;
margin-bottom: 20px;
}
}
:deep(.line-mode) {
.a-radio {
width: 150px;
margin: 5px;
}
}
:deep(.a-form-item__label) {
line-height: 25px;
}
:deep(.approve-mode) {
.a-radio {
float: left;
width: 100%;
display: block;
margin-top: 15px;
}
}
:deep(.approve-end) {
position: relative;
.a-radio-group {
width: 160px;
}
.a-radio {
margin-bottom: 5px;
width: 100%;
}
.approve-end-leave {
position: absolute;
bottom: -5px;
left: 150px;
}
}
:deep(.a-divider--horizontal) {
margin: 10px 0;
}
</style>

86
apps/vue/src/components/FlowDesign/src/components/config/HttpEndPointNodeConfig.vue

@ -0,0 +1,86 @@
<template>
<div class="end-point">
<Form layout="vertical">
<Tabs v-model:activeKey="state.activeKey">
<TabPane key="common" tab="常用">
<FormItem label="名称" extra="活动的名称。" name="name">
<Input v-model:value="nodeProps.name" />
</FormItem>
<FormItem label="显示名称" extra="活动的显示名称。" name="displayName">
<Input v-model:value="nodeProps.displayName" />
</FormItem>
<FormItem label="描述" extra="活动的描述。" name="description">
<TextArea v-model:value="nodeProps.description" show-count :autoSize="{ minRows: 3 }" />
</FormItem>
</TabPane>
<TabPane key="properties" tab="属性">
<FormItem label="路径" extra="触发此活动的相对路径" name="path">
<Input v-model:value="nodeProps.path" />
</FormItem>
<FormItem label="请求方法" extra="触发此活动的HTTP方法。" name="methods">
<Select v-model:value="nodeProps.methods" mode="multiple">
<SelectOption value="GET">GET</SelectOption>
<SelectOption value="POST">POST</SelectOption>
<SelectOption value="PUT">PUT</SelectOption>
<SelectOption value="PATCH">PATCH</SelectOption>
<SelectOption value="DELETE">DELETE</SelectOption>
<SelectOption value="OPTIONS">OPTIONS</SelectOption>
<SelectOption value="HEAD">HEAD</SelectOption>
</Select>
</FormItem>
<FormItem label="读取内容" extra="指示是否应将HTTP请求内容体作为HTTP请求模型的一部分读取和存储。存储的格式取决于内容类型头。" name="readContent" class="user-type">
<Checkbox v-model:checked="nodeProps.readContent" />
</FormItem>
</TabPane>
<TabPane key="advanced" tab="高级">
<FormItem label="数据类型" extra="表单数据类型" name="targetType">
<Input v-model:value="nodeProps.targetType" />
</FormItem>
<FormItem label="架构" extra="Json格式数据架构" name="schema">
<CodeEditor v-model:value="nodeProps.schema" />
</FormItem>
</TabPane>
<TabPane key="security" tab="安全">
<FormItem label="身份认证" extra="选中以只允许经过身份验证的请求" name="authorize">
<Checkbox v-model:checked="nodeProps.authorize" />
</FormItem>
<FormItem label="策略" extra="提供一个要评估的策略。如果策略失败,请求将被禁止。" name="policy">
<Input v-model:value="nodeProps.policy" />
</FormItem>
</TabPane>
</Tabs>
</Form>
</div>
</template>
<script setup lang="ts">
import { computed, reactive } from 'vue';
import { Checkbox, Form, Input, Select, Tabs } from 'ant-design-vue';
import { CodeEditor } from '/@/components/CodeEditor';
import { useFlowStoreWithOut } from '/@/store/modules/flow';
const FormItem = Form.Item;
const SelectOption = Select.Option;
const TabPane = Tabs.TabPane;
const TextArea = Input.TextArea;
defineProps({
config: {
type: Object,
default: () => {
return {}
},
},
});
const flowStore = useFlowStoreWithOut();
const nodeProps = computed(() => {
return flowStore.selectedNode.props;
});
const state = reactive({
activeKey: 'common',
});
</script>
<style scoped>
</style>

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

@ -0,0 +1,58 @@
<template>
<div>
<Tabs v-model="state.active" v-if="name && formConfig.length > 0">
<TabPane :label="name" name="properties">
<component :is="componentRef[(selectNode.type||'').toLowerCase()]" :config="selectNode.props"/>
</TabPane>
<!-- <TabPane label="表单权限设置" name="permissions">
<form-authority-config/>
</TabPane> -->
</Tabs>
<component :is="componentRef[(selectNode.type||'').toLowerCase()]" v-else :config="selectNode.props"/>
</div>
</template>
<script lang="ts" setup>
import { computed, reactive, shallowRef } from 'vue';
import { Tabs } from 'ant-design-vue';
import { useFlowStoreWithOut } from '/@/store/modules/flow';
import Approval from './ApprovalNodeConfig.vue'
import Trigger from './TriggerNodeConfig.vue'
//import FormAuthorityConfig from './FormAuthorityConfig.vue'
import Root from './RootNodeConfig.vue'
import HttpEndPoint from './HttpEndPointNodeConfig.vue';
const TabPane = Tabs.TabPane;
const componentRef = shallowRef({
'root': Root,
'approval': Approval,
'trigger': Trigger,
'httpendpoint': HttpEndPoint,
});
const flowStore = useFlowStoreWithOut();
const selectNode = computed(() => {
return flowStore.selectedNode;
});
const formConfig = computed(() => {
return flowStore.design.formItems;
});
const name = computed(() => {
switch (selectNode.value.type) {
case 'ROOT':
return '设置发起人';
case 'APPROVAL':
return '设置审批人';
case 'CC':
return '设置抄送人';
default:
return undefined;
}
});
const state = reactive({
active: 'properties',
});
</script>
<style lang="less" scoped>
</style>

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

@ -1,15 +1,22 @@
<template>
<div>
<p class="desc">选择能发起该审批的人员/部门不选则默认开放给所有人</p>
<Button size="small" @click="selectOrg" type="primary" round>请选择</Button>
<OrgItems v-model="select"/>
<Button size="small" @click="selectOrg" type="primary" round>
<template #icon>
<PlusOutlined />
</template>
请选择</Button>
<OrgItems v-model:value="select"/>
<OrgPicker title="请选择可发起本审批的人员/部门" multiple ref="orgPickerRef" :selected="select" @ok="selected"/>
</div>
</template>
<script setup lang="ts">
import { computed, reactive } from 'vue';
import { computed, nextTick, reactive, ref, unref } from 'vue';
import { Button } from 'ant-design-vue';
import { PlusOutlined } from '@ant-design/icons-vue';
import OrgItems from "../OrgItems.vue";
import OrgPicker from "../OrgPicker.vue";
const props = defineProps({
config:{
@ -19,6 +26,7 @@
}
},
});
const orgPickerRef = ref<any>();
const select = computed(() => {
return props.config.assignedUser;
});
@ -27,12 +35,15 @@
});
function selectOrg() {
console.log(selectOrg);
nextTick(() => {
const orgPicker = unref(orgPickerRef);
orgPicker?.show();
});
}
function selected(sel: any) {
select.value.length = 0;
select.value.forEach(val => sel.push(val));
sel.forEach(val => select.value.push(val));
}
function removeOrgItem(index: number){

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

@ -1,73 +1,74 @@
<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>
<Form layout="vertical">
<FormItem label="选择触发的动作" name="text" class="user-type">
<RadioGroup v-model:value="config.type">
<Radio value="WEBHOOK">发送网络请求</Radio>
<Radio value="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>
<FormItem label="请求地址" name="text">
<InputGroup compact>
<Select style="width: 20%" v-model:value="config.http.method" placeholder="URL">
<Option value="GET">GET</Option>
<Option value="POST">POST</Option>
<Option value="PUT">PUT</Option>
<Option value="DELETE">DELETE</Option>
</Select>
</Input>
<Input style="width: 80%" placeholder="请输入URL地址" size="middle" v-model:value="config.http.url" />
</InputGroup>
</FormItem>
<FormItem label="Header请求头" prop="text">
<div slot="label">
<FormItem name="text">
<template #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>
<Button type="link" @click="addItem(config.http.headers)"> + 添加</Button>
</template>
<div style="margin-top: 6px" v-for="(header, index) in config.http.headers" :key="header.name">
- <Input placeholder="参数名" size="small" style="width: 100px;" v-model:value="header.name" />
<RadioGroup size="small" style="margin: 0 5px;" v-model:value="header.isField">
<RadioButton :value="true">表单</RadioButton>
<RadioButton :value="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 v-if="header.isField" style="width: 180px;" v-model:value="header.value" size="small" placeholder="请选择表单字段">
<Option v-for="form in forms" :key="form.id" :value="form.title">{{ form.title }}</Option>
</Select>
<Input v-else placeholder="请设置字段值" size="small" v-model="header.value" style="width: 180px;"/>
<Input v-else placeholder="请设置字段值" size="small" v-model:value="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">
<FormItem name="text">
<template #label>
<span style="margin-right: 10px">Header请求参数 </span>
<Button style="margin-right: 20px" type="text" @click="addItem(config.http.params)"> + 添加</Button>
<Button style="margin-right: 20px" type="link" @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 size="small" style="margin: 0 5px;" v-model:value="config.http.contentType">
<RadioButton value="JSON">json</RadioButton>
<RadioButton value="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>
</template>
<div style="margin-top: 6px" v-for="(param, index) in config.http.params" :key="param.name">
- <Input placeholder="参数名" size="small" style="width: 100px;" v-model:value="param.name" />
<RadioGroup size="small" style="margin: 0 5px;" v-model:value="param.isField">
<RadioButton :value="true">表单</RadioButton>
<RadioButton :value="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 v-if="param.isField" style="width: 180px;" v-model:value="param.value" size="small" placeholder="请选择表单字段">
<Option v-for="form in forms" :key="form.id" :value="form.title">{{ form.title }}</Option>
</Select>
<Input v-else placeholder="请设置字段值" size="small" v-model="param.value" style="width: 180px;"/>
<Input v-else placeholder="请设置字段值" size="small" v-model:value="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">
<FormItem name="text">
<template #label>
<span>请求结果处理</span>
<span style="margin-left: 20px">自定义脚本: </span>
<Switch v-model="config.http.handlerByScript"></Switch>
</div>
<Switch v-model:checked="config.http.handlerByScript"></Switch>
</template>
<span class="item-desc" v-if="config.http.handlerByScript">
👉 返回值为 ture 则流程通过 false 则流程将被驳回
<div>支持函数
@ -81,26 +82,26 @@
<div v-if="config.http.handlerByScript">
<div>
<span>请求成功😀</span>
<Textarea v-model="config.http.success" :rows="3"></Textarea>
<Textarea v-model:value="config.http.success" :rows="3"></Textarea>
</div>
<div>
<span>请求失败😥</span>
<Textarea v-model="config.http.fail" :rows="3"></Textarea>
<Textarea v-model:value="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 label="邮件主题" name="text">
<Input placeholder="请输入邮件主题" size="middle" v-model:value="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>
<FormItem label="收件方" name="text">
<Select size="small" style="width: 100%;" v-model:value="config.email.to" filterable multiple allow-create default-first-option placeholder="请输入收件人">
<Option v-for="item in config.email.to" :key="item" :value="item">{{ item }}</Option>
</Select>
</FormItem>
<FormItem label="邮件正文" prop="text">
<Textarea v-model="config.email.content" :rows="4" placeholder="邮件内容,支持变量提取表单数据 ${表单字段名} "></Textarea>
<FormItem label="邮件正文" name="text">
<Textarea v-model:value="config.email.content" :rows="4" placeholder="邮件内容,支持变量提取表单数据 ${表单字段名} "></Textarea>
</FormItem>
</div>
</Form>
@ -116,6 +117,7 @@
const FormItem = Form.Item;
const Option = Select.Option;
const InputGroup = Input.Group;
const RadioGroup = Radio.Group;
const RadioButton = Radio.Button;

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

@ -47,10 +47,6 @@
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 '表单内联系人(未选择)';
@ -64,7 +60,9 @@
}
case "ROLE":
if (config.role.length > 0) {
return String(config.role).replaceAll(',', '、');
let texts: string[] = [];
config.role.forEach(org => texts.push(org.name));
return String(texts).replaceAll(',', '、');
} else {
return '指定角色(未设置)';
}

73
apps/vue/src/components/FlowDesign/src/components/nodes/HttpEndPointNode.vue

@ -0,0 +1,73 @@
<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>
<GlobalOutlined />
</template>
</Node>
</template>
<script setup lang="ts">
import { computed, reactive } from 'vue';
import { GlobalOutlined } 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 {};
},
},
});
const content = computed(() => {
return props.config.props.path;
});
const state = reactive({
showError: false,
errorInfo: '',
});
function validate(err: string[]) {
state.showError = false;
if (isNullOrWhiteSpace(props.config.props.name)) {
state.showError = true;
state.errorInfo = '请设置活动的名称';
return;
}
if (isNullOrWhiteSpace(props.config.props.path)) {
state.showError = true;
state.errorInfo = '请设置触发活动的请求地址';
return;
}
if (Array.isArray(props.config.props.methods) &&
props.config.props.methods.length === 0) {
state.showError = true;
state.errorInfo = '请设置触发活动的请求方法';
return;
}
if (state.showError){
err.push(`${props.config.name} 活动配置未设置完善`);
}
return !state.showError;
}
defineExpose({
validate,
});
</script>
<style scoped>
</style>

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

@ -16,8 +16,11 @@
<RightOutlined class="i" />
</div>
<div class="node-error" v-if="showError">
<Tooltip effect="dark" :content="errorInfo" placement="top">
<Tooltip placement="right">
<WarningOutlined />
<template #title>
<span>{{ errorInfo }}</span>
</template>
</Tooltip>
</div>
</div>

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

@ -107,6 +107,32 @@ export const DELAY_PROPS = {
dateTime: "" //如果当天没有超过设置的此时间点,就延时到这个指定的时间,到了就直接跳过不延时
}
//HTTP EndPoint默认属性
export const HTTPENDPOINT_PROPS = {
path: '',
methods: [],
readCotntent: false,
targetType: '',
schema: '',
authorize: false,
policy: '',
}
export default {
APPROVAL_PROPS, CC_PROPS, DELAY_PROPS, CONDITION_PROPS, ROOT_PROPS, TRIGGER_PROPS
APPROVAL_PROPS,
CC_PROPS,
DELAY_PROPS,
CONDITION_PROPS,
ROOT_PROPS,
TRIGGER_PROPS,
HTTPENDPOINT_PROPS,
}
export const PrimaryNodes = [
'ROOT',
'APPROVAL',
'CC',
'DELAY',
'TRIGGER',
'HTTPENDPOINT'
];

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

@ -4,6 +4,7 @@
import { useMessage } from '/@/hooks/web/useMessage';
import { useFlowStoreWithOut } from '/@/store/modules/flow';
//
import HttpEndPoint from '../nodes/HttpEndPointNode.vue';
import Approval from '../nodes/ApprovalNode.vue';
import Cc from '../nodes/CcNode.vue';
import Concurrent from '../nodes/ConcurrentNode.vue';
@ -14,12 +15,13 @@
import Root from '../nodes/RootNode.vue';
import Node from '../nodes/Node.vue';
import DefaultProps from "./DefaultNodeProps";
import DefaultProps, { PrimaryNodes } from "./DefaultNodeProps";
import { cloneDeep } from 'lodash-es';
export default defineComponent({
name: 'ProcessTree',
components: {
httpendpoint: HttpEndPoint,
Approval,
Cc,
Concurrent,
@ -49,25 +51,25 @@
function getDomTree(node) {
toMapping(node);
if (isPrimaryNode(node)){
if (isPrimaryNode(node)) {
//
let childDoms = getDomTree(node.children)
decodeAppendDom(node, childDoms)
let childDoms = getDomTree(node.children);
decodeAppendDom(node, childDoms);
return [h('div', {'class':{'primary-node': true}}, { default: () => childDoms })];
} else if (isBranchNode(node)){
} 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})
let childDoms = getDomTree(branchNode.children);
decodeAppendDom(branchNode, childDoms, {level: index + 1, size: node.branchs.length});
//4线线
insertCoverLine(index, childDoms, node.branchs)
insertCoverLine(index, childDoms, node.branchs);
//
index++;
return h('div', {'class':{'branch-node-item': true}}, { default: () => childDoms });
})
});
///
branchItems.unshift(
h(
@ -85,25 +87,24 @@
},
{ default: () => [] })
]}));
let bchDom = [h('div', {'class':{'branch-node': true}}, { default: () => branchItems })]
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 afterChildDoms = getDomTree(node.children);
return [h('div', {}, { default: () => [bchDom, afterChildDoms] })];
} else if (isEmptyNode(node)) {
//
let childDoms = getDomTree(node.children)
decodeAppendDom(node, childDoms)
let childDoms = getDomTree(node.children);
decodeAppendDom(node, childDoms);
return [h('div', {'class':{'empty-node': true}}, { default: () => childDoms })];
}else {
} else {
//
return [];
}
}
//dom
function decodeAppendDom(node, dom, props = {} as any){
console.log('decodeAppendDom', node);
props.config = node
function decodeAppendDom(node, dom, props = {} as any) {
props.config = node;
const component = resolveComponent(node.type.toLowerCase());
dom?.unshift(h(component, {
...props,
@ -116,26 +117,26 @@
onCopy: () => copyBranch(node),
onLeftMove: () => branchMove(node, -1),
onRightMove: () => branchMove(node, 1),
}))
}));
}
//idmap
function toMapping(node){
if (node && node.id){
function toMapping(node) {
if (node && node.id) {
//console.log("node=> " + node.id + " name:" + node.name + " type:" + node.type)
nodeMap.value.set(node.id, node)
nodeMap.value.set(node.id, node);
}
}
function insertCoverLine(index, doms, branchs){
if (index === 0){
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-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: () => [] }))
doms.unshift(h('div', {'class':{'line-top-right': true}}, { default: () => [] }));
doms.unshift(h('div', {'class':{'line-bot-right': true}}, { default: () => [] }));
}
}
@ -146,69 +147,67 @@
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)
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
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 isPrimaryNode(node) {
return node && PrimaryNodes.includes(node.type);
}
function isBranchNode(node){
function isBranchNode(node) {
return node && (node.type === 'CONDITIONS' || node.type === 'CONCURRENTS');
}
function isEmptyNode(node){
return node && (node.type === 'EMPTY')
function isEmptyNode(node) {
return node && (node.type === 'EMPTY');
}
//
function isConditionNode(node){
function isConditionNode(node) {
return node.type === 'CONDITIONS';
}
//
function isBranchSubNode(node){
function isBranchSubNode(node) {
return node && (node.type === 'CONDITION' || node.type === 'CONCURRENT');
}
function isConcurrentNode(node){
return node.type === 'CONCURRENTS'
function isConcurrentNode(node) {
return node.type === 'CONCURRENTS';
}
function getRandomId(){
return `node_${new Date().getTime().toString().substring(5)}${Math.round(Math.random()*9000+1000)}`
function getRandomId() {
return `node_${new Date().getTime().toString().substring(5)}${Math.round(Math.random()*9000+1000)}`;
}
//
function selectNode(node){
function selectNode(node) {
console.log('selectNode', node);
flowStore.selectNode(node);
emit('selectedNode', node)
emit('selectedNode', node);
}
//
function insertNode(type, parentNode){
function insertNode(type, parentNode) {
console.log('insertNode', type, parentNode);
const rootEl = unref(rootRef);
rootEl?.click()
//
let afterNode = parentNode.children
let afterNode = parentNode.children;
//
parentNode.children = {
id: getRandomId(),
@ -223,17 +222,18 @@
case 'TRIGGER': insertTriggerNode(parentNode); break;
case 'CONDITIONS': insertConditionsNode(parentNode); break;
case 'CONCURRENTS': insertConcurrentsNode(parentNode); break;
case 'HTTPENDPOINT': insertHttpEndPointNode(parentNode); break;
default: break;
}
//
if (isBranchNode({type: type})){
if (afterNode && afterNode.id){
afterNode.parentId = parentNode.children.children.id
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
if (afterNode && afterNode.id) {
afterNode.parentId = parentNode.children.id;
}
parentNode.children.children = afterNode;
}
@ -243,7 +243,6 @@
function insertApprovalNode(parentNode){
parentNode.children.name = '审批人';
parentNode.children.props = cloneDeep(DefaultProps.APPROVAL_PROPS);
console.log('parentNode', parentNode);
}
function insertCcNode(parentNode){
@ -266,7 +265,7 @@
parentNode.children.children = {
id: getRandomId(),
parentId: parentNode.children.id,
type: "EMPTY"
type: "EMPTY",
};
parentNode.children.branchs = [
{
@ -275,14 +274,14 @@
type: "CONDITION",
props: cloneDeep(DefaultProps.CONDITION_PROPS),
name: "条件1",
children:{}
children:{},
},{
id: getRandomId(),
parentId: parentNode.children.id,
type: "CONDITION",
props: cloneDeep(DefaultProps.CONDITION_PROPS),
name: "条件2",
children:{}
children:{},
}
];
}
@ -308,131 +307,136 @@
parentId: parentNode.children.id,
type: "CONCURRENT",
props: {},
children:{}
children:{},
}
];
}
function getBranchEndNode(conditionNode){
if (!conditionNode.children || !conditionNode.children.id){
function insertHttpEndPointNode(parentNode) {
parentNode.children.name = 'HttpEndPoint';
parentNode.children.props = cloneDeep(DefaultProps.HTTPENDPOINT_PROPS);
}
function getBranchEndNode(conditionNode) {
if (!conditionNode.children || !conditionNode.children.id) {
return conditionNode;
}
return getBranchEndNode(conditionNode.children);
}
function addBranchNode(node){
if (node.branchs.length < 8){
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:{}
})
children: {},
});
} else {
createMessage.warning("最多只能添加 8 项😥");
}
}
//
function delNode(node){
function delNode(node) {
console.log("删除节点", node);
//
let parentNode = nodeMap.value.get(node.parentId)
if (parentNode){
let parentNode = nodeMap.value.get(node.parentId);
if (parentNode) {
//
if (isBranchNode(parentNode)){
if (isBranchNode(parentNode)) {
//
parentNode.branchs.splice(parentNode.branchs.indexOf(node), 1)
parentNode.branchs.splice(parentNode.branchs.indexOf(node), 1);
//1
if (parentNode.branchs.length < 2){
if (parentNode.branchs.length < 2) {
//
let ppNode = nodeMap.value.get(parentNode.parentId)
let ppNode = nodeMap.value.get(parentNode.parentId);
//
if (parentNode.branchs[0].children && parentNode.branchs[0].children.id){
if (parentNode.branchs[0].children && parentNode.branchs[0].children.id) {
//
ppNode.children = parentNode.branchs[0].children
ppNode.children.parentId = ppNode.id
ppNode.children = parentNode.branchs[0].children;
ppNode.children.parentId = ppNode.id;
//
let endNode = getBranchEndNode(parentNode.branchs[0])
let endNode = getBranchEndNode(parentNode.branchs[0]);
//, EMPTY
endNode.children = parentNode.children.children
if (endNode.children && endNode.children.id){
endNode.children.parentId = endNode.id
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
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
node.children.parentId = parentNode.id;
}
parentNode.children = node.children
parentNode.children = node.children;
}
instance?.proxy?.$forceUpdate();
} else {
createMessage.warning("出现错误,找不到上级节点😥")
createMessage.warning("出现错误,找不到上级节点😥");
}
}
function validateProcess(){
state.valid = true
let err = []
validate(err, dom.value)
return err
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)
if (nodeRef?.validate) {
state.valid = nodeRef.validate(err);
}
}
//dom
function nodeDomUpdate(node){
const nodeRef = instance?.refs[node.id] as unknown as any;
nodeRef?.$forceUpdate()
nodeRef?.$forceUpdate();
}
//
function forEachNode(parent, node, callback){
if (isBranchNode(node)){
callback(parent, node)
forEachNode(node, node.children, 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)
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)){
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)
validateNode(err, branchNode);
//
validate(err, branchNode.children)
})
validate(err, node.children)
} else if (isEmptyNode(node)){
validate(err, node.children)
validate(err, branchNode.children);
});
validate(err, node.children);
} else if (isEmptyNode(node)) {
validate(err, node.children);
}
}

2
apps/vue/src/components/FormDesign/src/components/VFormDesign/components/ComponentProps.vue

@ -56,9 +56,11 @@
v-if="
[
'Select',
'ApiSelect',
'CheckboxGroup',
'RadioGroup',
'TreeSelect',
'ApiTreeSelect',
'Cascader',
'AutoComplete',
].includes(formConfig.currentItem.component)

18
apps/vue/src/components/FormDesign/src/components/VFormDesign/config/componentPropsConfig.ts

@ -19,6 +19,7 @@ export const baseComponentControlAttrs: Omit<IBaseFormAttrs, 'tag'>[] = [
includes: [
'Input',
'Select',
'ApiSelect',
'InputTextArea',
'InputNumber',
'DatePicker',
@ -27,6 +28,7 @@ export const baseComponentControlAttrs: Omit<IBaseFormAttrs, 'tag'>[] = [
'TimePicker',
'Cascader',
'TreeSelect',
'ApiTreeSelect',
'Switch',
'AutoComplete',
'Slider',
@ -38,6 +40,7 @@ export const baseComponentControlAttrs: Omit<IBaseFormAttrs, 'tag'>[] = [
includes: [
'Input',
'Select',
'ApiSelect',
'InputTextArea',
'InputNumber',
'DatePicker',
@ -46,6 +49,7 @@ export const baseComponentControlAttrs: Omit<IBaseFormAttrs, 'tag'>[] = [
'TimePicker',
'Cascader',
'TreeSelect',
'ApiTreeSelect',
'AutoComplete',
],
},
@ -53,7 +57,7 @@ export const baseComponentControlAttrs: Omit<IBaseFormAttrs, 'tag'>[] = [
{
name: 'showSearch',
label: '可搜索',
includes: ['Select', 'TreeSelect', 'Cascader', 'Transfer'],
includes: ['Select', 'ApiSelect', 'TreeSelect', 'ApiTreeSelect', 'Cascader', 'Transfer'],
},
{
name: 'showTime',
@ -73,7 +77,7 @@ export const baseComponentControlAttrs: Omit<IBaseFormAttrs, 'tag'>[] = [
{
name: 'multiple',
label: '多选',
includes: ['Select', 'TreeSelect', 'Upload'],
includes: ['Select', 'ApiSelect', 'TreeSelect', 'ApiTreeSelect', 'Upload'],
},
{
name: 'directory',
@ -88,19 +92,19 @@ export const baseComponentControlAttrs: Omit<IBaseFormAttrs, 'tag'>[] = [
{
name: 'bordered',
label: '是否有边框',
includes: ['Select', 'Input'],
includes: ['Select', 'ApiSelect', 'Input'],
},
{
name: 'defaultActiveFirstOption',
label: '高亮第一个选项',
component: 'Checkbox',
includes: ['Select', 'AutoComplete'],
includes: ['Select', 'ApiSelect', 'AutoComplete'],
},
{
name: 'dropdownMatchSelectWidth',
label: '下拉菜单和选择器同宽',
component: 'Checkbox',
includes: ['Select', 'TreeSelect', 'AutoComplete'],
includes: ['Select', 'ApiSelect', 'TreeSelect', 'ApiTreeSelect', 'AutoComplete'],
},
];
@ -142,10 +146,12 @@ export const baseComponentCommonAttrs: Omit<IBaseFormAttrs, 'tag'>[] = [
'Input',
'InputTextArea',
'Select',
'ApiSelect',
'DatePicker',
'MonthPicker',
'TimePicker',
'TreeSelect',
'ApiTreeSelect',
'Cascader',
],
},
@ -177,7 +183,7 @@ export const baseComponentCommonAttrs: Omit<IBaseFormAttrs, 'tag'>[] = [
},
],
},
includes: ['Select', 'AutoComplete'],
includes: ['Select', 'ApiSelect', 'AutoComplete'],
},
];

Loading…
Cancel
Save