committed by
GitHub
15 changed files with 1400 additions and 193 deletions
@ -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> |
||||
@ -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> |
||||
@ -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> |
||||
@ -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> |
||||
@ -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> |
||||
Loading…
Reference in new issue