!654 Simple设计器功能完善

Merge pull request !654 from Lesan/feature/bpm-n
pull/669/head
芋道源码 2025-01-08 14:12:37 +00:00 committed by Gitee
commit d11a82a521
No known key found for this signature in database
GPG Key ID: 173E9B9CA92EEF8F
10 changed files with 444 additions and 692 deletions

View File

@ -73,6 +73,7 @@
"vue-i18n": "9.10.2", "vue-i18n": "9.10.2",
"vue-router": "4.4.5", "vue-router": "4.4.5",
"vue-types": "^5.1.1", "vue-types": "^5.1.1",
"vue3-signature": "^0.2.4",
"vuedraggable": "^4.1.0", "vuedraggable": "^4.1.0",
"web-storage-cache": "^1.1.1", "web-storage-cache": "^1.1.1",
"xml-js": "^1.6.11" "xml-js": "^1.6.11"

View File

@ -1,3 +0,0 @@
import ESign from './src/ESign.vue'
export { ESign }

View File

@ -1,289 +0,0 @@
<!-- TODO @lesan看着没啥问题哈哈哈我还搜了下嘿嘿有没人封装好了组件库哈https://github.com/WangShayne/vue3-signature https://www.npmjs.com/package/vue3-signature -->
<template>
<div style="position: relative">
<canvas
ref="canvasRef"
@mousedown="mouseDown"
@mousemove="mouseMove"
@mouseup="mouseUp"
@touchstart="touchStart"
@touchmove="touchMove"
@touchend="touchEnd"
style="border: 1px solid lightgrey; max-width: 100%; display: block"
>
</canvas>
<el-button
style="position: absolute; bottom: 20px; right: 10px"
type="primary"
text
size="small"
@click="reset"
>
<Icon icon="ep:delete" class="mr-5px" />清除
</el-button>
</div>
</template>
<script lang="ts" setup>
import { propTypes } from '@/utils/propTypes'
defineOptions({ name: 'ESign' })
const emits = defineEmits(['update:bgColor'])
const props = defineProps({
//
width: propTypes.number.def(900),
//
height: propTypes.number.def(400),
//
lineWidth: propTypes.number.def(10),
//
lineColor: propTypes.string.def('#000000'),
//
bgColor: propTypes.string.def(''),
//
isCrop: propTypes.bool.def(false),
//
isClearBgColor: propTypes.bool.def(true),
//
format: propTypes.string.def('image/png'),
// 0 1
quality: propTypes.number.def(1)
})
const canvasRef = ref()
const hasDrew = ref(false)
const resultImg = ref('')
const points = ref<any>([])
const canvasTxt = ref()
const startX = ref(0)
const startY = ref(0)
const isDrawing = ref(false)
const sratio = ref(1)
const ratio = computed(() => {
return props.height / props.width
})
const stageInfo = computed(() => {
return canvasRef.value.getBoundingClientRect()
})
const bgColor = computed(() => {
return props.bgColor ? props.bgColor : 'rgba(255, 255, 255, 0)'
})
watch(
() => bgColor.value,
() => {
if (canvasRef.value) {
canvasRef.value.style.background = bgColor.value
}
},
{
immediate: true
}
)
const resizeHandler = () => {
const canvas = canvasRef.value
canvas.style.width = props.width + 'px'
const realw = parseFloat(window.getComputedStyle(canvas).width)
canvas.style.height = ratio.value * realw + 'px'
canvasTxt.value = canvas.getContext('2d')
canvasTxt.value.scale(1 * sratio.value, 1 * sratio.value)
sratio.value = realw / props.width
canvasTxt.value.scale(1 / sratio.value, 1 / sratio.value)
}
// For PC
const mouseDown = (e) => {
e.preventDefault()
isDrawing.value = true
hasDrew.value = true
let obj = {
x: e.offsetX,
y: e.offsetY
}
drawStart(obj)
}
const mouseMove = (e) => {
e.preventDefault()
if (isDrawing.value) {
let obj = {
x: e.offsetX,
y: e.offsetY
}
drawMove(obj)
}
}
const mouseUp = (e) => {
e.preventDefault()
let obj = {
x: e.offsetX,
y: e.offsetY
}
drawEnd(obj)
isDrawing.value = false
}
// For Mobile
const touchStart = (e) => {
e.preventDefault()
hasDrew.value = true
if (e.touches.length === 1) {
let obj = {
x: e.targetTouches[0].clientX - canvasRef.value.getBoundingClientRect().left,
y: e.targetTouches[0].clientY - canvasRef.value.getBoundingClientRect().top
}
drawStart(obj)
}
}
const touchMove = (e) => {
e.preventDefault()
if (e.touches.length === 1) {
let obj = {
x: e.targetTouches[0].clientX - canvasRef.value.getBoundingClientRect().left,
y: e.targetTouches[0].clientY - canvasRef.value.getBoundingClientRect().top
}
drawMove(obj)
}
}
const touchEnd = (e) => {
e.preventDefault()
if (e.touches.length === 1) {
let obj = {
x: e.targetTouches[0].clientX - canvasRef.value.getBoundingClientRect().left,
y: e.targetTouches[0].clientY - canvasRef.value.getBoundingClientRect().top
}
drawEnd(obj)
}
}
//
const drawStart = (obj) => {
startX.value = obj.x
startY.value = obj.y
canvasTxt.value.beginPath()
canvasTxt.value.moveTo(startX.value, startY.value)
canvasTxt.value.lineTo(obj.x, obj.y)
canvasTxt.value.lineCap = 'round'
canvasTxt.value.lineJoin = 'round'
canvasTxt.value.lineWidth = props.lineWidth * sratio.value
canvasTxt.value.stroke()
canvasTxt.value.closePath()
points.value.push(obj)
}
const drawMove = (obj) => {
canvasTxt.value.beginPath()
canvasTxt.value.moveTo(startX.value, startY.value)
canvasTxt.value.lineTo(obj.x, obj.y)
canvasTxt.value.strokeStyle = props.lineColor
canvasTxt.value.lineWidth = props.lineWidth * sratio.value
canvasTxt.value.lineCap = 'round'
canvasTxt.value.lineJoin = 'round'
canvasTxt.value.stroke()
canvasTxt.value.closePath()
startY.value = obj.y
startX.value = obj.x
points.value.push(obj)
}
const drawEnd = (obj) => {
canvasTxt.value.beginPath()
canvasTxt.value.moveTo(startX.value, startY.value)
canvasTxt.value.lineTo(obj.x, obj.y)
canvasTxt.value.lineCap = 'round'
canvasTxt.value.lineJoin = 'round'
canvasTxt.value.stroke()
canvasTxt.value.closePath()
points.value.push(obj)
points.value.push({ x: -1, y: -1 })
}
//
const generate = (options) => {
let imgFormat = options && options.format ? options.format : props.format
let imgQuality = options && options.quality ? options.quality : props.quality
const pm = new Promise((resolve, reject) => {
if (!hasDrew.value) {
reject(`Warning: Not Signned!`)
return
}
let resImgData = canvasTxt.value.getImageData(
0,
0,
canvasRef.value.width,
canvasRef.value.height
)
canvasTxt.value.globalCompositeOperation = 'destination-over'
canvasTxt.value.fillStyle = bgColor.value
canvasTxt.value.fillRect(0, 0, canvasRef.value.width, canvasRef.value.height)
resultImg.value = canvasRef.value.toDataURL(imgFormat, imgQuality)
canvasTxt.value.clearRect(0, 0, canvasRef.value.width, canvasRef.value.height)
canvasTxt.value.putImageData(resImgData, 0, 0)
canvasTxt.value.globalCompositeOperation = 'source-over'
if (props.isCrop) {
const crop_area = getCropArea(resImgData.data)
let crop_canvas = document.createElement('canvas')
const crop_ctx = crop_canvas.getContext('2d')
crop_canvas.width = crop_area[2] - crop_area[0]
crop_canvas.height = crop_area[3] - crop_area[1]
const crop_imgData = canvasTxt.value.getImageData(...crop_area)
crop_ctx.globalCompositeOperation = 'destination-over'
crop_ctx.putImageData(crop_imgData, 0, 0)
crop_ctx.fillStyle = bgColor.value
crop_ctx.fillRect(0, 0, crop_canvas.width, crop_canvas.height)
resultImg.value = crop_canvas.toDataURL(imgFormat, imgQuality)
}
resolve(resultImg.value)
})
return pm
}
const reset = () => {
canvasTxt.value.clearRect(0, 0, canvasRef.value.width, canvasRef.value.height)
if (props.isClearBgColor) {
emits('update:bgColor', '')
canvasRef.value.style.background = 'rgba(255, 255, 255, 0)'
}
points.value = []
hasDrew.value = false
resultImg.value = ''
}
const getCropArea = (imgData) => {
let topX = canvasRef.value.width
let btmX = 0
let topY = canvasRef.value.height
let btnY = 0
for (let i = 0; i < canvasRef.value.width; i++) {
for (let j = 0; j < canvasRef.value.height; j++) {
let pos = (i + canvasRef.value.width * j) * 4
if (imgData[pos] > 0 || imgData[pos + 1] > 0 || imgData[pos + 2] || imgData[pos + 3] > 0) {
btnY = Math.max(j, btnY)
btmX = Math.max(i, btmX)
topY = Math.min(j, topY)
topX = Math.min(i, topX)
}
}
}
topX++
btmX++
topY++
btnY++
const data = [topX, topY, btmX, btnY]
return data
}
defineExpose({
generate
})
onBeforeMount(() => {
window.addEventListener('resize', resizeHandler)
})
onBeforeUnmount(() => {
window.removeEventListener('resize', resizeHandler)
})
onMounted(() => {
canvasRef.value.height = props.height
canvasRef.value.width = props.width
canvasRef.value.style.background = bgColor.value
resizeHandler()
//
document.onmouseup = () => {
isDrawing.value = false
}
})
</script>

View File

@ -242,7 +242,6 @@ const addNode = (type: number) => {
emits('update:childNode', data) emits('update:childNode', data)
} }
if (type === NodeType.ROUTE_BRANCH_NODE) { if (type === NodeType.ROUTE_BRANCH_NODE) {
// TODO @lesan
const data: SimpleFlowNode = { const data: SimpleFlowNode = {
id: 'GateWay_' + generateUUID(), id: 'GateWay_' + generateUUID(),
name: NODE_DEFAULT_NAME.get(NodeType.ROUTE_BRANCH_NODE) as string, name: NODE_DEFAULT_NAME.get(NodeType.ROUTE_BRANCH_NODE) as string,

View File

@ -116,8 +116,10 @@ export interface SimpleFlowNode {
// 延迟设置 // 延迟设置
delaySetting?: DelaySetting delaySetting?: DelaySetting
// 路由分支 // 路由分支
routeGroup?: RouteCondition[] routerGroups?: RouteCondition[]
defaultFlowId?: string defaultFlowId?: string
// 签名
signEnable?: boolean
} }
// 候选人策略枚举 用于审批节点。抄送节点 ) // 候选人策略枚举 用于审批节点。抄送节点 )
export enum CandidateStrategy { export enum CandidateStrategy {
@ -241,15 +243,15 @@ export type AssignEmptyHandler = {
export type ListenerHandler = { export type ListenerHandler = {
enable: boolean enable: boolean
path?: string path?: string
header?: ListenerMap[] header?: ListenerParam[]
body?: ListenerMap[] body?: ListenerParam[]
} }
export type ListenerMap = { export type ListenerParam = {
key: string key: string
type: number type: number
value: string value: string
} }
export enum ListenerMapTypeEnum { export enum ListenerParamTypeEnum {
/** /**
* *
*/ */
@ -510,8 +512,8 @@ export const APPROVE_METHODS: DictDataVO[] = [
] ]
export const CONDITION_CONFIG_TYPES: DictDataVO[] = [ export const CONDITION_CONFIG_TYPES: DictDataVO[] = [
{ label: '条件表达式', value: ConditionType.EXPRESSION }, { label: '条件规则', value: ConditionType.RULE },
{ label: '条件规则', value: ConditionType.RULE } { label: '条件表达式', value: ConditionType.EXPRESSION }
] ]
// 时间单位类型 // 时间单位类型
@ -660,7 +662,7 @@ export const DELAY_TYPE = [
*/ */
export type RouteCondition = { export type RouteCondition = {
nodeId: string nodeId: string
conditionType: number // TODO @lesanConditionType conditionType: ConditionType
conditionExpression: string conditionExpression: string
conditionGroups: ConditionGroup conditionGroups: ConditionGroup
} }

View File

@ -15,7 +15,7 @@ import {
AssignStartUserHandlerType, AssignStartUserHandlerType,
AssignEmptyHandlerType, AssignEmptyHandlerType,
FieldPermissionType, FieldPermissionType,
ListenerMap ListenerParam
} from './consts' } from './consts'
import { parseFormFields } from '@/components/FormCreate/src/utils/index' import { parseFormFields } from '@/components/FormCreate/src/utils/index'
export function useWatchNode(props: { flowNode: SimpleFlowNode }): Ref<SimpleFlowNode> { export function useWatchNode(props: { flowNode: SimpleFlowNode }): Ref<SimpleFlowNode> {
@ -139,16 +139,17 @@ export type UserTaskFormType = {
buttonsSetting: any[] buttonsSetting: any[]
taskCreateListenerEnable?: boolean taskCreateListenerEnable?: boolean
taskCreateListenerPath?: string taskCreateListenerPath?: string
taskCreateListenerHeader?: ListenerMap[] taskCreateListenerHeader?: ListenerParam[]
taskCreateListenerBody?: ListenerMap[] taskCreateListenerBody?: ListenerParam[]
taskAssignListenerEnable?: boolean taskAssignListenerEnable?: boolean
taskAssignListenerPath?: string taskAssignListenerPath?: string
taskAssignListenerHeader?: ListenerMap[] taskAssignListenerHeader?: ListenerParam[]
taskAssignListenerBody?: ListenerMap[] taskAssignListenerBody?: ListenerParam[]
taskCompleteListenerEnable?: boolean taskCompleteListenerEnable?: boolean
taskCompleteListenerPath?: string taskCompleteListenerPath?: string
taskCompleteListenerHeader?: ListenerMap[] taskCompleteListenerHeader?: ListenerParam[]
taskCompleteListenerBody?: ListenerMap[] taskCompleteListenerBody?: ListenerParam[]
signEnable: boolean
} }
export type CopyTaskFormType = { export type CopyTaskFormType = {

View File

@ -25,7 +25,7 @@
</template> </template>
<div> <div>
<el-form label-position="top"> <el-form label-position="top">
<el-card class="mb-15px" v-for="(item, index) in routeGroup" :key="index"> <el-card class="mb-15px" v-for="(item, index) in routerGroups" :key="index">
<template #header> <template #header>
<div class="flex flex-items-center"> <div class="flex flex-items-center">
<el-text size="large">路由{{ index + 1 }}</el-text> <el-text size="large">路由{{ index + 1 }}</el-text>
@ -42,123 +42,7 @@
> >
</div> </div>
</template> </template>
<el-form-item label="配置方式" prop="conditionType"> <Condition v-model="routerGroups[index]" />
<el-radio-group v-model="item.conditionType" @change="changeConditionType">
<el-radio
v-for="(dict, indexConditionType) in conditionConfigTypes"
:key="indexConditionType"
:value="dict.value"
:label="dict.value"
>
{{ dict.label }}
</el-radio>
</el-radio-group>
</el-form-item>
<!-- TODO @lesan112 使用枚举2默认先 条件组关系 条件表达式3这种可以封装成一个小组件么 -->
<el-form-item
v-if="item.conditionType === 1"
label="条件表达式"
prop="conditionExpression"
>
<el-input
type="textarea"
v-model="item.conditionExpression"
clearable
style="width: 100%"
/>
</el-form-item>
<el-form-item v-if="item.conditionType === 2" label="条件规则">
<div class="condition-group-tool">
<div class="flex items-center">
<div class="mr-4">条件组关系</div>
<el-switch
v-model="item.conditionGroups.and"
inline-prompt
active-text="且"
inactive-text="或"
/>
</div>
</div>
<el-space direction="vertical" :spacer="item.conditionGroups.and ? '且' : '或'">
<el-card
class="condition-group"
style="width: 530px"
v-for="(condition, cIdx) in item.conditionGroups.conditions"
:key="cIdx"
>
<div
class="condition-group-delete"
v-if="item.conditionGroups.conditions.length > 1"
>
<Icon
color="#0089ff"
icon="ep:circle-close-filled"
:size="18"
@click="deleteConditionGroup(item.conditionGroups.conditions, cIdx)"
/>
</div>
<template #header>
<div class="flex items-center justify-between">
<div>条件组</div>
<div class="flex">
<div class="mr-4">规则关系</div>
<el-switch
v-model="condition.and"
inline-prompt
active-text="且"
inactive-text="或"
/>
</div>
</div>
</template>
<div class="flex pt-2" v-for="(rule, rIdx) in condition.rules" :key="rIdx">
<div class="mr-2">
<el-select style="width: 160px" v-model="rule.leftSide">
<el-option
v-for="(field, fIdx) in fieldOptions"
:key="fIdx"
:label="field.title"
:value="field.field"
:disabled="!field.required"
/>
</el-select>
</div>
<div class="mr-2">
<el-select v-model="rule.opCode" style="width: 100px">
<el-option
v-for="operator in COMPARISON_OPERATORS"
:key="operator.value"
:label="operator.label"
:value="operator.value"
/>
</el-select>
</div>
<div class="mr-2">
<el-input v-model="rule.rightSide" style="width: 160px" />
</div>
<div class="mr-1 flex items-center" v-if="condition.rules.length > 1">
<Icon
icon="ep:delete"
:size="18"
@click="deleteConditionRule(condition, rIdx)"
/>
</div>
<div class="flex items-center">
<Icon icon="ep:plus" :size="18" @click="addConditionRule(condition, rIdx)" />
</div>
</div>
</el-card>
</el-space>
<div title="添加条件组" class="mt-4 cursor-pointer">
<Icon
color="#0089ff"
icon="ep:plus"
:size="24"
@click="addConditionGroup(item.conditionGroups.conditions)"
/>
</div>
</el-form-item>
</el-card> </el-card>
</el-form> </el-form>
@ -177,18 +61,9 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { Plus } from '@element-plus/icons-vue' import { Plus } from '@element-plus/icons-vue'
import { import { SimpleFlowNode, NodeType, ConditionType, RouteCondition } from '../consts'
SimpleFlowNode,
NodeType,
CONDITION_CONFIG_TYPES,
ConditionType,
COMPARISON_OPERATORS,
RouteCondition,
ProcessVariableEnum
} from '../consts'
import { useWatchNode, useDrawer, useNodeName } from '../node' import { useWatchNode, useDrawer, useNodeName } from '../node'
import { BpmModelFormType } from '@/utils/constants' import Condition from './components/Condition.vue'
import { useFormFields } from '../node'
defineOptions({ defineOptions({
name: 'RouteNodeConfig' name: 'RouteNodeConfig'
}) })
@ -206,29 +81,7 @@ const { settingVisible, closeDrawer, openDrawer } = useDrawer()
const currentNode = useWatchNode(props) const currentNode = useWatchNode(props)
// //
const { nodeName, showInput, clickIcon, blurEvent } = useNodeName(NodeType.ROUTE_BRANCH_NODE) const { nodeName, showInput, clickIcon, blurEvent } = useNodeName(NodeType.ROUTE_BRANCH_NODE)
const formType = inject<Ref<number>>('formType') // const routerGroups = ref<RouteCondition[]>([])
const conditionConfigTypes = computed(() => {
return CONDITION_CONFIG_TYPES.filter((item) => {
//
if (formType?.value === BpmModelFormType.CUSTOM && item.value === ConditionType.RULE) {
return false
} else {
return true
}
})
})
/** 条件规则可选择的表单字段 */
const fieldOptions = computed(() => {
const fieldsCopy = useFormFields().slice()
// ID
fieldsCopy.unshift({
field: ProcessVariableEnum.START_USER_ID,
title: '发起人',
required: true
})
return fieldsCopy
})
const routeGroup = ref<RouteCondition[]>([])
const nodeOptions = ref() const nodeOptions = ref()
// //
@ -237,26 +90,26 @@ const saveConfig = async () => {
if (!showText) return false if (!showText) return false
currentNode.value.name = nodeName.value! currentNode.value.name = nodeName.value!
currentNode.value.showText = showText currentNode.value.showText = showText
currentNode.value.routeGroup = routeGroup.value currentNode.value.routerGroups = routerGroups.value
settingVisible.value = false settingVisible.value = false
return true return true
} }
// //
const showRouteNodeConfig = (node: SimpleFlowNode) => { const showRouteNodeConfig = (node: SimpleFlowNode) => {
getRoutableNode() getRoutableNode()
routeGroup.value = [] routerGroups.value = []
nodeName.value = node.name nodeName.value = node.name
if (node.routeGroup) { if (node.routerGroups) {
routeGroup.value = node.routeGroup routerGroups.value = node.routerGroups
} }
} }
const getShowText = () => { const getShowText = () => {
if (!routeGroup.value || !Array.isArray(routeGroup.value) || routeGroup.value.length <= 0) { if (!routerGroups.value || !Array.isArray(routerGroups.value) || routerGroups.value.length <= 0) {
message.warning('请配置路由!') message.warning('请配置路由!')
return '' return ''
} }
for (const route of routeGroup.value) { for (const route of routerGroups.value) {
if (!route.nodeId || !route.conditionType) { if (!route.nodeId || !route.conditionType) {
message.warning('请完善路由配置项!') message.warning('请完善路由配置项!')
return '' return ''
@ -276,51 +129,13 @@ const getShowText = () => {
} }
} }
} }
return `${routeGroup.value.length}条路由分支` return `${routerGroups.value.length}条路由分支`
}
// TODO @lesan
const changeConditionType = () => {}
const deleteConditionGroup = (conditions, index) => {
conditions.splice(index, 1)
}
const deleteConditionRule = (condition, index) => {
condition.rules.splice(index, 1)
}
const addConditionRule = (condition, index) => {
const rule = {
type: 1,
opName: '等于',
opCode: '==',
leftSide: '',
rightSide: ''
}
condition.rules.splice(index + 1, 0, rule)
}
const addConditionGroup = (conditions) => {
const condition = {
and: true,
rules: [
{
type: 1,
opName: '等于',
opCode: '==',
leftSide: '',
rightSide: ''
}
]
}
conditions.push(condition)
} }
const addRouteGroup = () => { const addRouteGroup = () => {
routeGroup.value.push({ routerGroups.value.push({
nodeId: '', nodeId: '',
conditionType: ConditionType.EXPRESSION, conditionType: ConditionType.RULE,
conditionExpression: '', conditionExpression: '',
conditionGroups: { conditionGroups: {
and: true, and: true,
@ -343,11 +158,11 @@ const addRouteGroup = () => {
} }
const deleteRouteGroup = (index) => { const deleteRouteGroup = (index) => {
routeGroup.value.splice(index, 1) routerGroups.value.splice(index, 1)
} }
const getRoutableNode = () => { const getRoutableNode = () => {
// TODO // TODO @lesan
// //
// //
let node = processNodeTree?.value let node = processNodeTree?.value
@ -369,39 +184,3 @@ const getRoutableNode = () => {
defineExpose({ openDrawer, showRouteNodeConfig }) // defineExpose({ openDrawer, showRouteNodeConfig }) //
</script> </script>
<style lang="scss" scoped>
.condition-group-tool {
display: flex;
justify-content: space-between;
width: 500px;
margin-bottom: 20px;
}
.condition-group {
position: relative;
&:hover {
border-color: #0089ff;
.condition-group-delete {
opacity: 1;
}
}
.condition-group-delete {
position: absolute;
top: 0;
left: 0;
display: flex;
cursor: pointer;
opacity: 0;
}
}
::v-deep(.el-card__header) {
padding: 8px var(--el-card-padding);
border-bottom: 1px solid var(--el-card-border-color);
box-sizing: border-box;
}
</style>

View File

@ -356,6 +356,15 @@
</div> </div>
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>
<el-divider content-position="left">是否需要签名</el-divider>
<el-form-item prop="signEnable">
<el-switch
v-model="configForm.signEnable"
active-text="是"
inactive-text="否"
/>
</el-form-item>
</el-form> </el-form>
</div> </div>
</el-tab-pane> </el-tab-pane>
@ -436,155 +445,161 @@
</div> </div>
</el-tab-pane> </el-tab-pane>
<el-tab-pane label="监听器" name="listener"> <el-tab-pane label="监听器" name="listener">
<div v-for="(listener, listenerIdx) in taskListener" :key="listenerIdx"> <el-form :model="configForm" label-position="top">
<el-form label-position="top"> <div v-for="(listener, listenerIdx) in taskListener" :key="listenerIdx">
<div> <el-divider content-position="left">
<el-divider content-position="left"> <el-text tag="b" size="large">{{ listener.name }}</el-text>
<el-text tag="b" size="large">{{ listener.name }}</el-text> </el-divider>
</el-divider> <el-form-item>
<el-switch
v-model="configForm[`task${listener.type}ListenerEnable`]"
active-text="开启"
inactive-text="关闭"
/>
</el-form-item>
<div v-if="configForm[`task${listener.type}ListenerEnable`]">
<el-form-item> <el-form-item>
<el-switch <el-alert
v-model="configForm[`task${listener.type}ListenerEnable`]" title="仅支持 POST 请求,以请求体方式接收参数"
active-text="开启" type="warning"
inactive-text="关闭" show-icon
:closable="false"
/> />
</el-form-item> </el-form-item>
<div v-if="configForm[`task${listener.type}ListenerEnable`]"> <el-form-item
<el-form-item> label="请求地址"
<el-alert :prop="`task${listener.type}ListenerPath`"
title="仅支持 POST 请求,以请求体方式接收参数" :rules="{
type="warning" required: true,
show-icon message: '请求地址不能为空',
:closable="false" trigger: 'blur'
/> }"
</el-form-item> >
<el-form-item label="请求地址"> <el-input v-model="configForm[`task${listener.type}ListenerPath`]" />
<el-input v-model="configForm[`task${listener.type}ListenerPath`]" /> </el-form-item>
</el-form-item> <el-form-item label="请求头">
<el-form-item label="请求头"> <div
<div class="flex pt-2"
class="flex pt-2" v-for="(item, index) in configForm[`task${listener.type}ListenerHeader`]"
v-for="(item, index) in configForm[`task${listener.type}ListenerHeader`]" :key="index"
:key="index" >
> <div class="mr-2">
<div class="mr-2"> <el-input class="w-160px" v-model="item.key" />
<el-input class="w-160px" v-model="item.key" />
</div>
<div class="mr-2">
<el-select class="w-100px!" v-model="item.type">
<el-option
v-for="types in LISTENER_MAP_TYPES"
:key="types.value"
:label="types.label"
:value="types.value"
/>
</el-select>
</div>
<div class="mr-2">
<el-input
v-if="item.type === ListenerMapTypeEnum.FIXED_VALUE"
class="w-160px"
v-model="item.value"
/>
<el-select
v-if="item.type === ListenerMapTypeEnum.FROM_FORM"
class="w-160px!"
v-model="item.value"
>
<el-option
v-for="(field, fIdx) in formFieldOptions"
:key="fIdx"
:label="field.title"
:value="field.field"
:disabled="!field.required"
/>
</el-select>
</div>
<div class="mr-1 flex items-center">
<Icon
icon="ep:delete"
:size="18"
@click="
deleteTaskListenerMap(
configForm[`task${listener.type}ListenerHeader`],
index
)
"
/>
</div>
</div> </div>
<el-button <div class="mr-2">
type="primary" <el-select class="w-100px!" v-model="item.type">
text <el-option
@click="addTaskListenerMap(configForm[`task${listener.type}ListenerHeader`])" v-for="types in LISTENER_MAP_TYPES"
> :key="types.value"
<Icon icon="ep:plus" class="mr-5px" />添加一行 :label="types.label"
</el-button> :value="types.value"
</el-form-item>
<el-form-item label="请求体">
<div
class="flex pt-2"
v-for="(item, index) in configForm[`task${listener.type}ListenerBody`]"
:key="index"
>
<div class="mr-2">
<el-input class="w-160px" v-model="item.key" />
</div>
<div class="mr-2">
<el-select class="w-100px!" v-model="item.type">
<el-option
v-for="types in LISTENER_MAP_TYPES"
:key="types.value"
:label="types.label"
:value="types.value"
/>
</el-select>
</div>
<div class="mr-2">
<el-input
v-if="item.type === ListenerMapTypeEnum.FIXED_VALUE"
class="w-160px"
v-model="item.value"
/> />
<el-select </el-select>
v-if="item.type === ListenerMapTypeEnum.FROM_FORM"
class="w-160px!"
v-model="item.value"
>
<el-option
v-for="(field, fIdx) in formFieldOptions"
:key="fIdx"
:label="field.title"
:value="field.field"
:disabled="!field.required"
/>
</el-select>
</div>
<div class="mr-1 flex items-center">
<Icon
icon="ep:delete"
:size="18"
@click="
deleteTaskListenerMap(
configForm[`task${listener.type}ListenerBody`],
index
)
"
/>
</div>
</div> </div>
<el-button <div class="mr-2">
type="primary" <el-input
text v-if="item.type === ListenerParamTypeEnum.FIXED_VALUE"
@click="addTaskListenerMap(configForm[`task${listener.type}ListenerBody`])" class="w-160px"
> v-model="item.value"
<Icon icon="ep:plus" class="mr-5px" />添加一行 />
</el-button> <el-select
</el-form-item> v-if="item.type === ListenerParamTypeEnum.FROM_FORM"
</div> class="w-160px!"
v-model="item.value"
>
<el-option
v-for="(field, fIdx) in formFieldOptions"
:key="fIdx"
:label="field.title"
:value="field.field"
:disabled="!field.required"
/>
</el-select>
</div>
<div class="mr-1 flex items-center">
<Icon
icon="ep:delete"
:size="18"
@click="
deleteTaskListenerParam(
configForm[`task${listener.type}ListenerHeader`],
index
)
"
/>
</div>
</div>
<el-button
type="primary"
text
@click="addTaskListenerParam(configForm[`task${listener.type}ListenerHeader`])"
>
<Icon icon="ep:plus" class="mr-5px" />添加一行
</el-button>
</el-form-item>
<el-form-item label="请求体">
<div
class="flex pt-2"
v-for="(item, index) in configForm[`task${listener.type}ListenerBody`]"
:key="index"
>
<div class="mr-2">
<el-input class="w-160px" v-model="item.key" />
</div>
<div class="mr-2">
<el-select class="w-100px!" v-model="item.type">
<el-option
v-for="types in LISTENER_MAP_TYPES"
:key="types.value"
:label="types.label"
:value="types.value"
/>
</el-select>
</div>
<div class="mr-2">
<el-input
v-if="item.type === ListenerParamTypeEnum.FIXED_VALUE"
class="w-160px"
v-model="item.value"
/>
<el-select
v-if="item.type === ListenerParamTypeEnum.FROM_FORM"
class="w-160px!"
v-model="item.value"
>
<el-option
v-for="(field, fIdx) in formFieldOptions"
:key="fIdx"
:label="field.title"
:value="field.field"
:disabled="!field.required"
/>
</el-select>
</div>
<div class="mr-1 flex items-center">
<Icon
icon="ep:delete"
:size="18"
@click="
deleteTaskListenerParam(
configForm[`task${listener.type}ListenerBody`],
index
)
"
/>
</div>
</div>
<el-button
type="primary"
text
@click="addTaskListenerParam(configForm[`task${listener.type}ListenerBody`])"
>
<Icon icon="ep:plus" class="mr-5px" />添加一行
</el-button>
</el-form-item>
</div> </div>
</el-form> </div>
</div> </el-form>
</el-tab-pane> </el-tab-pane>
</el-tabs> </el-tabs>
<template #footer> <template #footer>
@ -623,7 +638,7 @@ import {
FieldPermissionType, FieldPermissionType,
ProcessVariableEnum, ProcessVariableEnum,
LISTENER_MAP_TYPES, LISTENER_MAP_TYPES,
ListenerMapTypeEnum ListenerParamTypeEnum
} from '../consts' } from '../consts'
import { import {
@ -852,6 +867,8 @@ const saveConfig = async () => {
header: configForm.value.taskCompleteListenerHeader, header: configForm.value.taskCompleteListenerHeader,
body: configForm.value.taskCompleteListenerBody body: configForm.value.taskCompleteListenerBody
} }
//
currentNode.value.signEnable = configForm.value.signEnable
currentNode.value.showText = showText currentNode.value.showText = showText
settingVisible.value = false settingVisible.value = false
@ -919,6 +936,8 @@ const showUserTaskNodeConfig = (node: SimpleFlowNode) => {
configForm.value.taskCompleteListenerPath = node.taskCompleteListener!.path configForm.value.taskCompleteListenerPath = node.taskCompleteListener!.path
configForm.value.taskCompleteListenerHeader = node.taskCompleteListener?.header ?? [] configForm.value.taskCompleteListenerHeader = node.taskCompleteListener?.header ?? []
configForm.value.taskCompleteListenerBody = node.taskCompleteListener?.body ?? [] configForm.value.taskCompleteListenerBody = node.taskCompleteListener?.body ?? []
// 6.
configForm.value.signEnable = node.signEnable ?? false
} }
defineExpose({ openDrawer, showUserTaskNodeConfig }) // defineExpose({ openDrawer, showUserTaskNodeConfig }) //
@ -1032,14 +1051,14 @@ function useTimeoutHandler() {
} }
} }
const addTaskListenerMap = (arr) => { const addTaskListenerParam = (arr) => {
arr.push({ arr.push({
key: '', key: '',
type: 1, type: 1,
value: '' value: ''
}) })
} }
const deleteTaskListenerMap = (arr, index) => { const deleteTaskListenerParam = (arr, index) => {
arr.splice(index, 1) arr.splice(index, 1)
} }
</script> </script>

View File

@ -0,0 +1,242 @@
<template>
<el-form ref="formRef" :model="condition" :rules="formRules" label-position="top">
<el-form-item label="配置方式" prop="conditionType">
<el-radio-group v-model="condition.conditionType">
<el-radio
v-for="(dict, indexConditionType) in conditionConfigTypes"
:key="indexConditionType"
:value="dict.value"
:label="dict.value"
>
{{ dict.label }}
</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item
v-if="condition.conditionType === ConditionType.EXPRESSION"
label="条件表达式"
prop="conditionExpression"
>
<el-input
type="textarea"
v-model="condition.conditionExpression"
clearable
style="width: 100%"
/>
</el-form-item>
<el-form-item v-if="condition.conditionType === ConditionType.RULE" label="条件规则">
<div class="condition-group-tool">
<div class="flex items-center">
<div class="mr-4">条件组关系</div>
<el-switch
v-model="condition.conditionGroups.and"
inline-prompt
active-text="且"
inactive-text="或"
/>
</div>
</div>
<el-space direction="vertical" :spacer="condition.conditionGroups.and ? '且' : '或'">
<el-card
class="condition-group"
style="width: 530px"
v-for="(equation, cIdx) in condition.conditionGroups.conditions"
:key="cIdx"
>
<div
class="condition-group-delete"
v-if="condition.conditionGroups.conditions.length > 1"
>
<Icon
color="#0089ff"
icon="ep:circle-close-filled"
:size="18"
@click="deleteConditionGroup(condition.conditionGroups.conditions, cIdx)"
/>
</div>
<template #header>
<div class="flex items-center justify-between">
<div>条件组</div>
<div class="flex">
<div class="mr-4">规则关系</div>
<el-switch
v-model="equation.and"
inline-prompt
active-text="且"
inactive-text="或"
/>
</div>
</div>
</template>
<div class="flex pt-2" v-for="(rule, rIdx) in equation.rules" :key="rIdx">
<div class="mr-2">
<el-select style="width: 160px" v-model="rule.leftSide">
<el-option
v-for="(field, fIdx) in fieldOptions"
:key="fIdx"
:label="field.title"
:value="field.field"
:disabled="!field.required"
/>
</el-select>
</div>
<div class="mr-2">
<el-select v-model="rule.opCode" style="width: 100px">
<el-option
v-for="operator in COMPARISON_OPERATORS"
:key="operator.value"
:label="operator.label"
:value="operator.value"
/>
</el-select>
</div>
<div class="mr-2">
<el-input v-model="rule.rightSide" style="width: 160px" />
</div>
<div class="mr-1 flex items-center" v-if="equation.rules.length > 1">
<Icon icon="ep:delete" :size="18" @click="deleteConditionRule(equation, rIdx)" />
</div>
<div class="flex items-center">
<Icon icon="ep:plus" :size="18" @click="addConditionRule(equation, rIdx)" />
</div>
</div>
</el-card>
</el-space>
<div title="添加条件组" class="mt-4 cursor-pointer">
<Icon
color="#0089ff"
icon="ep:plus"
:size="24"
@click="addConditionGroup(condition.conditionGroups.conditions)"
/>
</div>
</el-form-item>
</el-form>
</template>
<script setup lang="ts">
import {
CONDITION_CONFIG_TYPES,
COMPARISON_OPERATORS,
ConditionType,
ProcessVariableEnum
} from '../../consts'
import { BpmModelFormType } from '@/utils/constants'
import { useFormFields } from '../../node'
const props = defineProps({
modelValue: {
type: Object,
required: true
}
})
const emit = defineEmits(['update:modelValue'])
const condition = computed({
get() {
return props.modelValue
},
set(newValue) {
emit('update:modelValue', newValue)
}
})
const formType = inject<Ref<number>>('formType') //
const conditionConfigTypes = computed(() => {
return CONDITION_CONFIG_TYPES.filter((item) => {
//
if (formType?.value === BpmModelFormType.CUSTOM && item.value === ConditionType.RULE) {
return false
} else {
return true
}
})
})
/** 条件规则可选择的表单字段 */
const fieldOptions = computed(() => {
const fieldsCopy = useFormFields().slice()
// ID
fieldsCopy.unshift({
field: ProcessVariableEnum.START_USER_ID,
title: '发起人',
required: true
})
return fieldsCopy
})
//
const formRules = reactive({
conditionType: [{ required: true, message: '配置方式不能为空', trigger: 'blur' }],
conditionExpression: [{ required: true, message: '条件表达式不能为空', trigger: 'blur' }]
})
const formRef = ref() // Ref
const deleteConditionGroup = (conditions, index) => {
conditions.splice(index, 1)
}
const deleteConditionRule = (condition, index) => {
condition.rules.splice(index, 1)
}
const addConditionRule = (condition, index) => {
const rule = {
type: 1,
opName: '等于',
opCode: '==',
leftSide: '',
rightSide: ''
}
condition.rules.splice(index + 1, 0, rule)
}
const addConditionGroup = (conditions) => {
const condition = {
and: true,
rules: [
{
type: 1,
opName: '等于',
opCode: '==',
leftSide: '',
rightSide: ''
}
]
}
conditions.push(condition)
}
</script>
<style lang="scss" scoped>
.condition-group-tool {
display: flex;
justify-content: space-between;
width: 500px;
margin-bottom: 20px;
}
.condition-group {
position: relative;
&:hover {
border-color: #0089ff;
.condition-group-delete {
opacity: 1;
}
}
.condition-group-delete {
position: absolute;
top: 0;
left: 0;
display: flex;
cursor: pointer;
opacity: 0;
}
}
::v-deep(.el-card__header) {
padding: 8px var(--el-card-padding);
border-bottom: 1px solid var(--el-card-border-color);
box-sizing: border-box;
}
</style>

View File

@ -128,7 +128,8 @@ const setSimpleModelNodeTaskStatus = (
if ( if (
simpleModel.type === NodeType.CONDITION_BRANCH_NODE || simpleModel.type === NodeType.CONDITION_BRANCH_NODE ||
simpleModel.type === NodeType.PARALLEL_BRANCH_NODE || simpleModel.type === NodeType.PARALLEL_BRANCH_NODE ||
simpleModel.type === NodeType.INCLUSIVE_BRANCH_NODE simpleModel.type === NodeType.INCLUSIVE_BRANCH_NODE ||
simpleModel.type === NodeType.ROUTE_BRANCH_NODE
) { ) {
// //
if (finishedActivityIds.includes(simpleModel.id)) { if (finishedActivityIds.includes(simpleModel.id)) {