|
@@ -0,0 +1,695 @@
|
|
|
|
|
+<template>
|
|
|
|
|
+ <el-scrollbar class="w-full box-border p-12px">
|
|
|
|
|
+ <div class="webhook-setter">
|
|
|
|
|
+ <section class="section-block">
|
|
|
|
|
+ <div class="text-14px font-bold text-gray-700">WEBHOOK URL</div>
|
|
|
|
|
+ <div class="url-row">
|
|
|
|
|
+ <el-select v-model="formData.method" class="method-select">
|
|
|
|
|
+ <el-option
|
|
|
|
|
+ v-for="item in methodOptions"
|
|
|
|
|
+ :key="item.value"
|
|
|
|
|
+ :label="item.label"
|
|
|
|
|
+ :value="item.value"
|
|
|
|
|
+ />
|
|
|
|
|
+ </el-select>
|
|
|
|
|
+
|
|
|
|
|
+ <el-input :model-value="formData.webhook_url" readonly class="url-input">
|
|
|
|
|
+ <template #append>
|
|
|
|
|
+ <IconButton
|
|
|
|
|
+ link
|
|
|
|
|
+ icon="lucide:copy"
|
|
|
|
|
+ @click="copyText(formData.webhook_url, 'Webhook URL')"
|
|
|
|
|
+ >
|
|
|
|
|
+ </IconButton>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-input>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="debug-tip">
|
|
|
|
|
+ <div class="debug-tip__label">测试运行时,请始终使用此 URL</div>
|
|
|
|
|
+ <div
|
|
|
|
|
+ class="debug-tip__value cursor-pointer"
|
|
|
|
|
+ @click="copyText(formData.webhook_debug_url, '测试URL')"
|
|
|
|
|
+ >
|
|
|
|
|
+ {{ formData.webhook_debug_url || '--' }}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </section>
|
|
|
|
|
+
|
|
|
|
|
+ <section class="section-block">
|
|
|
|
|
+ <div class="field-title">内容类型</div>
|
|
|
|
|
+ <el-select v-model="formData.content_type" class="w-full">
|
|
|
|
|
+ <el-option
|
|
|
|
|
+ v-for="item in contentTypeOptions"
|
|
|
|
|
+ :key="item.value"
|
|
|
|
|
+ :label="item.label"
|
|
|
|
|
+ :value="item.value"
|
|
|
|
|
+ />
|
|
|
|
|
+ </el-select>
|
|
|
|
|
+ </section>
|
|
|
|
|
+
|
|
|
|
|
+ <section class="section-block">
|
|
|
|
|
+ <div class="text-14px font-bold text-gray-700">QUERY PARAMETERS</div>
|
|
|
|
|
+ <div class="table-shell">
|
|
|
|
|
+ <el-table :data="queryRows" border row-class-name="param-table">
|
|
|
|
|
+ <el-table-column label="变量名">
|
|
|
|
|
+ <template #default="{ row, $index }">
|
|
|
|
|
+ <el-input
|
|
|
|
|
+ v-model="row.name"
|
|
|
|
|
+ placeholder="输入变量名..."
|
|
|
|
|
+ @focus="ensureExtraQueryRow($index)"
|
|
|
|
|
+ />
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column label="类型" width="100">
|
|
|
|
|
+ <template #default="{ row }">
|
|
|
|
|
+ <el-select v-model="row.type" class="w-full">
|
|
|
|
|
+ <el-option
|
|
|
|
|
+ v-for="option in scalarTypeOptions"
|
|
|
|
|
+ :key="option.value"
|
|
|
|
|
+ :label="option.label"
|
|
|
|
|
+ :value="option.value"
|
|
|
|
|
+ />
|
|
|
|
|
+ </el-select>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column label="必填" width="88" align="center">
|
|
|
|
|
+ <template #default="{ row, $index }">
|
|
|
|
|
+ <div class="flex justify-between">
|
|
|
|
|
+ <el-checkbox v-model="row.required" />
|
|
|
|
|
+ <IconButton
|
|
|
|
|
+ v-if="queryRows.length > 1"
|
|
|
|
|
+ link
|
|
|
|
|
+ icon="lucide:trash-2"
|
|
|
|
|
+ class="delete-button"
|
|
|
|
|
+ @click="removeQueryRow($index)"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ </el-table>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </section>
|
|
|
|
|
+
|
|
|
|
|
+ <section class="section-block">
|
|
|
|
|
+ <div class="text-14px font-bold text-gray-700">HEADER PARAMETERS</div>
|
|
|
|
|
+ <div class="table-shell">
|
|
|
|
|
+ <el-table :data="headerRows" border row-class-name="param-table">
|
|
|
|
|
+ <el-table-column label="变量名">
|
|
|
|
|
+ <template #default="{ row, $index }">
|
|
|
|
|
+ <el-input
|
|
|
|
|
+ v-model="row.name"
|
|
|
|
|
+ placeholder="输入变量名..."
|
|
|
|
|
+ @focus="ensureExtraHeaderRow($index)"
|
|
|
|
|
+ />
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column label="必填" width="88" align="center">
|
|
|
|
|
+ <template #default="{ row, $index }">
|
|
|
|
|
+ <div class="flex justify-between">
|
|
|
|
|
+ <el-checkbox v-model="row.required" />
|
|
|
|
|
+ <IconButton
|
|
|
|
|
+ v-if="headerRows.length > 1"
|
|
|
|
|
+ link
|
|
|
|
|
+ icon="lucide:trash-2"
|
|
|
|
|
+ class="delete-button"
|
|
|
|
|
+ @click="removeHeaderRow($index)"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ </el-table>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </section>
|
|
|
|
|
+
|
|
|
|
|
+ <section class="section-block">
|
|
|
|
|
+ <div class="text-14px font-bold text-gray-700">REQUEST BODY PARAMETERS</div>
|
|
|
|
|
+ <div class="table-shell">
|
|
|
|
|
+ <el-table :data="bodyRows" border row-class-name="param-table">
|
|
|
|
|
+ <el-table-column label="变量名">
|
|
|
|
|
+ <template #default="{ row, $index }">
|
|
|
|
|
+ <el-input
|
|
|
|
|
+ v-model="row.name"
|
|
|
|
|
+ placeholder="输入变量名..."
|
|
|
|
|
+ @focus="ensureExtraBodyRow($index)"
|
|
|
|
|
+ />
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column label="类型" width="100">
|
|
|
|
|
+ <template #default="{ row }">
|
|
|
|
|
+ <el-select v-model="row.type" class="w-full">
|
|
|
|
|
+ <el-option
|
|
|
|
|
+ v-for="option in bodyTypeOptions"
|
|
|
|
|
+ :key="option.value"
|
|
|
|
|
+ :label="option.label"
|
|
|
|
|
+ :value="option.value"
|
|
|
|
|
+ />
|
|
|
|
|
+ </el-select>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column label="必填" width="88" align="center">
|
|
|
|
|
+ <template #default="{ row, $index }">
|
|
|
|
|
+ <div class="flex justify-between">
|
|
|
|
|
+ <el-checkbox v-model="row.required" />
|
|
|
|
|
+ <IconButton
|
|
|
|
|
+ v-if="bodyRows.length > 1"
|
|
|
|
|
+ link
|
|
|
|
|
+ icon="lucide:trash-2"
|
|
|
|
|
+ class="delete-button"
|
|
|
|
|
+ @click="removeBodyRow($index)"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ </el-table>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </section>
|
|
|
|
|
+
|
|
|
|
|
+ <section class="section-block section-block--response">
|
|
|
|
|
+ <div class="field-title">响应</div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="response-grid">
|
|
|
|
|
+ <div class="text-14px">状态码</div>
|
|
|
|
|
+ <el-input-number
|
|
|
|
|
+ v-model="formData.response_status_code"
|
|
|
|
|
+ :min="100"
|
|
|
|
|
+ :max="599"
|
|
|
|
|
+ :step="1"
|
|
|
|
|
+ class="status-input"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="field-stack">
|
|
|
|
|
+ <div class="text-14px">响应体</div>
|
|
|
|
|
+ <CodeEditor
|
|
|
|
|
+ v-model="formData.response_body"
|
|
|
|
|
+ :tools="false"
|
|
|
|
|
+ :copy-code="false"
|
|
|
|
|
+ :allow-change-language="false"
|
|
|
|
|
+ :language="responseEditorLanguage"
|
|
|
|
|
+ :height="180"
|
|
|
|
|
+ theme="vs-light"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </section>
|
|
|
|
|
+
|
|
|
|
|
+ <section class="section-block mb-10">
|
|
|
|
|
+ <button type="button" class="output-toggle" @click="outputsExpanded = !outputsExpanded">
|
|
|
|
|
+ <span>输出变量</span>
|
|
|
|
|
+ <Icon :icon="outputsExpanded ? 'lucide:chevron-down' : 'lucide:chevron-right'" />
|
|
|
|
|
+ </button>
|
|
|
|
|
+
|
|
|
|
|
+ <div v-if="outputsExpanded" class="output-list">
|
|
|
|
|
+ <div v-for="output in formData.outputs" :key="output.name" class="output-item">
|
|
|
|
|
+ <span class="output-item__name">{{ output.name }}</span>
|
|
|
|
|
+ <span class="output-item__type">{{ output.type }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </section>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </el-scrollbar>
|
|
|
|
|
+</template>
|
|
|
|
|
+
|
|
|
|
|
+<script setup lang="ts">
|
|
|
|
|
+import { computed, ref, watch } from 'vue'
|
|
|
|
|
+import { cloneDeep, isEqual } from 'lodash-es'
|
|
|
|
|
+import { ElMessage } from 'element-plus'
|
|
|
|
|
+import { Icon, IconButton } from '@repo/ui'
|
|
|
|
|
+
|
|
|
|
|
+import CodeEditor from '@/nodes/_base/CodeEditor.vue'
|
|
|
|
|
+import { VARIABLE_TYPE_OPTIONS } from '@/constant'
|
|
|
|
|
+import { useSetterModel } from '../_shared/useSetterModel'
|
|
|
|
|
+
|
|
|
|
|
+import type { NodeVariableType } from '../../Interface'
|
|
|
|
|
+import type {
|
|
|
|
|
+ WebhookBodyParam,
|
|
|
|
|
+ WebhookContentType,
|
|
|
|
|
+ WebhookData,
|
|
|
|
|
+ WebhookHeader,
|
|
|
|
|
+ WebhookMethod,
|
|
|
|
|
+ WebhookParam
|
|
|
|
|
+} from './index'
|
|
|
|
|
+
|
|
|
|
|
+interface Emits {
|
|
|
|
|
+ (e: 'update', value: WebhookData): void
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const props = defineProps<{
|
|
|
|
|
+ data: WebhookData
|
|
|
|
|
+}>()
|
|
|
|
|
+
|
|
|
|
|
+const emit = defineEmits<Emits>()
|
|
|
|
|
+
|
|
|
|
|
+type QueryRow = WebhookParam
|
|
|
|
|
+type HeaderRow = WebhookHeader
|
|
|
|
|
+type BodyRow = WebhookBodyParam
|
|
|
|
|
+
|
|
|
|
|
+const methodOptions: Array<{ label: string; value: WebhookMethod }> = [
|
|
|
|
|
+ { label: 'GET', value: 'get' },
|
|
|
|
|
+ { label: 'POST', value: 'post' },
|
|
|
|
|
+ { label: 'PUT', value: 'put' },
|
|
|
|
|
+ { label: 'DELETE', value: 'delete' },
|
|
|
|
|
+ { label: 'PATCH', value: 'patch' },
|
|
|
|
|
+ { label: 'HEAD', value: 'head' }
|
|
|
|
|
+]
|
|
|
|
|
+
|
|
|
|
|
+const contentTypeOptions: Array<{ label: string; value: WebhookContentType }> = [
|
|
|
|
|
+ { label: 'application/json', value: 'application/json' },
|
|
|
|
|
+ {
|
|
|
|
|
+ label: 'application/x-www-form-urlencoded',
|
|
|
|
|
+ value: 'application/x-www-form-urlencoded'
|
|
|
|
|
+ },
|
|
|
|
|
+ { label: 'text/plain', value: 'text/plain' },
|
|
|
|
|
+ { label: 'application/octet-stream', value: 'application/octet-stream' },
|
|
|
|
|
+ { label: 'multipart/form-data', value: 'multipart/form-data' }
|
|
|
|
|
+]
|
|
|
|
|
+
|
|
|
|
|
+const scalarTypeOptions = VARIABLE_TYPE_OPTIONS.filter((item) =>
|
|
|
|
|
+ ['string', 'number', 'boolean'].includes(item.value)
|
|
|
|
|
+) as Array<{ label: string; value: QueryRow['type'] }>
|
|
|
|
|
+
|
|
|
|
|
+const bodyTypeOptions = VARIABLE_TYPE_OPTIONS.filter((item) =>
|
|
|
|
|
+ [
|
|
|
|
|
+ 'string',
|
|
|
|
|
+ 'number',
|
|
|
|
|
+ 'boolean',
|
|
|
|
|
+ 'object',
|
|
|
|
|
+ 'array[string]',
|
|
|
|
|
+ 'array[number]',
|
|
|
|
|
+ 'array[boolean]',
|
|
|
|
|
+ 'array[object]'
|
|
|
|
|
+ ].includes(item.value)
|
|
|
|
|
+) as Array<{ label: string; value: BodyRow['type'] }>
|
|
|
|
|
+
|
|
|
|
|
+const formData = useSetterModel<WebhookData>(props, emit)
|
|
|
|
|
+const outputsExpanded = ref(true)
|
|
|
|
|
+const queryRows = ref<QueryRow[]>([])
|
|
|
|
|
+const headerRows = ref<HeaderRow[]>([])
|
|
|
|
|
+const bodyRows = ref<BodyRow[]>([])
|
|
|
|
|
+
|
|
|
|
|
+const createEmptyQueryRow = (): QueryRow => ({
|
|
|
|
|
+ name: '',
|
|
|
|
|
+ type: 'string',
|
|
|
|
|
+ required: false
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+const createEmptyHeaderRow = (): HeaderRow => ({
|
|
|
|
|
+ name: '',
|
|
|
|
|
+ required: false
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+const createEmptyBodyRow = (): BodyRow => ({
|
|
|
|
|
+ name: '',
|
|
|
|
|
+ type: 'string',
|
|
|
|
|
+ required: false
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+const defaultOutputs = () => [
|
|
|
|
|
+ {
|
|
|
|
|
+ name: 'payload._webhook_raw',
|
|
|
|
|
+ describe: 'Webhook 原始请求体',
|
|
|
|
|
+ type: 'object' as NodeVariableType
|
|
|
|
|
+ }
|
|
|
|
|
+]
|
|
|
|
|
+
|
|
|
|
|
+const normalizeWebhookState = () => {
|
|
|
|
|
+ if (!formData.value.title) {
|
|
|
|
|
+ formData.value.title = 'Webhook触发'
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!formData.value.type) {
|
|
|
|
|
+ formData.value.type = 'trigger-webhook'
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!formData.value.variables) {
|
|
|
|
|
+ formData.value.variables = []
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!formData.value.retry_config) {
|
|
|
|
|
+ formData.value.retry_config = {
|
|
|
|
|
+ max_retries: 3,
|
|
|
|
|
+ retry_enabled: false,
|
|
|
|
|
+ retry_interval: 100
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!formData.value.error_strategy) {
|
|
|
|
|
+ formData.value.error_strategy = 'none'
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!formData.value.fail_branch_node_id) {
|
|
|
|
|
+ formData.value.fail_branch_node_id = ''
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!formData.value.default_value) {
|
|
|
|
|
+ formData.value.default_value = []
|
|
|
|
|
+ }
|
|
|
|
|
+ if (typeof formData.value.output_can_alter !== 'boolean') {
|
|
|
|
|
+ formData.value.output_can_alter = false
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!Array.isArray(formData.value.outputs) || !formData.value.outputs.length) {
|
|
|
|
|
+ formData.value.outputs = defaultOutputs()
|
|
|
|
|
+ }
|
|
|
|
|
+ if (typeof formData.value.async_mode !== 'boolean') {
|
|
|
|
|
+ formData.value.async_mode = false
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!formData.value.method) {
|
|
|
|
|
+ formData.value.method = 'post'
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!formData.value.content_type) {
|
|
|
|
|
+ formData.value.content_type = 'application/json'
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!formData.value.response_content_type) {
|
|
|
|
|
+ formData.value.response_content_type = 'application/json'
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!Array.isArray(formData.value.headers)) {
|
|
|
|
|
+ formData.value.headers = []
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!Array.isArray(formData.value.params)) {
|
|
|
|
|
+ formData.value.params = []
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!Array.isArray(formData.value.body)) {
|
|
|
|
|
+ formData.value.body = []
|
|
|
|
|
+ }
|
|
|
|
|
+ if (typeof formData.value.response_body !== 'string') {
|
|
|
|
|
+ formData.value.response_body = '{\n "success": true\n}'
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const statusCode = Number(formData.value.response_status_code)
|
|
|
|
|
+ formData.value.response_status_code = Number.isFinite(statusCode)
|
|
|
|
|
+ ? Math.min(599, Math.max(100, Math.trunc(statusCode)))
|
|
|
|
|
+ : 200
|
|
|
|
|
+
|
|
|
|
|
+ formData.value.webhook_url = `${formData.value.webhook_url || ''}`.trim()
|
|
|
|
|
+ formData.value.webhook_debug_url = `${formData.value.webhook_debug_url || ''}`.trim()
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+normalizeWebhookState()
|
|
|
|
|
+
|
|
|
|
|
+watch(
|
|
|
|
|
+ () => props.data,
|
|
|
|
|
+ () => {
|
|
|
|
|
+ normalizeWebhookState()
|
|
|
|
|
+ syncEditorRowsFromFormData()
|
|
|
|
|
+ },
|
|
|
|
|
+ { deep: true }
|
|
|
|
|
+)
|
|
|
|
|
+
|
|
|
|
|
+const syncEditorRowsFromFormData = () => {
|
|
|
|
|
+ const nextQueryRows = formData.value.params.length
|
|
|
|
|
+ ? cloneDeep(formData.value.params)
|
|
|
|
|
+ : [createEmptyQueryRow()]
|
|
|
|
|
+ if (!isEqual(nextQueryRows, queryRows.value)) {
|
|
|
|
|
+ queryRows.value = nextQueryRows
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const nextHeaderRows = formData.value.headers.length
|
|
|
|
|
+ ? cloneDeep(formData.value.headers)
|
|
|
|
|
+ : [createEmptyHeaderRow()]
|
|
|
|
|
+ if (!isEqual(nextHeaderRows, headerRows.value)) {
|
|
|
|
|
+ headerRows.value = nextHeaderRows
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const nextBodyRows = formData.value.body.length
|
|
|
|
|
+ ? cloneDeep(formData.value.body)
|
|
|
|
|
+ : [createEmptyBodyRow()]
|
|
|
|
|
+ if (!isEqual(nextBodyRows, bodyRows.value)) {
|
|
|
|
|
+ bodyRows.value = nextBodyRows
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+watch(
|
|
|
|
|
+ () => [formData.value.params, formData.value.headers, formData.value.body],
|
|
|
|
|
+ () => {
|
|
|
|
|
+ syncEditorRowsFromFormData()
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ deep: true,
|
|
|
|
|
+ immediate: true
|
|
|
|
|
+ }
|
|
|
|
|
+)
|
|
|
|
|
+
|
|
|
|
|
+watch(
|
|
|
|
|
+ () => formData.value.content_type,
|
|
|
|
|
+ (value) => {
|
|
|
|
|
+ formData.value.response_content_type = value
|
|
|
|
|
+ },
|
|
|
|
|
+ { immediate: true }
|
|
|
|
|
+)
|
|
|
|
|
+
|
|
|
|
|
+watch(
|
|
|
|
|
+ queryRows,
|
|
|
|
|
+ (value) => {
|
|
|
|
|
+ const normalized = value
|
|
|
|
|
+ .map((item) => ({
|
|
|
|
|
+ name: item.name.trim(),
|
|
|
|
|
+ type: item.type,
|
|
|
|
|
+ required: Boolean(item.required)
|
|
|
|
|
+ }))
|
|
|
|
|
+ .filter((item) => item.name)
|
|
|
|
|
+
|
|
|
|
|
+ if (!isEqual(normalized, formData.value.params)) {
|
|
|
|
|
+ formData.value.params = normalized
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ { deep: true }
|
|
|
|
|
+)
|
|
|
|
|
+
|
|
|
|
|
+watch(
|
|
|
|
|
+ headerRows,
|
|
|
|
|
+ (value) => {
|
|
|
|
|
+ const normalized = value
|
|
|
|
|
+ .map((item) => ({
|
|
|
|
|
+ name: item.name.trim(),
|
|
|
|
|
+ required: Boolean(item.required)
|
|
|
|
|
+ }))
|
|
|
|
|
+ .filter((item) => item.name)
|
|
|
|
|
+
|
|
|
|
|
+ if (!isEqual(normalized, formData.value.headers)) {
|
|
|
|
|
+ formData.value.headers = normalized
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ { deep: true }
|
|
|
|
|
+)
|
|
|
|
|
+
|
|
|
|
|
+watch(
|
|
|
|
|
+ bodyRows,
|
|
|
|
|
+ (value) => {
|
|
|
|
|
+ const normalized = value
|
|
|
|
|
+ .map((item) => ({
|
|
|
|
|
+ name: item.name.trim(),
|
|
|
|
|
+ type: item.type,
|
|
|
|
|
+ required: Boolean(item.required)
|
|
|
|
|
+ }))
|
|
|
|
|
+ .filter((item) => item.name)
|
|
|
|
|
+
|
|
|
|
|
+ if (!isEqual(normalized, formData.value.body)) {
|
|
|
|
|
+ formData.value.body = normalized
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ { deep: true }
|
|
|
|
|
+)
|
|
|
|
|
+
|
|
|
|
|
+const responseEditorLanguage = computed(() =>
|
|
|
|
|
+ formData.value.response_content_type === 'application/json' ? 'json' : 'javascript'
|
|
|
|
|
+)
|
|
|
|
|
+
|
|
|
|
|
+const ensureExtraQueryRow = (index: number) => {
|
|
|
|
|
+ if (index === queryRows.value.length - 1) {
|
|
|
|
|
+ queryRows.value.push(createEmptyQueryRow())
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const ensureExtraHeaderRow = (index: number) => {
|
|
|
|
|
+ if (index === headerRows.value.length - 1) {
|
|
|
|
|
+ headerRows.value.push(createEmptyHeaderRow())
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const ensureExtraBodyRow = (index: number) => {
|
|
|
|
|
+ if (index === bodyRows.value.length - 1) {
|
|
|
|
|
+ bodyRows.value.push(createEmptyBodyRow())
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const removeQueryRow = (index: number) => {
|
|
|
|
|
+ queryRows.value.splice(index, 1)
|
|
|
|
|
+ if (!queryRows.value.length) {
|
|
|
|
|
+ queryRows.value.push(createEmptyQueryRow())
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const removeHeaderRow = (index: number) => {
|
|
|
|
|
+ headerRows.value.splice(index, 1)
|
|
|
|
|
+ if (!headerRows.value.length) {
|
|
|
|
|
+ headerRows.value.push(createEmptyHeaderRow())
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const removeBodyRow = (index: number) => {
|
|
|
|
|
+ bodyRows.value.splice(index, 1)
|
|
|
|
|
+ if (!bodyRows.value.length) {
|
|
|
|
|
+ bodyRows.value.push(createEmptyBodyRow())
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const copyText = async (value: string, label: string) => {
|
|
|
|
|
+ const text = `${value || ''}`.trim()
|
|
|
|
|
+ if (!text) {
|
|
|
|
|
+ ElMessage.warning(`${label} 为空`)
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ await navigator.clipboard.writeText(text)
|
|
|
|
|
+ ElMessage.success(`${label} 已复制`)
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error(`copy ${label} failed`, error)
|
|
|
|
|
+ ElMessage.error(`${label} 复制失败`)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+</script>
|
|
|
|
|
+
|
|
|
|
|
+<style scoped lang="less">
|
|
|
|
|
+.webhook-setter {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ gap: 20px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.section-block {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ gap: 12px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.section-block--response {
|
|
|
|
|
+ padding-top: 4px;
|
|
|
|
|
+ border-top: 1px solid #eaecf0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.field-title {
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ font-weight: 700;
|
|
|
|
|
+ color: #344054;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.field-title--minor {
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.field-stack {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ gap: 10px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.url-row {
|
|
|
|
|
+ display: grid;
|
|
|
|
|
+ grid-template-columns: 108px minmax(0, 1fr);
|
|
|
|
|
+ gap: 12px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.method-select,
|
|
|
|
|
+.url-input {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.debug-tip {
|
|
|
|
|
+ padding-left: 12px;
|
|
|
|
|
+ border-left: 3px solid #eaecf0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.debug-tip__label {
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: #667085;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.debug-tip__value {
|
|
|
|
|
+ margin-top: 2px;
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ color: #344054;
|
|
|
|
|
+ word-break: break-all;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.table-shell {
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ border: 1px solid #dfe4ea;
|
|
|
|
|
+ background: #fff;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.delete-button {
|
|
|
|
|
+ color: #f04438;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.response-grid {
|
|
|
|
|
+ display: grid;
|
|
|
|
|
+ grid-template-columns: minmax(0, 1fr) 180px;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 16px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.status-input {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.output-toggle {
|
|
|
|
|
+ display: inline-flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 8px;
|
|
|
|
|
+ padding: 0;
|
|
|
|
|
+ border: 0;
|
|
|
|
|
+ background: transparent;
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ font-weight: 700;
|
|
|
|
|
+ color: #344054;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.output-list {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ gap: 10px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.output-item {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 10px;
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ color: #475467;
|
|
|
|
|
+ word-break: break-all;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.output-item__name {
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: #344054;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.output-item__type {
|
|
|
|
|
+ color: #667085;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+:deep(.param-table) {
|
|
|
|
|
+ --el-table-border-color: #eef2f6;
|
|
|
|
|
+ --el-table-header-bg-color: #fff;
|
|
|
|
|
+ --el-table-tr-bg-color: #fff;
|
|
|
|
|
+ --el-table-row-hover-bg-color: #f8fbff;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+:deep(.param-table th.el-table__cell) {
|
|
|
|
|
+ background: #fff;
|
|
|
|
|
+ color: #667085;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+:deep(.param-table .cell) {
|
|
|
|
|
+ padding: 0 8px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+:deep(.param-table .el-table__inner-wrapper::before) {
|
|
|
|
|
+ display: none;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+:deep(.param-table .el-input__wrapper),
|
|
|
|
|
+:deep(.param-table .el-select__wrapper) {
|
|
|
|
|
+ box-shadow: none;
|
|
|
|
|
+}
|
|
|
|
|
+</style>
|