!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> </Form.Item>
<Divider orientation="left">审批人为空时</Divider> <Divider orientation="left">审批人为空时</Divider>
<Form.Item prop="assignEmptyHandlerType"> <Form.Item name="assignEmptyHandlerType">
<RadioGroup <RadioGroup
v-model:value="assignEmptyHandlerType" v-model:value="assignEmptyHandlerType"
@change="updateAssignEmptyHandlerType" @change="updateAssignEmptyHandlerType"
@@ -517,7 +517,7 @@ onMounted(async () => {
<Form.Item <Form.Item
v-if="assignEmptyHandlerType === AssignEmptyHandlerType.ASSIGN_USER" v-if="assignEmptyHandlerType === AssignEmptyHandlerType.ASSIGN_USER"
label="指定用户" label="指定用户"
prop="assignEmptyHandlerUserIds" name="assignEmptyHandlerUserIds"
> >
<Select <Select
v-model:value="assignEmptyUserIds" v-model:value="assignEmptyUserIds"
@@ -677,7 +677,7 @@ onMounted(async () => {
</div> </div>
<Divider orientation="left">是否需要签名</Divider> <Divider orientation="left">是否需要签名</Divider>
<Form.Item prop="signEnable"> <Form.Item name="signEnable">
<Switch <Switch
v-model:checked="signEnable.value" v-model:checked="signEnable.value"
checked-children="" checked-children=""
@@ -687,7 +687,7 @@ onMounted(async () => {
</Form.Item> </Form.Item>
<Divider orientation="left">审批意见</Divider> <Divider orientation="left">审批意见</Divider>
<Form.Item prop="reasonRequire"> <Form.Item name="reasonRequire">
<Switch <Switch
v-model:checked="reasonRequire.value" v-model:checked="reasonRequire.value"
checked-children="必填" checked-children="必填"
@@ -697,162 +697,3 @@ onMounted(async () => {
</Form.Item> </Form.Item>
</div> </div>
</template> </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>
<FormItem <FormItem
label="重试周期" label="重试周期"
prop="timeCycle" name="timeCycle"
v-if="loopInstanceForm.asyncAfter || loopInstanceForm.asyncBefore" v-if="loopInstanceForm.asyncAfter || loopInstanceForm.asyncBefore"
key="timeCycle" key="timeCycle"
> >

View File

@@ -1,35 +1,20 @@
<script lang="ts" setup> <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 { IconifyIcon } from '@vben/icons';
import { Button, Input, Modal } from 'ant-design-vue'; import { Button, Input } from 'ant-design-vue';
defineOptions({ name: 'HttpHeaderEditor' }); defineOptions({ name: 'HttpHeaderEditor' });
const props = defineProps({ const emit = defineEmits(['save']);
modelValue: {
type: Boolean,
default: false,
},
headers: {
type: String,
default: '',
},
});
const emit = defineEmits(['update:modelValue', 'save']);
interface HeaderItem { interface HeaderItem {
key: string; key: string;
value: string; value: string;
} }
const dialogVisible = computed({
get: () => props.modelValue,
set: (val) => emit('update:modelValue', val),
});
const headerList = ref<HeaderItem[]>([]); const headerList = ref<HeaderItem[]>([]);
// 解析请求头字符串为列表 // 解析请求头字符串为列表
@@ -80,52 +65,42 @@ const removeHeader = (index: number) => {
const handleSave = () => { const handleSave = () => {
const headersStr = stringifyHeaders(headerList.value); const headersStr = stringifyHeaders(headerList.value);
emit('save', headersStr); emit('save', headersStr);
dialogVisible.value = false; modalApi.close();
}; };
// 关闭 const [Modal, modalApi] = useVbenModal({
const handleClose = () => { destroyOnClose: true,
dialogVisible.value = false; onOpenChange(isOpen) {
}; if (!isOpen) {
return;
// 监听对话框打开,初始化数据
watch(
() => props.modelValue,
(val) => {
if (val) {
headerList.value = parseHeaders(props.headers);
} }
const { headers } = modalApi.getData();
headerList.value = parseHeaders(headers);
}, },
{ immediate: true }, onConfirm: handleSave,
); });
</script> </script>
<template> <template>
<Modal <Modal title="编辑请求头" class="w-3/5">
v-model:open="dialogVisible" <div class="space-y-4">
title="编辑请求头" <div class="mb-2 space-y-3 overflow-y-auto">
width="600px"
:mask-closable="false"
@cancel="handleClose"
>
<div class="header-editor">
<div class="header-list">
<div <div
v-for="(item, index) in headerList" v-for="(item, index) in headerList"
:key="index" :key="index"
class="header-item" class="flex items-center gap-2"
> >
<Input <Input
v-model:value="item.key" v-model:value="item.key"
placeholder="请输入参数名" placeholder="请输入参数名"
class="header-key" class="w-48"
allow-clear allow-clear
/> />
<span class="separator">:</span> <span class="font-medium text-gray-600">:</span>
<Input <Input
v-model:value="item.value" v-model:value="item.value"
placeholder="请输入参数值 (支持表达式 ${变量名})" placeholder="请输入参数值 (支持表达式 ${变量名})"
class="header-value" class="flex-1"
allow-clear allow-clear
/> />
<Button type="text" danger size="small" @click="removeHeader(index)"> <Button type="text" danger size="small" @click="removeHeader(index)">
@@ -135,50 +110,12 @@ watch(
</Button> </Button>
</div> </div>
</div> </div>
<Button type="primary" class="add-btn" @click="addHeader"> <Button type="primary" class="w-full" @click="addHeader">
<template #icon> <template #icon>
<IconifyIcon icon="ep:plus" /> <IconifyIcon icon="ep:plus" />
</template> </template>
添加请求头 添加请求头
</Button> </Button>
</div> </div>
<template #footer>
<Button @click="handleClose">取消</Button>
<Button type="primary" @click="handleSave">保存</Button>
</template>
</Modal> </Modal>
</template> </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 { nextTick, onBeforeUnmount, ref, toRaw, watch } from 'vue';
import { import {
Form,
FormItem, FormItem,
Input, Input,
Select, Select,
@@ -75,47 +76,50 @@ watch(
<template> <template>
<div class="mt-4"> <div class="mt-4">
<FormItem label="脚本格式"> <Form :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }">
<Input <FormItem label="脚本格式">
v-model:value="scriptTaskForm.scriptFormat" <Input
allow-clear v-model:value="scriptTaskForm.scriptFormat"
@input="updateElementTask()" allow-clear
@change="updateElementTask()" @input="updateElementTask()"
/> @change="updateElementTask()"
</FormItem> />
<FormItem label="脚本类型"> </FormItem>
<Select v-model:value="scriptTaskForm.scriptType"> <!-- TODO scriptType 外部资源 内联脚本 flowable 文档 https://www.flowable.com/open-source/docs/bpmn/ch07b-BPMN-Constructs#script-task 没看到到有相应的属性 -->
<SelectOption value="inline">内联脚本</SelectOption> <FormItem label="脚本类型">
<SelectOption value="external">外部资源</SelectOption> <Select v-model:value="scriptTaskForm.scriptType">
</Select> <SelectOption value="inline">内联脚本</SelectOption>
</FormItem> <SelectOption value="external">外部资源</SelectOption>
<FormItem label="脚本" v-show="scriptTaskForm.scriptType === 'inline'"> </Select>
<Textarea </FormItem>
v-model:value="scriptTaskForm.script" <FormItem label="脚本" v-show="scriptTaskForm.scriptType === 'inline'">
:auto-size="{ minRows: 2, maxRows: 4 }" <Textarea
allow-clear v-model:value="scriptTaskForm.script"
@input="updateElementTask()" :auto-size="{ minRows: 2, maxRows: 4 }"
@change="updateElementTask()" allow-clear
/> @input="updateElementTask()"
</FormItem> @change="updateElementTask()"
<FormItem />
label="资源地址" </FormItem>
v-show="scriptTaskForm.scriptType === 'external'" <FormItem
> label="资源地址"
<Input v-show="scriptTaskForm.scriptType === 'external'"
v-model:value="scriptTaskForm.resource" >
allow-clear <Input
@input="updateElementTask()" v-model:value="scriptTaskForm.resource"
@change="updateElementTask()" allow-clear
/> @input="updateElementTask()"
</FormItem> @change="updateElementTask()"
<FormItem label="结果变量"> />
<Input </FormItem>
v-model:value="scriptTaskForm.resultVariable" <FormItem label="结果变量">
allow-clear <Input
@input="updateElementTask()" v-model:value="scriptTaskForm.resultVariable"
@change="updateElementTask()" allow-clear
/> @input="updateElementTask()"
</FormItem> @change="updateElementTask()"
/>
</FormItem>
</Form>
</div> </div>
</template> </template>

View File

@@ -1,11 +1,13 @@
<!-- eslint-disable prettier/prettier --> <!-- eslint-disable prettier/prettier -->
<script lang="ts" setup> <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 { IconifyIcon } from '@vben/icons';
import { import {
Button, Button,
Form,
FormItem, FormItem,
Input, Input,
RadioButton, RadioButton,
@@ -70,7 +72,6 @@ const serviceTaskForm = ref({ ...DEFAULT_TASK_FORM });
const httpTaskForm = ref<any>({ ...DEFAULT_HTTP_FORM }); const httpTaskForm = ref<any>({ ...DEFAULT_HTTP_FORM });
const bpmnElement = ref(); const bpmnElement = ref();
const httpInitializing = ref(false); const httpInitializing = ref(false);
const showHeaderEditor = ref(false);
const bpmnInstances = () => (window as any)?.bpmnInstances; const bpmnInstances = () => (window as any)?.bpmnInstances;
@@ -179,7 +180,10 @@ const shouldPersistField = (name: string, value: any) => {
}; };
const updateHttpExtensions = (force = false) => { const updateHttpExtensions = (force = false) => {
if (!bpmnElement.value) return; const instances = bpmnInstances();
if (!instances || !instances.bpmnElement) return;
// 直接使用原始BPMN元素避免Vue响应式代理问题
const bpmnElement = instances.bpmnElement;
if ( if (
!force && !force &&
(httpInitializing.value || serviceTaskForm.value.executeType !== 'http') (httpInitializing.value || serviceTaskForm.value.executeType !== 'http')
@@ -236,31 +240,37 @@ const updateHttpExtensions = (force = false) => {
}); });
}); });
updateElementExtensions(bpmnElement.value, [ updateElementExtensions(bpmnElement, [
...otherExtensions, ...otherExtensions,
...httpFieldElements, ...httpFieldElements,
]); ]);
}; };
const removeHttpExtensions = () => { const removeHttpExtensions = () => {
if (!bpmnElement.value) return; const instances = bpmnInstances();
if (!instances || !instances.bpmnElement) return;
// 直接使用原始BPMN元素避免Vue响应式代理问题
const bpmnElement = instances.bpmnElement;
const { httpFields, otherExtensions } = collectHttpExtensionInfo(); const { httpFields, otherExtensions } = collectHttpExtensionInfo();
if (httpFields.size === 0) { if (httpFields.size === 0) {
return; return;
} }
if (otherExtensions.length === 0) { if (otherExtensions.length === 0) {
bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), { bpmnInstances().modeling.updateProperties(bpmnElement, {
extensionElements: null, extensionElements: null,
}); });
return; return;
} }
updateElementExtensions(bpmnElement.value, otherExtensions); updateElementExtensions(bpmnElement, otherExtensions);
}; };
const updateElementTask = () => { 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> = { const taskAttr: Record<string, any> = {
class: null, class: null,
@@ -280,7 +290,7 @@ const updateElementTask = () => {
taskAttr[flowableTypeKey] = 'http'; taskAttr[flowableTypeKey] = 'http';
} }
bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), taskAttr); bpmnInstances().modeling.updateProperties(bpmnElement, taskAttr);
if (type === 'http') { if (type === 'http') {
updateHttpExtensions(true); updateHttpExtensions(true);
@@ -297,10 +307,24 @@ const handleExecuteTypeChange = (value: any) => {
updateElementTask(); updateElementTask();
}; };
/** 打开请求头编辑器 */
const openHttpHeaderEditor = () => {
httpHeaderEditorApi
.setData({
headers: httpTaskForm.value.requestHeaders,
})
.open();
};
/** 保存请求头 */
const handleHeadersSave = (headersStr: string) => { const handleHeadersSave = (headersStr: string) => {
httpTaskForm.value.requestHeaders = headersStr; httpTaskForm.value.requestHeaders = headersStr;
}; };
const [HttpHeaderEditorModal, httpHeaderEditorApi] = useVbenModal({
connectedComponent: HttpHeaderEditor,
});
onBeforeUnmount(() => { onBeforeUnmount(() => {
bpmnElement.value = null; bpmnElement.value = null;
}); });
@@ -327,110 +351,140 @@ watch(
<template> <template>
<div> <div>
<FormItem label="执行类型" key="executeType"> <Form :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }">
<Select <FormItem label="执行类型" key="executeType">
v-model:value="serviceTaskForm.executeType" <Select
:options="[ v-model:value="serviceTaskForm.executeType"
{ label: 'Java类', value: 'class' }, :options="[
{ label: '表达式', value: 'expression' }, { label: 'Java类', value: 'class' },
{ label: '代理表达式', value: 'delegateExpression' }, { label: '表达式', value: 'expression' },
{ label: 'HTTP 调用', value: 'http' }, { label: '代理表达式', value: 'delegateExpression' },
]" { label: 'HTTP 调用', value: 'http' },
@change="handleExecuteTypeChange" ]"
/> @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"
/> />
</FormItem> </FormItem>
<FormItem label="返回变量前缀" key="http-result-variable-prefix"> <FormItem
<Input v-model:value="httpTaskForm.resultVariablePrefix" /> v-if="serviceTaskForm.executeType === 'class'"
label="Java类"
name="class"
key="execute-class"
>
<Input
v-model:value="serviceTaskForm.class"
allow-clear
@change="updateElementTask"
/>
</FormItem> </FormItem>
<FormItem label="格式化返回为JSON" key="http-save-json"> <FormItem
<Switch v-model:checked="httpTaskForm.saveResponseVariableAsJson" /> v-if="serviceTaskForm.executeType === 'expression'"
label="表达式"
name="expression"
key="execute-expression"
>
<Input
v-model:value="serviceTaskForm.expression"
allow-clear
@change="updateElementTask"
/>
</FormItem> </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 <HttpHeaderEditorModal @save="handleHeadersSave" />
v-model="showHeaderEditor"
:headers="httpTaskForm.requestHeaders"
@save="handleHeadersSave"
/>
</div> </div>
</template> </template>