This commit is contained in:
clay
2024-03-04 19:13:43 +08:00
commit e44edd71c0
350 changed files with 52288 additions and 0 deletions

View File

@@ -0,0 +1,118 @@
//审批节点默认属性
export const APPROVAL_PROPS = {
assignedType: "ASSIGN_USER", //审批类型
mode: "AND", //会签模式
sign: false, //是否签字
headerBgc: '#ff943e', //节点背景颜色
nobody: { //没有审批的的时候需要的操作
handler: "TO_PASS", //操作
assignedUser: [] //审批人列表
},
timeLimit: { //边界事件
timeout: { //超时提醒时间
unit: "H",
value: 0
},
handler: { //超时提醒触发时候的操作
type: "REFUSE", //操作
notify: {
once: true, //是否循环
hour: 1
}
}
},
assignedUser: [], // 审批人列表
formPerms: [], //表单权限
selfSelect: { //用户自选
multiple: false //用户自选时是否是多选
},
leaderTop: { //领导
endCondition: "TOP",
endLevel: 1,
},
leader: { //第几级领导
level: 1
},
listener:{
state: false,
list:[]
},
roleList: [], //角色列表
refuse: { //拒绝的操作
type: 'TO_END', //驳回规则 TO_END TO_NODE TO_BEFORE
target: '' //驳回到指定ID的节点
},
formUser: '' //表单用户
}
//根节点默认属性
export const ROOT_PROPS = {
assignedUser: [], //审批人
formPerms: [] //表单权限
}
//条件节点默认属性
export const CONDITION_PROPS = {
groupsType: "OR", //条件组逻辑关系 OR、AND
groups: [
{
groupType: "AND", //条件组内条件关系 OR、AND
cids: [], //条件ID集合
conditions: [] //组内子条件
}
],
expression: "" //自定义表达式,灵活构建逻辑关系
}
//抄送节点默认属性
export const CC_PROPS = {
shouldAdd: false,
assignedUser: [],
formPerms: []
}
//触发器节点默认属性
export const TRIGGER_PROPS = {
type: 'WEBHOOK',
http: {
method: 'GET', //请求方法 支持GET/POST
url: '', //URL地址可以直接带参数
headers: [ //http header
{
name: '',
isField: true,
value: '' //支持表达式 ${xxx} xxx为表单字段id
}
],
contentType: 'FORM', //请求参数类型
params: [ //请求参数
{
name: '',
isField: true, //是表单字段还是自定义
value: '' //支持表达式 ${xxx} xxx为表单字段id
}
],
retry: 1,
handlerByScript: false,
success: 'function handlerSuccess(res) {\n return {\n state: true, \n msg: "请求成功!" \n };\n}',
fail: 'function handlerFail(res) {\n return {\n state: true, \n msg: "请求失败!" \n };\n}'
},
email: {
subject: '',
to: [],
cc: [],
content: ''
}
}
//延时节点默认属性
export const DELAY_PROPS = {
type: "FIXED", //延时类型 FIXED:到达当前节点后延时固定时长 、AUTO:延时到 dateTime设置的时间
time: 0, //延时时间
unit: "M", //时间单位 D天 H小时 M分钟
dateTime: "" //如果当天没有超过设置的此时间点,就延时到这个指定的时间,到了就直接跳过不延时
}
export default {
APPROVAL_PROPS, CC_PROPS, DELAY_PROPS, CONDITION_PROPS, ROOT_PROPS, TRIGGER_PROPS
}

View File

@@ -0,0 +1,72 @@
<template>
<div class="scale">
<el-button icon="Plus" size="mini" @click.stop="scale += 10" :disabled="scale >= 150" circle></el-button>
<span>{{ scale }}%</span>
<el-button icon="Minus" size="mini" @click.stop="scale -= 10" :disabled="scale <= 40" circle></el-button>
</div>
<div :style="'transform: scale('+ scale / 100 +');'">
<div id="processTree">
<process-tree ref="processTree" mode="design" id-name="processTree" @selectedNode="nodeSelected"/>
</div>
</div>
<el-drawer :title="selectedNode.name" v-model="showConfig"
:modal-append-to-body="false"
:size="selectedNode.type === 'CONDITION' ? '600px':'500px'"
direction="rtl">
<template #header>
<div>
<el-input v-model="selectedNode.name" size="medium" v-show="showInput"
style="width: 300px" @blur="showInput = false"></el-input>
<el-link v-show="!showInput" @click="showInput = true" style="font-size: medium">
<i class="el-icon-edit" style="margin-right: 10px"></i>
{{ selectedNode.name }}
</el-link>
</div>
</template>
<template #default>
<div slot="title">
</div>
<div class="node-config-content">
<node-config v-if="showConfig" @initRender="initRender"/>
</div>
</template>
</el-drawer>
</template>
<script setup>
import ProcessTree from './ProcessTree.vue'
import NodeConfig from './config/NodeConfig.vue'
import {computed, ref, defineExpose} from 'vue';
import {useProcessStore} from '@/stores/processStore.js'
const processStore = useProcessStore()
const scale = ref(100)
const showConfig = ref(false)
const processTree = ref()
const showInput = ref(false)
const selectedNode = computed(() => {
return processStore.getSelectedNode()
})
const nodeSelected = (node) => {
showConfig.value = true
}
const initRender = () => {
nextTick(() => {
processTree.value.init()
})
}
const validate = () => {
return processTree.value.validateProcess()
}
defineExpose({
validate,
initRender
})
</script>

View File

@@ -0,0 +1,342 @@
<template>
<div style="margin-top: 15px">
<el-button @click="changPan('formDesign')">表单</el-button>
<el-button @click="changPan('processDesign')">流程</el-button>
<el-button @click="publishProcess">发布</el-button>
<div class="layout-body" v-if="visible">
<div v-show="activeSelect === 'processDesign'">
<process-design ref="processDesign"/>
</div>
<div v-show="activeSelect === 'formDesign'" >
<form-design ref="formDesign"/>
</div>
</div>
</div>
<el-dialog v-model="validVisible" title="设置项检查">
<el-steps align-center :active="validStep" finish-status="success">
<el-step v-for="(step, i) in validOptions" :title="step.title" :key="i"
:icon="step.icon" :status="step.status" :description="step.description"/>
</el-steps>
<el-result :icon="validIcon" :title="errTitle" :sub-title="validResult.desc">
<template #icon>
<el-icon style="font-size: 30px" v-if="!validResult.finished">
<Loading/>
</el-icon>
<div slot="subTitle" class="err-info" v-if="validResult.errs.length > 0">
<ellipsis hover-tip v-for="(err, i) in validResult.errs" :key="i + '_err'" :content="err">
<div slot="pre">
<el-icon>
<WarningFilled/>
</el-icon>
</div>
</ellipsis>
</div>
<div v-if="validResult.finished && validResult.success">
<el-icon color="#67c23a" size="70"><CircleCheckFilled /></el-icon>
</div>
</template>
<template #extra>
<el-button type="primary" v-if="validResult.finished" size="medium" @click="doAfter">
{{ validResult.action }}
</el-button>
</template>
</el-result>
</el-dialog>
</template>
<script setup>
import {getProcessDefinitionInfo, addProcessDefinition} from "@/api/workflow/process-definition.js";
import ProcessDesign from './ProcessDesign.vue'
import FormDesign from '../form/FormDesign.vue'
import Ellipsis from './common/Ellipsis.vue'
import {getCurrentInstance} from '@vue/runtime-core';
let {proxy} = getCurrentInstance();
import {Loading,WarningFilled,CircleCheckFilled} from '@element-plus/icons-vue'
import {ref,computed} from 'vue'
const router = useRouter()
const params = reactive(router.currentRoute.value.params)
import {useProcessStore} from '@/stores/processStore.js'
const processStore = useProcessStore()
import {ElMessage, ElMessageBox} from "element-plus";
const processDesign = ref()
const visible = ref(false)
const timer = ref(null)
const validComponents = ref(['formDesign','processDesign'])
// const activeSelect = ref('formDesign')
const activeSelect = ref('processDesign')
const validVisible = ref(false)
const validStep = ref(0)
const validResult = ref({})
const validOptions = ref([
{title: '基础信息', description: '', icon: '', status: ''},
{title: '审批表单', description: '', icon: '', status: ''},
// {title: '审批流程', description: '', icon: '', status: ''},
// {title: '扩展设置', description: '', icon: '', status: ''}
])
const errTitle = computed(()=>{
if (validResult.finished && !validResult.success) {
return validResult.title + ` (${validResult.errs.length}项错误) 😥`
}
return validResult.title
})
const validIcon = computed(()=>{
if (!validResult.finished) {
return Loading
} else if (validResult.success) {
return CircleCheckFilled
} else {
return WarningFilled
}
})
const init = () => {
let deploymentId = params.deploymentId
if (deploymentId === undefined){
loadInitFrom()
}else {
getProcessInfo()
}
}
const loadInitFrom = () => {
let design = {
processDefinitionKey: 'pro' + getRandomId(),
deploymentName: "未命名表单",
logo: {
icon: "el-icon-eleme",
background: "#1e90ff"
},
settings: {
commiter: [],
admin: [],
sign: false,
notify: {
types: ["APP"],
title: "消息通知标题"
}
},
groupId: 1,
formItems: [],
process: [
{
id: "root",
parentId: "admin",
type: "ROOT",
name: "发起人",
desc: "任何人",
props: {
assignedUser: [],
formPerms: []
},
},
{
id: "end",
parentId: "root",
type: "END",
}
],
remark: "备注说明"
}
processStore.setDesign(design)
visible.value = true
nextTick(()=>{
processDesign.value.initRender()
})
}
const getRandomId = () => {
let d = new Date().getTime()
// x 是 0-9 或 a-f 范围内的一个32位十六进制数
let id = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
let r = (d + Math.random() * 16) % 16 | 0
d = Math.floor(d / 16)
return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16)
})
return 'node_' + id
}
const getProcessInfo = async () => {
getProcessDefinitionInfo(params.deploymentId).then(res => {
if (res.code === 1000) {
processStore.setDesign(res.data)
visible.value = true
nextTick(()=>{
processDesign.value.initRender()
})
} else {
ElMessage.error(res.msg)
}
})
}
const validateDesign = () => {
validVisible.value = true
validStep.value = 0
showValiding()
stopTimer()
timer.value = setInterval(() => {
validResult.value.errs = proxy.$refs[validComponents.value[validStep.value]].validate()
if (Array.isArray(validResult.value.errs) && validResult.value.errs.length === 0) {
validStep.value++;
if (validStep.value >= validOptions.value.length) {
stopTimer()
showValidFinish(true)
}
} else {
stopTimer()
validOptions.value[validStep.value].status = 'error'
showValidFinish(false, getDefaultValidErr())
}
}, 300)
}
const getDefaultValidErr = () => {
switch (validStep.value) {
case 0:
return '请检查基础设置项';
case 1:
return '请检查审批表单相关设置'
// case 2:
// return '请检查审批流程,查看对应标注节点错误信息'
// case 3:
// return '请检查扩展设置'
default:
return '未知错误'
}
}
const showValidFinish = (success, err) => {
console.log("处理完成")
validResult.value.success = success
validResult.value.finished = true
validResult.value.title = success ? '校验完成 😀' : '校验失败 '
validResult.value.desc = success ? '设置项校验成功,是否提交?' : err
validResult.value.action = success ? '提 交' : '去修改'
}
const showValiding = () => {
validResult.value = {
errs: [],
finished: false,
success: false,
title: '检查中...',
action: '处理',
desc: '正在检查设置项'
}
validStep.value = 0
validOptions.value.forEach(op => {
op.status = ''
op.icon = ''
op.description = ''
})
}
const doAfter = () => {
if (validResult.value.success) {
doPublish()
} else {
activeSelect.value = validComponents.value[validStep.value]
validVisible.value = false
}
}
const tarry = (node) => {
if (node && node.id) {
let newNode = {...node}
newNode.children = null
array.push(newNode)
tarry(node.children)
}
}
const stopTimer = () => {
if (timer.value) {
clearInterval(timer.value)
}
}
const preview = () => {
validateDesign()
}
//发布流程
const publishProcess = () => {
validateDesign()
}
// todo 提交数据
const doPublish = () => {
ElMessageBox.confirm('如果您只想预览请选择预览,确认发布后流程立即生效,是否继续?', '提示', {
confirmButtonText: '发布',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
let design = processStore.getDesign()
console.log(design)
let template = {
...design
}
console.log(template)
addProcessDefinition(template).then(res => {
if (res.code === 1000){
ElMessage.success(res.msg)
// this.$router.push("/formsPanel")
}else {
ElMessage.error(res.msg)
}
}).catch(err => {
ElMessage.error(err)
})
})
}
const changPan = (val) => {
activeSelect.value = val
}
init()
</script>
<style lang="scss" scoped>
.layout-body {
min-width: 980px;
}
.el-step {
.is-success {
color: #2a99ff;
border-color: #2a99ff;
}
}
.err-info {
max-height: 180px;
overflow-y: auto;
& > div {
padding: 5px;
margin: 2px 0;
width: 220px;
text-align: left;
border-radius: 3px;
background: rgb(242 242 242);
}
i {
margin: 0 5px;
}
}
::-webkit-scrollbar {
width: 2px;
height: 2px;
background-color: white;
}
::-webkit-scrollbar-thumb {
border-radius: 16px;
background-color: #e8e8e8;
}
</style>

View File

@@ -0,0 +1,762 @@
<script setup>
import RootNode from './nodes/RootNode.vue'
import END from './nodes/ProcessEndNode.vue'
import APPROVAL from './nodes/ApprovalNode.vue'
import CcNode from './nodes/CcNode.vue'
import ConcurrentNode from './nodes/ConcurrentNode.vue'
import ConditionNode from './nodes/ConditionNode.vue'
import EmptyNode from './nodes/EmptyNode.vue'
import TriggerNode from './nodes/TriggerNode.vue'
import MergeNode from './nodes/MergeNode.vue'
import DelayNode from './nodes/DelayNode.vue'
import AddBranchNode from './nodes/AddBranchNode.vue'
import {defineExpose, h, render, ref} from 'vue'
import DefaultProps from "./DefaultNodeProps"
import {ElMessage, ElMessageBox} from 'element-plus'
import {useProcessStore} from '@/stores/processStore.js'
const processStore = useProcessStore()
const emit = defineEmits()
const props = defineProps({
mode: {
type: String,
default: 'design'
},
idName: {
type: String,
default: 'processTree'
}
})
const valid = ref(true)
let vNode = {}
const init = () => {
processStore.init()
initMapping(processStore.getProcess())
// 定义类名(可忽略)
let processTrees = getDomTree(h, "admin")
vNode = []
const dom = document.getElementById(props.idName);
vNode = h('div', {class: {'_root': true}}, processTrees);
render(vNode, dom)
}
// 初始化map集合,以便数据整理
const initMapping = (node) => {
console.log("初始化数据", node)
node.forEach(nodeItem => {
processStore.nodeMap.set(nodeItem.id, nodeItem)
processStore.parentMap.set(nodeItem.parentId, nodeItem)
})
}
const initHeaderBgc = (node) => {
if (node.props && props.mode === 'preview') {
let headerBgc = '#ff943e'
if (processStore.runningList.value.includes(node.id)) {
headerBgc = '#1e90ff'
} else if (processStore.endList.value.includes(node.id)) {
headerBgc = '#20b2aa'
} else if (processStore.noTakeList.value.includes(node.id)) {
headerBgc = '#909399'
} else if (processStore.refuseList.value.includes(node.id)) {
headerBgc = '#f56c6c'
} else if (processStore.passList.value.includes(node.id)) {
headerBgc = '#ff943e'
}
node.props.headerBgc = headerBgc
}
}
// 获取demo的树形结构
const getDomTree = (h, id) => {
let node = processStore.parentMap.get(id)
if (!(node && node.id)) {
return []
}
initHeaderBgc(node)
if (isPrimaryNode(node)) {
//普通业务节点
let childDoms = getDomTree(h, node.id)
decodeAppendDom(h, node, childDoms)
return [h('div', {'class': {'primary-node': true}}, childDoms)];
} else if (isBranchNode(node)) {
let index = 0;
//遍历分支节点,包含并行及条件节点
let branchItems = node.branchs.map(branchNode => {
//处理每个分支内子节点
toMapping(branchNode)
let childDoms = getDomTree(h, branchNode.id)
decodeAppendDom(h, branchNode, childDoms, {level: index + 1, size: node.branchs.length})
//插入4条横线遮挡掉条件节点左右半边线条
insertCoverLine(h, index, childDoms, node.branchs)
//遍历子分支尾部分支
index++;
return h('div', {'class': {'branch-node-item': true}}, childDoms);
})
//插入添加分支/条件的按钮
branchItems.unshift(h('div', {'class': {'add-branch-btn': true}}, [
h(AddBranchNode, {
mode: props.mode,
size: 'small', round: true,
value: `添加${isConditionNodes(node) ? '条件' : '分支'}`,
onAddBranch: () => addBranchNode(node),
}, [])
]));
let bchDom = [h('div', {'class': {'branch-node': true}}, branchItems)]
//继续遍历分支后的节点
let afterChildDoms = getDomTree(h, node.id)
return [h('div', {}, [bchDom, afterChildDoms])]
} else if (isMergeNode(node)) {
//空节点,存在于分支尾部
let childDoms = getDomTree(h, node.id)
decodeAppendDom(h, node, childDoms)
return [h('div', {'class': {'empty-node': true}}, childDoms)];
} else if (isEmptyNode(node)) {
//空节点,存在于分支尾部
let childDoms = getDomTree(h, node.id)
decodeAppendDom(h, node, childDoms)
return [h('div', {'class': {'empty-node': true}}, childDoms)];
} else if (isEndNode(node)) {
let childDoms = getDomTree(h, node.id)
decodeAppendEndDom(h, node, childDoms)
return [h('div', {class: 'process-end'}, childDoms)];
}
return []
}
const decodeAppendEndDom = (h, node, dom, props = {}) => {
props.config = node
dom.unshift(h(END, {
id: node.id,
key: node.id,
}, []))
}
//解码渲染的时候插入dom到同级
const decodeAppendDom = (h, node, dom, nodeProps = {}) => {
nodeProps.config = node
dom.unshift(h(getNodeType(node), {
mode: props.mode,
...nodeProps,
id: node.id,
key: node.id,
//定义事件,插入节点,删除节点,选中节点,复制/移动
'onInsertNode': type => insertNode(type, node),
'onDelNode': () => delNode(node),
'onSelected': () => selectNode(node),
'onCopy': () => copyBranch(node),
'onLeftMove': () => branchMove(node, -1),
'onRightMove': () => branchMove(node, 1)
}, []))
}
const getNodeType = (node) => {
switch (node.type) {
case "ROOT":
return RootNode;
case "APPROVAL":
return APPROVAL;
case "CC":
return CcNode;
case "CONDITION":
return ConditionNode;
case "CONCURRENT":
return ConcurrentNode;
case "DELAY":
return DelayNode;
case "MERGE":
return MergeNode;
case "EMPTY":
return EmptyNode;
case "TRIGGER":
return TriggerNode;
}
}
//id映射到map用来向上遍历
const toMapping = (node) => {
if (node && node.id) {
let newNode = {
...node
}
newNode.children = []
processStore.nodeMap.set(newNode.id, newNode)
}
}
// 新增线条
const insertCoverLine = (h, index, doms, branchs) => {
if (index === 0) {
//最左侧分支
doms.unshift(h('div', {'class': {'line-top-left': true}}, []))
doms.unshift(h('div', {'class': {'line-bot-left': true}}, []))
}
if (index === branchs.length - 1) {
//最右侧分支
doms.unshift(h('div', {'class': {'line-top-right': true}}, []))
doms.unshift(h('div', {'class': {'line-bot-right': true}}, []))
}
}
const copyBranch = (node) => {
let parentNode = processStore.nodeMap.get(node.parentId)
let branchNode = deepCopy(node)
branchNode.name = branchNode.name + '-copy'
forEachNode(parentNode, branchNode, (parent, node) => {
node.id = getRandomId()
node.parentId = parent.id
})
parentNode.branchs.splice(parentNode.branchs.indexOf(node), 0, branchNode)
init()
}
//移动分支节点
const branchMove = (node, offset) => {
let parentNode = processStore.nodeMap.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
init()
}
//判断是否为主要业务节点
const isPrimaryNode = (node) => {
return node &&
(node.type === 'ROOT' || node.type === 'APPROVAL'
|| node.type === 'CC' || node.type === 'DELAY'
|| node.type === 'TRIGGER');
}
//是否为分支节点
const isBranchNode = (node) => {
return node && (node.type === 'CONDITIONS' || node.type === 'CONCURRENTS');
}
//是否为空节点
const isEmptyNode = (node) => {
return node && (node.type === 'EMPTY')
}
const isEndNode = (node) => {
return node && (node.type === 'END')
}
//是否为空节点
const isMergeNode = (node) => {
return node && (node.type === 'MERGE')
}
//是分支节点
const isConditionNodes = (node) => {
return node.type === 'CONDITIONS';
}
const isConditionNode = (node) => {
return node.type === 'CONDITION';
}
//是分支节点
const isBranchSubNode = (node) => {
return node && (node.type === 'CONDITION' || node.type === 'CONCURRENT');
}
//时候并行节点
const isConcurrentNodes = (node) => {
return node.type === 'CONCURRENTS'
}
//时候并行节点
const isConcurrentNode = (node) => {
return node.type === 'CONCURRENT'
}
//新增一个节点id
const getRandomId = () => {
let d = new Date().getTime()
// x 是 0-9 或 a-f 范围内的一个32位十六进制数
let id = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
let r = (d + Math.random() * 16) % 16 | 0
d = Math.floor(d / 16)
return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16)
})
return 'node_' + id
}
//选中一个节点
const selectNode = (node) => {
processStore.setSelectedNode(node)
if (!isConcurrentNode(node)) {
emit('selectedNode', node)
}
}
//处理节点插入逻辑
const insertNode = (type, parentNode) => {
//插入新节点
let id = getRandomId();
updateParentId(id, parentNode.id)
let children = {
id: id,
parentId: parentNode.id,
type: type,
}
switch (type) {
case 'APPROVAL':
insertApprovalNode(children);
break;
case 'CC':
insertCcNode(children);
break;
case 'DELAY':
insertDelayNode(children);
break;
case 'TRIGGER':
insertTriggerNode(children);
break;
case 'CONDITIONS':
insertConditionsNode(children);
break;
case 'CONCURRENTS':
insertConcurrentsNode(children);
break;
default:
break;
}
console.log('开始刷新')
init()
}
/**
* 更新父id
* @param newId
* @param oldId
*/
const updateParentId = (newId, oldId) => {
processStore.getProcess().map(node => {
if (node.parentId === oldId) {
node.parentId = newId
}
})
}
/**
* 审批人
* @param parentNode
*/
const deepCopy = (obj) => {
return JSON.parse(JSON.stringify(obj))
}
const insertApprovalNode = (parentNode) => {
let node = {
...parentNode,
name: "审批人",
props: deepCopy(DefaultProps.APPROVAL_PROPS)
}
processStore.addProcess(node)
}
/**
* 抄送人
* @param node
*/
const insertCcNode = (node) => {
let newNode = {
...node,
name: "抄送人",
props: deepCopy(DefaultProps.CC_PROPS)
}
processStore.addProcess(newNode)
}
/**
* 延时处理
* @param node
*/
const insertDelayNode = (node) => {
let newNode = {
...node,
name: "延时处理",
props: deepCopy(DefaultProps.DELAY_PROPS)
}
processStore.addProcess(newNode)
}
/**
* 触发器
* @param node
*/
const insertTriggerNode = (node) => {
let newNode = {
...node,
name: "触发器",
props: deepCopy(DefaultProps.TRIGGER_PROPS)
}
processStore.addProcess(newNode)
}
/**
* 新增条件分支F
* @param node
*/
const insertConditionsNode = (node) => {
let newNode = {
...node,
name: "条件分支",
branchs: [
{
id: getRandomId(),
parentId: node.id,
type: "CONDITION",
props: deepCopy(DefaultProps.CONDITION_PROPS),
name: "条件1",
children: {}
}, {
id: getRandomId(),
parentId: node.id,
type: "CONDITION",
props: deepCopy(DefaultProps.CONDITION_PROPS),
name: "条件2",
children: {}
}
]
}
processStore.addProcess(newNode)
let emptyNode = {
id: getRandomId(),
parentId: node.id,
type: "EMPTY"
}
updateParentId(emptyNode.id, newNode.id)
processStore.addProcess(emptyNode)
}
/**
* 新增同步运行节点
* @param node
*/
const insertConcurrentsNode = (node) => {
let newNode = {
...node,
name: "并行分支",
branchs: [
{
id: getRandomId(),
parentId: node.id,
type: "CONCURRENT",
props: deepCopy(DefaultProps.CONDITION_PROPS),
name: "分支1",
children: {}
}, {
id: getRandomId(),
parentId: node.id,
type: "CONCURRENT",
props: deepCopy(DefaultProps.CONDITION_PROPS),
name: "分支2",
children: {}
}
]
}
processStore.addProcess(newNode)
let emptyNode = {
id: getRandomId(),
parentId: node.id,
type: "MERGE"
}
updateParentId(emptyNode.id, newNode.id)
processStore.addProcess(emptyNode)
}
const addBranchNode = (node) => {
if (node.branchs.length < 8) {
node.branchs.push({
id: getRandomId(),
parentId: node.id,
name: (isConditionNodes(node) ? '条件' : '分支') + (node.branchs.length + 1),
props: isConditionNodes(node) ? deepCopy(DefaultProps.CONDITION_PROPS) : {},
type: isConditionNodes(node) ? "CONDITION" : "CONCURRENT",
children: {}
})
init()
} else {
ElMessage.warning("最多只能添加 8 项😥")
}
}
//删除当前节点
const delNode = (node) => {
//获取该节点的父节点
let parentNode = processStore.nodeMap.get(node.parentId)
if (parentNode) {
if (isBranchNode(parentNode)) {
delBranchNode(parentNode, node)
} else {
delNodeInDomChange(node.id, parentNode.id)
}
init()
} else {
ElMessage.warning("出现错误,找不到上级节点😥")
}
}
/**
* 从dom中删除
* @param delId
* @param parentId
*/
const delNodeInDomChange = (delId, parentId) => {
updateParentId(parentId, delId)
let delNode = processStore.nodeMap.get(delId)
processStore.delProcess(delNode)
init()
}
//删除分支
const delBranchNode = (parentNode, node) => {
let sunNode = processStore.parentMap.get(node.id)
//判断当前节点下有没有字节点,有则需要提示
if (sunNode) {
ElMessageBox.confirm('当前分支下有子节点,是否继续?', '提示', {
confirmButtonText: '确 定',
cancelButtonText: '取 消',
type: 'warning'
}).then(() => {
//确认后进行子节点的操作
delBranchSunNode(sunNode.id)
doDelBranchNode(parentNode, node)
})
} else {
// 没有直接开始删除
doDelBranchNode(parentNode, node)
}
}
//删除分支节点
const doDelBranchNode = (parentNode, node) => {
//判断当前分时是否为2
if (parentNode.branchs.length === 2) {
let nodeList = [...parentNode.branchs]
nodeList.splice(nodeList.indexOf(node), 1)
//查看另外一个分支上是否有节点
let sunNode = processStore.parentMap.get(nodeList[0].id)
//有则需要放到主分支上
if (sunNode) {
//更改分支上第一个节点的父id
updateParentId(parentNode.parentId, sunNode.parentId)
//找到最后一个节点
let lastNode = getLastBranchNode(sunNode.id)
let emptyNode = processStore.parentMap.get(parentNode.id)
//更新空节点下的第一个节点的id为当前分支最后一个节点
updateParentId(lastNode.id, emptyNode.id)
//删除分支的主节点
delNodeInDom(parentNode)
//删除分支的空节点
delNodeInDom(emptyNode)
} else {
//没有则直接删除
delEntireBranch(parentNode)
}
} else {
parentNode.branchs.splice(parentNode.branchs.indexOf(node), 1)
}
}
//获取最后一个节点
const getLastBranchNode = (id) => {
let node = processStore.parentMap.get(id)
if (node) {
return getLastBranchNode(node.id)
} else {
return processStore.nodeMap.get(id)
}
}
const delEntireBranch = (node) => {
//删除分支节点和空节点
let emptyNode = processStore.parentMap.get(node.id)
delNodeInDomChange(node.id, node.parentId)
delNodeInDomChange(emptyNode.id, emptyNode.parentId)
}
const delNodeInDom = (delNode) => {
processStore.delProcess(delNode)
init()
}
//删除分支的子节点
const delBranchSunNode = (id) => {
let node = processStore.parentMap.get(id)
delNodeInDomChange(id)
if (node) {
delBranchSunNode(node.id)
}
}
//给定一个起始节点,遍历内部所有节点
const forEachNode = (parent, node, callback) => {
if (isBranchNode(node)) {
callback(parent, node)
forEachNode(node, node.children, callback)
node.branchs.map(branchNode => {
callback(node, branchNode)
forEachNode(branchNode, branchNode.children, callback)
})
} else if (isPrimaryNode(node) || isEmptyNode(node) || isBranchSubNode(node)) {
callback(parent, node)
forEachNode(node, node.children, callback)
}
}
const validateProcess = () => {
valid.value = true
let err = []
validate(err, processStore.getProcess())
return err
}
const validateNode = (err, node, nodeMap) => {
let component = nodeMap.get(node.id)
if (component !== undefined) {
valid.value = component.exposed.validate(err)
}
}
const validateNodeList = ['APPROVAL', 'CC', 'CONDITION', 'DELAY', 'TRIGGER']
const analysisNode = (vNode, nodeMap) => {
let children = vNode.children
if (!children || children.length === 0) {
return
}
for (let child of children) {
let node = processStore.nodeMap.get(child.key)
if (node !== undefined && node && validateNodeList.includes(node.type)) {
nodeMap.set(node.id, child.component)
} else {
analysisNode(child, nodeMap)
}
}
}
//校验所有节点设置
const validate = (err, nodeList) => {
const nodeMap = new Map()
analysisNode(vNode, nodeMap)
nodeList.map(node => {
if (isPrimaryNode(node)) {
//校验条件节点
validateNode(err, node, nodeMap)
} else if (isBranchNode(node)) {
node.branchs.map(branchNode => {
//校验条件节点
validateNode(err, branchNode, nodeMap)
})
}
})
}
defineExpose({
init,
validateProcess
})
</script>
<style lang="scss">
._root {
margin: 0 auto;
}
.process-end {
width: 60px;
margin: 0 auto;
margin-bottom: 20px;
border-radius: 15px;
text-align: center;
padding: 5px 10px;
font-size: small;
color: #747474;
background-color: #f2f2f2;
box-shadow: 0 0 10px 0 #bcbcbc;
}
.primary-node {
display: flex;
align-items: center;
flex-direction: column;
}
.branch-node {
display: flex;
justify-content: center;
position: relative;
/*border-top: 2px solid #cccccc;
border-bottom: 2px solid #cccccc;*/
}
.branch-node-item {
position: relative;
display: flex;
//background: #f5f6f6;
flex-direction: column;
align-items: center;
border-top: 2px solid #cccccc;
border-bottom: 2px solid #cccccc;
&:before {
content: "";
position: absolute;
top: 0;
left: calc(50% - 1px);
margin: auto;
width: 2px;
height: 100%;
background-color: #CACACA;
}
.line-top-left, .line-top-right, .line-bot-left, .line-bot-right {
position: absolute;
width: 50%;
height: 4px;
background-color: #fff;
}
.line-top-left {
top: -2px;
left: -1px;
}
.line-top-right {
top: -2px;
right: -1px;
}
.line-bot-left {
bottom: -2px;
left: -1px;
}
.line-bot-right {
bottom: -2px;
right: -1px;
}
}
.add-branch-btn {
position: absolute;
width: 80px;
.add-branch-btn-el {
z-index: 999;
position: absolute;
top: -15px;
}
}
.empty-node {
display: flex;
justify-content: center;
flex-direction: column;
align-items: center;
}
</style>

View File

@@ -0,0 +1,107 @@
<template>
<div :class="{'line': row === 1, 'lines': row > 1}"
:title="hoverTip ? content: null"
:style="{'--row':row}">
<slot name="pre"></slot>
<div style="display: flex;flex-wrap: wrap;">
<div v-for="(user,index) in userInfo" :key="index" class="avatar_name">
<el-avatar size="large"
:src="user.avatar"></el-avatar>
<div v-if="user.icon"
class="el-timeline-item__node" :style="{
backgroundColor: user.color
}">
<el-icon v-if="user.icon" size="15" :class="user.class">
<component :is="user.icon"/>
</el-icon>
</div>
<el-tooltip class="item" effect="dark" :content="user.name" placement="bottom-start">
<span class="item_name">{{ user.name }}</span>
</el-tooltip>
</div>
</div>
</div>
</template>
<script setup>
import {Loading,Close,CircleCheckFilled,MoreFilled} from '@element-plus/icons-vue'
import {defineProps} from "vue";
const props = defineProps({
row: {
type: Number,
default: 1
},
hoverTip: {
type: Boolean,
default: false
},
userInfo: {
type: Array,
default: []
}
})
const init = () => {
for (let user of props.userInfo) {
initUser(user)
}
}
const initUser = (user) => {
let state = user.state
//创建节点
if (state === 'CREATE') {
user["icon"] = CircleCheckFilled
user["color"] = "#0bbd87"
}
//审批通过
if (state === 'AGREE' || state === 'AUTO_PASS') {
user["icon"] = CircleCheckFilled
user["color"] = "#0bbd87"
}
//审批处理中
if (state === 'RUNNING') {
user["icon"] = Loading
user["color"] = "#f78f5f"
user["class"] = 'is-loading'
}
//拒绝后评论
if (state === 'REFUSE') {
user["icon"] = Close
user["color"] = "#f56c6c"
}
if (state === 'PASS'){
user["icon"] = MoreFilled
user["color"] = "#c0c4cc"
}
return user;
}
init()
</script>
<style scoped>
.avatar_name {
display: flex;
flex-direction: column;
align-items: center;
margin-right: 1px;
/*width: 45px;*/
position: relative;
}
.el-timeline-item__node {
position: absolute;
bottom: 20px;
right: 1px;
}
.item_name {
width: 45px;
text-align: center;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
padding-top: 3px;
}
</style>

View File

@@ -0,0 +1,329 @@
<template>
<el-dialog custom-class="custom-dialog" class="border" :border="false" closeFree width="600px"
:title="title" :visible.sync="visible" v-model="visible" append-to-body :close-on-click-modal="true"
:destroy-on-close="true">
<div>
<div class="picker">
<div class="candidate" v-loading="loading">
<div style="padding: 5px 8px;">
<el-input v-model="filterText" style="width: 100%;" size="small"
clearable placeholder="输入关键字进行过滤" prefix-icon="Search"/>
</div>
<!-- 部门 check-strictly-->
<el-empty :image-size="100" description="似乎没有数据" v-show="deptList.length === 0"/>
<el-scrollbar style="height:350px">
<el-tree :data="deptList" ref="tree" :props="deptProps" empty-text="" node-key="value" default-expand-all
:show-checkbox="showCheckbox" highlight-current :check-strictly="multiple===false"
@check-change="handleCheckChange" @node-click="(node,check)=>handle(node,check)"
:filter-node-method="filterNode">
<template #default="{ node, data }">
<el-icon style="margin-right: 5px">
<FolderOpened/>
</el-icon>
{{ node.label }}
</template>
</el-tree>
</el-scrollbar>
</div>
<div class="selected">
<div class="count">
<span>已选 {{ selectList.length }} 项</span>
<span @click="clearSelected">清空</span>
</div>
<div class="org-items" style="height: 350px;">
<el-empty :image-size="100" description="请点击左侧列表选择数据" v-show="selectList.length === 0"/>
<div v-for="(selectItem, selectIndex) in selectList" :key="selectIndex" class="org-item">
<i class="el-icon-folder-opened"></i>
<span>{{ selectItem.label }}</span>
<i class="el-icon-close" @click="noSelected(selectItem)" v-if="showCheckbox===false"></i>
</div>
</div>
</div>
</div>
</div>
<div slot="footer">
<el-button size="mini" @click="visible = false">取 消</el-button>
<el-button size="mini" type="primary" @click="selectConfirm">确 定</el-button>
</div>
</el-dialog>
</template>
<script setup>
import {getDepartmentTree} from "@/api/workflow/process-user";
import {computed, defineExpose, defineProps} from "vue";
import {ElMessage,ElMessageBox} from "element-plus";
const emit = defineEmits(["input"]);
const props = defineProps({
value: {
type: Array,
default: () => {
return [];
}
},
multiple: { //是否多选
default: true,
type: Boolean
},
showCheckbox: { //是否显示左侧选择框
default: true,
type: Boolean
}
});
const visible = ref(false);
const loading = ref(false);
const title = ref("请选择");
const selectList = ref([]);
const deptList = ref([]);
const filterText = ref("");
const tree = ref([]);
const deptProps = reactive({
value: "value",
label: "label",
children: "children"
});
watch(() => filterText, (newVal, oldVal) => {
tree.value.filter(newVal);
});
const _value = computed({
get() {
return props.value;
}
,
set(val) {
emit("input", val);
}
});
const getDepartmentTreeList = () => {
//获取部门信息
getDepartmentTree().then(res => {
deptList.value = res.data;
console.log("获取部门信息===========", res.data);
});
};
const filterNode = (value, data) => {
//通过关键字过滤树节点
if (!value) return true;
return data.deptName.indexOf(value) !== -1;
};
const showDeptPicker = () => {
//用于弹开部门选择
visible.value = true;
};
/**
* 选中部门
* @param data 选择的每个节点item
* @param checked 是否选中
*/
const handleCheckChange = (data, checked) => {
// 左侧有选择框
if (props.showCheckbox) {
// 左侧有选择框 + 多选
if (props.multiple) {
//不添加重复的数据到右边
for (let i = 0; i < selectList.value.length; i++) {
if (selectList.value[i].value === data.value) {
selectList.value.splice(i, 1);
break;
}
}
if (checked) {
if (data.children === undefined) {
selectList.value.push(data);
}
} else if (data === "1") {
tree.value.setCheckedKeys([]);
selectList.value = [];
}
} else {// 左侧有选择框 + 单选
//不添加重复的数据到右边
for (let i = 0; i < selectList.value.length; i++) {
if (selectList.value[i].value === data.value) {
selectList.value.splice(i, 1);
break;
}
}
if (checked) {
tree.value.setCheckedNodes([data]);
// this.$refs.tree.setCheckedKeys([]);
selectList.value = [data];
} else if (data === "1") {
selectList.value = [];
tree.value.setCheckedKeys([]);
}
}
}
// _value.value = selectList.value
};
//左侧没有选择框时,点击tree-item
/**
* 可以点击树节点deptName,进行选择
* @param node 选择的每个节点item
* @param check checked(checkbox选择框)是否选中
*/
const handle = (node, check) => {
if (check.isLeaf !== false) {
if (props.multiple) {
//不添加重复的数据到右边
for (let i = 0; i < selectList.value.length; i++) {
if (selectList.value[i].value === node.value) {
selectList.value.splice(i, 1);
break;
}
}
check.checked = true;
selectList.value.push(node);
} else {
check.checked = true;
selectList.value = [node];
}
}
// _value.value = selectList.value
};
const noSelected = (selectItem) => {
//左侧无选择框时,右侧显示×
for (let i = 0; i < selectList.value.length; i++) {
if (selectList.value[i].value === selectItem.value) {
selectList.value.splice(i, 1);
tree.value.setCheckedKeys(i);
break;
}
}
if (props.showCheckbox) {
// 左侧有选择框 + 单选
if (props.multiple === false) {
tree.value.setCheckedKeys([]);
}
}
};
//清空
const clearSelected = () => {
ElMessageBox.confirm("您确定要清空已选中的项?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
if (!props.showCheckbox) {
selectList.value = [];
} else {
handleCheckChange("1");
}
});
};
//确定按钮
const selectConfirm = () => {
emit("ok", selectList.value);
visible.value = false;
};
defineExpose({
showDeptPicker
});
getDepartmentTreeList();
</script>
<style lang="scss" scoped>
$containWidth: 278px;
///deep/ .el-tree-node {
// .is-leaf + .el-checkbox .el-checkbox__inner {
// display: inline-block;
// }
//
// .el-checkbox .el-checkbox__inner {
// display: none;
// }
//}
.el-dialog__body {
padding: 10px 20px;
}
.picker {
height: 402px;
position: relative;
text-align: left;
.candidate {
left: 0;
top: 0;
}
}
.candidate, .selected {
position: absolute;
display: inline-block;
width: $containWidth;
height: 400px;
border: 1px solid #e8e8e8;
}
.selected {
border-left: none;
right: 0;
top: 0;
.count {
width: calc(var($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;
}
}
}
.org-items {
overflow-y: auto;
height: 350px;
.el-icon-close {
position: absolute;
right: 5px;
cursor: pointer;
font-size: larger;
}
.org-item {
margin: 0 5px;
border-radius: 5px;
position: relative;
padding: 7px 5px;
display: flex;
align-items: center;
cursor: pointer;
&:hover {
background: #f1f1f1;
}
> span {
margin-left: 5px;
}
}
}
.el-scrollbar .el-scrollbar__wrap {
overflow-x: hidden;
}
::-webkit-scrollbar {
float: right;
width: 4px;
height: 4px;
background-color: white;
}
::-webkit-scrollbar-thumb {
border-radius: 16px;
background-color: #efefef;
}
</style>

View File

@@ -0,0 +1,45 @@
<template>
<div :class="{'line': row === 1, 'lines': row > 1}"
:title="hoverTip ? content: null"
:style="{'--row':row}">
<slot name="pre"></slot>
{{content}}
</div>
</template>
<script setup>
import {defineProps} from "vue";
const props = defineProps({
row: {
type: Number,
default: 1
},
hoverTip:{
type: Boolean,
default: false
},
content:{
type: String,
default: ''
}
})
</script>
<style lang="scss" scoped>
.line{
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-right: 10px;
}
.lines{
display: -webkit-box;
word-break: break-all;
overflow: hidden;
text-overflow: ellipsis;
-webkit-line-clamp: var(--row);
-webkit-box-orient: vertical;
margin-right: 10px;
}
</style>

View File

@@ -0,0 +1,117 @@
<template>
<el-popover :visible="visible" placement="bottom-start" title="添加流程节点" width="350">
<div class="node-select">
<div @click="addApprovalNode">
<el-icon style="color:rgb(255, 148, 62);">
<Coordinate/>
</el-icon>
<span>审批人</span>
</div>
<div @click="addCcNode">
<el-icon style="color:rgb(50, 150, 250);">
<Promotion/>
</el-icon>
<span>抄送人</span>
</div>
<div @click="addConditionsNode">
<el-icon style="color:rgb(21, 188, 131);">
<Share/>
</el-icon>
<span>条件分支</span>
</div>
<div @click="addConcurrentsNode">
<el-icon style="color:#718dff;">
<Operation/>
</el-icon>
<span>并行分支</span>
</div>
<div @click="addDelayNode">
<el-icon style="color:#f25643;">
<Clock/>
</el-icon>
<span>延迟等待</span>
</div>
<div @click="addTriggerNode">
<el-icon style="color:#15BC83;">
<SetUp/>
</el-icon>
<span>触发器</span>
</div>
</div>
<template #reference>
<el-button :icon="Plus" slot="reference" type="primary" @click="visible = !visible" size="small" circle></el-button>
</template>
</el-popover>
</template>
<script setup>
import {Plus } from '@element-plus/icons-vue'
const emit = defineEmits(['insertNode'])
const visible = ref(false)
const addApprovalNode = () => {
emit('insertNode', "APPROVAL")
disVisible()
}
const addCcNode = () => {
emit('insertNode', "CC")
disVisible()
}
const addDelayNode = () => {
emit('insertNode', "DELAY")
disVisible()
}
const addConditionsNode = () => {
emit('insertNode', "CONDITIONS")
disVisible()
}
const addConcurrentsNode = () => {
emit('insertNode', "CONCURRENTS")
disVisible()
}
const addTriggerNode = () => {
emit('insertNode', "TRIGGER")
disVisible()
}
const disVisible = () =>{
visible.value = false
}
</script>
<style lang="scss" scoped>
.node-select {
div {
display: inline-block;
margin: 5px 5px;
cursor: pointer;
padding: 10px 15px;
border: 1px solid #F8F9F9;
background-color: #F8F9F9;
border-radius: 10px;
width: 130px;
position: relative;
span {
position: absolute;
left: 65px;
top: 18px;
}
&:hover {
background-color: #fff;
box-shadow: 0 0 8px 2px #d6d6d6;
}
i {
font-size: 25px;
padding: 5px;
border: 1px solid #dedfdf;
border-radius: 14px;
}
}
}
</style>

View File

@@ -0,0 +1,36 @@
<template>
<div style="margin-top: 10px">
<el-tag class="role-item" v-model="_value" v-for="(role, index) in _value" :key="index + '_role'"
closable size="mini" @close="removeRoleItem(index)">
{{ role.roleName }}
</el-tag>
</div>
</template>
<script setup>
import {computed, defineProps,defineEmits} from "vue";
const emit =defineEmits(["input"])
const props = defineProps({
modelValue: {
type: Array,
default: []
}
})
const _value = computed({
get(){
return props.modelValue||''
},
set(val) {
emit("input", val)
}
})
const removeRoleItem = (index) => {
_value.value.splice(index, 1)
}
</script>
<style scoped>
.role-item {
margin: 5px;
}
</style>

View File

@@ -0,0 +1,276 @@
<template>
<el-dialog custom-class="custom-dialog" class="border" :border="false" closeFree width="600px" :title="title"
:visible.sync="visible" v-model="visible" append-to-body :close-on-click-modal="true"
:destroy-on-close="true">
<div class="picker">
<div class="candidate" v-loading="loading">
<div class="role-header">
<div>系统角色</div>
</div>
<div class="org-items">
<el-empty :image-size="100" description="似乎没有数据" v-show="roleList.length === 0"/>
<!-- 系统角色-->
<div class="org-role-item" v-for="(roleItem , roleIndex) in roleList" :key="roleIndex"
@click="selectChange(roleItem)">
<el-checkbox v-model="roleItem.selected" v-if="showCheckbox" @change="selectChange(roleItem)"></el-checkbox>
<i class="el-icon-user-solid" style="margin-left: 5px"></i>
<span>{{ roleItem.roleName }}</span>
</div>
</div>
</div>
<div class="selected">
<div class="count">
<span>已选 {{ selectList.length }} </span>
<span @click="clearSelected">清空</span>
</div>
<div class="org-items">
<el-empty :image-size="100" description="请点击左侧列表选择数据" v-show="selectList.length === 0"/>
<div v-for="(selectItem, selectIndex) in selectList" :key="selectIndex" class="org-role-item">
<i class="el-icon-user-solid"></i>
<span>{{ selectItem.roleName }}</span>
<i class="el-icon-close" @click="noSelected(selectItem)"></i>
</div>
</div>
</div>
</div>
<div slot="footer">
<el-button size="mini" @click="visible = false"> </el-button>
<el-button size="mini" type="primary" @click="selectConfirm"> </el-button>
</div>
</el-dialog>
</template>
<script setup>
import {getRole} from "@/api/workflow/process-user";
import {computed, defineProps, defineExpose} from "vue";
import {ElMessageBox} from "element-plus";
const emit = defineEmits();
const props = defineProps({
selected: {
default: () => {
return [];
},
type: Array
},
multiple: { //是否多选
default: true,
type: Boolean
},
showCheckbox: { //是否显示左侧选择框
default: true,
type: Boolean
}
});
const _value = computed({
get() {
return this.value;
},
set(val) {
emit("input", val);
}
});
const visible = ref(false);
const loading = ref(false);
const title = ref("请选择");
const selectList = ref([]);
const roleList = ref([]);
const getRoleList = () => {
//获取角色信息
getRole().then(res => {
roleList.value = res.data.map(function (val) {
return {roleId: val.value, roleName: val.label};
});
});
};
const showRolePicker = () => {
//用于弹出角色选择器
getRoleList();
visible.value = true;
};
const selectChange = (roleItem) => {
//选中角色
// 左侧有选择框
if (props.showCheckbox) {
// 左侧有选择框 + 多选
if (props.multiple) {
for (let i = 0; i < selectList.value.length; i++) {
if (selectList.value[i].roleId === roleItem.roleId) {
selectList.value.splice(i, 1);
break;
}
}
if (roleItem.selected) {
selectList.value.push(roleItem);
}
} else {// 左侧有选择框 + 单选
//用于左侧选择框选中取消,引起右侧数据变化
for (let i = 0; i < selectList.value.length; i++) {
if (selectList.value[i].roleId === roleItem.roleId) {
selectList.value.splice(i, 1);
break;
}
}
if (roleItem.selected) {
selectList.value = [roleItem];
}
for (let i = 0; i < roleList.value.length; i++) {
for (let j = 0; j < selectList.value.length; j++) {
if (roleList.value[i].roleId !== selectList.value[j].roleId) {
roleList.value[i].selected = false;
}
}
}
}
} else {// 左侧没有选择框
// 左侧没有选择框 + 多选
if (props.multiple) {
for (let i = 0; i < selectList.value.length; i++) {
if (selectList.value[i].roleId === roleItem.roleId) {
selectList.value.splice(i, 1);
break;
}
}
selectList.value.push(roleItem);
} else {// 左侧没有选择框 + 单选
selectList.value = [roleItem];
}
}
_value.value = selectList.value;
};
const noSelected = (selectItem) => {
//右侧的×
for (let i = 0; i < selectList.value.length; i++) {
if (selectList.value[i].roleId === selectItem.roleId) {
selectList.value.splice(i, 1);
break;
}
}
selectItem.selected = false;
};
const clearSelected = () => {
//清空
ElMessageBox.confirm("您确定要清空已选中的项?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
for (let i = 0; i < selectList.value.length; i++) {
if (selectList.value[i].selected === true) {
selectList.value[i].selected = false;
}
}
selectList.value = [];
});
};
const selectConfirm = () => {
//确定按钮
emit("ok", selectList.value);
visible.value = false;
};
defineExpose({
showRolePicker
});
</script>
<style lang="scss" scoped>
$containWidth: 278px;
.el-dialog__body {
padding: 10px 20px;
}
.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;
}
}
.candidate, .selected {
position: absolute;
display: inline-block;
width: $containWidth;
height: 400px;
border: 1px solid #e8e8e8;
}
.selected {
border-left: none;
right: 0;
top: 0;
.count {
width: calc(var($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;
}
}
}
.org-items {
overflow-y: auto;
height: 350px;
.el-icon-close {
position: absolute;
right: 5px;
cursor: pointer;
font-size: larger;
}
.org-role-item {
padding: 7px 5px;
display: flex;
align-items: center;
cursor: pointer;
margin: 0 5px;
border-radius: 5px;
position: relative;
&:hover {
background: #f1f1f1;
}
> span {
margin-left: 5px;
}
}
}
::-webkit-scrollbar {
float: right;
width: 4px;
height: 4px;
background-color: white;
}
::-webkit-scrollbar-thumb {
border-radius: 16px;
background-color: #efefef;
}
</style>

View File

@@ -0,0 +1,381 @@
<template>
<el-dialog custom-class="custom-dialog" class="border" :border="false" closeFree width="600px"
:title="title" :visible.sync="visible" v-model="visible" append-to-body :close-on-click-modal="true"
:destroy-on-close="true">
<div>
<div class="picker">
<div class="candidate" v-loading="loading">
<div style="padding: 5px 8px;">
<el-input v-model="filterText" style="width: 100%;" size="small"
clearable placeholder="输入关键字进行过滤" prefix-icon="el-icon-search"/>
<div style="margin-top: 5px">
<el-radio-group v-model="radio" size="mini" @change="radioChange">
<el-radio-button :label="0">角色</el-radio-button>
<el-radio-button :label="1">部门</el-radio-button>
</el-radio-group>
</div>
</div>
<!-- 人员选择 -->
<el-empty :image-size="100" description="似乎没有数据" v-show="dataList.length === 0"/>
<el-scrollbar style="height:317px">
<div class="tree">
<el-tree :data="dataList" ref="tree" :props="defaultProps" empty-text="" node-key="value"
:default-expanded-keys="expandedKeys" lazy accordion
@node-click="handleChange"
>
<template #default="{ node, data }">
<div class="tree-node">
<div v-if="data.type === 0" style="display: flex;align-items: center;padding: 3px 0">
<el-avatar :src="data.avatar"></el-avatar>
{{ node.label }}
</div>
<div v-else-if="data.type ===1">
<el-icon>
<UserFilled/>
</el-icon>
{{ node.label }}
</div>
<div v-else>
<el-icon>
<FolderOpened/>
</el-icon>
{{ node.label }}
</div>
</div>
</template>
</el-tree>
</div>
</el-scrollbar>
</div>
<div class="selected">
<div class="count">
<span>已选 {{ selectList.length }} </span>
<span @click="clearSelected">清空</span>
</div>
<div class="org-items" style="height: 350px;">
<el-empty :image-size="100" description="请点击左侧列表选择数据" v-show="selectList.length === 0"/>
<div v-for="(selectItem, selectIndex) in selectList" :key="selectIndex" class="org-item">
<el-avatar :src="selectItem.avatar" style="margin-right: 5px;"></el-avatar>
{{ selectItem.name}}
<i class="el-icon-close" @click="noSelected(selectItem)"></i>
</div>
</div>
</div>
</div>
</div>
<div slot="footer">
<el-button size="mini" @click="visible = false"> </el-button>
<el-button size="mini" type="primary" @click="selectConfirm"> </el-button>
</div>
</el-dialog>
</template>
<script setup>
import {getUserTree} from "@/api/workflow/process-user";
import {computed, defineProps, defineExpose} from "vue";
import {ElMessageBox} from "element-plus";
const props = defineProps({
value: {
type: Array,
default: () => {
return [];
}
},
multiple: { //是否多选
default: true,
type: Boolean
},
showCheckbox: { //是否显示左侧选择框
default: true,
type: Boolean
}
});
const radio = ref(0);
const chooseId = ref(0);
let selectItem = reactive({
type: -1,
value: "0"
});
const activeNames = ref(["1"]);
const visible = ref(false);
const loading = ref(false);
const title = ref("请选择");
const selectList = ref([]);
const filterText = ref("");
const dataList = ref([]);
const tree = ref([]);
const expandedKeys = ref([]);
const defaultProps = {
value: "value",
label: "name",
children: "children"
// isLeaf: function (data, node) {
// return data.type === 0;
};
const emit = defineEmits();
const _value = computed({
get() {
return props.value;
},
set(val) {
emit("input", val);
}
});
watch(() => filterText, (newVal, oldVal) => {
tree.value.filter(newVal);
});
const radioChange = (e) => {
selectItem.type = -2;
chooseId.value = 0;
radio.value = e;
expandedKeys.value = [];
getList();
};
const getList = () => {
getUserTree(radio.value, chooseId.value).then(res => {
// if (res.data) {
if (selectItem.type === -1 || selectItem.type === -2) {//角色/部门
dataList.value = res.data;
} else if (selectItem.type === 1) {
selectItem.children = res.data;
if(chooseId.value!==0&&res.data.length===0){
selectItem.children=[{
type:1,
name:'暂无数据'
}]
}
} else if (selectItem.type === 2) {
selectItem.children = res.data;
}
// }
});
};
const setData = (source) => {
// for (let item of source) {
// this.$set(item, "value", selectItem.value + "-" + item.id)
// }
return source;
};
//通过关键字过滤树节点
// filterNode(value, data) {
// if (!value) return true;
// return data.name.indexOf(value) !== -1;
// },
//用于用户选择
const showUserPicker = () => {
selectItem = {
type: -1,
value: "0"
};
dataList.value = [];
selectList.value = []
chooseId.value = 0;
radio.value = 0;
visible.value = true;
expandedKeys.value = [];
getList();
};
const handleChange = (item, data, node) => {
//渲染子节点用户或部门及用户数据
selectItem = item;
if(expandedKeys.value.indexOf(item.value)==-1){
expandedKeys.value.push(item.value);
}else {
return;
}
if (data.expanded === true) {
if (item.type !== 0) {
chooseId.value = item.id;
getList();
return;
}
}
//仅选择用户
if (item.avatar !== null) {
if (props.multiple) {
//不添加重复的数据到右边
for (let i = 0; i < selectList.value.length; i++) {
if (selectList.value[i].id === item.id) {
selectList.value.splice(i, 1);
break;
}
}
selectList.value.push(item);
} else {
selectList.value = [item];
}
}
// _value = selectList.value
};
const noSelected = (selectItem) => {
//右侧的x
for (let i = 0; i < selectList.value.length; i++) {
if (selectList.value[i].value === selectItem.value) {
selectList.value.splice(i, 1);
break;
}
}
selectItem.selected = false;
};
const clearSelected = () => {
//清空
ElMessageBox.confirm("您确定要清空已选中的项?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
selectList.value = [];
});
};
const selectConfirm = () => {
//确定按钮
emit("ok", selectList.value);
dataList.value = []
visible.value = false;
};
defineExpose({
showUserPicker
});
</script>
<style lang="scss" scoped>
$containWidth: 278px;
:deep(.tree) {
.el-tree {
.el-tree-node {
.el-tree-node__children {
.el-tree-node {
.el-tree-node__content {
height: 45px !important;
.tree-node {
}
}
}
}
}
}
.el-tree-node {
.is-leaf + .el-checkbox .el-checkbox__inner {
display: inline-block;
}
.el-checkbox .el-checkbox__inner {
display: none;
}
}
.tree-node {
div {
.el-avatar {
width: 40px;
height: 40px;
margin-right: 5px;
}
}
}
}
.el-dialog__body {
padding: 10px 20px;
}
.picker {
height: 402px;
position: relative;
text-align: left;
.candidate {
left: 0;
top: 0;
}
}
.candidate, .selected {
position: absolute;
display: inline-block;
width: $containWidth;
height: 400px;
border: 1px solid #e8e8e8;
}
.selected {
border-left: none;
right: 0;
top: 0;
.count {
width: calc(var($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;
}
}
}
.org-items {
overflow-y: auto;
height: 350px;
.el-icon-close {
position: absolute;
right: 5px;
cursor: pointer;
font-size: larger;
}
.org-item {
margin: 0 5px;
border-radius: 5px;
position: relative;
padding: 7px 5px;
display: flex;
align-items: center;
cursor: pointer;
&:hover {
background: #f1f1f1;
}
> span {
margin-left: 5px;
}
}
}
.el-scrollbar .el-scrollbar__wrap {
overflow-x: hidden;
}
::-webkit-scrollbar {
float: right;
width: 4px;
height: 4px;
background-color: white;
}
::-webkit-scrollbar-thumb {
border-radius: 16px;
background-color: #efefef;
}
</style>

View File

@@ -0,0 +1,402 @@
<template>
<el-form label-position="top" label-width="90px">
<el-form-item label="⚙ 选择审批对象" prop="text" class="user-type">
<el-radio-group v-model="nodeProps.assignedType">
<el-radio v-for="item in approvalTypes" :label="item.type" :key="item.type">{{ item.name }}</el-radio>
</el-radio-group>
<div v-if="nodeProps.assignedType === 'ASSIGN_USER'">
<el-button size="mini" icon="Plus" type="primary" @click="showSysRolePicker" round>
选择人员
</el-button>
<user-picker title="请选择系统角色" :multiple="false" ref="sysRolePicker" :v-model="assignedUser" @ok="selectedUser"/>
<avatar-ellipsis :row="3" :user-info="assignedUser"/>
</div>
<div v-else-if="nodeProps.assignedType === 'SELF_SELECT'">
<el-radio-group size="mini" v-model="nodeProps.selfSelect.multiple">
<el-radio-button :label="false">自选一个人</el-radio-button>
<el-radio-button :label="true">自选多个人</el-radio-button>
</el-radio-group>
</div>
<div v-else-if="nodeProps.assignedType === 'LEADER_TOP'">
<el-divider/>
<el-form-item label="审批终点" prop="text" class="approve-end">
<el-radio-group v-model="nodeProps.leaderTop.endCondition">
<el-radio label="TOP">直到最上层主管</el-radio>
<el-radio label="LEAVE">不超过发起人的</el-radio>
</el-radio-group>
<div class="approve-end-leave" v-if="nodeProps.leaderTop.endCondition === 'LEAVE'">
<span> </span>
<el-input-number :min="1" :max="20" :step="1" size="mini" v-model="nodeProps.leaderTop.level"/>
<span> 级主管</span>
</div>
</el-form-item>
</div>
<div v-else-if="nodeProps.assignedType === 'LEADER'">
<el-divider/>
<el-form-item label="指定主管" prop="text">
<span>发起人的第 </span>
<el-input-number :min="1" :max="20" :step="1" size="mini"
v-model="nodeProps.leader.level"></el-input-number>
<span> 级主管</span>
<div style="color: #409EFF; font-size: small;">👉 直接主管为 1 级主管</div>
</el-form-item>
</div>
<div v-else-if="nodeProps.assignedType === 'ROLE'">
<el-button size="mini" icon="Plus" type="primary" @click="showRolePicker()" round>
选择系统角色
</el-button>
<role-picker title="请选择人员" :multiple="false" ref="rolePicker" :v-model="roleList" @ok="selectedRole"/>
<role-items v-model="nodeProps.roleList"/>
</div>
<div v-else-if="nodeProps.assignedType === 'FORM_USER'">
<el-form-item label="选择表单联系人项" prop="text" class="approve-end">
<el-select style="width: 80%;" size="small" v-model="nodeProps.formUser" placeholder="请选择包含联系人的表单项" filterable>
<el-option v-for="(op,i) in forms" :label="op.title" :value="op.id" :key="i"></el-option>
</el-select>
</el-form-item>
</div>
<div v-else>
<span class="item-desc">发起人自己作为审批人进行审批</span>
</div>
</el-form-item>
<el-divider></el-divider>
<el-form-item label="👤 审批人为空时" prop="text" class="line-mode">
<el-radio-group v-model="nodeProps.nobody.handler">
<el-radio label="TO_PASS">自动通过</el-radio>
<el-radio label="TO_REFUSE">自动驳回</el-radio>
<el-radio label="TO_ADMIN">转交审批管理员</el-radio>
<el-radio label="TO_USER">转交到指定人员</el-radio>
</el-radio-group>
<div style="margin-top: 10px" v-if="nodeProps.nobody.handler === 'TO_USER'">
<el-button size="mini" icon="Plus" type="primary" @click="showUserPicker()" round>
选择人员
</el-button>
<user-picker title="请指定用户" :multiple="false" ref="toUserPicker" :v-model="nobodyAssignedUser"
@ok="selectNoSetUser"/>
<avatar-ellipsis :row="3" :user-info="nobodyAssignedUser"/>
</div>
</el-form-item>
<div v-if="showMode">
<el-divider/>
<el-form-item label="👩‍👦‍👦 多人审批时审批方式" prop="text" class="approve-mode">
<el-radio-group v-model="nodeProps.mode">
<el-radio label="NEXT">会签 按选择顺序审批每个人必须同意</el-radio>
<el-radio label="AND">会签可同时审批每个人必须同意</el-radio>
<el-radio label="OR">或签有一人同意即可</el-radio>
</el-radio-group>
</el-form-item>
</div>
<el-divider>高级设置</el-divider>
<el-form-item label="✍ 审批同意时是否需要签字" prop="text">
<el-switch inactive-text="不用" active-text="需要" v-model="nodeProps.sign"></el-switch>
<el-tooltip class="item" effect="dark" content="如果全局设置了需要签字,则此处不生效" placement="top-start">
<i class="el-icon-question" style="margin-left: 10px; font-size: medium; color: #b0b0b1"></i>
</el-tooltip>
</el-form-item>
<el-form-item label="⏱ 审批期限(为 0 则不生效)" prop="timeLimit">
<el-input style="width: 180px;" placeholder="时长" type="number"
v-model="nodeProps.timeLimit.timeout.value">
<el-select style="width: 75px;" v-model="nodeProps.timeLimit.timeout.unit" slot="append" placeholder="请选择" filterable>
<el-option label="天" value="D"></el-option>
<el-option label="小时" value="H"></el-option>
</el-select>
</el-input>
</el-form-item>
<el-form-item label="审批期限超时后执行" prop="level" v-if="nodeProps.timeLimit.timeout.value > 0">
<el-radio-group v-model="nodeProps.timeLimit.handler.type">
<el-radio label="PASS">自动通过</el-radio>
<el-radio label="REFUSE">自动驳回</el-radio>
<el-radio label="NOTIFY">发送提醒</el-radio>
</el-radio-group>
<div v-if="nodeProps.timeLimit.handler.type === 'NOTIFY'">
<div style="color:#409EEF; font-size: small">默认提醒当前审批人</div>
<el-switch inactive-text="循环" active-text="一次" v-model="nodeProps.timeLimit.handler.notify.once"></el-switch>
<span style="margin-left: 20px" v-if="!nodeProps.timeLimit.handler.notify.once">
每隔
<el-input-number :min="0" :max="10000" :step="1" size="mini"
v-model="nodeProps.timeLimit.handler.notify.hour"/>
小时提醒一次
</span>
</div>
</el-form-item>
<el-form-item label="🙅‍ 如果审批被驳回 👇">
<el-radio-group v-model="nodeProps.refuse.type">
<el-radio label="TO_INITIAL">重新开始流程</el-radio>
<el-radio label="TO_BEFORE">驳回到上级审批节点</el-radio>
<el-radio label="TO_NODE">驳回到指定节点</el-radio>
</el-radio-group>
<div v-if="nodeProps.refuse.type === 'TO_NODE'">
<span>指定节点:</span>
<el-select style="margin-left: 10px; width: 150px;" placeholder="选择跳转步骤" size="small"
v-model="nodeProps.refuse.target" filterable>
<el-option v-for="(node, index) in nodeOptions" :key="index" :label="node.name"
:value="node.id"></el-option>
</el-select>
</div>
</el-form-item>
<el-form-item label="自定义监听器">
<div slot="label">
<span style="margin-left: 20px">使用自定义监听器: </span>
<!-- <el-switch v-model="config.listener.state" @change="getListener"></el-switch>-->
</div>
<div v-if="config.listener.state">
<div slot="label">
<span style="margin-right: 10px">设置监听器</span>
<el-button type="primary" @click="addListener(config.listener.list)" link> + 添加</el-button>
</div>
<div v-for="(listen, index) in config.listener.list" :key="index">
<el-input v-if="listen.isSys" placeholder="监听器名称" :disabled="true" size="small" style="width: 100px;"
v-model="listen.listenerName"/>
<el-input v-if="!listen.isSys" placeholder="监听器名称" size="small" style="width: 100px;"
v-model="listen.listenerName"/>
<el-radio-group size="small" style="margin: 0 5px;" @change="typeChange(listen)" v-model="listen.isSys">
<el-radio-button :label="true">内置</el-radio-button>
<el-radio-button :label="false">自定义</el-radio-button>
</el-radio-group>
<el-select v-if="listen.isSys" style="width: 180px;" v-model="listen.listenerValue" size="small"
@change="listenerOptionChange(listen)"
placeholder="请选择表单字段" filterable>
<el-option v-for="option in listenerOption" :key="option.value" :label="option.label"
:value="option.value"/>
</el-select>
<!-- <el-input v-if="listen.isSys" placeholder="请设置字段值" size="small" v-model="listen.listenerValue" style="width: 180px;"/>-->
<el-button v-if="!listen.isSys" type="primary" size="small" @click="settingListener(listen)" link>设置</el-button>
<el-button @click="delListener(config.listener.list, index)"
class="el-icon-delete" type="primary"
style="margin-left: 5px; color: #c75450;" link/>
</div>
</div>
</el-form-item>
</el-form>
<!--
<el-dialog custom-class="custom-dialog" class="border" width="600px" title="定义监听器设置"
append-to-body :close-on-click-modal="true"
:destroy-on-close="true" :visible.sync="editListenShow">
<el-form ref="listenerForm" label-position="left" label-width="100px" :rules="listenerRules">
<el-form-item label="监听器名称" prop="listenerName" class="listen">
<el-input placeholder="请设置监听器名称" size="small" v-model="selectListen.listenerName"/>
</el-form-item>
<el-form-item label="监听器类型" prop="eventType" class="listen">
<el-select placeholder="请选择监听器类型" @change="selectListen.eventType = []"
v-model="selectListen.listenerType" filterable>
<el-option :value="'1'" label="任务监听"/>
<el-option :value="'2'" label="执行监听"/>
</el-select>
</el-form-item>
<el-form-item label="事件类型" prop="listenerType" class="listen">
<el-select multiple placeholder="请选择事件类型" v-model="selectListen.eventType" filterable>
<el-option value="create" label="create"/>
<el-option v-if="selectListen.listenerType === '2'" value="end" label="end"/>
<el-option v-if="selectListen.listenerType === '2'" value="take" label="take"/>
<el-option v-if="selectListen.listenerType === '1'" value="assignment" label="assignment"/>
<el-option v-if="selectListen.listenerType === '1'" value="complete" label="complete"/>
</el-select>
</el-form-item>
<el-form-item label="值类型" prop="listenerValueType" class="listen">
<el-radio-group size="small" style="margin: 0 5px;" @change="listenerValueTypeChange"
v-model="selectListen.listenerValueType">
<el-radio-button :label="'0'">Java类</el-radio-button>
<el-radio-button :label="'1'">表达式</el-radio-button>
<el-radio-button :label="'2'">代理表达式</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item label="值" prop="listenerValue" class="listen">
<el-input :placeholder="listenerValuePlaceholder" size="small" v-model="selectListen.listenerValue"/>
</el-form-item>
</el-form>
<div slot="footer">
<el-button size="mini" @click="editListenShow = false">取消</el-button>
<el-button size="mini" type="primary" @click="editListenShow = false">确认</el-button>
</div>
</el-dialog>-->
</template>
<script setup>
import {useProcessStore} from '@/stores/processStore.js'
import UserPicker from '../common/UserPicker.vue'
import RolePicker from '../common/RolePicker.vue'
import AvatarEllipsis from '../common/AvatarEllipsis.vue'
import RoleItems from "../common/RoleItems.vue";
import {computed, defineProps} from 'vue'
const processStore = useProcessStore()
const toUserPicker = ref()
const rolePicker = ref()
const sysRolePicker = ref()
const showOrgSelect = ref(false)
const orgPickerSelected = ref([])
const approvalTypes = reactive([
{name: "指定人员", type: "ASSIGN_USER"},
{name: "发起人自选", type: "SELF_SELECT"},
{name: "连续多级主管", type: "LEADER_TOP"},
{name: "主管", type: "LEADER"},
{name: "角色", type: "ROLE"},
{name: "发起人自己", type: "SELF"},
{name: "表单内联系人", type: "FORM_USER"}
])
const listenerOption = ref([])
const selectListen = ref({})
const editListenShow = ref(false)
const listenerValuePlaceholder = ref('请输入类路径')
const listenerRules = ref({
listenerName: [
{required: true, message: '监听器名称不能为空', trigger: 'blur'}
],
eventType: [
{required: true, message: '监听器类型不能为空', trigger: 'blur'},
// {validator: checkLength,trigger:'blur'}
],
listenerType: [
{required: true, message: '监听器类型不能为空', trigger: 'blur'},
],
listenerValue: [
{required: true, message: '值不能为空', trigger: 'blur'}
],
})
const props = defineProps({
config: {
type: Object,
default: () => {
return {};
}
}
})
const nodeProps = computed(() => {
return processStore.getSelectedNode().props;
})
const assignedUser = computed({
get() {
return props.config.assignedUser || [];
},
set(val) {
props.config.assignedUser = val
}
})
const nobodyAssignedUser = computed({
get() {
return props.config.nobody.assignedUser || [];
},
set(val) {
props.config.nobody.assignedUser = val
}
})
const roleList = computed({
get() {
return props.config.roleList || [];
},
set(val) {
props.config.roleList = val
}
})
watch(()=>props.config.roleList,(value)=>{
roleList.value = value
})
const forms = computed(() => {
return processStore.getDesign().formItems.filter(f => {
return f.name === "UserPicker";
});
})
const nodeOptions = computed(() => {
let values = [];
const excType = ["ROOT", "EMPTY", "CONDITION", "CONDITIONS", "CONCURRENT", "CONCURRENTS", "CC", "END", "TRIGGER"];
processStore.nodeMap.forEach((v) => {
if (excType.indexOf(v.type) === -1) {
values.push({id: v.id, name: v.name});
}
});
return values;
})
const showMode = computed(() => {
let props = processStore.getSelectedNode().props;
switch (props.assignedType) {
case "ASSIGN_USER":
return props.assignedUser.length > 0;
case "SELF_SELECT":
return props.selfSelect.multiple;
case "LEADER_TOP":
return props.formUser !== "";
case "FORM_USER":
case "ROLE":
return true;
default:
return false;
}
})
//指定人员
const showSysRolePicker = () => {
sysRolePicker.value.showUserPicker()
}
//点击转交给指定人员
const showUserPicker = () => {
toUserPicker.value.showUserPicker()
}
//选择系统角色
const showRolePicker = () => {
rolePicker.value.showRolePicker()
}
const checkLength = (rule, value, callback) => {
if (value.length === 0) {
callback(new Error("事件类型不能为空!"))
} else {
callback()
}
}
const selectNoSetUser = (select) => {
let userInfoList = []
for (let val of select) {
let userInfo = {
id: val.id,
name: val.name,
avatar: val.avatar,
}
userInfoList.push(userInfo)
}
nobodyAssignedUser.value = userInfoList
}
const selectedRole = (select) => {
let roleInfoList = []
for (let val of select) {
let userInfo = {
roleId: val.roleId,
roleName: val.roleName
}
roleInfoList.push(userInfo)
}
roleList.value = roleInfoList
}
const selectedUser = (select) => {
let userInfoList = []
for (let val of select) {
let userInfo = {
id: val.id,
name: val.name,
avatar: val.avatar,
}
userInfoList.push(userInfo)
}
assignedUser.value = userInfoList
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,73 @@
<template>
<el-button size="mini" icon="Plus" type="primary" @click="selectUser" round>选择抄送人</el-button>
<div class="option">
<el-checkbox label="允许发起人添加抄送人" v-model="shouldAdd"></el-checkbox>
</div>
<!-- <org-items v-model="select"/>-->
<avatar-ellipsis :row="3" :user-info="assignedUser"/>
<user-picker title="请选择抄送人" multiple ref="userPicker" :v-model="assignedUser" @ok="selectedUser"/>
</template>
<script setup>
import {computed, defineProps} from 'vue'
import UserPicker from "../common/UserPicker.vue";
import AvatarEllipsis from "../common/AvatarEllipsis.vue";
const userPicker=ref()
const props = defineProps({
config: {
type: Object,
default: () => {
return {}
}
}
})
const shouldAdd = computed({
get() {
return props.config.shouldAdd || false
},
set(val) {
props.config.shouldAdd = val
}
})
const assignedUser = computed({
get() {
return props.config.assignedUser || []
},
set(val) {
props.config.assignedUser = val
}
})
const selectUser = () => {
userPicker.value.showUserPicker()
}
const selectedUser = (select) => {
let userInfoList = []
for (let val of select) {
let userInfo = {
id: val.id,
name: val.name,
avatar: val.avatar,
}
userInfoList.push(userInfo)
}
assignedUser.value = userInfoList
}
</script>
<style lang="scss" scoped>
.option {
color: #606266;
margin-top: 20px;
font-size: small;
}
.desc {
font-size: small;
color: #8c8c8c;
}
.org-item {
margin: 5px;
}
</style>

View File

@@ -0,0 +1,308 @@
<template>
<div v-for="(group, index) in selectedNode.props.groups" :key="index + '_g'" class="group">
<div class="group-header">
<span class="group-name">条件组 {{ groupNames[index] }}</span>
<div class="group-cp">
<span>组内条件关系</span>
<el-switch v-model="group.groupType" active-color="#409EFF"
inactive-color="#c1c1c1" active-value="AND" inactive-value="OR"
active-text="" inactive-text=""/>
</div>
<div class="group-operation">
<el-popover placement="bottom" title="选择审批条件" width="300" trigger="click">
<template #reference>
<el-icon :size="18" class="group-icon">
<Plus/>
</el-icon>
</template>
<div>以下条件将决定具体的审批流程</div>
<el-checkbox-group v-model="group.cids" value-key="id">
<el-checkbox :label="condition.id" v-for="(condition, cindex) in conditionList" :key="condition.id"
@change="conditionChange(cindex, group)">
{{ condition.title }}
</el-checkbox>
</el-checkbox-group>
</el-popover>
<el-icon :size="18" class="group-icon" @click="delGroup(index)">
<Close/>
</el-icon>
</div>
</div>
<div class="group-content">
<p v-if="group.conditions.length === 0">点击右上角 + 为本条件组添加条件</p>
<div v-else>
<el-form ref="condition-form">
<!--构建表达式-->
<el-form-item v-for="(condition, cindex) in group.conditions" :key="condition.id + '_' + cindex">
<ellipsis slot="label" hover-tip :content="condition.title"/>
<span v-if="condition.valueType === ValueType.string">
<el-select placeholder="判断符" style="width: 120px;" v-model="condition.compare"
@change="condition.value = []" filterable>
<el-option label="等于" value="="></el-option>
<el-option label="包含在" value="IN"></el-option>
</el-select>
<span v-if="isSelect(condition.id)" style="margin-left: 10px">
<el-select v-if="condition.compare === 'IN'" style="width: 280px;" clearable multiple size="small"
v-model="condition.value" placeholder="选择值" filterable>
<el-option v-for="(option, oi) in getOptions(condition.id)" :key="oi" :label="option"
:value="option"></el-option>
</el-select>
<el-select v-else style="width: 280px;" clearable size="small" v-model="condition.value[0]"
placeholder="选择值" filterable>
<el-option v-for="(option, oi) in getOptions(condition.id)" :key="oi" :label="option"
:value="option"></el-option>
</el-select>
</span>
<span v-else style="margin-left: 10px">
<el-input v-if="condition.compare === '='" style="width: 280px;" placeholder="输入比较值"
v-model="condition.value[0]"/>
<el-select v-else style="width: 280px;" multiple clearable filterable allow-create size="small"
v-model="condition.value" placeholder="输入可能包含的值"></el-select>
</span>
</span>
<span v-else-if="condition.valueType === ValueType.number">
<el-select size="small" placeholder="判断符" style="width: 120px;" v-model="condition.compare" filterable>
<el-option :label="exp.label" :value="exp.value" :key="exp.value" v-for="exp in explains"></el-option>
</el-select>
<span style="margin-left: 10px">
<el-input style="width: 280px;" v-if="conditionValType(condition.compare) === 0"
placeholder="输入比较值" type="number" v-model="condition.value[0]"/>
<el-select style="width: 280px;" multiple filterable allow-create
v-else-if="conditionValType(condition.compare) === 1"
v-model="condition.value" placeholder="输入可能包含的值"></el-select>
<span v-else>
<el-input style="width: 130px;" type="number" placeholder="输入比较值"
v-model="condition.value[0]"/>
<span> ~
<el-input style="width: 130px;" type="number" placeholder="输入比较值"
v-model="condition.value[1]"/>
</span>
</span>
</span>
</span>
<span v-else-if="condition.valueType === ValueType.user">
<span class="item-desc" style="margin-right: 20px">属于某部门 / 为某些人其中之一</span>
<el-button size="mini" :icon="Plus" type="primary" @click="selectUser(condition.value, 'user')"
round>选择人员/部门</el-button>
<org-items :modelValue="users"/>
</span>
<span v-else-if="condition.valueType === ValueType.dept">
<span class="item-desc" style="margin-right: 20px">为某部门 / 某部门下的部门</span>
<el-button size="mini" :icon="Plus" type="primary" @click="selectUser(condition.value, 'dept')"
round>选择部门</el-button>
<org-items :modelValue="users"/>
</span>
<span v-else-if="condition.valueType === ValueType.date"></span>
<el-icon class="delete-icon" @click="delSubCondition(group, cindex)"><Minus /></el-icon>
</el-form-item>
</el-form>
</div>
</div>
</div>
<org-picker title="请选择人员/部门" multiple ref="orgPicker" :v-model="users" @ok="selected"></org-picker>
</template>
<script setup>
import OrgPicker from "../common/UserPicker.vue";
import OrgItems from "../common/RoleItems.vue";
import {ValueType} from '@/views/workflow/form/ComponentsConfigExport.js'
import {computed, ref} from 'vue'
import {useProcessStore} from '@/stores/processStore.js'
import Ellipsis from '../common/Ellipsis.vue'
import {Plus, Minus} from '@element-plus/icons-vue'
const processStore = useProcessStore()
const orgPicker = ref()
const users = ref([])
// const orgType = ref('user')
const showOrgSelect = ref(false)
const groupNames = ref(['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J'])
const supportTypes = ref([ValueType.number, ValueType.string, ValueType.date, ValueType.dateRange, ValueType.dept, ValueType.user])
const explains = ref(
[
{label: '等于', value: '='},
{label: '大于', value: '>'},
{label: '大于等于', value: '>='},
{label: '小于', value: '<'},
{label: '小于等于', value: '<='},
{label: '包含在', value: 'IN'},
{label: 'x < 值 < x', value: 'B'},
{label: 'x ≤ 值 < x', value: 'AB'},
{label: 'x < 值 ≤ x', value: 'BA'},
{label: 'x ≤ 值 ≤ x', value: 'ABA'},
]
)
const selectedNode = computed(() => {
//当前选择的节点
return processStore.getSelectedNode()
})
const conditionList = computed(() => {
//条件数组
//构造可用条件选项
const conditionItems = []
processStore.getDesign().formItems.forEach(item => filterCondition(item, conditionItems))
if (conditionItems.length === 0 || conditionItems[0].id !== 'root') {
conditionItems.unshift({id: 'root', title: '发起人', valueType: 'User'})
}
return conditionItems
})
const isSelect = (processDefinitionKey) => {
//判断是否被选择
let form = processStore.getFormMap().get(processDefinitionKey)
return (form && (form.name === 'SelectInput' || form.name === 'MultipleSelect'))
}
const getOptions = (processDefinitionKey) => {
//获取到对应的option选项
return processStore.getFormMap().get(processDefinitionKey).props.options || []
}
const conditionValType = (type) => {
//条件类型选择
switch (type) {
case '=':
case '>':
case '>=':
case '<':
case '<=':
return 0;
case 'IN':
return 1;
default:
return 2;
}
}
const selectUser = (value, orgType) => {
//选择用户,倒开组织选择器
// orgType.value = orgType
users.value = value
orgPicker.value.showUserPicker()
}
const filterCondition = (item, list) => {
//从表单中过滤出可以选择的条件
if (item.name === 'SpanLayout') {
item.props.items.forEach(sub => filterCondition(sub, list))
} else if (supportTypes.value.indexOf(item.valueType) > -1 && item.props.required) {
list.push({title: item.title, id: item.id, valueType: item.valueType})
}
}
const selected = (selected) => {
let userInfoList = []
for (let val of selected) {
let userInfo = {
id: val.id,
roleName: val.name
}
userInfoList.push(userInfo)
}
users.value = userInfoList
//组织选择器的选中回调函数
// users.value.length = 0
// console.log('processStore.getAssignedUser()',processStore.getAssignedUser())
processStore.getAssignedUser().forEach(u => users.value=userInfoList)
}
const delGroup = (index) => {
//删除条件组
processStore.getSelectedNode().props.groups.splice(index, 1)
}
const delSubCondition = (group, index) => {
//删除
group.cids.splice(index, 1)
group.conditions.splice(index, 1)
}
const conditionChange = (index, group) => {
//条件组进行发生了改变
//判断新增的
group.cids.forEach(cid => {
if (0 > group.conditions.findIndex(cd => cd.id === cid)) {
//新增条件
let condition = {...conditionList.value[index]}
console.log('fs', condition, conditionList.value, index)
condition.compare = '';
condition.value = []
group.conditions.push(condition)
}
})
for (let i = 0; i < group.conditions.length; i++) {
//去除没有选中的
if (group.cids.indexOf(group.conditions[i].id) < 0) {
group.conditions.splice(i, 1)
}
}
}
</script>
<style lang="scss" scoped>
.group {
margin-bottom: 20px;
color: #5e5e5e;
overflow: hidden;
border-radius: 6px;
border: 1px solid #e3e3e3;
.group-header {
padding: 5px 10px;
background: #e3e3e3;
position: relative;
div {
display: inline-block;
}
.group-name {
font-size: small;
}
.group-cp {
font-size: small;
position: absolute;
left: 100px;
display: flex;
top: 5px;
justify-content: center;
align-items: center;
}
.group-operation {
position: absolute;
right: 10px;
.group-icon {
cursor: pointer;
}
}
}
.group-content {
padding: 10px 5px;
p {
text-align: center;
font-size: small;
}
.delete-icon {
position: absolute;
cursor: pointer;
top: 12px;
right: 10px;
}
}
.condition-title {
display: block;
width: 100px;
}
}
</style>

View File

@@ -0,0 +1,141 @@
<template>
<el-form inline label-width="100px">
<el-form-item label="调整优先级" prop="level">
<el-popover placement="right" title="拖拽条件调整优先级顺序" width="250" trigger="click">
<draggable style="width: 100%; min-height:25px" :list="prioritySortList" group="from"
@end="onEnd"
:options="sortOption">
<template #item="{ index,element }">
<div :class="{'drag-no-choose': true, 'drag-hover': element.id === selectedNode.id}">
<ellipsis style="width: 80px;" hover-tip :content="element.name"/>
<div>优先级 {{ index + 1 }}</div>
</div>
</template>
</draggable>
<template #reference>
<el-button slot="reference">{{ nowNodeLeave + 1 }}</el-button>
</template>
</el-popover>
</el-form-item>
<el-form-item label="条件组关系" label-width="150px">
<el-switch v-model="selectedNode.props.groupsType" active-color="#409EFF"
inactive-color="#c1c1c1" active-value="AND" inactive-value="OR"
active-text="" inactive-text="">
</el-switch>
</el-form-item>
<el-form-item label="条件组表达式">
<el-input v-model="config.expression" placeholder="输入条件组关系表达式 &为与,|为或"/>
<span class="item-desc">使用表达式构建复杂逻辑例如: (A & B) | C</span>
</el-form-item>
</el-form>
<div>
<el-button type="primary" icon="Plus" style="margin: 0 15px 15px 0" round
@click="addConditionGroup">
添加条件组
</el-button>
<span class="item-desc">只有必填选项才能作为审批条件</span>
</div>
<group-item/>
</template>
<script setup>
import draggable from 'vuedraggable';
import Ellipsis from '../common/Ellipsis.vue'
import {computed, defineProps, ref,defineEmits} from 'vue'
import {useProcessStore} from '@/stores/processStore.js'
const emit = defineEmits()
const processStore = useProcessStore()
import GroupItem from "./ConditionGroupItemConfig.vue"
const props = defineProps({
config: {
type: Object,
default: {}
}
})
const sortOption = reactive({
animation: 300,
chosenClass: 'choose',
scroll: true,
sort: true
})
const selectedNode = computed(() => {
//当前选择的节点
return processStore.getSelectedNode()
})
//条件节点
const prioritySortList = computed(() => {
let node = processStore.nodeMap.get(processStore.getSelectedNode().parentId)
if (node) {
return node.branchs || []
}
return []
})
const nowNodeLeave = computed(() => {
let node = processStore.nodeMap.get(processStore.getSelectedNode().parentId)
let list = []
if (node) {
list = node.branchs || []
}
return list.indexOf(processStore.getSelectedNode())
})
const onEnd = () => {
emit('initRender')
}
const addConditionGroup = (select) => {
props.config.groups.push({
cids: [],
groupType: "OR",
conditions: []
})
}
// const selected = (select) => {
// select.forEach(val => processStore.addAssignedUser(val))
// }
</script>
<style lang="scss" scoped>
.choose {
border-radius: 5px;
margin-top: 2px;
background: #f4f4f4;
border: 1px dashed #1890FF !important;
}
.drag-hover {
color: #1890FF
}
.drag-no-choose {
cursor: move;
background: #f8f8f8;
border-radius: 5px;
margin: 5px 0;
height: 25px;
line-height: 25px;
padding: 5px 10px;
border: 1px solid #ffffff;
div {
display: inline-block;
font-size: small !important;
}
div:nth-child(2) {
float: right !important;
}
}
</style>

View File

@@ -0,0 +1,41 @@
<template>
<div>
<div style="margin-bottom: 20px">
<p class="item-desc">延时方式</p>
<el-radio-group v-model="config.type">
<el-radio-button label="FIXED">固定时长</el-radio-button>
<el-radio-button label="AUTO">自动计算</el-radio-button>
</el-radio-group>
</div>
<div v-if="config.type === 'FIXED'">
<el-input style="width: 180px;" placeholder="时间单位" type="number" v-model="config.time">
</el-input>
<el-select style="width: 75px;" v-model="config.unit" slot="append" placeholder="请选择" filterable>
<el-option label="天" value="D"></el-option>
<el-option label="小时" value="H"></el-option>
<el-option label="分钟" value="M"></el-option>
</el-select>
<span class="item-desc"> 后进入下一步</span>
</div>
<div class="item-desc" v-else>
<el-time-picker value-format="HH:mm:ss" style="width: 150px;" v-model="config.dateTime" placeholder="任意时间点"></el-time-picker>
<span class="item-desc"> 后进入下一步</span>
</div>
</div>
</template>
<script setup>
import {defineProps} from 'vue'
const props = defineProps({
config:{
type: Object,
default: ()=>{
return {}
}
}
})
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,117 @@
<template>
<el-table :header-cell-style="{background:'#f5f6f6'}" :data="formPerms" border style="width: 100%" v-tabh>
<el-table-column prop="title" show-overflow-tooltip label="表单字段">
<template #default="scope">
<span v-if="scope.row.required" style="color: #c75450"> * </span>
<span>{{ scope.row.title }}</span>
</template>
</el-table-column>
<el-table-column v-if="nodeType !== 'ROOT'" prop="readOnly" label="只读" width="80">
<template slot="header" #header="scope">
<el-radio label="R" v-model="permSelect" @change="allSelect('R')">只读</el-radio>
</template>
<template #default="scope">
<el-radio v-model="scope.row.perm" label="R" :name="scope.row.id"></el-radio>
</template>
</el-table-column>
<el-table-column prop="editable" label="可编辑" width="90" v-if="nowNode.type !== 'CC'">
<template slot="header" #header="scope">
<el-radio label="E" v-model="permSelect" @change="allSelect('E')">可编辑</el-radio>
</template>
<template #default="scope">
<el-radio v-model="scope.row.perm" label="E" :name="scope.row.id"></el-radio>
</template>
</el-table-column>
<el-table-column prop="hide" label="隐藏" width="80">
<template slot="header" #header="scope">
<el-radio label="H" v-model="permSelect" @change="allSelect('H')">隐藏</el-radio>
</template>
<template #default="scope">
<el-radio v-model="scope.row.perm" label="H" :name="scope.row.id"></el-radio>
</template>
</el-table-column>
</el-table>
</template>
<script setup>
import {defineProps, watch,computed} from "vue";
import {useProcessStore} from "@/stores/processStore.js";
const props = defineProps({
nodeType: {
type: String,
default: null
}
});
const processStore = useProcessStore();
const tableData = ref([]);
const isIndeterminate = ref(false);
const permSelect = ref("");
const checkStatus = reactive({
readOnly: true,
editable: false,
hide: false
});
const init = () => {
let oldPermMap = new Map()
if (Array.isArray(formPerms.value) && formPerms.value.length){
for (let item of formPerms.value) {
oldPermMap.set(item.id,item)
}
}
processStore.getSelectedNode().props.formPerms = [];
formPermsLoad(oldPermMap, processStore.getDesign().formItems);
};
const formPerms = computed(() => {
return processStore.getSelectedNode().props.formPerms;
});
const nowNode = computed(() => {
return processStore.getSelectedNode();
});
const formItems = computed(() => {
return processStore.getDesign().formItems;
});
const allSelect = (type) => {
permSelect.value = type;
formPerms.value.forEach(f => f.perm = type);
};
const formPermsLoad = (oldPermMap, forms) => {
forms.forEach(form => {
if (form.name === "SpanLayout") {
formPermsLoad(oldPermMap, form.props.items);
} else {
//刷新名称
let old = oldPermMap.get(form.id)
if (old) {
old.title = form.title;
old.required = form.props.required;
formPerms.value.push(old);
} else {
formPerms.value.push({
id: form.id,
title: form.title,
required: form.props.required,
perm: nowNode.type === "ROOT" ? "E" : "R"
});
}
}
});
};
init();
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,92 @@
<template>
<el-tabs v-model="active" v-if="visible && name && formConfig.length > 0">
<el-tab-pane :label="name" name="properties">
<component :is="com" :config="selectNode.props" @initRender="emit('initRender')"/>
</el-tab-pane>
<el-tab-pane label="表单权限设置" name="permissions">
<form-authority-config :node-type="selectNode.type"/>
</el-tab-pane>
</el-tabs>
<div v-else>
<component :is="com" :config="selectNode.props" @initRender="emit('initRender')"/>
</div>
</template>
<script setup>
import FormAuthorityConfig from './FormAuthorityConfig.vue'
import Approval from './ApprovalNodeConfig.vue'
import Root from './RootConfig.vue'
import Delay from './DelayNodeConfig.vue'
import CcNode from './CcNodeConfig.vue'
import Condition from './ConditionNodeConfig.vue'
import Trigger from './TriggerNodeConfig.vue'
import {useProcessStore} from '@/stores/processStore.js'
import {computed,defineEmits} from 'vue'
const emit = defineEmits()
const processStore = useProcessStore()
const selectNode = computed(() => {
return processStore.getSelectedNode()
})
const formConfig = computed(() => {
return processStore.getDesign().formItems
})
const com = ref()
const active = ref('properties')
const visible = ref(false)
const name = computed(()=>{
switch (processStore.getSelectedNode().type) {
case 'ROOT':
return '设置发起人';
case 'APPROVAL':
return '设置审批人';
case 'CC':
return '设置抄送人';
default:
return null;
}
})
const init = () => {
console.log(processStore.getSelectedNode().type)
switch (processStore.getSelectedNode().type) {
case 'APPROVAL' :
com.value = Approval;
break;
case 'ROOT' :
com.value = Root;
break;
case 'DELAY' :
com.value = Delay;
break;
case 'CC' :
com.value = CcNode;
break;
case 'CONDITION' :
com.value = Condition;
break;
case 'TRIGGER' :
com.value = Trigger;
break;
}
visible.value = false
nextTick(() => {
visible.value = true
})
}
init()
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,44 @@
<template>
<div>
<p class="desc">选择能发起该审批的人员/部门不选则默认开放给所有人</p>
<el-button size="mini" @click="selectOrg" icon="el-icon-plus" type="primary" round>请选择</el-button>
<org-items v-model="select"/>
<org-picker title="请选择可发起本审批的人员/部门" multiple ref="orgPicker" :selected="select" @ok="selected"/>
</div>
</template>
<script setup>
import {computed, defineExpose} from 'vue'
const props = defineProps({
config:{
type: Object,
default: ()=>{
return {}
}
}
})
const showOrgSelect = ref(false)
const select = computed(()=>{
return props.config.assignedUser
})
const selectOrg = () => {
// this.$refs.orgPicker.show()
}
const selected = (item) => {
item.forEach(val => select.push(val))
}
const removeOrgItem = (index) => {
this.select.splice(index, 1)
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,342 @@
<template>
<div>
<el-form label-position="top" label-width="90px">
<el-form-item label="选择触发的动作" prop="text" class="user-type">
<el-radio-group v-model="config.type">
<el-radio label="WEBHOOK">发送网络请求</el-radio>
<el-radio label="EMAIL">发送邮件</el-radio>
</el-radio-group>
</el-form-item>
<div v-if="config.type === 'WEBHOOK'">
<el-form-item label="请求地址" prop="text">
<el-input placeholder="请输入URL地址" v-model="config.http.url">
<el-select v-model="config.http.method" style="width: 85px;" slot="prepend" placeholder="URL" filterable>
<el-option label="GET" value="GET"></el-option>
<el-option label="POST" value="POST"></el-option>
<el-option label="PUT" value="PUT"></el-option>
<el-option label="DELETE" value="DELETE"></el-option>
</el-select>
</el-input>
</el-form-item>
<el-form-item label="Header请求头" prop="text">
<div slot="label">
<span style="margin-right: 10px">Header请求头</span>
<el-button type="primary" @click="addItem(config.http.headers)" link> + 添加</el-button>
</div>
<div v-for="(header, index) in config.http.headers" :key="index">
-
<el-input placeholder="参数名" style="width: 100px;" v-model="header.name"/>
<el-radio-group v-model="header.isField">
<el-radio-button :label="true">表单</el-radio-button>
<el-radio-button :label="false">固定</el-radio-button>
</el-radio-group>
<el-select v-if="header.isField" style="width: 180px;" v-model="header.value"
placeholder="请选择表单字段" filterable>
<el-option v-for="form in forms" :key="form.id" :label="form.title" :value="form.title"></el-option>
</el-select>
<el-input v-else placeholder="请设置字段值" v-model="header.value" style="width: 180px;"/>
<el-icon class="el-icon-delete" @click="delItem(config.http.headers, index)"
style="margin-left: 5px; color: #c75450; cursor: pointer"/>
</div>
</el-form-item>
<el-form-item label="Header请求参数" prop="text">
<div slot="label">
<span style="margin-right: 10px">请求参数 </span>
<el-button style="margin-right: 20px" type="primary" @click="addItem(config.http.params)" link> + 添加</el-button>
<span>参数类型 - </span>
<el-radio-group style="margin: 0 5px;" v-model="config.http.contentType">
<el-radio-button label="JSON">json</el-radio-button>
<el-radio-button label="FORM">form</el-radio-button>
</el-radio-group>
</div>
<div v-for="(param, index) in config.http.params" :key="index">
-
<el-input placeholder="参数名" style="width: 100px;" v-model="param.name"/>
<el-radio-group style="margin: 0 5px;" v-model="param.isField">
<el-radio-button :label="true">表单</el-radio-button>
<el-radio-button :label="false">固定</el-radio-button>
</el-radio-group>
<el-select v-if="param.isField" style="width: 180px;" v-model="param.value"
placeholder="请选择表单字段" filterable>
<el-option v-for="form in forms" :key="form.id" :label="form.title" :value="form.id"></el-option>
</el-select>
<el-input v-else placeholder="请设置字段值" v-model="param.value" style="width: 180px;"/>
<el-icon class="el-icon-delete" @click="delItem(config.http.params, index)"
style="margin-left: 5px; color: #c75450; cursor: pointer"/>
</div>
<div>
</div>
</el-form-item>
<!-- <el-form-item label="请求结果处理" prop="text">-->
<div slot="label">
<span>请求结果处理</span>
<span style="margin-left: 20px">自定义脚本: </span>
<el-switch v-model="config.http.handlerByScript"></el-switch>
</div>
<span class="item-desc" v-if="config.http.handlerByScript">
<p>👉 返回值为 ture 则流程通过 false 则流程将被驳回</p>
<p>👉 (上线注意)占不支持ES高级语法</p>
<p>👉 (上线注意)不支持浏览器的内置函数</p>
<!-- <div>支持函数-->
<!-- <span style="color: dodgerblue">setFormByName(-->
<!-- <span style="color: #939494">'表单字段名', '表单字段值'</span>-->
<!-- )</span>-->
<!-- 可改表单数据-->
<!-- </div>-->
</span>
<span class="item-desc" v-else>👉 无论请求结果如何均通过</span>
<div v-if="config.http.handlerByScript">
<div>
<el-button @click="requestTestHandler">测试</el-button>
</div>
<div>
<span>请求成功😀</span>
<js-code-edit v-model="config.http.success" :editor-placeholder="'请输入js代码'"
:editor-height="250" :tab-size="2"/>
</div>
<div>
<span>请求失败😥</span>
<js-code-edit v-model="config.http.fail" :editor-placeholder="'请输入js代码'"
:editor-height="250" :tab-size="2"/>
</div>
</div>
<!-- </el-form-item>-->
</div>
<div v-else-if="config.type === 'EMAIL'">
<el-form-item label="邮件主题" prop="text">
<el-input placeholder="请输入邮件主题" v-model="config.email.subject"/>
</el-form-item>
<el-form-item label="收件方" prop="text">
<el-select style="width: 100%;" v-model="config.email.to" filterable multiple allow-create
default-first-option placeholder="请输入收件人">
<el-option v-for="sender in config.email.to" :key="sender" :label="sender" :value="sender"></el-option>
</el-select>
</el-form-item>
<el-form-item label="抄送方" prop="text">
<el-select style="width: 100%;" v-model="config.email.cc" filterable multiple allow-create
default-first-option placeholder="请输入收件人">
<el-option v-for="item in config.email.cc" :key="item" :label="item" :value="item"></el-option>
</el-select>
</el-form-item>
<el-form-item label="邮件正文" prop="text">
<el-input type="textarea" v-model="config.email.content" :rows="4"
placeholder="邮件内容,支持变量提取表单数据 ${表单字段名} "></el-input>
</el-form-item>
</div>
</el-form>
</div>
</template>
<script setup>
import {computed, defineProps} from 'vue'
import {ElMessage, ElMessageBox} from 'element-plus'
import axios from "axios";
import {useProcessStore} from '@/stores/processStore.js'
import JsCodeEdit from "@/components/codeEdit/JsCodeEdit.vue";
const processStore = useProcessStore()
const props = defineProps({
config: {
type: Object,
default: () => {
return {}
}
}
})
const forms = computed(() => {
return processStore.getDesign().formItems || []
})
const addItem = (items) => {
if (items.length > 0 && (items[items.length - 1].name.trim() === ''
|| items[items.length - 1].value.trim() === '')) {
ElMessage.warning("请完善之前项后在添加")
return;
}
items.push({name: '', value: '', isField: true})
}
const delItem = (items, index) => {
items.splice(index, 1)
}
//url规范性检查
const restfulCheck = (url) => {
const httpProtocolPattern = /^http:/;
const httpsProtocolPattern = /^https:/;
const restfulUrlPattern = /\/\w+\/\w+(\/\{[^}]+\})*/;
if (httpProtocolPattern.test(url) || httpsProtocolPattern.test(url)) {
return restfulUrlPattern.test(url);
} else {
return false;
}
}
//是否含有动态参数
const hasUrlParams = (url) => {
const pattern = /{[^{}]+}/g;
let match;
while ((match = pattern.exec(url)) !== null) {
return true;
}
return false;
}
//获取到动态参数的名称
const getDynamicParamNames = (url) => {
const pattern = /{([^{}]+)}/g;
let match;
const paramNames = [];
while ((match = pattern.exec(url)) !== null) {
if (match[0].startsWith('{') && match[0].endsWith('}')) {
const paramName = match[1];
paramNames.push(paramName);
}
}
return paramNames;
}
//替换rul动态参数
const replaceDynamicParams = (url, params) => {
const dynamicParamPattern = /{\s*(\w+)\s*}/g;
return url.replace(dynamicParamPattern, (match, param) => {
return params[param] || '0';
});
}
//获取到参数值
const getParamsValue = (params, paramName) => {
for (let param of params) {
if (param.name === paramName) {
return param.value
}
}
return null;
}
//设置头部
const setHeaders = (http) => {
let headers = {}
for (let header of http.headers) {
if (header.isField) {
ElMessage.error("测试只支持固定参数")
return
}
if (header.name !== "" && header.value !== "") {
this.$set(headers, header.name, header.value);
}
}
if (http.contentType === "FORM") {
this.$set(headers, "Content-Type", "multipart/form-data")
} else {
this.$set(headers, "Content-Type", "application/json")
}
return headers;
}
//设置post和put参数
const setPostAndPutParams = (http) => {
let params = {}
for (let param of http.params) {
params[param.name] = param.value
}
return params;
}
//设置get和delete的参数
const setGetAndDeleteParams = (http) => {
let dynamicParams = []
let url = http.url
let hasParams = hasUrlParams(url)
if (hasParams) {
dynamicParams = getDynamicParamNames(url);
let replaceParams = {}
for (let paramsName of dynamicParams) {
let value = getParamsValue(http.params, paramsName)
if (null == value) {
ElMessage.error(paramsName + '参数未设置')
return;
}
replaceParams[paramsName] = value
}
url = replaceDynamicParams(url, replaceParams);
}
if (http.method === "DELETE") {
return url;
}
let getParams = []
for (let param of http.params) {
if (dynamicParams.indexOf(param.name) === -1 && param.name !== '' && param.value !== '') {
getParams.push(param.name + "=" + param.value)
}
}
if (getParams.length > 0) {
url += "?"
for (let i = 0; i < getParams.length; i++) {
if (i !== getParams.length - 1) {
url += getParams[i] + "&";
} else {
url += getParams[i];
}
}
}
return url;
}
//请求测试
const requestTestHandler = () => {
let http = props.config.http;
if (http.url == null || http.url === '') {
ElMessage.error("请填写请求路径!")
return
}
if (!restfulCheck(http.url)) {
ElMessage.error("当前只支持 RESTful URL!")
return
}
let headers = setHeaders(http)
let request
switch (http.method) {
case "GET":
case "DELETE":
let url = setGetAndDeleteParams(http)
if (null == url) {
return;
}
request = axios.request({
method: http.method,
url: url,
headers: headers,
});
break;
case "POST":
case "PUT":
request = axios.request({
method: http.method,
url: http.url,
headers: headers,
data: setPostAndPutParams(http)
});
break;
default:
break;
}
console.log("==================[测试打印内容]==================")
request.then(res => {
console.log(res)
if (res.status === 200) {
let data = res.data
console.log(data)
let successFun = eval("(false ||" + http.success + ")");
let result = successFun(data);
console.log(result, "成功函数执行的返回结果")
} else {
let failFun = eval("(false ||" + http.fail + ")");
let result = failFun(res);
console.log(result, "失败函数执行的返回结果")
}
}).finally(() => {
console.log("==================[测试打印结束]==================")
})
}
</script>
<style lang="scss" scoped>
.item-desc {
color: #939494;
}
</style>

View File

@@ -0,0 +1,266 @@
<template>
<div>
<el-form :model="queryParams" inline class="query-form" ref="queryForm" @submit.prevent="getList">
<el-form-item label="部署名称" prop="deploymentName">
<el-input v-model="queryParams.deploymentName" placeholder="请输入部署名称" clearable></el-input>
</el-form-item>
<!-- <el-form-item label="状态" prop="enable">-->
<!-- <el-select v-model="queryParams.enable" placeholder="请选择状态" clearable filterable>-->
<!-- <el-option-->
<!-- v-for="dict in cacheStore.getDict('regular_enable')"-->
<!-- :key="dict.value"-->
<!-- :label="dict.label"-->
<!-- :value="dict.value"-->
<!-- />-->
<!-- </el-select>-->
<!-- </el-form-item>-->
<el-form-item>
<el-button type="primary" @click="getList" :icon="Search">搜索</el-button>
<el-button type="primary" v-perm="['rapid:regular:add']" @click="handleAdd" :icon="Plus">新增</el-button>
<el-button type="primary" @click="handleReset" :icon="Refresh" plain>重置</el-button>
<el-button type="primary" v-perm="['rapid:regular:export']" @click="handleExport" :icon="Download" plain>导出
</el-button>
</el-form-item>
</el-form>
<div class="table">
<el-table
:data="list"
row-key="id"
:lazy="true"
stripe
ref="singleTable"
v-loading="loading"
@select="handleSelect"
v-tabh
>
<el-table-column type="selection" width="30"/>
<el-table-column label="序号" type="index" width="60" align="center"/>
<el-table-column prop="deploymentName" label="部署名称" align="center"/>
<el-table-column prop="version" label="版本" align="center">
<template #default="scope">
<el-tag type="success">{{ scope.row.version }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="remark" label="备注说明" align="center"/>
<el-table-column prop="updateTime" label="更新时间" align="center"/>
<el-table-column prop="state" label="状态" align="center">
<template #default="scope">
<point-tag dict-type="normal_disable" :value="scope.row.state"/>
</template>
</el-table-column>
<el-table-column label="操作" align="center">
<template #default="scope">
<el-button type="primary" size="mini" v-perm="['rapid:regular:edit']"
@click="handleEdit(scope.row.deploymentId)" link>编辑
</el-button>
<el-button type="primary" size="mini" v-perm="['rapid:regular:edit']"
@click="viewHistoricalVersion(scope.row)" link>历史
</el-button>
<popover-delete :name="scope.row.version" :type="'版本'" :perm="['rapid:regular:del']"
@delete="handleDelete(scope.row.deploymentId)"/>
</template>
</el-table-column>
</el-table>
</div>
<paging :current-page="pageInfo.pageNum" :page-size="pageInfo.pageSize" :page-sizes="[10, 20, 30, 40,50]"
:total="total" @changeSize="handleSizeChange" @goPage="handleCurrentChange"/>
<el-dialog v-model="isVisited" title="历史" width="800px">
<div class="table">
<el-table
:data="historyVersionList"
row-key="id"
:lazy="true"
ref="singleTable"
v-loading="loading"
v-tabh
>
<el-table-column prop="deploymentName" label="部署名称" align="center"/>
<el-table-column prop="version" label="版本" align="center">
<template #default="scope">
<el-tag type="success">{{ scope.row.version }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="state" label="状态" align="center">
<template #default="scope">
<point-tag dict-type="normal_disable" :value="scope.row.state"/>
</template>
</el-table-column>
<el-table-column prop="remark" label="备注说明" align="center"/>
<el-table-column label="操作">
<template #default="scope">
<el-button type="primary" size="mini"
:style="scope.row.state=== '1' ?'color: red':''"
@click="suspendOrActivateHistoryView(scope.row)" link>
{{ scope.row.state === "1" ? "停用" : "启用" }}
</el-button>
<popover-delete :name="scope.row.version" :type="'版本'"
@delete="handleDeleteHistoryVersion(scope.row.deploymentId)"/>
</template>
</el-table-column>
</el-table>
</div>
</el-dialog>
</div>
</template>
<script setup>
import {
activateProcessDefinition,
getProcessDefinitionList,
suspendProcessDefinition,
getHistoryVersion,
deleteHistoryVersion
} from "@/api/workflow/process-definition.js";
import {Search, Refresh, Delete, Plus, Edit, Download, Document} from '@element-plus/icons-vue'
import {ElMessage, ElMessageBox} from "element-plus";
import {useCacheStore} from '@/stores/cache.js'
import PointTag from "@/components/PointTag.vue";
import Paging from "@/components/pagination/index.vue";
const dictStore = useCacheStore()
dictStore.setCacheKey(['normal_disable'])
const router = useRouter()
//查询参数
const queryParams = reactive({
deploymentName: '',
// startTime: null,
// endTime: null,
// state: undefined,
})
//页面信息
const pageInfo = reactive({
pageNum: 1,
pageSize: 10,
})
const loading = ref(true)
const list = ref([])
const queryForm = ref()
const total = ref()
const selectDefinition = ref(null)
const historyVersionList = ref([])
const singleTable = ref()
const isVisited = ref(false)
//重置搜索
const handleReset = () => {
queryForm.value.resetFields()
getList()
}
//获取数据
const getList = async () => {
let params = {
...queryParams,
...pageInfo
}
loading.value = true
getProcessDefinitionList(params).then(res => {
if (res.code === 1000) {
list.value = res.data.rows
total.value = res.data.total
loading.value = false
} else {
ElMessage.error(res.msg)
}
})
}
const handleAdd = () => {
router.push({
path: '/workflow/process/edit',
})
}
const handleEdit = (deploymentId) => {
router.push({
path: `/workflow/process/edit/${deploymentId}`,
})
}
//查看历史版本
const viewHistoricalVersion = (row) => {
selectDefinition.value = row
loading.value = true
getHistoryVersion(row.processDefinitionKey).then(res => {
loading.value = false;
// console.log("历史版本数据==", res.data);
res.data.forEach(row => {
row.logo = JSON.parse(row.logo);
});
historyVersionList.value = res.data;
}).catch(err => {
ElMessage.error(err.response.data);
});
isVisited.value = true;
}
//启用,挂起历史版本
const suspendOrActivateHistoryView = (row) => {
let tip = row.state === "1" ? " 您正在停用 [ " + row.deploymentName + " ] 流程,是否继续?" : " 您正在激活 [ " + row.deploymentName + " ] 流程,是否继续?";
ElMessageBox.confirm(tip, "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
// let processDefinitionId = row.processDefinitionId;
if (row.state === "1") {
//停用
suspendProcessDefinition(row.processDefinitionId).then(res => {
if (res.code === 1000) {
viewHistoricalVersion(selectDefinition.value)
ElMessage.success(res.msg);
} else {
ElMessage.error(res.msg);
}
});
} else {
//启用
activateProcessDefinition(row.processDefinitionId).then(res => {
if (res.code === 1000) {
viewHistoricalVersion(selectDefinition.value)
ElMessage.success(res.msg);
} else {
ElMessage.error(res.msg);
}
});
}
});
}
//删除工作流历史版本
const handleDeleteHistoryVersion = (deploymentId) => {
deleteHistoryVersion(deploymentId).then(res => {
viewHistoricalVersion(selectDefinition.value)
})
}
//勾选table数据行的 Checkbox
const handleSelect = async (selection, row) => {
if (selection.length !== 0) {
disabled.value = false
id.value = row.id
if (selection.length > 1) {
const del_row = selection.shift();
singleTable.value.toggleRowSelection(del_row, false);
}
} else {
disabled.value = true
}
}
//切换每页显示条数
const handleSizeChange = async (val) => {
pageInfo.value.pageSize = val
await getList()
}
//点击页码进行分页功能
const handleCurrentChange = async (val) => {
pageInfo.value.pageNum = val
await getList()
}
getList()
</script>

View File

@@ -0,0 +1,27 @@
<template>
<el-button v-if="mode === 'design'" size="small" round class="add-branch-btn-el" @click="emit('addBranch')">{{value}}</el-button>
</template>
<script setup>
import {defineEmits,defineProps,computed} from "vue";
const emit = defineEmits()
const props = defineProps({
value:{
type:String,
default: ""
},
mode: {
type: String,
default: 'design'
}
})
const viewer = computed(() => {
return false;
})
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,200 @@
<template>
<node :title="config.name" :show-error="showError" :content="content"
:show-avatar="config.props.assignedType === 'ASSIGN_USER'" :user-info="assignedUser"
:error-info="errorInfo"
:select-user="selectUser"
:mode="mode"
@selected="emit('selected')" @delNode="emit('delNode')" @insertNode="type => emit('insertNode', type)"
placeholder="请设置审批人" :header-bgc="headerBgc" :header-icon="Stamp"/>
</template>
<script setup>
import Node from './Node.vue'
import {computed, defineExpose} from 'vue'
import {Stamp} from '@element-plus/icons-vue'
const emit = defineEmits(['insertNode', 'selected', 'delNode'])
const props = defineProps({
config: {
type: Object,
default: () => {
return {}
}
},
mode: {
type: String,
default: 'design'
}
})
const showError = ref(false)
const errorInfo = ref('')
const selectUser = computed(() => {
return {
show: props.config.props.assignedType === 'SELF_SELECT',
multiple: props.config.props.selfSelect.multiple
};
})
const assignedUser = computed(() => {
if (props.config.props.assignedType === 'SELF_SELECT') {
props.config.props.assignedUser = []
}
return props.config.props.assignedUser;
})
const headerBgc = computed(() => {
if (props.mode === 'design' || props.mode === 'view') {
return '#ff943e'
} else {
return props.config.props.headerBgc
}
})
const content = computed(() => {
const config = props.config.props
switch (config.assignedType) {
case "ASSIGN_USER":
if (config.assignedUser.length > 0) {
let texts = []
config.assignedUser.forEach(org => texts.push(org.name))
return String(texts).replaceAll(',', '、')
} else {
return '请指定审批人'
}
case "SELF":
return '发起人自己'
case "SELF_SELECT":
return config.selfSelect.multiple ? '发起人自选多人' : '发起人自选一人'
case "LEADER_TOP":
return '多级主管依次审批'
case "LEADER":
return config.leader.level > 1 ? '发起人的第 ' + config.leader.level + ' 级主管' : '发起人的直接主管'
case "FORM_USER":
if (!config.formUser || config.formUser === '') {
return '表单内联系人(未选择)'
} else {
// let text = getFormItemById(config.formUser)
if (text && text.title) {
return `表单(${text.title})内的人员`
} else {
return '该表单已被移除😥'
}
}
case "ROLE":
if (config.roleList.length > 0) {
return config.roleList.map(role => {
return role.roleName;
}).join("、")
} else {
return '指定角色(未设置)'
}
default:
return '未知设置项😥'
}
})
//校验数据配置的合法性
const validate = (err) => {
try {
console.log(props.config.props.assignedType)
switch (props.config.props.assignedType) {
case "ASSIGN_USER":
showError.value = !validate_ASSIGN_USER(err);
break;
case "SELF":
showError.value = !validate_SELF(err);
break;
case "SELF_SELECT":
showError.value = !validate_SELF_SELECT(err);
console.log(showError.value);
break;
case "LEADER_TOP":
showError.value = !validate_LEADER_TOP(err);
break;
case "LEADER":
showError.value = !validate_LEADER(err);
break;
case "FORM_USER":
showError.value = !validate_FORM_USER(err);
break;
case "ROLE":
showError.value = !validate_ROLE(err);
break;
default:
showError.value = true
err.push("未知设置项😥")
break;
}
if (props.config.props.nobody.handler === 'TO_USER' && props.config.props.nobody.assignedUser.length === 0) {
errorInfo.value = '审批人为空时, 转交给指定人员:【请指定一个具体的人】'
err.push('审批人为空时, 转交给指定人员:【请指定一个具体的人】')
showError.value = true
}
return showError
} catch (e) {
console.log(e)
return false;
}
}
const validate_ASSIGN_USER = (err) => {
if (props.config.props.assignedUser.length > 0) {
return true;
} else {
errorInfo.value = '请指定审批人员'
err.push(`${props.config.name} 未指定审批人员`)
return false
}
}
const validate_SELF_SELECT = (err) => {
// if (!this.viewer) {
// return true
// }
// let userInfo = this.$store.state.selectUserMap.get(this.config.id);
// if (undefined !== userInfo && Array.isArray(userInfo) && userInfo.length > 0) {
// return true;
// }
errorInfo.value = '请指定审批人员'
err.push(`${props.config.name} 未指定审批人员`)
return false;
}
const validate_LEADER_TOP = (err) => {
return true;
}
const validate_LEADER = (err) => {
return true;
}
const validate_ROLE = (err) => {
if (props.config.props.roleList.length <= 0) {
errorInfo.value = '请指定负责审批的系统角色'
err.push(`${props.config.name} 未指定审批角色`)
return false
}
return true;
}
const validate_SELF = (err) => {
return true;
}
const validate_FORM_USER = (err) => {
if (props.config.props.formUser === '') {
errorInfo.value = '请指定表单中的人员组件'
err.push(`${props.config.name} 审批人为表单中人员,但未指定`)
return false
}
return true;
}
const validate_REFUSE = (err) => {
return true;
}
defineExpose({
validate
})
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,75 @@
<template>
<node :title="config.name" :show-error="showError" :select-user="selectUser" :mode="mode" :content="content" :node-id="config.id"
:error-info="errorInfo" :show-avatar="true" :user-info="config.props.assignedUser"
@selected="emit('selected')" @delNode="emit('delNode')" @insertNode="type => emit('insertNode', type)"
placeholder="请设置抄送人" :header-bgc="headerBgc" :header-icon="Promotion"/>
</template>
<script setup>
import Node from './Node.vue'
import {defineProps, defineEmits, computed, defineExpose} from "vue";
const emit = defineEmits(['insertNode', 'selected', 'delNode'])
const props = defineProps({
config: {
type: Object,
default: () => {
return {}
}
},
mode: {
type: String,
default: 'design'
}
})
const showError = ref(false)
const errorInfo = ref('')
import {Promotion} from '@element-plus/icons-vue'
const headerBgc = computed(() => {
if (props.mode === 'design' || props.mode === 'view') {
return '#3296fa'
} else {
return props.config.props.headerBgc
}
})
const selectUser = computed(() => {
return {
show: props.config.props.assignedType !== 'ASSIGN_USER' && props.config.props.shouldAdd,
multiple: true
};
})
const content = computed(() => {
if (props.config.props.shouldAdd) {
return '由发起人指定'
} else if (props.config.props.assignedUser.length > 0) {
let texts = []
props.config.props.assignedUser.forEach(org => texts.push(org.name))
return String(texts).replaceAll(',', '、')
} else {
return null
}
})
//校验数据配置的合法性
const validate = (err) => {
showError.value = false
if (props.config.props.shouldAdd) {
showError.value = false
} else if (props.config.props.assignedUser.length === 0) {
showError.value = true
errorInfo.value = '请选择需要抄送的人员'
}
if (showError.value) {
err.push(`抄送节点 ${props.config.name} 未设置抄送人`)
}
return !showError.value
}
defineExpose({
validate
})
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,195 @@
<template>
<div class="node">
<!-- 并行分支选择后右侧出现操作面板,占时不需要 <div class="node-body" @click="emit('selected')">-->
<div v-if="designState" class="node-body" @click="emit('selected')">
<div class="node-body-left" @click.stop="emit('leftMove')" v-if="level > 1">
<el-icon>
<ArrowLeftBold/>
</el-icon>
</div>
<div class="node-body-main">
<div class="node-body-main-header">
<span class="title">
<el-icon :size="15">
<Operation/>
</el-icon>
<ellipsis class="name" hover-tip :content="config.name ? config.name:('并行任务' + level)"/>
</span>
<span class="option">
<el-tooltip effect="dark" content="复制分支" placement="top">
<el-icon @click.stop="emit('copy')" :size="20">
<CopyDocument/>
</el-icon>
</el-tooltip>
<el-icon @click.stop="emit('delNode')" :size="20">
<CloseBold/>
</el-icon>
</span>
</div>
<div class="node-body-main-content">
<span>并行任务同时进行</span>
</div>
</div>
<div class="node-body-right" @click.stop="emit('rightMove')" v-if="level < size && designState ">
<el-icon>
<ArrowRightBold/>
</el-icon>
</div>
</div>
<div class="node-footer">
<div class="btn" :style="(designState ? '' : 'height:0px')">
<insert-button v-if="designState" @insertNode="type => emit('insertNode', type)"></insert-button>
</div>
</div>
</div>
</template>
<!--并行节点-->
<script setup>
import InsertButton from '../common/InsertButton.vue'
import {Operation, ArrowRightBold, ArrowLeftBold, CopyDocument, CloseBold, Warning} from '@element-plus/icons-vue'
import Ellipsis from '../common/Ellipsis.vue'
import {defineProps, defineEmits} from "vue";
const emit = defineEmits(['insertNode'])
const props = defineProps({
config: {
type: Object,
default: () => {
return {}
}
},
level: {
type: Number,
default: 1
},
//条件数
size: {
type: Number,
default: 0
},
mode: {
type: String,
default: 'design'
}
})
const designState = computed(()=>{
return props.mode === 'design'
})
const designStart = () => {
// return this.$store.state.diagramMode === 'design'
return true;
}
</script>
<style lang="scss" scoped>
.node {
padding: 30px 55px 0;
//width: 220px;
width: 320px;
.node-body {
overflow: hidden;
cursor: pointer;
min-height: 80px;
max-height: 120px;
position: relative;
border-radius: 5px;
background-color: white;
box-shadow: 0px 0px 5px 0px #d8d8d8;
&:hover {
.node-body-left, .node-body-right {
i {
display: block !important;
}
}
.node-body-main {
.option {
display: inline-block !important;
}
}
box-shadow: 0px 0px 3px 0px;
}
.node-body-left, .node-body-right {
display: flex;
align-items: center;
position: absolute;
height: 100%;
i {
display: none;
}
&:hover {
background-color: #ececec;
}
}
.node-body-left {
color: #888888;
left: 0;
}
.node-body-right {
color: #888888;
right: 0;
}
.node-body-main {
position: absolute;
width: 188px;
left: 17px;
display: inline-block;
.node-body-main-header {
padding: 10px 0px 5px;
font-size: xx-small;
position: relative;
.title {
color: #718dff;
.name {
display: inline-block;
height: 14px;
width: 130px;
margin-left: 2px;
}
}
.option {
position: absolute;
right: 10px;
display: none;
font-size: medium;
i {
color: #888888;
padding: 0 3px;
}
}
}
.node-body-main-content {
padding: 6px;
color: #656363;
font-size: 14px;
i {
position: absolute;
top: 55%;
right: 10px;
font-size: medium;
}
}
}
}
}
</style>

View File

@@ -0,0 +1,299 @@
<template>
<div :class="{'node': true, 'node-error-state': showError}">
<div :class="{'node-body': true, 'error': showError}">
<div class="node-body-left" @click="emit('leftMove')" v-if="level > 1 && designState">
<el-icon>
<ArrowLeftBold/>
</el-icon>
</div>
<div class="node-body-main" @click="emit('selected')">
<div class="node-body-main-header">
<ellipsis class="title" hover-tip :content="config.name ? config.name : ('条件' + level)"/>
<span class="level">优先级{{ level }}</span>
<span class="option" v-if="designState">
<el-tooltip effect="dark" content="复制条件" placement="top">
<el-icon @click.stop="emit('copy')" :size="20">
<CopyDocument/>
</el-icon>
</el-tooltip>
<el-icon @click.stop="emit('delNode')" :size="20">
<CloseBold/>
</el-icon>
</span>
</div>
<div class="node-body-main-content">
<span class="placeholder" v-if="(content || '').trim() === ''">{{ placeholder }}</span>
<ellipsis hoverTip :row="4" :content="content" v-else/>
</div>
</div>
<div class="node-body-right" @click="emit('rightMove')" v-if="level < size && designState">
<el-icon>
<ArrowRightBold/>
</el-icon>
</div>
<div class="node-error" v-if="showError">
<el-tooltip effect="dark" :content="errorInfo" placement="top-start">
<el-icon>
<Warning/>
</el-icon>
</el-tooltip>
</div>
</div>
<div class="node-footer">
<div class="btn">
<insert-button v-if="designState" @insertNode="type => emit('insertNode', type)"></insert-button>
</div>
</div>
</div>
</template>
<script setup>
import InsertButton from '../common/InsertButton.vue'
import Ellipsis from '../common/Ellipsis.vue'
import {ValueType} from '@/views/workflow/form/ComponentsConfigExport.js'
import {ArrowRightBold,ArrowLeftBold,CopyDocument,CloseBold,Warning} from '@element-plus/icons-vue'
import {defineProps, defineEmits, defineExpose} from "vue";
const emit = defineEmits()
const props = defineProps({
config: {
type: Object,
default: () => {
return {}
}
},
//索引位置
level: {
type: Number,
default: 1
},
//条件数
size: {
type: Number,
default: 0
},
mode: {
type: String,
default: 'design'
}
})
const groupNames = ref(['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J']);
const placeholder = ref('请设置条件');
const errorInfo = ref('');
const showError = ref(false);
const content = computed(() => {
const groups = props.config.props.groups
let confitions = []
groups.forEach(group => {
let subConditions = []
group.conditions.forEach(subCondition => {
let subConditionStr = ''
switch (subCondition.valueType) {
case ValueType.dept:
case ValueType.user:
subConditionStr = `${subCondition.title}属于[${String(subCondition.value.map(u => u.name)).replaceAll(',', '. ')}]之一`
break;
case ValueType.number:
case ValueType.string:
subConditionStr = getOrdinaryConditionContent(subCondition)
break;
}
subConditions.push(subConditionStr)
})
//根据子条件关系构建描述
let subConditionsStr = String(subConditions)
.replaceAll(',', subConditions.length > 1 ?
(group.groupType === 'AND' ? ') 且 (' : ') 或 (') :
(group.groupType === 'AND' ? ' 且 ' : ' 或 '))
confitions.push(subConditions.length > 1 ? `(${subConditionsStr})` : subConditionsStr)
})
//构建最终描述
return String(confitions).replaceAll(',', (props.config.props.groupsType === 'AND' ? ' 且 ' : ' 或 '))
})
const designState = computed(()=>{
return props.mode === 'design'
})
const getDefault = (val, df) => {
return val && val !== '' ? val : df;
}
const getOrdinaryConditionContent = (subCondition) => {
switch (subCondition.compare) {
case 'IN':
return `${subCondition.title}为[${String(subCondition.value).replaceAll(',', '、')}]中之一`
case 'B':
return `${subCondition.value[0]} < ${subCondition.title} < ${subCondition.value[1]}`
case 'AB':
return `${subCondition.value[0]}${subCondition.title} < ${subCondition.value[1]}`
case 'BA':
return `${subCondition.value[0]} < ${subCondition.title}${subCondition.value[1]}`
case 'ABA':
return `${subCondition.value[0]}${subCondition.title}${subCondition.value[1]}`
case '<=':
return `${subCondition.title}${getDefault(subCondition.value[0], ' ?')}`
case '>=':
return `${subCondition.title}${getDefault(subCondition.value[0], ' ?')}`
default:
return `${subCondition.title}${subCondition.compare}${getDefault(subCondition.value[0], ' ?')}`
}
}
//校验数据配置的合法性
const validate = (err) => {
const defineProps = props.config.props
if (defineProps.groups.length <= 0){
showError.value = true
errorInfo.value = '请设置分支条件'
err.push(`${defineProps.config.name} 未设置条件`)
}else {
for (let i = 0; i < defineProps.groups.length; i++) {
if (defineProps.groups[i].cids.length === 0){
showError.value = true
errorInfo.value = `请设置条件组${groupNames.value[i]}内的条件`
err.push(`条件 ${props.config.name} 条件组${groupNames.value[i]}内未设置条件`)
break
}else {
let conditions = defineProps.groups[i].conditions
for (let ci = 0; ci < conditions.length; ci++) {
let subc = conditions[ci]
showError.value = subc.value.length === 0;
if (showError.value){
errorInfo.value = `请完善条件组${groupNames.value[i]}内的${subc.title}条件`
err.push(`条件 ${props.config.name} 条件组${groupNames.value[i]}${subc.title}条件未完善`)
return false
}
}
}
}
}
return !showError.value
}
defineExpose({
validate
})
</script>
<style lang="scss" scoped>
.node {
padding: 30px 55px 0;
//width: 220px;
width: 320px;
.node-body {
cursor: pointer;
min-height: 80px;
max-height: 120px;
position: relative;
border-radius: 5px;
background-color: white;
box-shadow: 0px 0px 5px 0px #d8d8d8;
&:hover {
.node-body-left, .node-body-right {
i {
display: block !important;
}
}
.node-body-main {
.level {
display: none !important;
}
.option {
display: inline-block !important;
}
}
box-shadow: 0px 0px 3px 0px;
}
.node-body-left, .node-body-right {
display: flex;
align-items: center;
position: absolute;
height: 100%;
i {
display: none;
}
&:hover {
background-color: #ececec;
}
}
.node-body-left {
color: #888888;
left: 0;
}
.node-body-right {
color: #888888;
right: 0;
top: 0;
}
.node-body-main {
//position: absolute;
width: 188px;
margin-left: 17px;
display: inline-block;
.node-body-main-header {
padding: 10px 0px 5px;
font-size: xx-small;
position: relative;
.title {
color: #15bca3;
display: inline-block;
height: 14px;
width: 125px;
}
.level {
position: absolute;
right: 15px;
color: #888888;
}
.option {
position: absolute;
right: 10px;
display: none;
font-size: medium;
i {
color: #888888;
padding: 0 3px;
}
}
}
.node-body-main-content {
padding: 6px;
color: #656363;
font-size: 14px;
i {
position: absolute;
top: 55%;
right: 10px;
font-size: medium;
}
.placeholder {
color: #8c8c8c;
}
}
}
}
}
</style>

View File

@@ -0,0 +1,86 @@
<template>
<node :title="config.name" :mode="mode" :show-error="showError" :content="content" :error-info="errorInfo"
@selected="emit('selected')" @delNode="emit('delNode')" @insertNode="type => emit('insertNode', type)"
placeholder="请设置延时时间" :header-bgc="headerBgc" :header-icon="Clock"/>
</template>
<!--延时器节点-->
<script setup>
import Node from './Node.vue'
import {defineProps, defineEmits, computed, defineExpose} from "vue";
const emit = defineEmits()
import {Clock} from '@element-plus/icons-vue'
const props = defineProps({
config: {
type: Object,
default: () => {
return {}
}
},
mode: {
type: String,
default: 'design'
}
})
const showError = ref(false)
const errorInfo = ref('')
const headerBgc = computed(() => {
if (props.mode === 'design' || props.mode === 'view') {
return '#f25643'
} else {
return props.config.props.headerBgc
}
})
const content = computed(() => {
if (props.config.props.type === 'FIXED') {
return `等待 ${props.config.props.time} ${getName(props.config.props.unit)}`
} else if (props.config.props.type === 'AUTO') {
return `至当天 ${props.config.props.dateTime}`
} else {
return null
}
})
//校验数据配置的合法性
const validate = (err) => {
showError.value = false
try {
if (props.config.props.type === "AUTO") {
if ((props.config.props.dateTime || "") === "") {
showError.value = true
errorInfo.value = "请选择时间点"
}
} else {
if (props.config.props.time <= 0) {
showError.value = true
errorInfo.value = "请设置延时时长"
}
}
} catch (e) {
showError.value = true
errorInfo.value = "配置出现问题"
}
if (showError) {
err.push(`${props.config.name} 未设置延时规则`)
}
return !showError
}
const getName = (unit) => {
switch (unit) {
case 'D':
return '天';
case 'H':
return '小时';
case 'M':
return '分钟';
default:
return '未知';
}
}
defineExpose({
validate
})
</script>

View File

@@ -0,0 +1,20 @@
<template>
<node :show="false" :mode="mode" @insertNode="type => emit('insertNode', type)"/>
</template>
<script setup>
import Node from './Node.vue'
import {defineEmits,defineProps} from "vue";
const emit = defineEmits()
const props = defineProps({
mode: {
type: String,
default: 'design'
}
})
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,20 @@
<template>
<node :show="false" :merge="true" :mode="mode" @insertNode="type => emit('insertNode', type)"/>
</template>
<script setup>
import Node from './Node.vue'
import {defineEmits, defineProps} from "vue";
const emit = defineEmits()
const props = defineProps({
mode: {
type: String,
default: 'design'
}
})
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,269 @@
<template>
<div :class="{'node': true, 'root': isRoot || !show, 'node-error-state': showError}">
<div v-if="show" @click="emit('selected')" :class="{'node-body': true, 'error': showError}">
<div class="node-body-header" :style="{'background-color': headerBgc}">
<el-icon v-if="headerIcon" size="15">
<component :is="headerIcon"/>
</el-icon>
<ellipsis class="name" hover-tip :content="title"/>
<el-icon v-if="!isRoot && designState" size="15" style="float:right;" @click="emit('delNode')">
<Close/>
</el-icon>
</div>
<div class="node-body-content">
<el-icon v-if="leftIcon">
<component :is="leftIcon"/>
</el-icon>
<template v-if="selectUser.show && mode === 'view'">
<div class="avatar_button">
<avatar-ellipsis :row="3" v-if="userInfo.length > 0" :user-info="userInfo"/>
<el-button type="primary" :icon="Plus" circle/>
</div>
</template>
<template v-else-if="showAvatar">
<span class="placeholder" v-if="userInfo.length === 0">{{ placeholder }}</span>
<avatar-ellipsis :row="3" :user-info="userInfo" v-else/>
</template>
<template v-else>
<span class="placeholder" v-if="(content || '').trim() === ''">{{ placeholder }}</span>
<ellipsis :row="3" :content="content" v-else/>
</template>
</div>
<div class="node-error" v-if="showError">
<el-tooltip effect="dark" :content="errorInfo" placement="top-start">
<el-icon><Warning /></el-icon>
</el-tooltip>
</div>
</div>
<div class="node-footer">
<div v-if="merge" class="branch-merge">
<img data-v-1e7b1da5=""
src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAABXlJREFUeF7tmm2IVGUUx3/nLkQUqxmkpAVhSPUllZX8EEGhvVgZQWCU6wcpsqi2mXtnoaBwoqLCufeO9oJYUJQiFZWlZPQCfYiIXkylstTyS1oWWeYWCe6cuLsz7t3r3Ll3du6907oz35bnPM9z/r895zznmWeECf6RCa6fDoBOBExwAp0UmOAB0CmCnRTopECbCZi2nivK1CMD7FpXlH+ydqctKZBz9QyjwkqFG4HzfKK/0gpvl/ulmBWIzAHkVmlRDFY2EihwwLFkRhYQxgzAtHW2CpcBc4CpOsg26eK7yVPYVFwu/9ZzPufq2VLhQBxhAk85lvSF2Y5l/3prjQlAvqR3IjwJTKqz6DeGssQuyLfBsbyjz6PcFgeAZyOwzLFk/QnrjHH/RACYtj6ncHuUCDWYXs7Lz367XEn3inB+1NzauMLGsiW3+u1b2b9lAH22LuiCD2IJED5yTbmiZpt3dAbKT7Hmjhjtdi25oPZnK/uH7dtUCpi2vqVwQ1wRWuHhWkW3StpTEb6IO3fIThhwTemuzWll/0QA5G39FTgrtghhs2vKELA7inra6d38HXvusOEnriWXHo+iFvZvGUDfap3VdYzdzQgIHmd5W7cBc2Ovoax1C3KXZ5/E/i3VgCQciNMD+J1Ug55yXjxo7QfgOdFKCvjyeL/C9MgoEBa5przrt0ti/+C+mRVB/8amrWsU7m2Ql/c5lqwJjre9CDZ1DMFnriXzw0SatvZW4FqBW6o2vwGvCGxyLPmw3rym9g8cwy0XQV8Ix2qEgH0C1ziWNCyceVu1uvZrriVLGqVGsainHO7mRyDynlCvEWupCI7Kxcat6BFg+OwW9ohyfSMIcQF44v/sZovAlRH1I7QVTwyAt1Cjy8hfh9hYvepGQogDoI74HYbSXzHwusTYl7FEAURVcdPWN+NAiAJQTzzCda4p+6N8iDPe1CkQZ8FAtY+E0AhA2uKHszTlT1QkhAHIQnwmAKr1IjQS6gHISnxmABpBCALIUnymAMIgKHxf6wMmH6E3cNTtSLLgZXoKNOgAR6UDyqyq7esKk3znfOriM48AXzc5AmGE1EFgWvXPTMS3DcAJ6TA6XDIT31YAoRCEc5JqcuKc8Kn3AVFO+E4BryvJVHzbI8BzwAdgi2vJ4ihgSY//nyIg8jqctPi2RYBp61wV5qsyR2BFVdhBhBe0wtdisNU15VAagoNrZh4BOVsfFcVCODVUoLBn6E2hIBvShpApgLyt3nvhRXFFCWxwLOmNaz8Wu8wA5G31ntQWNOukwD2OJc80Oy+ufSYAciVdJsJLcZ0K2nV1MbuUk51jnd9oXuoACmW9eHCQj49/T+j3RtiJslPgFxXmoVxe11nhPdeUq8clANPWPoXVQef9D6e1saEHVINHUBYF7Q1lnl2QL5OGkHoE5G19GRhVyBQ+L1tyST0xoa/Iygq3IOvGI4BdwIWjHBeecE15IExM3tYfgJmBOetcU2o9Q2IcsoiAw8Gf0qhyU7kgb4QCcPRTlNGvSspmtzD81J7kJ30AJX0fYeHo2seLjiXLG0TA78CZ/nGFB8uWPJakeG+t9AE4+jjK/XGLWtgTusBix5It4w5ArqRLRTjhl14IW40KD/kre66ki0R4p57IrgozS/2yb9wByDvqhbKX07Xv/oIavMfOP6pFb0pdgcqrbkFuTlp8JingbRIaBfEUHUOYltbtMPUaUNNo2rpeYWk8zSNWqvSmeSvMDIAnybT1boWnY0I4aigL7YJ4bXRqn0wBeCqG7gYVVqFcFapKWXt0gP5nizKQmvLqwpkDqAkaanmhB4MelOkI27XCDjHY7pqyN23htfXbBiArgVH7dABEETrZxzsRcLL/h6P0/Qc1qphfvB2K3wAAAABJRU5ErkJggg=="
alt="">
</div>
<div class="btn">
<insert-button v-if="designState" @insertNode="type => emit('insertNode', type)"/>
</div>
</div>
<!-- <user-picker v-if="selectUser.show" title="请选择系统用户" :multiple="selectUser.multiple" ref="userPicker"-->
<!-- :selected="_userInfo"-->
<!-- @ok="selectedUser"/>-->
</div>
</template>
<script setup>
import InsertButton from '../common/InsertButton.vue'
import Ellipsis from '../common/Ellipsis.vue'
import AvatarEllipsis from '../common/AvatarEllipsis.vue'
import {defineProps,defineEmits} from "vue";
const emit = defineEmits(['insertNode'])
import {Close,Warning,Plus} from '@element-plus/icons-vue'
const props = defineProps({
//是否为根节点
isRoot: {
type: Boolean,
default: false
},
nodeId:{
type:String,
default:()=>{
return "";
}
},
//是否显示节点体
show: {
type: Boolean,
default: true
},
//是否显示节点体
merge: {
type: Boolean,
default: false
},
//节点内容区域文字
content: {
type: String,
default: ""
},
//节点内容区域文字
userInfo: {
type: Array,
default() {
return []
}
},
//节点内容区域文字
showAvatar: {
type: Boolean,
default: false
},
selectUser: {
type: Object,
default() {
return {
show: false,
multiple: false,
}
}
},
title: {
type: String,
default: "标题"
},
placeholder: {
type: String,
default: "请设置"
},
//节点体左侧图标
leftIcon: {
type: Object,
default: null
},
//头部图标
headerIcon: {
type: String,
default: ''
},
//头部背景色
headerBgc: {
type: String,
default: '#576a95'
},
//是否显示错误状态
showError: {
type: Boolean,
default: false
},
errorInfo: {
type: String,
default: '无信息'
},
mode: {
type: String,
default: 'design'
}
})
const designState = computed(()=>{
return props.mode === 'design'
})
const init = () => {
// let userInfo = this.$store.state.selectUserMap.get(this.nodeId);
// if (userInfo){
// let userInfoList = []
// for (let val of userInfo) {
// let userInfo = {
// id: val.id,
// name: val.name,
// avatar: val.avatar,
// }
// userInfoList.push(userInfo)
// }
// // this._userInfo = userInfoList
// }
}
init()
</script>
<style lang="scss" scoped>
.root {
&:before {
display: none !important;
}
}
.node {
padding: 0 50px;
//width: 220px;
width: 320px;
position: relative;
&:before {
content: '';
position: absolute;
top: -12px;
left: 50%;
-webkit-transform: translateX(-50%);
transform: translateX(-50%);
width: 0;
border-style: solid;
border-width: 8px 6px 4px;
border-color: #CACACA transparent transparent;
background: #F5F5F7;
}
.node-body {
cursor: pointer;
min-height: 63px;
position: relative;
border-radius: 5px;
background-color: white;
box-shadow: 0px 0px 5px 0px #d8d8d8;
&:hover {
box-shadow: 0px 0px 3px 0px;
.node-body-header {
.el-icon-close {
display: inline;
font-size: medium;
}
}
}
.node-body-header {
border-top-left-radius: 5px;
border-top-right-radius: 5px;
padding: 5px 15px;
color: white;
font-size: xx-small;
.el-icon-close {
display: none;
}
.name {
height: 14px;
width: 150px;
display: inline-block
}
}
.node-body-content {
padding: 18px;
color: #656363;
font-size: 14px;
.avatar_button {
//float: left;
display: flex;
//flex: 1;
flex-wrap: wrap;
button {
margin-top: 3px;
height: 40px;
//flex-shrink: 0;
//flex-grow: 0;
}
}
i {
position: absolute;
top: 55%;
right: 5px;
font-size: medium;
}
.placeholder {
color: #8c8c8c;
}
}
}
}
</style>

View File

@@ -0,0 +1,3 @@
<template>
流程结束
</template>

View File

@@ -0,0 +1,48 @@
<template>
<node title="发起人" :is-root="true" :mode="mode" :content="content" show-avatar :user-info="config.props.assignedUser"
@selected="emit('selected')" @insertNode="type => emit('insertNode', type)"
placeholder="所有人" :header-bgc="config.props.headerBgc" :header-icon="UserFilled"/>
</template>
<script setup>
import Node from './Node.vue'
import {UserFilled} from '@element-plus/icons-vue'
import {defineExpose, defineProps} from "vue";
const emit = defineEmits(['insertNode','selected'])
const props = defineProps({
config:{
type: Object,
default: () => {
return {}
}
},
mode: {
type: String,
default: 'design'
}
})
const content = computed(() => {
if (props.config.props.assignedUser.length > 0){
let texts = []
props.config.props.assignedUser.forEach(org => texts.push(org.name))
return String(texts).replaceAll(',', '、')
} else {
return '所有人'
}
})
const validate = () => {
console.log("调用成功")
return []
}
defineExpose({
validate
})
</script>

View File

@@ -0,0 +1,96 @@
<template>
<node :title="config.name" :mode="mode" :show-error="showError" :content="content" :error-info="errorInfo"
@selected="emit('selected')" @delNode="emit('delNode')" @insertNode="type => emit('insertNode', type)"
placeholder="请设置触发器" :header-bgc="headerBgc" :header-icon="SetUp"/>
</template>
<!--触发器节点-->
<script setup>
import Node from './Node.vue'
const emit = defineEmits()
import {defineProps, defineEmits, defineExpose} from "vue";
import {SetUp} from '@element-plus/icons-vue'
const props = defineProps({
config: {
type: Object,
default: () => {
return {}
}
},
mode: {
type: String,
default: 'design'
}
})
const showError = ref(false)
const errorInfo = ref('')
const viewer = computed(() => {
return false;
})
const preview = computed(() => {
return false;
})
const headerBgc = computed(() => {
return '#47bc82'
// if (preview || !viewer) {
// return '#ff943e'
// } else {
// return props.config.props.headerBgc
// }
})
const content = computed(() => {
return '请设置触发器'
})
//校验数据配置的合法性
const validate = (err) => {
showError.value = false
if (props.config.props.type === 'WEBHOOK') {
if (isNotEmpty(props.config.props.http.url)) {
showError.value = false
} else {
showError.value = true
errorInfo.value = '请设置WEBHOOK的URL地址'
}
if (restfulCheck(props.config.props.http.url)) {
showError.value = false
} else {
showError.value = true
errorInfo.value = 'WEBHOOK的URL地址不符合RESTful标准'
}
} else if (props.config.props.type === 'EMAIL') {
if (!isNotEmpty(props.config.props.email.subject)
|| props.config.props.email.to.length === 0
|| !isNotEmpty(props.config.props.email.content)) {
showError.value = true
errorInfo.value = '请设置邮件发送配置'
} else {
showError.value = false
}
}
if (showError) {
err.push(`${props.config.name} 触发动作未设置完善`)
}
return !showError
}
//url规范性检查
const isNotEmpty = (obj) => {
return (obj !== undefined && obj !== null && obj !== '' && obj !== 'null')
}
const restfulCheck = (url) => {
const httpProtocolPattern = /^http:/;
const httpsProtocolPattern = /^https:/;
const restfulUrlPattern = /\/\w+\/\w+(\/\{[^}]+\})*/;
if (httpProtocolPattern.test(url) || httpsProtocolPattern.test(url)) {
return restfulUrlPattern.test(url);
} else {
return false;
}
}
defineExpose({
validate
})
</script>
<style scoped>
</style>