!296 feat: [bpm][antd] bpmn设计器脚本任务优化

Merge pull request !296 from Jason/dev
This commit is contained in:
芋道源码
2025-12-09 01:11:31 +00:00
committed by Gitee
5 changed files with 236 additions and 400 deletions

View File

@@ -500,7 +500,7 @@ onMounted(async () => {
</Form.Item>
<Divider orientation="left">审批人为空时</Divider>
<Form.Item prop="assignEmptyHandlerType">
<Form.Item name="assignEmptyHandlerType">
<RadioGroup
v-model:value="assignEmptyHandlerType"
@change="updateAssignEmptyHandlerType"
@@ -517,7 +517,7 @@ onMounted(async () => {
<Form.Item
v-if="assignEmptyHandlerType === AssignEmptyHandlerType.ASSIGN_USER"
label="指定用户"
prop="assignEmptyHandlerUserIds"
name="assignEmptyHandlerUserIds"
>
<Select
v-model:value="assignEmptyUserIds"
@@ -677,7 +677,7 @@ onMounted(async () => {
</div>
<Divider orientation="left">是否需要签名</Divider>
<Form.Item prop="signEnable">
<Form.Item name="signEnable">
<Switch
v-model:checked="signEnable.value"
checked-children=""
@@ -687,7 +687,7 @@ onMounted(async () => {
</Form.Item>
<Divider orientation="left">审批意见</Divider>
<Form.Item prop="reasonRequire">
<Form.Item name="reasonRequire">
<Switch
v-model:checked="reasonRequire.value"
checked-children="必填"
@@ -697,162 +697,3 @@ onMounted(async () => {
</Form.Item>
</div>
</template>
<style lang="scss" scoped>
.button-setting-pane {
display: flex;
flex-direction: column;
margin-top: 8px;
font-size: 14px;
.button-setting-desc {
padding-right: 8px;
margin-bottom: 16px;
font-size: 16px;
font-weight: 700;
}
.button-setting-title {
display: flex;
align-items: center;
justify-content: space-between;
height: 45px;
padding-left: 12px;
background-color: #f8fafc0a;
border: 1px solid #1f38581a;
& > :first-child {
width: 100px !important;
text-align: left !important;
}
& > :last-child {
text-align: center !important;
}
.button-title-label {
width: 150px;
font-size: 13px;
font-weight: 700;
color: #000;
text-align: left;
}
}
.button-setting-item {
display: flex;
align-items: center;
justify-content: space-between;
height: 38px;
padding-left: 12px;
border: 1px solid #1f38581a;
border-top: 0;
& > :first-child {
width: 100px !important;
}
& > :last-child {
text-align: center !important;
}
.button-setting-item-label {
width: 150px;
overflow: hidden;
text-overflow: ellipsis;
text-align: left;
white-space: nowrap;
}
.editable-title-input {
max-width: 130px;
height: 24px;
margin-left: 4px;
line-height: 24px;
border: 1px solid #d9d9d9;
border-radius: 4px;
transition: all 0.3s;
&:focus {
outline: 0;
border-color: #40a9ff;
box-shadow: 0 0 0 2px rgb(24 144 255 / 20%);
}
}
}
}
.field-setting-pane {
display: flex;
flex-direction: column;
font-size: 14px;
.field-setting-desc {
padding-right: 8px;
margin-bottom: 16px;
font-size: 16px;
font-weight: 700;
}
.field-permit-title {
display: flex;
align-items: center;
justify-content: space-between;
height: 45px;
padding-left: 12px;
line-height: 45px;
background-color: #f8fafc0a;
border: 1px solid #1f38581a;
.first-title {
text-align: left !important;
}
.other-titles {
display: flex;
justify-content: space-between;
}
.setting-title-label {
display: inline-block;
width: 100px;
padding: 5px 0;
font-size: 13px;
font-weight: 700;
color: #000;
text-align: center;
}
}
.field-setting-item {
display: flex;
align-items: center;
justify-content: space-between;
height: 38px;
padding-left: 12px;
border: 1px solid #1f38581a;
border-top: 0;
.field-setting-item-label {
display: inline-block;
width: 100px;
min-height: 16px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
cursor: text;
}
.field-setting-item-group {
display: flex;
justify-content: space-between;
.item-radio-wrap {
display: inline-block;
width: 100px;
text-align: center;
}
}
}
}
</style>

View File

@@ -526,7 +526,7 @@ watch(
</FormItem>
<FormItem
label="重试周期"
prop="timeCycle"
name="timeCycle"
v-if="loopInstanceForm.asyncAfter || loopInstanceForm.asyncBefore"
key="timeCycle"
>

View File

@@ -1,35 +1,20 @@
<script lang="ts" setup>
import { computed, ref, watch } from 'vue';
import { ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { IconifyIcon } from '@vben/icons';
import { Button, Input, Modal } from 'ant-design-vue';
import { Button, Input } from 'ant-design-vue';
defineOptions({ name: 'HttpHeaderEditor' });
const props = defineProps({
modelValue: {
type: Boolean,
default: false,
},
headers: {
type: String,
default: '',
},
});
const emit = defineEmits(['update:modelValue', 'save']);
const emit = defineEmits(['save']);
interface HeaderItem {
key: string;
value: string;
}
const dialogVisible = computed({
get: () => props.modelValue,
set: (val) => emit('update:modelValue', val),
});
const headerList = ref<HeaderItem[]>([]);
// 解析请求头字符串为列表
@@ -80,52 +65,42 @@ const removeHeader = (index: number) => {
const handleSave = () => {
const headersStr = stringifyHeaders(headerList.value);
emit('save', headersStr);
dialogVisible.value = false;
modalApi.close();
};
// 关闭
const handleClose = () => {
dialogVisible.value = false;
};
// 监听对话框打开,初始化数据
watch(
() => props.modelValue,
(val) => {
if (val) {
headerList.value = parseHeaders(props.headers);
const [Modal, modalApi] = useVbenModal({
destroyOnClose: true,
onOpenChange(isOpen) {
if (!isOpen) {
return;
}
const { headers } = modalApi.getData();
headerList.value = parseHeaders(headers);
},
{ immediate: true },
);
onConfirm: handleSave,
});
</script>
<template>
<Modal
v-model:open="dialogVisible"
title="编辑请求头"
width="600px"
:mask-closable="false"
@cancel="handleClose"
>
<div class="header-editor">
<div class="header-list">
<Modal title="编辑请求头" class="w-3/5">
<div class="space-y-4">
<div class="mb-2 space-y-3 overflow-y-auto">
<div
v-for="(item, index) in headerList"
:key="index"
class="header-item"
class="flex items-center gap-2"
>
<Input
v-model:value="item.key"
placeholder="请输入参数名"
class="header-key"
class="w-48"
allow-clear
/>
<span class="separator">:</span>
<span class="font-medium text-gray-600">:</span>
<Input
v-model:value="item.value"
placeholder="请输入参数值 (支持表达式 ${变量名})"
class="header-value"
class="flex-1"
allow-clear
/>
<Button type="text" danger size="small" @click="removeHeader(index)">
@@ -135,50 +110,12 @@ watch(
</Button>
</div>
</div>
<Button type="primary" class="add-btn" @click="addHeader">
<Button type="primary" class="w-full" @click="addHeader">
<template #icon>
<IconifyIcon icon="ep:plus" />
</template>
添加请求头
</Button>
</div>
<template #footer>
<Button @click="handleClose">取消</Button>
<Button type="primary" @click="handleSave">保存</Button>
</template>
</Modal>
</template>
<style lang="scss" scoped>
.header-editor {
.header-list {
max-height: 400px;
margin-bottom: 16px;
overflow-y: auto;
}
.header-item {
display: flex;
gap: 8px;
align-items: center;
margin-bottom: 12px;
.header-key {
flex: 0 0 180px;
}
.separator {
font-weight: 500;
color: #606266;
}
.header-value {
flex: 1;
}
}
.add-btn {
width: 100%;
}
}
</style>

View File

@@ -2,6 +2,7 @@
import { nextTick, onBeforeUnmount, ref, toRaw, watch } from 'vue';
import {
Form,
FormItem,
Input,
Select,
@@ -75,47 +76,50 @@ watch(
<template>
<div class="mt-4">
<FormItem label="脚本格式">
<Input
v-model:value="scriptTaskForm.scriptFormat"
allow-clear
@input="updateElementTask()"
@change="updateElementTask()"
/>
</FormItem>
<FormItem label="脚本类型">
<Select v-model:value="scriptTaskForm.scriptType">
<SelectOption value="inline">内联脚本</SelectOption>
<SelectOption value="external">外部资源</SelectOption>
</Select>
</FormItem>
<FormItem label="脚本" v-show="scriptTaskForm.scriptType === 'inline'">
<Textarea
v-model:value="scriptTaskForm.script"
:auto-size="{ minRows: 2, maxRows: 4 }"
allow-clear
@input="updateElementTask()"
@change="updateElementTask()"
/>
</FormItem>
<FormItem
label="资源地址"
v-show="scriptTaskForm.scriptType === 'external'"
>
<Input
v-model:value="scriptTaskForm.resource"
allow-clear
@input="updateElementTask()"
@change="updateElementTask()"
/>
</FormItem>
<FormItem label="结果变量">
<Input
v-model:value="scriptTaskForm.resultVariable"
allow-clear
@input="updateElementTask()"
@change="updateElementTask()"
/>
</FormItem>
<Form :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }">
<FormItem label="脚本格式">
<Input
v-model:value="scriptTaskForm.scriptFormat"
allow-clear
@input="updateElementTask()"
@change="updateElementTask()"
/>
</FormItem>
<!-- TODO scriptType 外部资源 内联脚本 flowable 文档 https://www.flowable.com/open-source/docs/bpmn/ch07b-BPMN-Constructs#script-task 没看到到有相应的属性 -->
<FormItem label="脚本类型">
<Select v-model:value="scriptTaskForm.scriptType">
<SelectOption value="inline">内联脚本</SelectOption>
<SelectOption value="external">外部资源</SelectOption>
</Select>
</FormItem>
<FormItem label="脚本" v-show="scriptTaskForm.scriptType === 'inline'">
<Textarea
v-model:value="scriptTaskForm.script"
:auto-size="{ minRows: 2, maxRows: 4 }"
allow-clear
@input="updateElementTask()"
@change="updateElementTask()"
/>
</FormItem>
<FormItem
label="资源地址"
v-show="scriptTaskForm.scriptType === 'external'"
>
<Input
v-model:value="scriptTaskForm.resource"
allow-clear
@input="updateElementTask()"
@change="updateElementTask()"
/>
</FormItem>
<FormItem label="结果变量">
<Input
v-model:value="scriptTaskForm.resultVariable"
allow-clear
@input="updateElementTask()"
@change="updateElementTask()"
/>
</FormItem>
</Form>
</div>
</template>

View File

@@ -1,11 +1,13 @@
<!-- eslint-disable prettier/prettier -->
<script lang="ts" setup>
import { inject, nextTick, onBeforeUnmount, ref, toRaw, watch } from 'vue';
import { inject, nextTick, onBeforeUnmount, ref, watch } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { IconifyIcon } from '@vben/icons';
import {
Button,
Form,
FormItem,
Input,
RadioButton,
@@ -70,7 +72,6 @@ const serviceTaskForm = ref({ ...DEFAULT_TASK_FORM });
const httpTaskForm = ref<any>({ ...DEFAULT_HTTP_FORM });
const bpmnElement = ref();
const httpInitializing = ref(false);
const showHeaderEditor = ref(false);
const bpmnInstances = () => (window as any)?.bpmnInstances;
@@ -179,7 +180,10 @@ const shouldPersistField = (name: string, value: any) => {
};
const updateHttpExtensions = (force = false) => {
if (!bpmnElement.value) return;
const instances = bpmnInstances();
if (!instances || !instances.bpmnElement) return;
// 直接使用原始BPMN元素避免Vue响应式代理问题
const bpmnElement = instances.bpmnElement;
if (
!force &&
(httpInitializing.value || serviceTaskForm.value.executeType !== 'http')
@@ -236,31 +240,37 @@ const updateHttpExtensions = (force = false) => {
});
});
updateElementExtensions(bpmnElement.value, [
updateElementExtensions(bpmnElement, [
...otherExtensions,
...httpFieldElements,
]);
};
const removeHttpExtensions = () => {
if (!bpmnElement.value) return;
const instances = bpmnInstances();
if (!instances || !instances.bpmnElement) return;
// 直接使用原始BPMN元素避免Vue响应式代理问题
const bpmnElement = instances.bpmnElement;
const { httpFields, otherExtensions } = collectHttpExtensionInfo();
if (httpFields.size === 0) {
return;
}
if (otherExtensions.length === 0) {
bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
bpmnInstances().modeling.updateProperties(bpmnElement, {
extensionElements: null,
});
return;
}
updateElementExtensions(bpmnElement.value, otherExtensions);
updateElementExtensions(bpmnElement, otherExtensions);
};
const updateElementTask = () => {
if (!bpmnElement.value) return;
const instances = bpmnInstances();
if (!instances || !instances.bpmnElement) return;
// 直接使用原始BPMN元素避免Vue响应式代理问题
const bpmnElement = instances.bpmnElement;
const taskAttr: Record<string, any> = {
class: null,
@@ -280,7 +290,7 @@ const updateElementTask = () => {
taskAttr[flowableTypeKey] = 'http';
}
bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), taskAttr);
bpmnInstances().modeling.updateProperties(bpmnElement, taskAttr);
if (type === 'http') {
updateHttpExtensions(true);
@@ -297,10 +307,24 @@ const handleExecuteTypeChange = (value: any) => {
updateElementTask();
};
/** 打开请求头编辑器 */
const openHttpHeaderEditor = () => {
httpHeaderEditorApi
.setData({
headers: httpTaskForm.value.requestHeaders,
})
.open();
};
/** 保存请求头 */
const handleHeadersSave = (headersStr: string) => {
httpTaskForm.value.requestHeaders = headersStr;
};
const [HttpHeaderEditorModal, httpHeaderEditorApi] = useVbenModal({
connectedComponent: HttpHeaderEditor,
});
onBeforeUnmount(() => {
bpmnElement.value = null;
});
@@ -327,110 +351,140 @@ watch(
<template>
<div>
<FormItem label="执行类型" key="executeType">
<Select
v-model:value="serviceTaskForm.executeType"
:options="[
{ label: 'Java类', value: 'class' },
{ label: '表达式', value: 'expression' },
{ label: '代理表达式', value: 'delegateExpression' },
{ label: 'HTTP 调用', value: 'http' },
]"
@change="handleExecuteTypeChange"
/>
</FormItem>
<FormItem
v-if="serviceTaskForm.executeType === 'class'"
label="Java类"
name="class"
key="execute-class"
>
<Input
v-model:value="serviceTaskForm.class"
allow-clear
@change="updateElementTask"
/>
</FormItem>
<FormItem
v-if="serviceTaskForm.executeType === 'expression'"
label="表达式"
name="expression"
key="execute-expression"
>
<Input
v-model:value="serviceTaskForm.expression"
allow-clear
@change="updateElementTask"
/>
</FormItem>
<FormItem
v-if="serviceTaskForm.executeType === 'delegateExpression'"
label="代理表达式"
name="delegateExpression"
key="execute-delegate"
>
<Input
v-model:value="serviceTaskForm.delegateExpression"
allow-clear
@change="updateElementTask"
/>
</FormItem>
<template v-if="serviceTaskForm.executeType === 'http'">
<FormItem label="请求方法" key="http-method">
<RadioGroup v-model:value="httpTaskForm.requestMethod">
<RadioButton value="GET">GET</RadioButton>
<RadioButton value="POST">POST</RadioButton>
<RadioButton value="PUT">PUT</RadioButton>
<RadioButton value="DELETE">DELETE</RadioButton>
</RadioGroup>
</FormItem>
<FormItem label="请求地址" key="http-url" name="requestUrl">
<Input v-model:value="httpTaskForm.requestUrl" allow-clear />
</FormItem>
<FormItem label="请求头" key="http-headers">
<div class="flex w-full items-start gap-2">
<Textarea
v-model:value="httpTaskForm.requestHeaders"
:auto-size="{ minRows: 4, maxRows: 8 }"
readonly
placeholder="点击右侧编辑按钮添加请求头"
class="min-w-0 flex-1"
/>
<Button type="primary" @click="showHeaderEditor = true">
<template #icon>
<IconifyIcon icon="ep:edit" />
</template>
编辑
</Button>
</div>
</FormItem>
<FormItem label="禁止重定向" key="http-disallow-redirects">
<Switch v-model:checked="httpTaskForm.disallowRedirects" />
</FormItem>
<FormItem label="忽略异常" key="http-ignore-exception">
<Switch v-model:checked="httpTaskForm.ignoreException" />
</FormItem>
<FormItem label="保存返回变量" key="http-save-response">
<Switch v-model:checked="httpTaskForm.saveResponseParameters" />
</FormItem>
<FormItem label="是否瞬间变量" key="http-save-transient">
<Switch
v-model:checked="httpTaskForm.saveResponseParametersTransient"
<Form :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }">
<FormItem label="执行类型" key="executeType">
<Select
v-model:value="serviceTaskForm.executeType"
:options="[
{ label: 'Java类', value: 'class' },
{ label: '表达式', value: 'expression' },
{ label: '代理表达式', value: 'delegateExpression' },
{ label: 'HTTP 调用', value: 'http' },
]"
@change="handleExecuteTypeChange"
/>
</FormItem>
<FormItem label="返回变量前缀" key="http-result-variable-prefix">
<Input v-model:value="httpTaskForm.resultVariablePrefix" />
<FormItem
v-if="serviceTaskForm.executeType === 'class'"
label="Java类"
name="class"
key="execute-class"
>
<Input
v-model:value="serviceTaskForm.class"
allow-clear
@change="updateElementTask"
/>
</FormItem>
<FormItem label="格式化返回为JSON" key="http-save-json">
<Switch v-model:checked="httpTaskForm.saveResponseVariableAsJson" />
<FormItem
v-if="serviceTaskForm.executeType === 'expression'"
label="表达式"
name="expression"
key="execute-expression"
>
<Input
v-model:value="serviceTaskForm.expression"
allow-clear
@change="updateElementTask"
/>
</FormItem>
</template>
<FormItem
v-if="serviceTaskForm.executeType === 'delegateExpression'"
label="代理表达式"
name="delegateExpression"
key="execute-delegate"
>
<Input
v-model:value="serviceTaskForm.delegateExpression"
allow-clear
@change="updateElementTask"
/>
</FormItem>
<template v-if="serviceTaskForm.executeType === 'http'">
<FormItem label="请求方法" key="http-method" name="requestMethod">
<RadioGroup v-model:value="httpTaskForm.requestMethod">
<RadioButton value="GET">GET</RadioButton>
<RadioButton value="POST">POST</RadioButton>
<RadioButton value="PUT">PUT</RadioButton>
<RadioButton value="DELETE">DELETE</RadioButton>
</RadioGroup>
</FormItem>
<FormItem label="请求地址" key="http-url" name="requestUrl">
<Input v-model:value="httpTaskForm.requestUrl" allow-clear />
</FormItem>
<FormItem label="请求头" key="http-headers" name="requestHeaders">
<div class="flex w-full flex-col gap-2">
<Textarea
v-model:value="httpTaskForm.requestHeaders"
:auto-size="{ minRows: 4, maxRows: 8 }"
readonly
placeholder="点击右侧编辑按钮添加请求头"
class="min-w-0 flex-1"
/>
<div class="flex w-full items-center justify-center">
<Button
class="flex flex-1 items-center justify-center"
size="small"
type="primary"
@click="openHttpHeaderEditor"
>
<template #icon>
<IconifyIcon icon="ep:edit" />
</template>
编辑
</Button>
</div>
</div>
</FormItem>
<FormItem
label="禁止重定向"
key="http-disallow-redirects"
name="disallowRedirects"
>
<Switch v-model:checked="httpTaskForm.disallowRedirects" />
</FormItem>
<FormItem
label="忽略异常"
key="http-ignore-exception"
name="ignoreException"
>
<Switch v-model:checked="httpTaskForm.ignoreException" />
</FormItem>
<FormItem
label="保存返回变量"
key="http-save-response"
name="saveResponseParameters"
>
<Switch v-model:checked="httpTaskForm.saveResponseParameters" />
</FormItem>
<FormItem
label="是否瞬间变量"
key="http-save-transient"
name="saveResponseParametersTransient"
>
<Switch
v-model:checked="httpTaskForm.saveResponseParametersTransient"
/>
</FormItem>
<FormItem
label="返回变量前缀"
key="http-result-variable-prefix"
name="resultVariablePrefix"
>
<Input v-model:value="httpTaskForm.resultVariablePrefix" />
</FormItem>
<FormItem
label="保存为 JSON 变量"
:label-col="{ span: 8 }"
:wrapper-col="{ span: 16 }"
key="http-save-json"
name="saveResponseVariableAsJson"
>
<Switch v-model:checked="httpTaskForm.saveResponseVariableAsJson" />
</FormItem>
</template>
</Form>
<!-- 请求头编辑器 -->
<HttpHeaderEditor
v-model="showHeaderEditor"
:headers="httpTaskForm.requestHeaders"
@save="handleHeadersSave"
/>
<HttpHeaderEditorModal @save="handleHeadersSave" />
</div>
</template>