This commit is contained in:
YunaiV
2025-11-29 18:40:55 +08:00
6 changed files with 520 additions and 26 deletions

View File

@@ -89,6 +89,7 @@ F.prototype.getPaletteEntries = function () {
create.start(event, elementFactory.createParticipantShape())
}
assign(actions, {
'hand-tool': {
group: 'tools',

View File

@@ -96,6 +96,7 @@ PaletteProvider.prototype.getPaletteEntries = function () {
create.start(event, elementFactory.createParticipantShape())
}
assign(actions, {
'hand-tool': {
group: 'tools',

View File

@@ -10,7 +10,7 @@ export default {
'Append Gateway': '追加网关',
'Append Task': '追加任务',
'Append Intermediate/Boundary Event': '追加中间抛出事件/边界事件',
TextAnnotation: '文本注释',
'Activate the global connect tool': '激活全局连接工具',
'Append {type}': '添加 {type}',
'Add Lane above': '在上面添加道',
@@ -29,10 +29,16 @@ export default {
'Create expanded SubProcess': '创建扩展子过程',
'Create IntermediateThrowEvent/BoundaryEvent': '创建中间抛出事件/边界事件',
'Create Pool/Participant': '创建池/参与者',
'Parallel Multi Instance': '并行多重事件',
'Sequential Multi Instance': '时序多重事件',
'Participant Multiplicity': '参与者多重性',
'Empty pool/participant (removes content)': '清空池/参与者(移除内容)',
'Empty pool/participant': '收缩池/参与者',
'Expanded pool/participant': '展开池/参与者',
'Parallel Multi-Instance': '并行多重事件',
'Sequential Multi-Instance': '时序多重事件',
DataObjectReference: '数据对象参考',
DataStoreReference: '数据存储参考',
'Data object reference': '数据对象引用 ',
'Data store reference': '数据存储引用 ',
Loop: '循环',
'Ad-hoc': '即席',
'Create {type}': '创建 {type}',
@@ -47,6 +53,9 @@ export default {
'Call Activity': '调用活动',
'Sub-Process (collapsed)': '子流程(折叠的)',
'Sub-Process (expanded)': '子流程(展开的)',
'Ad-hoc sub-process': '即席子流程',
'Ad-hoc sub-process (collapsed)': '即席子流程(折叠的)',
'Ad-hoc sub-process (expanded)': '即席子流程(展开的)',
'Start Event': '开始事件',
StartEvent: '开始事件',
'Intermediate Throw Event': '中间事件',
@@ -109,10 +118,10 @@ export default {
'Parallel Gateway': '并行网关',
'Inclusive Gateway': '相容网关',
'Complex Gateway': '复杂网关',
'Event based Gateway': '事件网关',
'Event-based Gateway': '事件网关',
Transaction: '转运',
'Sub Process': '子流程',
'Event Sub Process': '事件子流程',
'sub-process': '子流程',
'Event sub-process': '事件子流程',
'Collapsed Pool': '折叠池',
'Expanded Pool': '展开池',

View File

@@ -0,0 +1,178 @@
<template>
<el-dialog
v-model="dialogVisible"
title="编辑请求头"
width="600px"
:close-on-click-modal="false"
@close="handleClose"
>
<div class="header-editor">
<div class="header-list">
<div v-for="(item, index) in headerList" :key="index" class="header-item">
<el-input v-model="item.key" placeholder="请输入参数名" class="header-key" clearable />
<span class="separator">:</span>
<el-input
v-model="item.value"
placeholder="请输入参数值 (支持表达式 ${变量名})"
class="header-value"
clearable
/>
<el-button
type="danger"
:icon="Delete"
circle
size="small"
@click="removeHeader(index)"
/>
</div>
</div>
<el-button type="primary" :icon="Plus" class="add-btn" @click="addHeader">
添加请求头
</el-button>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="handleClose">取消</el-button>
<el-button type="primary" @click="handleSave">保存</el-button>
</span>
</template>
</el-dialog>
</template>
<script lang="ts" setup>
import { Delete, Plus } from '@element-plus/icons-vue'
defineOptions({ name: 'HttpHeaderEditor' })
const props = defineProps({
modelValue: {
type: Boolean,
default: false
},
headers: {
type: String,
default: ''
}
})
const emit = defineEmits(['update:modelValue', 'save'])
interface HeaderItem {
key: string
value: string
}
const dialogVisible = computed({
get: () => props.modelValue,
set: (val) => emit('update:modelValue', val)
})
const headerList = ref<HeaderItem[]>([])
// 解析请求头字符串为列表
const parseHeaders = (headersStr: string): HeaderItem[] => {
if (!headersStr || !headersStr.trim()) {
return [{ key: '', value: '' }]
}
const lines = headersStr.split('\n').filter((line) => line.trim())
const parsed = lines.map((line) => {
const colonIndex = line.indexOf(':')
if (colonIndex > 0) {
return {
key: line.substring(0, colonIndex).trim(),
value: line.substring(colonIndex + 1).trim()
}
}
return { key: line.trim(), value: '' }
})
return parsed.length > 0 ? parsed : [{ key: '', value: '' }]
}
// 将列表转换为请求头字符串
const stringifyHeaders = (headers: HeaderItem[]): string => {
return headers
.filter((item) => item.key.trim())
.map((item) => `${item.key}: ${item.value}`)
.join('\n')
}
// 添加请求头
const addHeader = () => {
headerList.value.push({ key: '', value: '' })
}
// 移除请求头
const removeHeader = (index: number) => {
if (headerList.value.length === 1) {
// 至少保留一行
headerList.value = [{ key: '', value: '' }]
} else {
headerList.value.splice(index, 1)
}
}
// 保存
const handleSave = () => {
const headersStr = stringifyHeaders(headerList.value)
emit('save', headersStr)
dialogVisible.value = false
}
// 关闭
const handleClose = () => {
dialogVisible.value = false
}
// 监听对话框打开,初始化数据
watch(
() => props.modelValue,
(val) => {
if (val) {
headerList.value = parseHeaders(props.headers)
}
},
{ immediate: true }
)
</script>
<style lang="scss" scoped>
.header-editor {
.header-list {
max-height: 400px;
overflow-y: auto;
margin-bottom: 16px;
}
.header-item {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
.header-key {
flex: 0 0 180px;
}
.separator {
color: #606266;
font-weight: 500;
}
.header-value {
flex: 1;
}
}
.add-btn {
width: 100%;
}
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
}
</style>

View File

@@ -1,10 +1,11 @@
<template>
<div>
<el-form-item label="执行类型" key="executeType">
<el-select v-model="serviceTaskForm.executeType">
<el-select v-model="serviceTaskForm.executeType" @change="handleExecuteTypeChange">
<el-option label="Java类" value="class" />
<el-option label="表达式" value="expression" />
<el-option label="代理表达式" value="delegateExpression" />
<el-option label="HTTP 调用" value="http" />
</el-select>
</el-form-item>
<el-form-item
@@ -29,48 +30,345 @@
prop="delegateExpression"
key="execute-delegate"
>
<el-input v-model="serviceTaskForm.delegateExpression" clearable @change="updateElementTask" />
<el-input
v-model="serviceTaskForm.delegateExpression"
clearable
@change="updateElementTask"
/>
</el-form-item>
<template v-if="serviceTaskForm.executeType === 'http'">
<el-form-item label="请求方法" key="http-method">
<el-radio-group v-model="httpTaskForm.requestMethod">
<el-radio-button label="GET" value="GET" />
<el-radio-button label="POST" value="POST" />
<el-radio-button label="PUT" value="PUT" />
<el-radio-button label="DELETE" value="DELETE" />
</el-radio-group>
</el-form-item>
<el-form-item label="请求地址" key="http-url" prop="requestUrl">
<el-input v-model="httpTaskForm.requestUrl" clearable />
</el-form-item>
<el-form-item label="请求头" key="http-headers">
<div style="display: flex; gap: 8px; align-items: flex-start; width: 100%">
<el-input
v-model="httpTaskForm.requestHeaders"
type="textarea"
resize="vertical"
:autosize="{ minRows: 4, maxRows: 8 }"
readonly
placeholder="点击右侧编辑按钮添加请求头"
style="flex: 1; min-width: 0"
/>
<el-button
type="primary"
:icon="Edit"
@click="showHeaderEditor = true"
style="flex-shrink: 0"
>
编辑
</el-button>
</div>
</el-form-item>
<el-form-item label="禁止重定向" key="http-disallow-redirects">
<el-switch v-model="httpTaskForm.disallowRedirects" />
</el-form-item>
<el-form-item label="忽略异常" key="http-ignore-exception">
<el-switch v-model="httpTaskForm.ignoreException" />
</el-form-item>
<el-form-item label="保存返回变量" key="http-save-response">
<el-switch v-model="httpTaskForm.saveResponseParameters" />
</el-form-item>
<el-form-item label="是否瞬间变量" key="http-save-transient">
<el-switch v-model="httpTaskForm.saveResponseParametersTransient" />
</el-form-item>
<el-form-item label="返回变量前缀" key="http-result-variable-prefix">
<el-input v-model="httpTaskForm.resultVariablePrefix" />
</el-form-item>
<el-form-item label="格式化返回为JSON" key="http-save-json">
<el-switch v-model="httpTaskForm.saveResponseVariableAsJson" />
</el-form-item>
</template>
<!-- 请求头编辑器 -->
<HttpHeaderEditor
v-model="showHeaderEditor"
:headers="httpTaskForm.requestHeaders"
@save="handleHeadersSave"
/>
</div>
</template>
<script lang="ts" setup>
import { Edit } from '@element-plus/icons-vue'
import { updateElementExtensions } from '@/components/bpmnProcessDesigner/package/utils'
import HttpHeaderEditor from './HttpHeaderEditor.vue'
defineOptions({ name: 'ServiceTask' })
const props = defineProps({
id: String,
type: String
})
const defaultTaskForm = ref({
const prefix = (inject('prefix', 'flowable') || 'flowable') as string
const flowableTypeKey = `${prefix}:type`
const flowableFieldType = `${prefix}:Field`
const HTTP_FIELD_NAMES = [
'requestMethod',
'requestUrl',
'requestHeaders',
'disallowRedirects',
'ignoreException',
'saveResponseParameters',
'resultVariablePrefix',
'saveResponseParametersTransient',
'saveResponseVariableAsJson'
]
const HTTP_BOOLEAN_FIELDS = new Set([
'disallowRedirects',
'ignoreException',
'saveResponseParameters',
'saveResponseParametersTransient',
'saveResponseVariableAsJson'
])
const DEFAULT_TASK_FORM = {
executeType: '',
class: '',
expression: '',
delegateExpression: ''
})
}
const serviceTaskForm = ref<any>({})
const DEFAULT_HTTP_FORM = {
requestMethod: 'GET',
requestUrl: '',
requestHeaders: 'Content-Type: application/json',
resultVariablePrefix: '',
disallowRedirects: false,
ignoreException: false,
saveResponseParameters: false,
saveResponseParametersTransient: false,
saveResponseVariableAsJson: false
}
const serviceTaskForm = ref({ ...DEFAULT_TASK_FORM })
const httpTaskForm = ref({ ...DEFAULT_HTTP_FORM })
const bpmnElement = ref()
const httpInitializing = ref(false)
const showHeaderEditor = ref(false)
const bpmnInstances = () => (window as any)?.bpmnInstances
const resetTaskForm = () => {
for (let key in defaultTaskForm.value) {
let value = bpmnElement.value?.businessObject[key] || defaultTaskForm.value[key]
serviceTaskForm.value[key] = value
if (value) {
serviceTaskForm.value.executeType = key
// 判断字符串是否包含表达式
const isExpression = (value: string): boolean => {
if (!value) return false
// 检测 ${...} 或 #{...} 格式的表达式
return /\${[^}]+}/.test(value) || /#{[^}]+}/.test(value)
}
const collectHttpExtensionInfo = () => {
const businessObject = bpmnElement.value?.businessObject
const extensionElements = businessObject?.extensionElements
const httpFields = new Map<string, string>()
const httpFieldTypes = new Map<string, 'string' | 'expression'>()
const otherExtensions: any[] = []
extensionElements?.values?.forEach((item: any) => {
if (item?.$type === flowableFieldType && HTTP_FIELD_NAMES.includes(item.name)) {
const value = item.string ?? item.stringValue ?? item.expression ?? ''
const fieldType = item.expression ? 'expression' : 'string'
httpFields.set(item.name, value)
httpFieldTypes.set(item.name, fieldType)
} else {
otherExtensions.push(item)
}
})
return { httpFields, httpFieldTypes, otherExtensions }
}
const resetHttpDefaults = () => {
httpInitializing.value = true
httpTaskForm.value = { ...DEFAULT_HTTP_FORM }
nextTick(() => {
httpInitializing.value = false
})
}
const resetHttpForm = () => {
httpInitializing.value = true
const { httpFields } = collectHttpExtensionInfo()
const nextForm = { ...DEFAULT_HTTP_FORM }
HTTP_FIELD_NAMES.forEach((name) => {
const stored = httpFields.get(name)
if (stored !== undefined) {
nextForm[name] = HTTP_BOOLEAN_FIELDS.has(name) ? stored === 'true' : stored
}
})
httpTaskForm.value = nextForm
nextTick(() => {
httpInitializing.value = false
updateHttpExtensions(true)
})
}
const resetServiceTaskForm = () => {
const businessObject = bpmnElement.value?.businessObject
const nextForm = { ...DEFAULT_TASK_FORM }
if (businessObject) {
if (businessObject.class) {
nextForm.class = businessObject.class
nextForm.executeType = 'class'
}
if (businessObject.expression) {
nextForm.expression = businessObject.expression
nextForm.executeType = 'expression'
}
if (businessObject.delegateExpression) {
nextForm.delegateExpression = businessObject.delegateExpression
nextForm.executeType = 'delegateExpression'
}
if (businessObject.$attrs?.[flowableTypeKey] === 'http') {
nextForm.executeType = 'http'
} else {
// 兜底:如缺少 flowable:type=http但扩展里已有 HTTP 的字段,也认为是 HTTP
const { httpFields } = collectHttpExtensionInfo()
if (httpFields.size > 0) {
nextForm.executeType = 'http'
}
}
}
serviceTaskForm.value = nextForm
if (nextForm.executeType === 'http') {
resetHttpForm()
} else {
resetHttpDefaults()
}
}
const updateElementTask = () => {
let taskAttr = Object.create(null);
const type = serviceTaskForm.value.executeType;
for (let key in serviceTaskForm.value) {
if (key !== 'executeType' && key !== type) taskAttr[key] = null;
const shouldPersistField = (name: string, value: any) => {
if (HTTP_BOOLEAN_FIELDS.has(name)) return true
if (name === 'requestMethod') return true
if (name === 'requestUrl') return !!value
return value !== undefined && value !== ''
}
const updateHttpExtensions = (force = false) => {
if (!bpmnElement.value) return
if (!force && (httpInitializing.value || serviceTaskForm.value.executeType !== 'http')) {
return
}
taskAttr[type] = serviceTaskForm.value[type] || "";
const {
httpFields: existingFields,
httpFieldTypes: existingTypes,
otherExtensions
} = collectHttpExtensionInfo()
const desiredEntries: [string, string][] = []
HTTP_FIELD_NAMES.forEach((name) => {
const rawValue = httpTaskForm.value[name]
if (!shouldPersistField(name, rawValue)) {
return
}
const persisted = HTTP_BOOLEAN_FIELDS.has(name)
? String(!!rawValue)
: rawValue === undefined
? ''
: String(rawValue)
desiredEntries.push([name, persisted])
})
// 检查是否有变化不仅比较值还要比较字段类型string vs expression
if (!force && desiredEntries.length === existingFields.size) {
let noChange = true
for (const [name, value] of desiredEntries) {
const existingValue = existingFields.get(name)
const existingType = existingTypes.get(name)
const currentType = isExpression(value) ? 'expression' : 'string'
if (existingValue !== value || existingType !== currentType) {
noChange = false
break
}
}
if (noChange) {
return
}
}
const moddle = bpmnInstances().moddle
const httpFieldElements = desiredEntries.map(([name, value]) => {
// 根据值是否包含表达式来决定使用 string 还是 expression 属性
const isExpr = isExpression(value)
return moddle.create(flowableFieldType, {
name,
...(isExpr ? { expression: value } : { string: value })
})
})
updateElementExtensions(bpmnElement.value, [...otherExtensions, ...httpFieldElements])
}
const removeHttpExtensions = () => {
if (!bpmnElement.value) return
const { httpFields, otherExtensions } = collectHttpExtensionInfo()
if (!httpFields.size) {
return
}
if (!otherExtensions.length) {
bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
extensionElements: null
})
return
}
updateElementExtensions(bpmnElement.value, otherExtensions)
}
const updateElementTask = () => {
if (!bpmnElement.value) return
const taskAttr: Record<string, any> = {
class: null,
expression: null,
delegateExpression: null,
[flowableTypeKey]: null
}
const type = serviceTaskForm.value.executeType
if (type === 'class' || type === 'expression' || type === 'delegateExpression') {
taskAttr[type] = serviceTaskForm.value[type] || null
} else if (type === 'http') {
taskAttr[flowableTypeKey] = 'http'
}
bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), taskAttr)
if (type === 'http') {
updateHttpExtensions(true)
} else {
removeHttpExtensions()
}
}
const handleExecuteTypeChange = (value: string) => {
serviceTaskForm.value.executeType = value
if (value === 'http') {
resetHttpForm()
}
updateElementTask()
}
const handleHeadersSave = (headersStr: string) => {
httpTaskForm.value.requestHeaders = headersStr
}
onBeforeUnmount(() => {
@@ -82,10 +380,17 @@ watch(
() => {
bpmnElement.value = bpmnInstances().bpmnElement
nextTick(() => {
resetTaskForm()
resetServiceTaskForm()
})
},
{ immediate: true }
)
watch(
() => httpTaskForm.value,
() => {
updateHttpExtensions()
},
{ deep: true }
)
</script>

View File

@@ -39,8 +39,8 @@
<el-select v-model="formData.event" placeholder="请选择事件">
<el-option
v-for="event in formData.type == 'execution'
? ['start', 'end']
: ['create', 'assignment', 'complete', 'delete', 'update', 'timeout']"
? ['开始', '结束']
: ['创建', '指派', '完成', '删除', '更新', '超时']"
:label="event"
:value="event"
:key="event"