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