Bladeren bron

Merge pull request #200 from zhanglc0618/http-task

feat: bpmn流程设计器服务任务中新增执行类型:http任务
芋道源码 2 maanden geleden
bovenliggende
commit
cca090151e

+ 1 - 0
src/components/bpmnProcessDesigner/package/designer/plugins/palette/CustomPalette.js

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

+ 1 - 0
src/components/bpmnProcessDesigner/package/designer/plugins/palette/paletteProvider.js

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

+ 178 - 0
src/components/bpmnProcessDesigner/package/penal/task/task-components/HttpHeaderEditor.vue

@@ -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>

+ 322 - 17
src/components/bpmnProcessDesigner/package/penal/task/task-components/ServiceTask.vue

@@ -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 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<any>({})
+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 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
+  }
+
+  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 = () => {
-  let taskAttr = Object.create(null);
-  const type = serviceTaskForm.value.executeType;
-  for (let key in serviceTaskForm.value) {
-    if (key !== 'executeType' && key !== type) taskAttr[key] = null;
+  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'
   }
-  taskAttr[type] = serviceTaskForm.value[type] || "";
+
   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>