From 897079144c51e759a5b7fe2cde4b1bfd53316f03 Mon Sep 17 00:00:00 2001 From: biybuy <1368735060@qq.com> Date: Tue, 17 Mar 2026 11:32:53 +0800 Subject: [PATCH] 20260317 --- src/components/ApiTest/index.vue | 236 ++++----- src/components/InterfaceDetail/index.vue | 470 +++++++++++++----- .../ServiceProviderInterfaceModal.vue | 7 +- src/views/api/downstreamInterface/index.vue | 37 +- .../components/ApiDebugger.vue | 26 +- .../components/UpstreamInterfaceModal.vue | 7 +- src/views/api/upstreamInterface/index.vue | 67 ++- 7 files changed, 551 insertions(+), 299 deletions(-) diff --git a/src/components/ApiTest/index.vue b/src/components/ApiTest/index.vue index 9fab685..33260ce 100644 --- a/src/components/ApiTest/index.vue +++ b/src/components/ApiTest/index.vue @@ -742,7 +742,7 @@ console.log('ApiTest: 当前映射关系: 1=xml, 2=form-urlencoded, 3=json, 4=form-data'); console.log('ApiTest: 如果不对,请告诉我接口明细显示什么和应该映射到什么!'); } else { - // 如果没有contentType,默认设置为json,因为我们有inParams数据 + // 如果没有contentType,默认设置为json newBodyType = 'json'; console.log('ApiTest: no contentType found, defaulting bodyType to json'); } @@ -766,93 +766,7 @@ }, 100); }); - // 4. 设置请求容器的Demo数据(从接口对象的 inParams 中读取) - console.log('ApiTest: checking for inParams in interface...'); - console.log('ApiTest: newInterface has inParams:', !!newInterface.inParams); - console.log('ApiTest: newInterface.inParams type:', typeof newInterface.inParams); - console.log('ApiTest: current bodyType:', bodyConfig.bodyType); - - if (newInterface.inParams) { - console.log('ApiTest: found inParams in interface, processing...'); - console.log('ApiTest: inParams length:', newInterface.inParams.length); - console.log('ApiTest: inParams preview:', newInterface.inParams.substring(0, 200)); - - // 始终将处理后的inParams数据显示在CodeMirror中(用于显示demo数据) - const processedInParams = processInParamsData(newInterface.inParams); - bodyConfig.body = processedInParams; - console.log('ApiTest: always set bodyConfig.body with processed inParams for display, length:', processedInParams.length); - - // 根据不同的bodyType设置相应的容器数据(用于请求发送) - if (bodyConfig.bodyType === 'x-www-form-urlencoded') { - // 对于x-www-form-urlencoded,将JSON数据转换为表格数据 - console.log('ApiTest: processing for x-www-form-urlencoded bodyType'); - try { - const rawData = newInterface.inParams.replace(/\\r\\n/g, '\n').replace(/\\n/g, '\n').replace(/\\r/g, '\r').replace(/\\t/g, '\t').replace(/\\\\/g, '\\').replace(/\\"/g, '"'); - console.log('ApiTest: processed raw data length:', rawData.length); - - const parsedData = JSON5.parse(rawData); - console.log('ApiTest: parsed JSON successfully'); - - const flattenedData = flattenJsonToKeyValuePairs(parsedData); - console.log('ApiTest: flattened to', flattenedData.length, 'key-value pairs'); - - const tableData = flattenedData.map(item => ({ - id: random(0, 2099999999), - name: item.key, - type: 1, // string类型 - value: String(item.value), - description: item.description || '' - })); - - urlEncodedTableProps.data = tableData; - - console.log('ApiTest: set urlEncoded table data with', urlEncodedTableProps.data.length, 'items'); - console.log('ApiTest: first 3 items:', urlEncodedTableProps.data.slice(0, 3)); - console.log('ApiTest: urlEncodedTableProps.data is reactive:', isReactive(urlEncodedTableProps.data)); - console.log('ApiTest: current urlEncodedTableProps.data:', urlEncodedTableProps.data); - - // 强制更新UI - nextTick(() => { - console.log('ApiTest: nextTick - table data should be updated'); - }); - } catch (error) { - console.warn('ApiTest: failed to parse inParams for urlEncoded:', error); - console.warn('ApiTest: error details:', error instanceof Error ? error.message : String(error)); - } - - } else if (bodyConfig.bodyType === 'form-data') { - // 对于form-data,将JSON数据转换为表格数据 - console.log('ApiTest: processing for form-data bodyType'); - try { - const rawData = newInterface.inParams.replace(/\\r\\n/g, '\n').replace(/\\n/g, '\n').replace(/\\r/g, '\r').replace(/\\t/g, '\t').replace(/\\\\/g, '\\').replace(/\\"/g, '"'); - const parsedData = JSON5.parse(rawData); - const flattenedData = flattenJsonToKeyValuePairs(parsedData); - - formDataTableProps.data = flattenedData.map(item => ({ - id: random(0, 2099999999), - name: item.key, - type: 1, // string类型 - value: String(item.value), - description: item.description || '' - })); - - console.log('ApiTest: set formData table data with', formDataTableProps.data.length, 'items'); - } catch (error) { - console.warn('ApiTest: failed to parse inParams for formData:', error); - } - - } else if (bodyConfig.bodyType === 'raw') { - // 对于raw类型,直接显示处理后的数据 - console.log('ApiTest: processing for raw bodyType'); - bodyConfig.rawBody = processedInParams; - console.log('ApiTest: set bodyConfig.rawBody with processed inParams'); - } else { - console.log('ApiTest: bodyType is json/xml/raw, using CodeMirror display'); - } - } else { - console.log('ApiTest: no inParams found in interface'); - console.log('ApiTest: available fields in newInterface:', Object.keys(newInterface)); - } + // 4. 不再从接口明细的 inParams/curl 示例加载 body 数据,body 仅通过「导入cURL」或「调试」按钮导入的 cURL 的 --data-raw 显示 // 5. 设置 Header 和 Query 的默认值(只在启用默认值且为空时设置) console.log('ApiTest: in watch, enableDefaults:', props.enableDefaults); @@ -958,22 +872,17 @@ } - // 导入 cURL - const importCurl = () => { - // 使用Modal显示输入框让用户输入cURL命令 - const curlInput = ref(''); - const modalVisible = ref(true); - - const parseCurl = (curlCommand: string) => { + // 解析并导入 cURL 命令(可被外部调用) + function parseCurlCommand(curlCommand: string) { try { console.log('Parsing cURL command:', curlCommand); // 清理命令,但保留数据部分的多行结构 let cleanCurl = curlCommand.trim(); - // 先处理多行命令的连接(反斜杠+换行符) - // 注意:要先处理 \r\n,再处理 \n;这里统一用 \r?\n + // 先处理多行命令的连接:Linux/Mac 用 \,Windows 用 ^ cleanCurl = cleanCurl.replace(/\\\s*\r?\n\s*/g, ' '); + cleanCurl = cleanCurl.replace(/\^\s*\r?\n\s*/g, ' '); // 移除多余的空格,但保留数据部分 cleanCurl = cleanCurl.replace(/\s+/g, ' '); @@ -1086,39 +995,52 @@ } } - // 查找--data-raw - const dataRawIndex = cleanCurl.indexOf('--data-raw'); - - if (dataRawIndex !== -1) { - // 找到--data-raw的位置 - const dataPart = cleanCurl.substring(dataRawIndex + '--data-raw'.length).trim(); - console.log('Found --data-raw part:', dataPart.substring(0, 200) + '...'); - - // 检查是否有引号包围 - if (dataPart.startsWith("'") || dataPart.startsWith('"')) { - const quote = dataPart.charAt(0); - const endQuoteIndex = dataPart.indexOf(quote, 1); - if (endQuoteIndex !== -1) { - bodyData = dataPart.substring(1, endQuoteIndex); - console.log('Extracted quoted data-raw:', bodyData.substring(0, 100) + '...'); + // 查找--data-raw(支持双引号/单引号内含转义 \" 的 JSON,逐字符解析确保正确匹配结束引号) + function extractDataRawValue(str: string): string | null { + const idx = str.indexOf('--data-raw'); + if (idx === -1) return null; + let part = str.substring(idx + '--data-raw'.length).trim(); + if (!part) return null; + if (part.startsWith('"')) { + let result = ''; + for (let i = 1; i < part.length; i++) { + if (part[i] === '\\' && i + 1 < part.length) { + result += part[i] + part[i + 1]; + i++; + continue; + } + if (part[i] === '"') return result; + result += part[i]; } - } else { - // 没有引号,找到下一个--或行尾 - const nextFlagIndex = dataPart.search(/\s--/); - if (nextFlagIndex !== -1) { - bodyData = dataPart.substring(0, nextFlagIndex).trim(); - } else { - bodyData = dataPart.trim(); - } - console.log('Extracted unquoted data-raw:', bodyData.substring(0, 100) + '...'); + return null; } - } else { - // 检查是否有--data + if (part.startsWith("'")) { + let result = ''; + for (let i = 1; i < part.length; i++) { + if (part[i] === '\\' && i + 1 < part.length) { + result += part[i] + part[i + 1]; + i++; + continue; + } + if (part[i] === "'") return result; + result += part[i]; + } + return null; + } + const nextFlag = part.search(/\s--/); + return (nextFlag !== -1 ? part.substring(0, nextFlag) : part).trim() || null; + } + const extracted = extractDataRawValue(cleanCurl); + if (extracted !== null) { + bodyData = extracted.replace(/\\"/g, '"'); + console.log('Extracted data-raw:', bodyData.substring(0, 100) + '...'); + } + + // 若仍未解析到 body,尝试 --data(非 --data-raw) + if (!bodyData && !cleanCurl.includes('--data-raw')) { const dataIndex = cleanCurl.indexOf('--data'); if (dataIndex !== -1) { const dataPart = cleanCurl.substring(dataIndex + '--data'.length).trim(); - console.log('Found --data part:', dataPart.substring(0, 50) + '...'); - if (dataPart.startsWith("'") || dataPart.startsWith('"')) { const quote = dataPart.charAt(0); const endQuoteIndex = dataPart.indexOf(quote, 1); @@ -1126,7 +1048,6 @@ bodyData = dataPart.substring(1, endQuoteIndex); } } - console.log('Extracted data:', bodyData.substring(0, 100) + '...'); } } @@ -1175,15 +1096,48 @@ urlEncodedTableProps.data = formData; console.log('Set as form-urlencoded from --data-urlencode with', formData.length, 'params'); } else if (bodyData) { - // 根据Content-Type设置bodyType + // 根据 Content-Type 或内容推断 bodyType + let inferredType: 'json' | 'xml' | 'x-www-form-urlencoded' | 'form-data' | 'raw' = 'raw'; if (contentType.includes('application/json')) { - bodyConfig.bodyType = 'json'; - // 对JSON数据进行格式化 - bodyConfig.body = processInParamsData(bodyData); - console.log('Set as formatted JSON body'); + inferredType = 'json'; + } else if (contentType.includes('application/xml') || contentType.includes('text/xml')) { + inferredType = 'xml'; } else if (contentType.includes('application/x-www-form-urlencoded')) { + inferredType = 'x-www-form-urlencoded'; + } else if (contentType.includes('multipart/form-data')) { + inferredType = 'form-data'; + } else { + // 无 Content-Type 时根据内容推断 + const trimmed = bodyData.trim(); + if (trimmed.startsWith('{') || trimmed.startsWith('[')) { + try { + JSON5.parse(trimmed); + inferredType = 'json'; + } catch { + inferredType = 'raw'; + } + } else if (trimmed.startsWith(']/.test(trimmed))) { + inferredType = 'xml'; + } else if (trimmed.includes('=') && !trimmed.startsWith('<')) { + try { + const params = new URLSearchParams(trimmed); + if (params.toString()) inferredType = 'x-www-form-urlencoded'; + } catch { + /* keep raw */ + } + } + } + + if (inferredType === 'json') { + bodyConfig.bodyType = 'json'; + bodyConfig.body = processInParamsData(bodyData); + console.log('Set as JSON body (from Content-Type or inferred)'); + } else if (inferredType === 'xml') { + bodyConfig.bodyType = 'xml'; + bodyConfig.body = bodyData; + console.log('Set as XML body (from Content-Type or inferred)'); + } else if (inferredType === 'x-www-form-urlencoded') { bodyConfig.bodyType = 'x-www-form-urlencoded'; - // 解析form data try { const formData: any[] = []; const params = new URLSearchParams(bodyData); @@ -1203,7 +1157,7 @@ bodyConfig.bodyType = 'raw'; bodyConfig.rawBody = bodyData; } - } else if (contentType.includes('multipart/form-data')) { + } else if (inferredType === 'form-data') { bodyConfig.bodyType = 'form-data'; bodyConfig.body = bodyData; console.log('Set as form-data'); @@ -1236,10 +1190,12 @@ console.error('cURL parsing error:', error); Message.error(`cURL命令解析失败: ${error instanceof Error ? error.message : String(error)}`); } - }; + } - // 显示Modal - const modal = Modal.confirm({ + // 导入 cURL(打开 Modal 让用户粘贴) + const importCurl = () => { + const curlInput = ref(''); + Modal.confirm({ title: '导入 cURL 命令', content: () => h('div', [ h('p', { style: 'margin-bottom: 16px' }, '请粘贴完整的 cURL 命令:'), @@ -1269,7 +1225,7 @@ Message.warning('请输入 cURL 命令'); return false; } - parseCurl(curlInput.value.trim()); + parseCurlCommand(curlInput.value.trim()); return true; }, onCancel: () => { @@ -1633,6 +1589,12 @@ }); return formatted.substring(1, formatted.length - 3); } + + defineExpose({ + importCurlFromString: (curl: string) => { + if (curl && curl.trim()) parseCurlCommand(curl.trim()); + }, + }); diff --git a/src/views/api/downstreamInterface/index.vue b/src/views/api/downstreamInterface/index.vue index 0f14cbb..4bb2a93 100644 --- a/src/views/api/downstreamInterface/index.vue +++ b/src/views/api/downstreamInterface/index.vue @@ -1452,6 +1452,25 @@ } } + // 点击调试 / 切换 API 调试 tab:获取请求容器的 cURL 并导入到 API 调试 + const curlToImport = ref(''); + function importCurlToDebugger() { + const curl = interfaceDetailModalRef.value?.getRequestExample?.() || ''; + if (curl && curl.trim()) { + curlToImport.value = curl; + nextTick(() => { + setTimeout(() => { curlToImport.value = ''; }, 500); + }); + } + } + function handleDebugClick() { + interfaceDetailTab.value = 'debug'; + } + // 切换至 API 调试 tab 时也解析 cURL + watch(interfaceDetailTab, (tab) => { + if (tab === 'debug') importCurlToDebugger(); + }); + onMounted(() => { getAppList(); loadServiceProviderList(); @@ -1617,11 +1636,7 @@ {{ t('system.serviceProviders.detail.tab.debug') }} -
- - {{ t('system.serviceProviders.detail.actions.save') }} - -
+
@@ -1634,7 +1649,16 @@ :key="`interface-detail-${selectedInterface?.id}-${detailRefreshKey}`" ref="interfaceDetailModalRef" @saved="handleInterfaceSaved" - /> + > + + @@ -1644,6 +1668,7 @@ :selected-interface="selectedInterface" :selected-service-provider="selectedProvider" :enable-defaults="false" + :curl-to-import="curlToImport" /> diff --git a/src/views/api/upstreamInterface/components/ApiDebugger.vue b/src/views/api/upstreamInterface/components/ApiDebugger.vue index 682fe07..c966b56 100644 --- a/src/views/api/upstreamInterface/components/ApiDebugger.vue +++ b/src/views/api/upstreamInterface/components/ApiDebugger.vue @@ -1,6 +1,7 @@