20260317
This commit is contained in:
parent
1eaec68f92
commit
897079144c
@ -742,7 +742,7 @@
|
|||||||
console.log('ApiTest: 当前映射关系: 1=xml, 2=form-urlencoded, 3=json, 4=form-data');
|
console.log('ApiTest: 当前映射关系: 1=xml, 2=form-urlencoded, 3=json, 4=form-data');
|
||||||
console.log('ApiTest: 如果不对,请告诉我接口明细显示什么和应该映射到什么!');
|
console.log('ApiTest: 如果不对,请告诉我接口明细显示什么和应该映射到什么!');
|
||||||
} else {
|
} else {
|
||||||
// 如果没有contentType,默认设置为json,因为我们有inParams数据
|
// 如果没有contentType,默认设置为json
|
||||||
newBodyType = 'json';
|
newBodyType = 'json';
|
||||||
console.log('ApiTest: no contentType found, defaulting bodyType to json');
|
console.log('ApiTest: no contentType found, defaulting bodyType to json');
|
||||||
}
|
}
|
||||||
@ -766,93 +766,7 @@
|
|||||||
}, 100);
|
}, 100);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 4. 设置请求容器的Demo数据(从接口对象的 inParams 中读取)
|
// 4. 不再从接口明细的 inParams/curl 示例加载 body 数据,body 仅通过「导入cURL」或「调试」按钮导入的 cURL 的 --data-raw 显示
|
||||||
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));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. 设置 Header 和 Query 的默认值(只在启用默认值且为空时设置)
|
// 5. 设置 Header 和 Query 的默认值(只在启用默认值且为空时设置)
|
||||||
console.log('ApiTest: in watch, enableDefaults:', props.enableDefaults);
|
console.log('ApiTest: in watch, enableDefaults:', props.enableDefaults);
|
||||||
@ -958,22 +872,17 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// 导入 cURL
|
// 解析并导入 cURL 命令(可被外部调用)
|
||||||
const importCurl = () => {
|
function parseCurlCommand(curlCommand: string) {
|
||||||
// 使用Modal显示输入框让用户输入cURL命令
|
|
||||||
const curlInput = ref('');
|
|
||||||
const modalVisible = ref(true);
|
|
||||||
|
|
||||||
const parseCurl = (curlCommand: string) => {
|
|
||||||
try {
|
try {
|
||||||
console.log('Parsing cURL command:', curlCommand);
|
console.log('Parsing cURL command:', curlCommand);
|
||||||
|
|
||||||
// 清理命令,但保留数据部分的多行结构
|
// 清理命令,但保留数据部分的多行结构
|
||||||
let cleanCurl = curlCommand.trim();
|
let cleanCurl = curlCommand.trim();
|
||||||
|
|
||||||
// 先处理多行命令的连接(反斜杠+换行符)
|
// 先处理多行命令的连接:Linux/Mac 用 \,Windows 用 ^
|
||||||
// 注意:要先处理 \r\n,再处理 \n;这里统一用 \r?\n
|
|
||||||
cleanCurl = cleanCurl.replace(/\\\s*\r?\n\s*/g, ' ');
|
cleanCurl = cleanCurl.replace(/\\\s*\r?\n\s*/g, ' ');
|
||||||
|
cleanCurl = cleanCurl.replace(/\^\s*\r?\n\s*/g, ' ');
|
||||||
|
|
||||||
// 移除多余的空格,但保留数据部分
|
// 移除多余的空格,但保留数据部分
|
||||||
cleanCurl = cleanCurl.replace(/\s+/g, ' ');
|
cleanCurl = cleanCurl.replace(/\s+/g, ' ');
|
||||||
@ -1086,39 +995,52 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 查找--data-raw
|
// 查找--data-raw(支持双引号/单引号内含转义 \" 的 JSON,逐字符解析确保正确匹配结束引号)
|
||||||
const dataRawIndex = cleanCurl.indexOf('--data-raw');
|
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];
|
||||||
|
}
|
||||||
|
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];
|
||||||
|
}
|
||||||
|
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) + '...');
|
||||||
|
}
|
||||||
|
|
||||||
if (dataRawIndex !== -1) {
|
// 若仍未解析到 body,尝试 --data(非 --data-raw)
|
||||||
// 找到--data-raw的位置
|
if (!bodyData && !cleanCurl.includes('--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) + '...');
|
|
||||||
}
|
|
||||||
} 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) + '...');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 检查是否有--data
|
|
||||||
const dataIndex = cleanCurl.indexOf('--data');
|
const dataIndex = cleanCurl.indexOf('--data');
|
||||||
if (dataIndex !== -1) {
|
if (dataIndex !== -1) {
|
||||||
const dataPart = cleanCurl.substring(dataIndex + '--data'.length).trim();
|
const dataPart = cleanCurl.substring(dataIndex + '--data'.length).trim();
|
||||||
console.log('Found --data part:', dataPart.substring(0, 50) + '...');
|
|
||||||
|
|
||||||
if (dataPart.startsWith("'") || dataPart.startsWith('"')) {
|
if (dataPart.startsWith("'") || dataPart.startsWith('"')) {
|
||||||
const quote = dataPart.charAt(0);
|
const quote = dataPart.charAt(0);
|
||||||
const endQuoteIndex = dataPart.indexOf(quote, 1);
|
const endQuoteIndex = dataPart.indexOf(quote, 1);
|
||||||
@ -1126,7 +1048,6 @@
|
|||||||
bodyData = dataPart.substring(1, endQuoteIndex);
|
bodyData = dataPart.substring(1, endQuoteIndex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.log('Extracted data:', bodyData.substring(0, 100) + '...');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1175,15 +1096,48 @@
|
|||||||
urlEncodedTableProps.data = formData;
|
urlEncodedTableProps.data = formData;
|
||||||
console.log('Set as form-urlencoded from --data-urlencode with', formData.length, 'params');
|
console.log('Set as form-urlencoded from --data-urlencode with', formData.length, 'params');
|
||||||
} else if (bodyData) {
|
} 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')) {
|
if (contentType.includes('application/json')) {
|
||||||
bodyConfig.bodyType = 'json';
|
inferredType = 'json';
|
||||||
// 对JSON数据进行格式化
|
} else if (contentType.includes('application/xml') || contentType.includes('text/xml')) {
|
||||||
bodyConfig.body = processInParamsData(bodyData);
|
inferredType = 'xml';
|
||||||
console.log('Set as formatted JSON body');
|
|
||||||
} else if (contentType.includes('application/x-www-form-urlencoded')) {
|
} 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('<?xml') || (trimmed.startsWith('<') && /^<[\w-]+[\s>]/.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';
|
bodyConfig.bodyType = 'x-www-form-urlencoded';
|
||||||
// 解析form data
|
|
||||||
try {
|
try {
|
||||||
const formData: any[] = [];
|
const formData: any[] = [];
|
||||||
const params = new URLSearchParams(bodyData);
|
const params = new URLSearchParams(bodyData);
|
||||||
@ -1203,7 +1157,7 @@
|
|||||||
bodyConfig.bodyType = 'raw';
|
bodyConfig.bodyType = 'raw';
|
||||||
bodyConfig.rawBody = bodyData;
|
bodyConfig.rawBody = bodyData;
|
||||||
}
|
}
|
||||||
} else if (contentType.includes('multipart/form-data')) {
|
} else if (inferredType === 'form-data') {
|
||||||
bodyConfig.bodyType = 'form-data';
|
bodyConfig.bodyType = 'form-data';
|
||||||
bodyConfig.body = bodyData;
|
bodyConfig.body = bodyData;
|
||||||
console.log('Set as form-data');
|
console.log('Set as form-data');
|
||||||
@ -1236,10 +1190,12 @@
|
|||||||
console.error('cURL parsing error:', error);
|
console.error('cURL parsing error:', error);
|
||||||
Message.error(`cURL命令解析失败: ${error instanceof Error ? error.message : String(error)}`);
|
Message.error(`cURL命令解析失败: ${error instanceof Error ? error.message : String(error)}`);
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
// 显示Modal
|
// 导入 cURL(打开 Modal 让用户粘贴)
|
||||||
const modal = Modal.confirm({
|
const importCurl = () => {
|
||||||
|
const curlInput = ref('');
|
||||||
|
Modal.confirm({
|
||||||
title: '导入 cURL 命令',
|
title: '导入 cURL 命令',
|
||||||
content: () => h('div', [
|
content: () => h('div', [
|
||||||
h('p', { style: 'margin-bottom: 16px' }, '请粘贴完整的 cURL 命令:'),
|
h('p', { style: 'margin-bottom: 16px' }, '请粘贴完整的 cURL 命令:'),
|
||||||
@ -1269,7 +1225,7 @@
|
|||||||
Message.warning('请输入 cURL 命令');
|
Message.warning('请输入 cURL 命令');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
parseCurl(curlInput.value.trim());
|
parseCurlCommand(curlInput.value.trim());
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
onCancel: () => {
|
onCancel: () => {
|
||||||
@ -1633,6 +1589,12 @@
|
|||||||
});
|
});
|
||||||
return formatted.substring(1, formatted.length - 3);
|
return formatted.substring(1, formatted.length - 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
importCurlFromString: (curl: string) => {
|
||||||
|
if (curl && curl.trim()) parseCurlCommand(curl.trim());
|
||||||
|
},
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, reactive, watch, nextTick, onMounted, onUnmounted, computed } from 'vue';
|
import { ref, reactive, watch, nextTick, onMounted, onUnmounted, computed } from 'vue';
|
||||||
import { IconDragArrow, IconPlus, IconDelete } from '@arco-design/web-vue/es/icon';
|
import { IconDragArrow, IconPlus, IconDelete, IconDown } from '@arco-design/web-vue/es/icon';
|
||||||
import FormPro from '@/components/FormPro/index.vue';
|
import FormPro from '@/components/FormPro/index.vue';
|
||||||
import TablePro from '@/components/TablePro/index.vue';
|
import TablePro from '@/components/TablePro/index.vue';
|
||||||
import CodeMirror from '@/components/CodeMirror/index.vue';
|
import CodeMirror from '@/components/CodeMirror/index.vue';
|
||||||
@ -18,6 +18,8 @@
|
|||||||
import { postParameterParse, postParameterValidateParameter } from '@/api/ParameterServices/parameter';
|
import { postParameterParse, postParameterValidateParameter } from '@/api/ParameterServices/parameter';
|
||||||
import { $baseMessage } from '@/utils';
|
import { $baseMessage } from '@/utils';
|
||||||
import JSON5 from 'json5';
|
import JSON5 from 'json5';
|
||||||
|
import SelectPro from '@/components/SelectPro/index.vue';
|
||||||
|
import { getMethodText } from '@/views/api/upstreamInterface/utils';
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
@ -103,11 +105,8 @@
|
|||||||
let requestSortable: any = null;
|
let requestSortable: any = null;
|
||||||
let responseSortable: any = null;
|
let responseSortable: any = null;
|
||||||
|
|
||||||
// Query tab 切换
|
// Request tab 切换(含 Query、固定配置、Body、cURL)
|
||||||
const queryTab = ref<'body' | 'fixed'>('body');
|
const requestTab = ref<'query' | 'fixed' | 'body' | 'demo'>('body');
|
||||||
|
|
||||||
// Request tab 切换
|
|
||||||
const requestTab = ref<'body' | 'demo'>('body');
|
|
||||||
const requestExample = ref<string>(''); // Demo内容
|
const requestExample = ref<string>(''); // Demo内容
|
||||||
const requestExampleEditorRef = ref<any>(); // Demo编辑器引用
|
const requestExampleEditorRef = ref<any>(); // Demo编辑器引用
|
||||||
|
|
||||||
@ -146,18 +145,51 @@
|
|||||||
const contentTypeOptions = ref<any[]>([]);
|
const contentTypeOptions = ref<any[]>([]);
|
||||||
const responseContentTypeOptions = ref<any[]>([]);
|
const responseContentTypeOptions = ref<any[]>([]);
|
||||||
const mappedParamTypeOptions = ref<any[]>([]);
|
const mappedParamTypeOptions = ref<any[]>([]);
|
||||||
|
const requestMethodOptions = ref<any[]>([]);
|
||||||
|
|
||||||
// 过滤后的基本表单配置(排除 contentType、responseContentType、mappedParamType,当 showContentTypeSelectors 时由上方栏渲染)
|
// 只读模式下显示的请求方式文本
|
||||||
|
const displayRequestMethod = computed(() => {
|
||||||
|
const method = (props.basicParams as any)?.requestMethod;
|
||||||
|
return method !== undefined && method !== null ? getMethodText(method) : '';
|
||||||
|
});
|
||||||
|
|
||||||
|
// 过滤后的基本表单配置(排除 requestMethod、requestAddress 由上方 header 渲染;排除 contentType、responseContentType、mappedParamType,当 showContentTypeSelectors 时由上方栏渲染)
|
||||||
const filteredBasicFormItems = computed(() => {
|
const filteredBasicFormItems = computed(() => {
|
||||||
if (!props.showContentTypeSelectors) return props.basicFormItems;
|
|
||||||
return (props.basicFormItems || []).filter(
|
return (props.basicFormItems || []).filter(
|
||||||
(item: any) =>
|
(item: any) =>
|
||||||
item?.field !== 'contentType' &&
|
item?.field !== 'requestMethod' &&
|
||||||
item?.field !== 'responseContentType' &&
|
item?.field !== 'requestAddress' &&
|
||||||
item?.field !== 'mappedParamType'
|
(item?.field !== 'contentType' || !props.showContentTypeSelectors) &&
|
||||||
|
(item?.field !== 'responseContentType' || !props.showContentTypeSelectors) &&
|
||||||
|
(item?.field !== 'mappedParamType' || !props.showContentTypeSelectors)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 基本参数表单项(强制 span=4,使每行放 6 个输入框)
|
||||||
|
const basicFormItemsForLayout = computed(() => {
|
||||||
|
return filteredBasicFormItems.value.map((item: any) => ({
|
||||||
|
...item,
|
||||||
|
span: 4,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
// 只读模式下基本参数显示(排除 requestMethod、requestAddress,由上方 header 渲染)
|
||||||
|
const basicParamsForDisplay = computed(() => {
|
||||||
|
const params = props.basicParams || {};
|
||||||
|
const { requestMethod, requestAddress, ...rest } = params as Record<string, any>;
|
||||||
|
return rest;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 加载请求方式选项(用于 header 中的请求方式下拉)
|
||||||
|
async function loadRequestMethodOptions() {
|
||||||
|
try {
|
||||||
|
const res = await getDictionaryGetList({ type: 'RequestMethod' });
|
||||||
|
requestMethodOptions.value = Array.isArray(res) ? res : ((res as any)?.data || []);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('加载请求方式选项失败:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 加载请求类型、返回类型、根节点选项
|
// 加载请求类型、返回类型、根节点选项
|
||||||
async function loadContentTypeOptions() {
|
async function loadContentTypeOptions() {
|
||||||
if (!props.showContentTypeSelectors) return;
|
if (!props.showContentTypeSelectors) return;
|
||||||
@ -193,12 +225,32 @@
|
|||||||
return typeTagColors[index % typeTagColors.length];
|
return typeTagColors[index % typeTagColors.length];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 请求/返回类型标签样式类(每个类型独立颜色)
|
||||||
|
const contentTypeColorClasses = ['content-type-json', 'content-type-xml', 'content-type-form', 'content-type-raw', 'content-type-purple', 'content-type-cyan', 'content-type-pink'];
|
||||||
|
function getContentTypeTagClass(value: any, index: number = 0) {
|
||||||
|
const v = (value || '').toString().toLowerCase();
|
||||||
|
if (v.includes('json')) return 'content-type-json';
|
||||||
|
if (v.includes('xml')) return 'content-type-xml';
|
||||||
|
if (v.includes('form') || v.includes('urlencoded')) return 'content-type-form';
|
||||||
|
if (v.includes('raw')) return 'content-type-raw';
|
||||||
|
return contentTypeColorClasses[4 + (index % 3)]; // purple, cyan, pink 循环
|
||||||
|
}
|
||||||
|
|
||||||
// 根据参数类型 key 获取颜色(用于参数类型列)
|
// 根据参数类型 key 获取颜色(用于参数类型列)
|
||||||
function getParameterTypeColor(typeKey: any) {
|
function getParameterTypeColor(typeKey: any) {
|
||||||
const idx = ParameterTypeList.value.findIndex((item: any) => item.key === typeKey);
|
const idx = ParameterTypeList.value.findIndex((item: any) => item.key === typeKey);
|
||||||
return idx >= 0 ? getTypeTagColor(idx) : 'gray';
|
return idx >= 0 ? getTypeTagColor(idx) : 'gray';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 根节点选中项的显示信息(用于下拉框选中后显示带颜色的标签)
|
||||||
|
const selectedMappedParamOption = computed(() => {
|
||||||
|
const key = props.basicFormData?.mappedParamType;
|
||||||
|
if (key === undefined || key === null) return null;
|
||||||
|
const idx = mappedParamTypeOptions.value.findIndex((item: any) => item.key === key);
|
||||||
|
const opt = mappedParamTypeOptions.value[idx];
|
||||||
|
return opt ? { label: opt.value, color: getTypeTagColor(idx) } : null;
|
||||||
|
});
|
||||||
|
|
||||||
// Query 表格配置(使用下游接口的配置方式)
|
// Query 表格配置(使用下游接口的配置方式)
|
||||||
const queryTableProps = reactive<any>({
|
const queryTableProps = reactive<any>({
|
||||||
columns: [
|
columns: [
|
||||||
@ -255,7 +307,7 @@
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Query 固定配置表格
|
// Query 固定配置表格(不传 height,随内容自然增高,与 Query 列表的固定高度区分)
|
||||||
const queryFixedTableProps = computed(() => ({
|
const queryFixedTableProps = computed(() => ({
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
@ -289,7 +341,7 @@
|
|||||||
showToolbar: false,
|
showToolbar: false,
|
||||||
keepSource: true,
|
keepSource: true,
|
||||||
tableConfig: {
|
tableConfig: {
|
||||||
height: '100%',
|
// 不传 height,vxe-table 的 height:'auto' 是铺满父容器,会固定高度;不传则随内容增高
|
||||||
data: fixedParams.value,
|
data: fixedParams.value,
|
||||||
editConfig: {
|
editConfig: {
|
||||||
trigger: 'click',
|
trigger: 'click',
|
||||||
@ -301,9 +353,7 @@
|
|||||||
showIndex: false,
|
showIndex: false,
|
||||||
useForm: false,
|
useForm: false,
|
||||||
size: 'small',
|
size: 'small',
|
||||||
minHeight: 150,
|
|
||||||
autoHeight: false,
|
autoHeight: false,
|
||||||
height: '100%',
|
|
||||||
editRules: {},
|
editRules: {},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -482,7 +532,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 监听Query tab切换,检查是否有未保存的更改
|
// 监听Query tab切换,检查是否有未保存的更改
|
||||||
watch(queryTab, async (newTab, oldTab) => {
|
watch(requestTab, async (newTab, oldTab) => {
|
||||||
// 如果从固定配置tab切换到其他tab,检查是否有未保存的更改
|
// 如果从固定配置tab切换到其他tab,检查是否有未保存的更改
|
||||||
if (oldTab === 'fixed' && newTab !== 'fixed') {
|
if (oldTab === 'fixed' && newTab !== 'fixed') {
|
||||||
if (checkFixedConfigChanges()) {
|
if (checkFixedConfigChanges()) {
|
||||||
@ -509,7 +559,7 @@
|
|||||||
|
|
||||||
if (!result) {
|
if (!result) {
|
||||||
// 如果保存失败,回到原来的tab
|
// 如果保存失败,回到原来的tab
|
||||||
queryTab.value = oldTab;
|
requestTab.value = oldTab;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -521,6 +571,13 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 当 showQuery 为 false 时,若当前在 query/fixed tab,切回 body
|
||||||
|
watch(() => props.showQuery, (val) => {
|
||||||
|
if (!val && (requestTab.value === 'query' || requestTab.value === 'fixed')) {
|
||||||
|
requestTab.value = 'body';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// 监听 requestParams prop 变化,同步到表格数据
|
// 监听 requestParams prop 变化,同步到表格数据
|
||||||
watch(
|
watch(
|
||||||
() => props.requestParams,
|
() => props.requestParams,
|
||||||
@ -753,8 +810,8 @@
|
|||||||
|
|
||||||
// 固定配置:新增
|
// 固定配置:新增
|
||||||
async function addFixedParam() {
|
async function addFixedParam() {
|
||||||
if (queryTab.value !== 'fixed') {
|
if (requestTab.value !== 'fixed') {
|
||||||
queryTab.value = 'fixed';
|
requestTab.value = 'fixed';
|
||||||
await nextTick();
|
await nextTick();
|
||||||
}
|
}
|
||||||
const record = {
|
const record = {
|
||||||
@ -1092,6 +1149,12 @@
|
|||||||
return String(inParams);
|
return String(inParams);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const trimmed = inParams.trim();
|
||||||
|
// 非 JSON 内容(如 cURL、XML 等)直接返回,不尝试解析
|
||||||
|
if (!trimmed.startsWith('{') && !trimmed.startsWith('[')) {
|
||||||
|
return inParams;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 步骤1: 处理转义字符 - 将字面量的转义序列转换为实际字符
|
// 步骤1: 处理转义字符 - 将字面量的转义序列转换为实际字符
|
||||||
let processed = inParams;
|
let processed = inParams;
|
||||||
@ -1154,6 +1217,7 @@
|
|||||||
if (props.showContentTypeSelectors) {
|
if (props.showContentTypeSelectors) {
|
||||||
loadContentTypeOptions();
|
loadContentTypeOptions();
|
||||||
}
|
}
|
||||||
|
loadRequestMethodOptions();
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
@ -1720,9 +1784,45 @@
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="interface-detail">
|
<div class="interface-detail">
|
||||||
|
<!-- 请求方式 + 请求地址(独立容器,参考 ApiTest api-test-header) -->
|
||||||
|
<div class="api-test-header">
|
||||||
|
<a-select
|
||||||
|
v-if="useFormEdit"
|
||||||
|
:model-value="basicFormData?.requestMethod"
|
||||||
|
:class="['method-select', `method-${(getMethodText(basicFormData?.requestMethod) || 'get').toLowerCase()}`]"
|
||||||
|
placeholder="请求方法"
|
||||||
|
:style="{ width: '120px' }"
|
||||||
|
allow-clear
|
||||||
|
@update:model-value="emit('update:basicFormData', { ...basicFormData, requestMethod: $event })"
|
||||||
|
>
|
||||||
|
<a-option
|
||||||
|
v-for="opt in requestMethodOptions"
|
||||||
|
:key="opt.key"
|
||||||
|
:value="opt.key"
|
||||||
|
:class="`method-${(opt.value || '').toLowerCase()}`"
|
||||||
|
>
|
||||||
|
{{ opt.value }}
|
||||||
|
</a-option>
|
||||||
|
</a-select>
|
||||||
|
<span v-else class="method-badge" :class="`method-${(displayRequestMethod || 'get').toLowerCase()}`">
|
||||||
|
{{ displayRequestMethod || '-' }}
|
||||||
|
</span>
|
||||||
|
<a-input
|
||||||
|
v-if="useFormEdit"
|
||||||
|
:model-value="basicFormData?.requestAddress"
|
||||||
|
placeholder="接口路径"
|
||||||
|
class="url-input"
|
||||||
|
@update:model-value="emit('update:basicFormData', { ...basicFormData, requestAddress: $event })"
|
||||||
|
/>
|
||||||
|
<span v-else class="url-text">{{ (basicParams as any)?.requestAddress || '-' }}</span>
|
||||||
|
<div v-if="$slots.headerActions" class="header-actions">
|
||||||
|
<slot name="headerActions" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="detail-grid">
|
<div class="detail-grid">
|
||||||
<!-- 左上:基本参数 -->
|
<!-- 顶部:基本参数(一行) -->
|
||||||
<div class="detail-section section-basic">
|
<div class="detail-section section-basic section-basic-fullwidth">
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
<span class="section-title">基本参数</span>
|
<span class="section-title">基本参数</span>
|
||||||
<div class="section-actions"></div>
|
<div class="section-actions"></div>
|
||||||
@ -1738,7 +1838,7 @@
|
|||||||
:default-span="8"
|
:default-span="8"
|
||||||
:row-gutter="16"
|
:row-gutter="16"
|
||||||
:form-layout="'vertical'"
|
:form-layout="'vertical'"
|
||||||
:item-list="filteredBasicFormItems"
|
:item-list="basicFormItemsForLayout"
|
||||||
:required-position="'none'"
|
:required-position="'none'"
|
||||||
:rules="basicFormRules"
|
:rules="basicFormRules"
|
||||||
:hide-colon="false"
|
:hide-colon="false"
|
||||||
@ -1747,11 +1847,11 @@
|
|||||||
/>
|
/>
|
||||||
<!-- 只读显示模式 -->
|
<!-- 只读显示模式 -->
|
||||||
<div v-else class="basic-form">
|
<div v-else class="basic-form">
|
||||||
<div class="form-item" v-for="(value, key) in basicParams" :key="key">
|
<div class="form-item" v-for="(value, key) in basicParamsForDisplay" :key="key">
|
||||||
<div class="form-label">{{ key }}</div>
|
<div class="form-label">{{ key }}</div>
|
||||||
<div class="form-value">{{ value || '单行输入' }}</div>
|
<div class="form-value">{{ value || '单行输入' }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="Object.keys(basicParams).length === 0" class="empty-placeholder">
|
<div v-if="Object.keys(basicParamsForDisplay).length === 0" class="empty-placeholder">
|
||||||
<div class="form-item" v-for="i in 6" :key="i">
|
<div class="form-item" v-for="i in 6" :key="i">
|
||||||
<div class="form-label">字段{{ i }}</div>
|
<div class="form-label">字段{{ i }}</div>
|
||||||
<div class="form-value">单行输入</div>
|
<div class="form-value">单行输入</div>
|
||||||
@ -1761,20 +1861,22 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 右上:Query -->
|
<!-- 左下:Request(含 Query、固定配置、Body、cURL) -->
|
||||||
<div class="detail-section section-query" :class="{ 'section-query-hidden': !showQuery }">
|
<div class="detail-section section-request" :class="{ 'section-request-fixed-tab': requestTab === 'fixed' }">
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
<a-tabs v-model:active-key="queryTab" type="line" size="mini" direction="horizontal" class="request-header-tabs">
|
<a-tabs v-model:active-key="requestTab" type="line" size="mini" direction="horizontal" class="request-header-tabs">
|
||||||
<a-tab-pane key="body" title="Query" />
|
<a-tab-pane v-if="showQuery" key="query" title="Query" />
|
||||||
<a-tab-pane key="fixed" title="固定配置" />
|
<a-tab-pane v-if="showQuery" key="fixed" title="固定配置" />
|
||||||
|
<a-tab-pane key="body" title="Body" />
|
||||||
|
<a-tab-pane key="demo" title="cURL" />
|
||||||
</a-tabs>
|
</a-tabs>
|
||||||
<div class="section-actions">
|
<div class="section-actions">
|
||||||
<template v-if="queryTab === 'body'">
|
<template v-if="requestTab === 'query'">
|
||||||
<a-button size="small" status="success" @click="addNewQueryParam">
|
<a-button size="small" status="success" @click="addNewQueryParam">
|
||||||
新增
|
新增
|
||||||
</a-button>
|
</a-button>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else-if="requestTab === 'fixed'">
|
||||||
<a-button size="small" status="success" @click="addFixedParam">
|
<a-button size="small" status="success" @click="addFixedParam">
|
||||||
新增
|
新增
|
||||||
</a-button>
|
</a-button>
|
||||||
@ -1782,10 +1884,57 @@
|
|||||||
保存
|
保存
|
||||||
</a-button>
|
</a-button>
|
||||||
</template>
|
</template>
|
||||||
|
<template v-else-if="requestTab === 'body'">
|
||||||
|
<a-button size="small" status="success" @click="addNewRequestParam">
|
||||||
|
新增
|
||||||
|
</a-button>
|
||||||
|
<a-button size="small" status="warning" @click="drawerClick">
|
||||||
|
一键导入
|
||||||
|
</a-button>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="section-content">
|
<div v-if="showContentTypeSelectors && requestTab === 'body'" class="content-type-bar">
|
||||||
<div v-if="queryTab === 'body'" class="table-content">
|
<div class="content-type-tags">
|
||||||
|
<span
|
||||||
|
v-for="(opt, idx) in contentTypeOptions"
|
||||||
|
:key="opt.key"
|
||||||
|
class="content-type-tag"
|
||||||
|
:class="[getContentTypeTagClass(opt.value, idx), { active: basicFormData?.contentType === opt.key }]"
|
||||||
|
@click="updateContentType(opt.key)"
|
||||||
|
>
|
||||||
|
{{ opt.value }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="mapped-param-select-wrap">
|
||||||
|
<span class="mapped-param-label">根节点</span>
|
||||||
|
<a-dropdown trigger="click">
|
||||||
|
<div class="mapped-param-trigger" :style="{ width: '100px' }">
|
||||||
|
<a-tag
|
||||||
|
v-if="selectedMappedParamOption"
|
||||||
|
:color="selectedMappedParamOption.color"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
{{ selectedMappedParamOption.label }}
|
||||||
|
</a-tag>
|
||||||
|
<span v-else class="mapped-param-placeholder">请选择</span>
|
||||||
|
<IconDown class="mapped-param-arrow" />
|
||||||
|
</div>
|
||||||
|
<template #content>
|
||||||
|
<a-doption
|
||||||
|
v-for="(opt, optIdx) in mappedParamTypeOptions"
|
||||||
|
:key="opt.key"
|
||||||
|
@click="updateMappedParamType(opt.key)"
|
||||||
|
>
|
||||||
|
<a-tag :color="getTypeTagColor(optIdx)" size="small">{{ opt.value }}</a-tag>
|
||||||
|
</a-doption>
|
||||||
|
</template>
|
||||||
|
</a-dropdown>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="section-content" :class="{ 'section-content-fixed-tab': requestTab === 'fixed' }">
|
||||||
|
<!-- Query tab 内容 -->
|
||||||
|
<div v-if="requestTab === 'query'" class="table-content">
|
||||||
<TablePro
|
<TablePro
|
||||||
ref="queryTableRef"
|
ref="queryTableRef"
|
||||||
v-bind="queryTableProps"
|
v-bind="queryTableProps"
|
||||||
@ -1879,7 +2028,9 @@
|
|||||||
</template>
|
</template>
|
||||||
</TablePro>
|
</TablePro>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="table-content query-fixed-content">
|
|
||||||
|
<!-- 固定配置 tab 内容 -->
|
||||||
|
<div v-else-if="requestTab === 'fixed'" class="table-content query-fixed-content">
|
||||||
<a-spin :loading="fixedParamsLoading" class="query-fixed-spin">
|
<a-spin :loading="fixedParamsLoading" class="query-fixed-spin">
|
||||||
<TablePro
|
<TablePro
|
||||||
ref="queryFixedTableRef"
|
ref="queryFixedTableRef"
|
||||||
@ -1929,62 +2080,9 @@
|
|||||||
</TablePro>
|
</TablePro>
|
||||||
</a-spin>
|
</a-spin>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 左下:Request -->
|
|
||||||
<div class="detail-section section-request">
|
|
||||||
<div class="section-header">
|
|
||||||
<a-tabs v-model:active-key="requestTab" type="line" size="mini" direction="horizontal" class="request-header-tabs">
|
|
||||||
<a-tab-pane key="body" title="Body" />
|
|
||||||
<a-tab-pane key="demo" title="Demo" />
|
|
||||||
</a-tabs>
|
|
||||||
<div class="section-actions" v-if="requestTab === 'body'">
|
|
||||||
<a-button size="small" status="success" @click="addNewRequestParam">
|
|
||||||
新增
|
|
||||||
</a-button>
|
|
||||||
<a-button size="small" status="warning" @click="drawerClick">
|
|
||||||
一键导入
|
|
||||||
</a-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-if="showContentTypeSelectors" class="content-type-bar">
|
|
||||||
<a-radio-group
|
|
||||||
:model-value="basicFormData?.contentType"
|
|
||||||
size="small"
|
|
||||||
type="button"
|
|
||||||
@update:model-value="updateContentType"
|
|
||||||
>
|
|
||||||
<a-radio
|
|
||||||
v-for="opt in contentTypeOptions"
|
|
||||||
:key="opt.key"
|
|
||||||
:value="opt.key"
|
|
||||||
>
|
|
||||||
{{ opt.value }}
|
|
||||||
</a-radio>
|
|
||||||
</a-radio-group>
|
|
||||||
<div class="mapped-param-select-wrap">
|
|
||||||
<a-select
|
|
||||||
:model-value="basicFormData?.mappedParamType"
|
|
||||||
size="small"
|
|
||||||
placeholder="请选择根节点"
|
|
||||||
allow-clear
|
|
||||||
:style="{ width: '140px' }"
|
|
||||||
@update:model-value="updateMappedParamType"
|
|
||||||
>
|
|
||||||
<a-option
|
|
||||||
v-for="opt in mappedParamTypeOptions"
|
|
||||||
:key="opt.key"
|
|
||||||
:value="opt.key"
|
|
||||||
>
|
|
||||||
{{ opt.value }}
|
|
||||||
</a-option>
|
|
||||||
</a-select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="section-content">
|
|
||||||
<!-- 请求体 tab 内容 -->
|
<!-- 请求体 tab 内容 -->
|
||||||
<div v-if="requestTab === 'body'" class="table-content">
|
<div v-else-if="requestTab === 'body'" class="table-content">
|
||||||
<TablePro
|
<TablePro
|
||||||
ref="requestTableRef"
|
ref="requestTableRef"
|
||||||
v-bind="requestTableProps"
|
v-bind="requestTableProps"
|
||||||
@ -2101,7 +2199,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Demo tab 内容 -->
|
<!-- Demo tab 内容 -->
|
||||||
<div v-if="requestTab === 'demo'" class="request-example-content">
|
<div v-else-if="requestTab === 'demo'" class="request-example-content">
|
||||||
<div class="request-example-container">
|
<div class="request-example-container">
|
||||||
<CodeMirror
|
<CodeMirror
|
||||||
ref="requestExampleEditorRef"
|
ref="requestExampleEditorRef"
|
||||||
@ -2128,7 +2226,7 @@
|
|||||||
<a-tabs v-model:active-key="responseTab" type="line" size="mini" direction="horizontal" class="response-header-tabs">
|
<a-tabs v-model:active-key="responseTab" type="line" size="mini" direction="horizontal" class="response-header-tabs">
|
||||||
<a-tab-pane key="body" title="Response" />
|
<a-tab-pane key="body" title="Response" />
|
||||||
<a-tab-pane v-if="showReturnConfig" key="returnConfig" title="返回配置" :disabled="!hasResponseParams" />
|
<a-tab-pane v-if="showReturnConfig" key="returnConfig" title="返回配置" :disabled="!hasResponseParams" />
|
||||||
<a-tab-pane key="demo" title="Demo" />
|
<a-tab-pane key="demo" title="示例" />
|
||||||
</a-tabs>
|
</a-tabs>
|
||||||
<div class="section-actions">
|
<div class="section-actions">
|
||||||
<!-- Response tab的操作按钮 -->
|
<!-- Response tab的操作按钮 -->
|
||||||
@ -2151,21 +2249,18 @@
|
|||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="showContentTypeSelectors" class="content-type-bar">
|
<div v-if="showContentTypeSelectors && responseTab === 'body'" class="content-type-bar">
|
||||||
<a-radio-group
|
<div class="content-type-tags">
|
||||||
:model-value="basicFormData?.responseContentType"
|
<span
|
||||||
size="small"
|
v-for="(opt, idx) in responseContentTypeOptions"
|
||||||
type="button"
|
|
||||||
@update:model-value="updateResponseContentType"
|
|
||||||
>
|
|
||||||
<a-radio
|
|
||||||
v-for="opt in responseContentTypeOptions"
|
|
||||||
:key="opt.key"
|
:key="opt.key"
|
||||||
:value="opt.key"
|
class="content-type-tag"
|
||||||
|
:class="[getContentTypeTagClass(opt.value, idx), { active: basicFormData?.responseContentType === opt.key }]"
|
||||||
|
@click="updateResponseContentType(opt.key)"
|
||||||
>
|
>
|
||||||
{{ opt.value }}
|
{{ opt.value }}
|
||||||
</a-radio>
|
</span>
|
||||||
</a-radio-group>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="section-content">
|
<div class="section-content">
|
||||||
<!-- Response body tab 内容 -->
|
<!-- Response body tab 内容 -->
|
||||||
@ -2466,17 +2561,74 @@
|
|||||||
background-color: var(--color-bg-1);
|
background-color: var(--color-bg-1);
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 请求方式 + 请求地址(参考 ApiTest api-test-header)
|
||||||
|
.api-test-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
padding: 12px;
|
||||||
|
background: var(--color-bg-2);
|
||||||
|
border: 1px solid var(--color-border-2);
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||||
|
flex-shrink: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
min-height: 56px;
|
||||||
|
|
||||||
|
.method-select,
|
||||||
|
.method-badge {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.method-badge {
|
||||||
|
min-width: 80px;
|
||||||
|
padding: 0 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.method-get { color: #10b981 !important; }
|
||||||
|
.method-post { color: #f59e0b !important; }
|
||||||
|
.method-delete { color: #ef4444 !important; }
|
||||||
|
.method-put, .method-patch, .method-head, .method-options { color: #6b7280 !important; }
|
||||||
|
|
||||||
|
.url-input {
|
||||||
|
flex: 0.5;
|
||||||
|
margin-left: 12px;
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
.url-text {
|
||||||
|
flex: 0.5;
|
||||||
|
margin-left: 12px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: var(--color-fill-2);
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--color-text-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-left: 12px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-grid {
|
.detail-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 1fr 1fr;
|
||||||
grid-template-rows: 0.67fr 1.33fr;
|
grid-template-rows: auto 1fr;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
height: 100%;
|
flex: 1;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.section-basic-fullwidth {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
|
||||||
.detail-section {
|
.detail-section {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@ -2496,19 +2648,79 @@
|
|||||||
.content-type-bar {
|
.content-type-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 12px;
|
||||||
padding: 8px 16px 8px 30px;
|
padding: 10px 30px;
|
||||||
|
min-height: 52px;
|
||||||
|
box-sizing: border-box;
|
||||||
border-bottom: 1px solid var(--color-border-2);
|
border-bottom: 1px solid var(--color-border-2);
|
||||||
background-color: var(--color-bg-1);
|
background-color: var(--color-bg-1);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
|
||||||
.arco-radio-group {
|
.content-type-tags {
|
||||||
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-type-tag {
|
||||||
|
padding: 4px 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
border: none;
|
||||||
|
background: var(--color-fill-2);
|
||||||
|
color: var(--color-text-3);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
&.active {
|
||||||
|
color: #fff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.content-type-json.active { background: #165dff; }
|
||||||
|
&.content-type-xml.active { background: #00b42a; }
|
||||||
|
&.content-type-form.active { background: #f59e0b; }
|
||||||
|
&.content-type-raw.active,
|
||||||
|
&.content-type-purple.active { background: #8b5cf6; }
|
||||||
|
&.content-type-cyan.active { background: #06b6d4; }
|
||||||
|
&.content-type-pink.active { background: #ec4899; }
|
||||||
}
|
}
|
||||||
|
|
||||||
.mapped-param-select-wrap {
|
.mapped-param-select-wrap {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
margin-right: 20px;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mapped-param-trigger {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 2px 8px;
|
||||||
|
min-height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mapped-param-placeholder {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--color-text-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mapped-param-arrow {
|
||||||
|
margin-left: auto;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--color-text-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mapped-param-label {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-text-2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2618,7 +2830,8 @@
|
|||||||
.form-item {
|
.form-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
gap: 8px;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-label {
|
.form-label {
|
||||||
@ -2662,46 +2875,48 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 固定配置:弹窗内防止列表挤在一起 */
|
/* 固定配置 tab 时:section-request 允许内容溢出,section-content 不限制高度,列表自然展开 */
|
||||||
.query-fixed-content {
|
.section-request.section-request-fixed-tab {
|
||||||
min-height: 180px;
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-content.section-content-fixed-tab {
|
||||||
|
overflow: visible;
|
||||||
|
min-height: auto;
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 固定配置:无滚动面板,列表自然展开 */
|
||||||
|
.query-fixed-content.table-content {
|
||||||
|
overflow: visible;
|
||||||
|
flex: none;
|
||||||
|
min-height: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.query-fixed-spin {
|
.query-fixed-spin {
|
||||||
flex: 1;
|
|
||||||
min-height: 0;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.query-fixed-spin :deep(.arco-spin) {
|
.query-fixed-spin :deep(.arco-spin) {
|
||||||
flex: 1;
|
|
||||||
min-height: 0;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.query-fixed-spin :deep(.arco-spin-children) {
|
.query-fixed-spin :deep(.arco-spin-children) {
|
||||||
flex: 1;
|
|
||||||
min-height: 0;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.query-fixed-params-table {
|
.query-fixed-params-table {
|
||||||
flex: 1;
|
|
||||||
min-height: 0;
|
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
:deep(.vxe-table) {
|
:deep(.vxe-table) {
|
||||||
height: 100%;
|
height: auto !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.vxe-table--body-wrapper) {
|
:deep(.vxe-table--body-wrapper) {
|
||||||
max-height: 100%;
|
overflow: visible !important;
|
||||||
overflow-y: auto;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 防止弹窗内行被压缩挤在一起 */
|
/* 防止弹窗内行被压缩挤在一起 */
|
||||||
@ -3028,9 +3243,4 @@
|
|||||||
justify-content: center !important;
|
justify-content: center !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 隐藏 Query 容器但保留空间
|
|
||||||
.section-query-hidden {
|
|
||||||
visibility: hidden;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -768,6 +768,7 @@
|
|||||||
init,
|
init,
|
||||||
saveInterface,
|
saveInterface,
|
||||||
closeModal,
|
closeModal,
|
||||||
|
getRequestExample: () => interfaceDetailRef.value?.getRequestExample?.() || '',
|
||||||
});
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
@ -789,7 +790,11 @@
|
|||||||
:interface-id="tableForm.id"
|
:interface-id="tableForm.id"
|
||||||
:service-provider-id="tableForm.serviceProviderId"
|
:service-provider-id="tableForm.serviceProviderId"
|
||||||
@update:basic-form-data="(val) => Object.assign(tableForm, val)"
|
@update:basic-form-data="(val) => Object.assign(tableForm, val)"
|
||||||
/>
|
>
|
||||||
|
<template v-if="$slots.headerActions" #headerActions>
|
||||||
|
<slot name="headerActions" />
|
||||||
|
</template>
|
||||||
|
</InterfaceDetail>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@ -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(() => {
|
onMounted(() => {
|
||||||
getAppList();
|
getAppList();
|
||||||
loadServiceProviderList();
|
loadServiceProviderList();
|
||||||
@ -1617,11 +1636,7 @@
|
|||||||
{{ t('system.serviceProviders.detail.tab.debug') }}
|
{{ t('system.serviceProviders.detail.tab.debug') }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="tabs-extra">
|
<div class="tabs-extra" v-if="interfaceDetailTab !== 'detail'"></div>
|
||||||
<a-button size="small" type="primary" @click="handleInterfaceDetailSave">
|
|
||||||
{{ t('system.serviceProviders.detail.actions.save') }}
|
|
||||||
</a-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -1634,7 +1649,16 @@
|
|||||||
:key="`interface-detail-${selectedInterface?.id}-${detailRefreshKey}`"
|
:key="`interface-detail-${selectedInterface?.id}-${detailRefreshKey}`"
|
||||||
ref="interfaceDetailModalRef"
|
ref="interfaceDetailModalRef"
|
||||||
@saved="handleInterfaceSaved"
|
@saved="handleInterfaceSaved"
|
||||||
/>
|
>
|
||||||
|
<template #headerActions>
|
||||||
|
<a-button size="small" type="primary" @click="handleInterfaceDetailSave">
|
||||||
|
{{ t('system.serviceProviders.detail.actions.save') }}
|
||||||
|
</a-button>
|
||||||
|
<a-button size="small" @click="handleDebugClick">
|
||||||
|
调试
|
||||||
|
</a-button>
|
||||||
|
</template>
|
||||||
|
</ServiceProviderInterfaceModal>
|
||||||
</a-spin>
|
</a-spin>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -1644,6 +1668,7 @@
|
|||||||
:selected-interface="selectedInterface"
|
:selected-interface="selectedInterface"
|
||||||
:selected-service-provider="selectedProvider"
|
:selected-service-provider="selectedProvider"
|
||||||
:enable-defaults="false"
|
:enable-defaults="false"
|
||||||
|
:curl-to-import="curlToImport"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="api-debugger-container">
|
<div class="api-debugger-container">
|
||||||
<ApiTest
|
<ApiTest
|
||||||
|
ref="apiTestRef"
|
||||||
:selected-interface="selectedInterface"
|
:selected-interface="selectedInterface"
|
||||||
:selected-service-provider="selectedServiceProvider"
|
:selected-service-provider="selectedServiceProvider"
|
||||||
:enable-defaults="enableDefaults"
|
:enable-defaults="enableDefaults"
|
||||||
@ -11,6 +12,7 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import ApiTest from '@/components/ApiTest/index.vue';
|
import ApiTest from '@/components/ApiTest/index.vue';
|
||||||
import { getInterfaceGetInterfaceById } from '@/api/InterfaceServices/interface';
|
import { getInterfaceGetInterfaceById } from '@/api/InterfaceServices/interface';
|
||||||
|
import { ref, computed, watch, nextTick } from 'vue';
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: 'ApiDebugger',
|
name: 'ApiDebugger',
|
||||||
@ -21,8 +23,11 @@
|
|||||||
selectedInterface?: any;
|
selectedInterface?: any;
|
||||||
selectedServiceProvider?: any;
|
selectedServiceProvider?: any;
|
||||||
enableDefaults?: boolean; // 是否启用默认值
|
enableDefaults?: boolean; // 是否启用默认值
|
||||||
|
curlToImport?: string; // 从接口明细请求容器导入的 cURL 内容
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const apiTestRef = ref<InstanceType<typeof ApiTest> | null>(null);
|
||||||
|
|
||||||
// 为enableDefaults设置默认值
|
// 为enableDefaults设置默认值
|
||||||
const enableDefaults = computed(() => props.enableDefaults ?? true);
|
const enableDefaults = computed(() => props.enableDefaults ?? true);
|
||||||
|
|
||||||
@ -62,9 +67,6 @@
|
|||||||
if (interfaceData) {
|
if (interfaceData) {
|
||||||
console.log('ApiDebugger: interfaceData:', interfaceData);
|
console.log('ApiDebugger: interfaceData:', interfaceData);
|
||||||
console.log('ApiDebugger: interfaceData keys:', Object.keys(interfaceData || {}));
|
console.log('ApiDebugger: interfaceData keys:', Object.keys(interfaceData || {}));
|
||||||
console.log('ApiDebugger: has inParams:', !!(interfaceData as any)?.inParams);
|
|
||||||
console.log('ApiDebugger: has requestExample:', !!(interfaceData as any)?.requestExample);
|
|
||||||
console.log('ApiDebugger: has demoData:', !!(interfaceData as any)?.demoData);
|
|
||||||
console.log('ApiDebugger: contentType value:', (interfaceData as any)?.contentType);
|
console.log('ApiDebugger: contentType value:', (interfaceData as any)?.contentType);
|
||||||
console.log('ApiDebugger: contentType type:', typeof (interfaceData as any)?.contentType);
|
console.log('ApiDebugger: contentType type:', typeof (interfaceData as any)?.contentType);
|
||||||
console.log('ApiDebugger: requestMethod:', (interfaceData as any)?.requestMethod);
|
console.log('ApiDebugger: requestMethod:', (interfaceData as any)?.requestMethod);
|
||||||
@ -90,18 +92,16 @@
|
|||||||
console.log('ApiDebugger: selectedInterface changed:', newVal);
|
console.log('ApiDebugger: selectedInterface changed:', newVal);
|
||||||
if (newVal && newVal.id) {
|
if (newVal && newVal.id) {
|
||||||
console.log('ApiDebugger: selectedInterface has id:', newVal.id);
|
console.log('ApiDebugger: selectedInterface has id:', newVal.id);
|
||||||
console.log('ApiDebugger: selectedInterface has inParams:', !!newVal.inParams);
|
|
||||||
console.log('ApiDebugger: selectedInterface contentType:', newVal.contentType);
|
console.log('ApiDebugger: selectedInterface contentType:', newVal.contentType);
|
||||||
|
|
||||||
// 如果传递的接口对象不完整(没有关键字段),则获取完整详情
|
// 如果传递的接口对象不完整(没有关键字段),则获取完整详情;body 不再从接口明细加载,仅从导入的 cURL 的 --data-raw 显示
|
||||||
const interfaceObj = newVal as any;
|
const interfaceObj = newVal as any;
|
||||||
const hasDemoData = !!(interfaceObj?.inParams || interfaceObj?.requestExample || interfaceObj?.demoData);
|
|
||||||
const hasContentType = !!interfaceObj?.contentType;
|
const hasContentType = !!interfaceObj?.contentType;
|
||||||
const hasRequestMethod = !!interfaceObj?.requestMethod;
|
const hasRequestMethod = !!interfaceObj?.requestMethod;
|
||||||
|
|
||||||
console.log('ApiDebugger: hasDemoData:', hasDemoData, 'hasContentType:', hasContentType, 'hasRequestMethod:', hasRequestMethod);
|
console.log('ApiDebugger: hasContentType:', hasContentType, 'hasRequestMethod:', hasRequestMethod);
|
||||||
|
|
||||||
if (!hasDemoData || !hasContentType || !hasRequestMethod) {
|
if (!hasContentType || !hasRequestMethod) {
|
||||||
console.log('ApiDebugger: interface data incomplete, fetching full detail...');
|
console.log('ApiDebugger: interface data incomplete, fetching full detail...');
|
||||||
await fetchInterfaceDetail(newVal.id);
|
await fetchInterfaceDetail(newVal.id);
|
||||||
} else {
|
} else {
|
||||||
@ -117,6 +117,16 @@
|
|||||||
watch(() => props.selectedServiceProvider, (newVal) => {
|
watch(() => props.selectedServiceProvider, (newVal) => {
|
||||||
console.log('ApiDebugger: selectedServiceProvider changed:', newVal);
|
console.log('ApiDebugger: selectedServiceProvider changed:', newVal);
|
||||||
}, { immediate: true });
|
}, { immediate: true });
|
||||||
|
|
||||||
|
// 当收到 curlToImport 时,导入到 ApiTest(immediate 确保切换 tab 时若已有值也能触发)
|
||||||
|
watch(() => props.curlToImport, (curl) => {
|
||||||
|
if (curl && curl.trim()) {
|
||||||
|
const toImport = curl.trim();
|
||||||
|
const doImport = () => apiTestRef.value?.importCurlFromString?.(toImport);
|
||||||
|
nextTick(doImport);
|
||||||
|
setTimeout(doImport, 150);
|
||||||
|
}
|
||||||
|
}, { immediate: true });
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="less">
|
<style scoped lang="less">
|
||||||
|
|||||||
@ -609,6 +609,7 @@
|
|||||||
init,
|
init,
|
||||||
submit: saveInterface,
|
submit: saveInterface,
|
||||||
saveInterface,
|
saveInterface,
|
||||||
|
getRequestExample: () => interfaceDetailRef.value?.getRequestExample?.() || '',
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -629,7 +630,11 @@
|
|||||||
:show-query="false"
|
:show-query="false"
|
||||||
:show-return-config="false"
|
:show-return-config="false"
|
||||||
@update:basic-form-data="(val) => Object.assign(tableForm, val)"
|
@update:basic-form-data="(val) => Object.assign(tableForm, val)"
|
||||||
/>
|
>
|
||||||
|
<template v-if="$slots.headerActions" #headerActions>
|
||||||
|
<slot name="headerActions" />
|
||||||
|
</template>
|
||||||
|
</InterfaceDetail>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@ -884,6 +884,24 @@ import { IconClose, IconEdit, IconPlus, IconSearch, IconMore, IconMinus, IconRef
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 点击调试 / 切换 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 handleDetailDebugClick() {
|
||||||
|
handleTabChange('debugger');
|
||||||
|
if (!MapServiceProviderROW.value) return;
|
||||||
|
importCurlToDebugger();
|
||||||
|
activeContentTab.value = 'debugger';
|
||||||
|
}
|
||||||
|
|
||||||
// 统一的保存函数,根据当前tab调用不同的接口
|
// 统一的保存函数,根据当前tab调用不同的接口
|
||||||
async function handleTabSave() {
|
async function handleTabSave() {
|
||||||
// 使用独立的currentActiveTab变量,在handleTabChange中实时更新
|
// 使用独立的currentActiveTab变量,在handleTabChange中实时更新
|
||||||
@ -1048,22 +1066,19 @@ import { IconClose, IconEdit, IconPlus, IconSearch, IconMore, IconMinus, IconRef
|
|||||||
setTimeout(checkAndInit, 200);
|
setTimeout(checkAndInit, 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 监听 interfaceDetailModalRef 变化,当组件挂载后自动初始化
|
// 监听 interfaceDetailModalRef 变化,当组件挂载后自动初始化(detail 用 v-show 保持挂载,需在有 selectedInterface 时初始化以便切换调试 tab 时能获取 cURL)
|
||||||
watch(interfaceDetailModalRef, async (newRef, oldRef) => {
|
watch(interfaceDetailModalRef, async (newRef, oldRef) => {
|
||||||
// 只有当 ref 从 undefined/null 变为有值时,才初始化(避免重复初始化)
|
if (newRef && !oldRef && selectedInterface.value && newRef.init) {
|
||||||
if (newRef && !oldRef && selectedInterface.value && activeContentTab.value === 'detail') {
|
|
||||||
await nextTick();
|
await nextTick();
|
||||||
await nextTick();
|
await nextTick();
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
// 再次检查条件,可能在等待过程中状态已改变
|
if (selectedInterface.value && newRef && newRef.init) {
|
||||||
if (activeContentTab.value === 'detail' && selectedInterface.value && newRef && newRef.init) {
|
|
||||||
try {
|
try {
|
||||||
newRef.init({
|
newRef.init({
|
||||||
id: selectedInterface.value.id,
|
id: selectedInterface.value.id,
|
||||||
interfaceId: selectedInterface.value.id,
|
interfaceId: selectedInterface.value.id,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// 初始化失败,静默处理
|
|
||||||
console.error('初始化接口明细组件失败:', error);
|
console.error('初始化接口明细组件失败:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1074,7 +1089,9 @@ import { IconClose, IconEdit, IconPlus, IconSearch, IconMore, IconMinus, IconRef
|
|||||||
// 监听 activeContentTab 变化,同步更新 currentActiveTab
|
// 监听 activeContentTab 变化,同步更新 currentActiveTab
|
||||||
watch(activeContentTab, async (newTab) => {
|
watch(activeContentTab, async (newTab) => {
|
||||||
currentActiveTab.value = newTab;
|
currentActiveTab.value = newTab;
|
||||||
if (newTab === 'detail' && selectedInterface.value) {
|
if (newTab === 'debugger') {
|
||||||
|
importCurlToDebugger();
|
||||||
|
} else if (newTab === 'detail' && selectedInterface.value) {
|
||||||
// 等待 DOM 更新完成后再初始化
|
// 等待 DOM 更新完成后再初始化
|
||||||
await nextTick();
|
await nextTick();
|
||||||
await nextTick();
|
await nextTick();
|
||||||
@ -1096,15 +1113,23 @@ import { IconClose, IconEdit, IconPlus, IconSearch, IconMore, IconMinus, IconRef
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 监听 selectedInterface 变化,初始化组件
|
// 监听 selectedInterface 变化,初始化组件(detail 用 v-show 保持挂载,需在切换接口时初始化以便切换调试 tab 时能获取 cURL)
|
||||||
watch(selectedInterface, async (newInterface) => {
|
watch(selectedInterface, async (newInterface) => {
|
||||||
if (newInterface && activeContentTab.value === 'detail') {
|
if (newInterface) {
|
||||||
// 等待 DOM 更新完成后再初始化
|
|
||||||
await nextTick();
|
await nextTick();
|
||||||
await nextTick();
|
await nextTick();
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
initInterfaceConfig();
|
if (interfaceDetailModalRef.value?.init && selectedInterface.value) {
|
||||||
}, 100);
|
try {
|
||||||
|
interfaceDetailModalRef.value.init({
|
||||||
|
id: selectedInterface.value.id,
|
||||||
|
interfaceId: selectedInterface.value.id,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('初始化接口明细组件失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, activeContentTab.value === 'detail' ? 100 : 300);
|
||||||
}
|
}
|
||||||
}, { immediate: false });
|
}, { immediate: false });
|
||||||
|
|
||||||
@ -1422,7 +1447,7 @@ import { IconClose, IconEdit, IconPlus, IconSearch, IconMore, IconMinus, IconRef
|
|||||||
</div>
|
</div>
|
||||||
<div class="tabs-extra">
|
<div class="tabs-extra">
|
||||||
<a-button
|
<a-button
|
||||||
v-if="activeContentTab === 'mapping' || activeContentTab === 'detail'"
|
v-if="activeContentTab === 'mapping'"
|
||||||
size="small"
|
size="small"
|
||||||
type="primary"
|
type="primary"
|
||||||
@click="handleTabSave"
|
@click="handleTabSave"
|
||||||
@ -1455,16 +1480,26 @@ import { IconClose, IconEdit, IconPlus, IconSearch, IconMore, IconMinus, IconRef
|
|||||||
:selected-interface="selectedInterface"
|
:selected-interface="selectedInterface"
|
||||||
:selected-service-provider="MapServiceProviderROW"
|
:selected-service-provider="MapServiceProviderROW"
|
||||||
:enable-defaults="true"
|
:enable-defaults="true"
|
||||||
|
:curl-to-import="curlToImport"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 接口明细内容 -->
|
<!-- 接口明细内容(v-show 保持挂载,便于切换至调试 tab 时获取 cURL) -->
|
||||||
<div v-if="activeContentTab === 'detail' && selectedInterface" class="detail-content">
|
<div v-show="activeContentTab === 'detail' && selectedInterface" class="detail-content">
|
||||||
<UpstreamInterfaceModal
|
<UpstreamInterfaceModal
|
||||||
:key="`${selectedInterface?.id}-${detailRefreshKey}`"
|
:key="`${selectedInterface?.id}-${detailRefreshKey}`"
|
||||||
ref="interfaceDetailModalRef"
|
ref="interfaceDetailModalRef"
|
||||||
@saved="() => { /* 保留在当前tab,按需刷新左侧树 */ }"
|
@saved="() => { /* 保留在当前tab,按需刷新左侧树 */ }"
|
||||||
/>
|
>
|
||||||
|
<template #headerActions>
|
||||||
|
<a-button size="small" type="primary" @click="handleTabSave">
|
||||||
|
{{ $t('system.interface.actions.save') }}
|
||||||
|
</a-button>
|
||||||
|
<a-button size="small" @click="handleDetailDebugClick">
|
||||||
|
调试
|
||||||
|
</a-button>
|
||||||
|
</template>
|
||||||
|
</UpstreamInterfaceModal>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user