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: 如果不对,请告诉我接口明细显示什么和应该映射到什么!');
|
||||
} 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('<?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';
|
||||
// 解析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());
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
<script lang="ts" setup>
|
||||
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 TablePro from '@/components/TablePro/index.vue';
|
||||
import CodeMirror from '@/components/CodeMirror/index.vue';
|
||||
@ -18,6 +18,8 @@
|
||||
import { postParameterParse, postParameterValidateParameter } from '@/api/ParameterServices/parameter';
|
||||
import { $baseMessage } from '@/utils';
|
||||
import JSON5 from 'json5';
|
||||
import SelectPro from '@/components/SelectPro/index.vue';
|
||||
import { getMethodText } from '@/views/api/upstreamInterface/utils';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
@ -103,11 +105,8 @@
|
||||
let requestSortable: any = null;
|
||||
let responseSortable: any = null;
|
||||
|
||||
// Query tab 切换
|
||||
const queryTab = ref<'body' | 'fixed'>('body');
|
||||
|
||||
// Request tab 切换
|
||||
const requestTab = ref<'body' | 'demo'>('body');
|
||||
// Request tab 切换(含 Query、固定配置、Body、cURL)
|
||||
const requestTab = ref<'query' | 'fixed' | 'body' | 'demo'>('body');
|
||||
const requestExample = ref<string>(''); // Demo内容
|
||||
const requestExampleEditorRef = ref<any>(); // Demo编辑器引用
|
||||
|
||||
@ -146,18 +145,51 @@
|
||||
const contentTypeOptions = ref<any[]>([]);
|
||||
const responseContentTypeOptions = 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(() => {
|
||||
if (!props.showContentTypeSelectors) return props.basicFormItems;
|
||||
return (props.basicFormItems || []).filter(
|
||||
(item: any) =>
|
||||
item?.field !== 'contentType' &&
|
||||
item?.field !== 'responseContentType' &&
|
||||
item?.field !== 'mappedParamType'
|
||||
item?.field !== 'requestMethod' &&
|
||||
item?.field !== 'requestAddress' &&
|
||||
(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() {
|
||||
if (!props.showContentTypeSelectors) return;
|
||||
@ -193,12 +225,32 @@
|
||||
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 获取颜色(用于参数类型列)
|
||||
function getParameterTypeColor(typeKey: any) {
|
||||
const idx = ParameterTypeList.value.findIndex((item: any) => item.key === typeKey);
|
||||
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 表格配置(使用下游接口的配置方式)
|
||||
const queryTableProps = reactive<any>({
|
||||
columns: [
|
||||
@ -255,7 +307,7 @@
|
||||
},
|
||||
});
|
||||
|
||||
// Query 固定配置表格
|
||||
// Query 固定配置表格(不传 height,随内容自然增高,与 Query 列表的固定高度区分)
|
||||
const queryFixedTableProps = computed(() => ({
|
||||
columns: [
|
||||
{
|
||||
@ -289,7 +341,7 @@
|
||||
showToolbar: false,
|
||||
keepSource: true,
|
||||
tableConfig: {
|
||||
height: '100%',
|
||||
// 不传 height,vxe-table 的 height:'auto' 是铺满父容器,会固定高度;不传则随内容增高
|
||||
data: fixedParams.value,
|
||||
editConfig: {
|
||||
trigger: 'click',
|
||||
@ -301,9 +353,7 @@
|
||||
showIndex: false,
|
||||
useForm: false,
|
||||
size: 'small',
|
||||
minHeight: 150,
|
||||
autoHeight: false,
|
||||
height: '100%',
|
||||
editRules: {},
|
||||
}));
|
||||
|
||||
@ -482,7 +532,7 @@
|
||||
}
|
||||
|
||||
// 监听Query tab切换,检查是否有未保存的更改
|
||||
watch(queryTab, async (newTab, oldTab) => {
|
||||
watch(requestTab, async (newTab, oldTab) => {
|
||||
// 如果从固定配置tab切换到其他tab,检查是否有未保存的更改
|
||||
if (oldTab === 'fixed' && newTab !== 'fixed') {
|
||||
if (checkFixedConfigChanges()) {
|
||||
@ -509,7 +559,7 @@
|
||||
|
||||
if (!result) {
|
||||
// 如果保存失败,回到原来的tab
|
||||
queryTab.value = oldTab;
|
||||
requestTab.value = oldTab;
|
||||
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 变化,同步到表格数据
|
||||
watch(
|
||||
() => props.requestParams,
|
||||
@ -753,8 +810,8 @@
|
||||
|
||||
// 固定配置:新增
|
||||
async function addFixedParam() {
|
||||
if (queryTab.value !== 'fixed') {
|
||||
queryTab.value = 'fixed';
|
||||
if (requestTab.value !== 'fixed') {
|
||||
requestTab.value = 'fixed';
|
||||
await nextTick();
|
||||
}
|
||||
const record = {
|
||||
@ -1092,6 +1149,12 @@
|
||||
return String(inParams);
|
||||
}
|
||||
|
||||
const trimmed = inParams.trim();
|
||||
// 非 JSON 内容(如 cURL、XML 等)直接返回,不尝试解析
|
||||
if (!trimmed.startsWith('{') && !trimmed.startsWith('[')) {
|
||||
return inParams;
|
||||
}
|
||||
|
||||
try {
|
||||
// 步骤1: 处理转义字符 - 将字面量的转义序列转换为实际字符
|
||||
let processed = inParams;
|
||||
@ -1154,6 +1217,7 @@
|
||||
if (props.showContentTypeSelectors) {
|
||||
loadContentTypeOptions();
|
||||
}
|
||||
loadRequestMethodOptions();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
@ -1720,9 +1784,45 @@
|
||||
|
||||
<template>
|
||||
<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-section section-basic">
|
||||
<!-- 顶部:基本参数(一行) -->
|
||||
<div class="detail-section section-basic section-basic-fullwidth">
|
||||
<div class="section-header">
|
||||
<span class="section-title">基本参数</span>
|
||||
<div class="section-actions"></div>
|
||||
@ -1738,7 +1838,7 @@
|
||||
:default-span="8"
|
||||
:row-gutter="16"
|
||||
:form-layout="'vertical'"
|
||||
:item-list="filteredBasicFormItems"
|
||||
:item-list="basicFormItemsForLayout"
|
||||
:required-position="'none'"
|
||||
:rules="basicFormRules"
|
||||
:hide-colon="false"
|
||||
@ -1747,11 +1847,11 @@
|
||||
/>
|
||||
<!-- 只读显示模式 -->
|
||||
<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-value">{{ value || '单行输入' }}</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-label">字段{{ i }}</div>
|
||||
<div class="form-value">单行输入</div>
|
||||
@ -1761,20 +1861,22 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右上:Query -->
|
||||
<div class="detail-section section-query" :class="{ 'section-query-hidden': !showQuery }">
|
||||
<!-- 左下:Request(含 Query、固定配置、Body、cURL) -->
|
||||
<div class="detail-section section-request" :class="{ 'section-request-fixed-tab': requestTab === 'fixed' }">
|
||||
<div class="section-header">
|
||||
<a-tabs v-model:active-key="queryTab" type="line" size="mini" direction="horizontal" class="request-header-tabs">
|
||||
<a-tab-pane key="body" title="Query" />
|
||||
<a-tab-pane key="fixed" title="固定配置" />
|
||||
<a-tabs v-model:active-key="requestTab" type="line" size="mini" direction="horizontal" class="request-header-tabs">
|
||||
<a-tab-pane v-if="showQuery" key="query" title="Query" />
|
||||
<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>
|
||||
<div class="section-actions">
|
||||
<template v-if="queryTab === 'body'">
|
||||
<template v-if="requestTab === 'query'">
|
||||
<a-button size="small" status="success" @click="addNewQueryParam">
|
||||
新增
|
||||
</a-button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<template v-else-if="requestTab === 'fixed'">
|
||||
<a-button size="small" status="success" @click="addFixedParam">
|
||||
新增
|
||||
</a-button>
|
||||
@ -1782,10 +1884,57 @@
|
||||
保存
|
||||
</a-button>
|
||||
</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 class="section-content">
|
||||
<div v-if="queryTab === 'body'" class="table-content">
|
||||
<div v-if="showContentTypeSelectors && requestTab === 'body'" class="content-type-bar">
|
||||
<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
|
||||
ref="queryTableRef"
|
||||
v-bind="queryTableProps"
|
||||
@ -1879,7 +2028,9 @@
|
||||
</template>
|
||||
</TablePro>
|
||||
</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">
|
||||
<TablePro
|
||||
ref="queryFixedTableRef"
|
||||
@ -1929,62 +2080,9 @@
|
||||
</TablePro>
|
||||
</a-spin>
|
||||
</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 内容 -->
|
||||
<div v-if="requestTab === 'body'" class="table-content">
|
||||
<div v-else-if="requestTab === 'body'" class="table-content">
|
||||
<TablePro
|
||||
ref="requestTableRef"
|
||||
v-bind="requestTableProps"
|
||||
@ -2101,7 +2199,7 @@
|
||||
</div>
|
||||
|
||||
<!-- 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">
|
||||
<CodeMirror
|
||||
ref="requestExampleEditorRef"
|
||||
@ -2128,7 +2226,7 @@
|
||||
<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 v-if="showReturnConfig" key="returnConfig" title="返回配置" :disabled="!hasResponseParams" />
|
||||
<a-tab-pane key="demo" title="Demo" />
|
||||
<a-tab-pane key="demo" title="示例" />
|
||||
</a-tabs>
|
||||
<div class="section-actions">
|
||||
<!-- Response tab的操作按钮 -->
|
||||
@ -2151,21 +2249,18 @@
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="showContentTypeSelectors" class="content-type-bar">
|
||||
<a-radio-group
|
||||
:model-value="basicFormData?.responseContentType"
|
||||
size="small"
|
||||
type="button"
|
||||
@update:model-value="updateResponseContentType"
|
||||
>
|
||||
<a-radio
|
||||
v-for="opt in responseContentTypeOptions"
|
||||
<div v-if="showContentTypeSelectors && responseTab === 'body'" class="content-type-bar">
|
||||
<div class="content-type-tags">
|
||||
<span
|
||||
v-for="(opt, idx) in responseContentTypeOptions"
|
||||
: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 }}
|
||||
</a-radio>
|
||||
</a-radio-group>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section-content">
|
||||
<!-- Response body tab 内容 -->
|
||||
@ -2466,17 +2561,74 @@
|
||||
background-color: var(--color-bg-1);
|
||||
box-sizing: border-box;
|
||||
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 {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-rows: 0.67fr 1.33fr;
|
||||
grid-template-rows: auto 1fr;
|
||||
gap: 16px;
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.section-basic-fullwidth {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.detail-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@ -2496,19 +2648,79 @@
|
||||
.content-type-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 16px 8px 30px;
|
||||
gap: 12px;
|
||||
padding: 10px 30px;
|
||||
min-height: 52px;
|
||||
box-sizing: border-box;
|
||||
border-bottom: 1px solid var(--color-border-2);
|
||||
background-color: var(--color-bg-1);
|
||||
flex-shrink: 0;
|
||||
|
||||
.arco-radio-group {
|
||||
.content-type-tags {
|
||||
display: flex;
|
||||
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 {
|
||||
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 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
@ -2662,46 +2875,48 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* 固定配置:弹窗内防止列表挤在一起 */
|
||||
.query-fixed-content {
|
||||
min-height: 180px;
|
||||
/* 固定配置 tab 时:section-request 允许内容溢出,section-content 不限制高度,列表自然展开 */
|
||||
.section-request.section-request-fixed-tab {
|
||||
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;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.query-fixed-spin {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.query-fixed-spin :deep(.arco-spin) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.query-fixed-spin :deep(.arco-spin-children) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.query-fixed-params-table {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
|
||||
:deep(.vxe-table) {
|
||||
height: 100%;
|
||||
height: auto !important;
|
||||
}
|
||||
|
||||
:deep(.vxe-table--body-wrapper) {
|
||||
max-height: 100%;
|
||||
overflow-y: auto;
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
/* 防止弹窗内行被压缩挤在一起 */
|
||||
@ -3028,9 +3243,4 @@
|
||||
justify-content: center !important;
|
||||
}
|
||||
|
||||
// 隐藏 Query 容器但保留空间
|
||||
.section-query-hidden {
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -768,6 +768,7 @@
|
||||
init,
|
||||
saveInterface,
|
||||
closeModal,
|
||||
getRequestExample: () => interfaceDetailRef.value?.getRequestExample?.() || '',
|
||||
});
|
||||
|
||||
</script>
|
||||
@ -789,7 +790,11 @@
|
||||
:interface-id="tableForm.id"
|
||||
:service-provider-id="tableForm.serviceProviderId"
|
||||
@update:basic-form-data="(val) => Object.assign(tableForm, val)"
|
||||
/>
|
||||
>
|
||||
<template v-if="$slots.headerActions" #headerActions>
|
||||
<slot name="headerActions" />
|
||||
</template>
|
||||
</InterfaceDetail>
|
||||
</div>
|
||||
</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(() => {
|
||||
getAppList();
|
||||
loadServiceProviderList();
|
||||
@ -1617,11 +1636,7 @@
|
||||
{{ t('system.serviceProviders.detail.tab.debug') }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="tabs-extra">
|
||||
<a-button size="small" type="primary" @click="handleInterfaceDetailSave">
|
||||
{{ t('system.serviceProviders.detail.actions.save') }}
|
||||
</a-button>
|
||||
</div>
|
||||
<div class="tabs-extra" v-if="interfaceDetailTab !== 'detail'"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1634,7 +1649,16 @@
|
||||
:key="`interface-detail-${selectedInterface?.id}-${detailRefreshKey}`"
|
||||
ref="interfaceDetailModalRef"
|
||||
@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>
|
||||
</div>
|
||||
|
||||
@ -1644,6 +1668,7 @@
|
||||
:selected-interface="selectedInterface"
|
||||
:selected-service-provider="selectedProvider"
|
||||
:enable-defaults="false"
|
||||
:curl-to-import="curlToImport"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<div class="api-debugger-container">
|
||||
<ApiTest
|
||||
ref="apiTestRef"
|
||||
:selected-interface="selectedInterface"
|
||||
:selected-service-provider="selectedServiceProvider"
|
||||
:enable-defaults="enableDefaults"
|
||||
@ -11,6 +12,7 @@
|
||||
<script lang="ts" setup>
|
||||
import ApiTest from '@/components/ApiTest/index.vue';
|
||||
import { getInterfaceGetInterfaceById } from '@/api/InterfaceServices/interface';
|
||||
import { ref, computed, watch, nextTick } from 'vue';
|
||||
|
||||
defineOptions({
|
||||
name: 'ApiDebugger',
|
||||
@ -21,8 +23,11 @@
|
||||
selectedInterface?: any;
|
||||
selectedServiceProvider?: any;
|
||||
enableDefaults?: boolean; // 是否启用默认值
|
||||
curlToImport?: string; // 从接口明细请求容器导入的 cURL 内容
|
||||
}>();
|
||||
|
||||
const apiTestRef = ref<InstanceType<typeof ApiTest> | null>(null);
|
||||
|
||||
// 为enableDefaults设置默认值
|
||||
const enableDefaults = computed(() => props.enableDefaults ?? true);
|
||||
|
||||
@ -62,9 +67,6 @@
|
||||
if (interfaceData) {
|
||||
console.log('ApiDebugger: interfaceData:', 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 type:', typeof (interfaceData as any)?.contentType);
|
||||
console.log('ApiDebugger: requestMethod:', (interfaceData as any)?.requestMethod);
|
||||
@ -90,18 +92,16 @@
|
||||
console.log('ApiDebugger: selectedInterface changed:', newVal);
|
||||
if (newVal && 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);
|
||||
|
||||
// 如果传递的接口对象不完整(没有关键字段),则获取完整详情
|
||||
// 如果传递的接口对象不完整(没有关键字段),则获取完整详情;body 不再从接口明细加载,仅从导入的 cURL 的 --data-raw 显示
|
||||
const interfaceObj = newVal as any;
|
||||
const hasDemoData = !!(interfaceObj?.inParams || interfaceObj?.requestExample || interfaceObj?.demoData);
|
||||
const hasContentType = !!interfaceObj?.contentType;
|
||||
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...');
|
||||
await fetchInterfaceDetail(newVal.id);
|
||||
} else {
|
||||
@ -117,6 +117,16 @@
|
||||
watch(() => props.selectedServiceProvider, (newVal) => {
|
||||
console.log('ApiDebugger: selectedServiceProvider changed:', newVal);
|
||||
}, { 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>
|
||||
|
||||
<style scoped lang="less">
|
||||
|
||||
@ -609,6 +609,7 @@
|
||||
init,
|
||||
submit: saveInterface,
|
||||
saveInterface,
|
||||
getRequestExample: () => interfaceDetailRef.value?.getRequestExample?.() || '',
|
||||
});
|
||||
</script>
|
||||
|
||||
@ -629,7 +630,11 @@
|
||||
:show-query="false"
|
||||
:show-return-config="false"
|
||||
@update:basic-form-data="(val) => Object.assign(tableForm, val)"
|
||||
/>
|
||||
>
|
||||
<template v-if="$slots.headerActions" #headerActions>
|
||||
<slot name="headerActions" />
|
||||
</template>
|
||||
</InterfaceDetail>
|
||||
</div>
|
||||
</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调用不同的接口
|
||||
async function handleTabSave() {
|
||||
// 使用独立的currentActiveTab变量,在handleTabChange中实时更新
|
||||
@ -1048,22 +1066,19 @@ import { IconClose, IconEdit, IconPlus, IconSearch, IconMore, IconMinus, IconRef
|
||||
setTimeout(checkAndInit, 200);
|
||||
}
|
||||
|
||||
// 监听 interfaceDetailModalRef 变化,当组件挂载后自动初始化
|
||||
// 监听 interfaceDetailModalRef 变化,当组件挂载后自动初始化(detail 用 v-show 保持挂载,需在有 selectedInterface 时初始化以便切换调试 tab 时能获取 cURL)
|
||||
watch(interfaceDetailModalRef, async (newRef, oldRef) => {
|
||||
// 只有当 ref 从 undefined/null 变为有值时,才初始化(避免重复初始化)
|
||||
if (newRef && !oldRef && selectedInterface.value && activeContentTab.value === 'detail') {
|
||||
if (newRef && !oldRef && selectedInterface.value && newRef.init) {
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
setTimeout(() => {
|
||||
// 再次检查条件,可能在等待过程中状态已改变
|
||||
if (activeContentTab.value === 'detail' && selectedInterface.value && newRef && newRef.init) {
|
||||
if (selectedInterface.value && newRef && newRef.init) {
|
||||
try {
|
||||
newRef.init({
|
||||
id: selectedInterface.value.id,
|
||||
interfaceId: selectedInterface.value.id,
|
||||
});
|
||||
} catch (error) {
|
||||
// 初始化失败,静默处理
|
||||
console.error('初始化接口明细组件失败:', error);
|
||||
}
|
||||
}
|
||||
@ -1074,7 +1089,9 @@ import { IconClose, IconEdit, IconPlus, IconSearch, IconMore, IconMinus, IconRef
|
||||
// 监听 activeContentTab 变化,同步更新 currentActiveTab
|
||||
watch(activeContentTab, async (newTab) => {
|
||||
currentActiveTab.value = newTab;
|
||||
if (newTab === 'detail' && selectedInterface.value) {
|
||||
if (newTab === 'debugger') {
|
||||
importCurlToDebugger();
|
||||
} else if (newTab === 'detail' && selectedInterface.value) {
|
||||
// 等待 DOM 更新完成后再初始化
|
||||
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) => {
|
||||
if (newInterface && activeContentTab.value === 'detail') {
|
||||
// 等待 DOM 更新完成后再初始化
|
||||
if (newInterface) {
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
setTimeout(() => {
|
||||
initInterfaceConfig();
|
||||
}, 100);
|
||||
if (interfaceDetailModalRef.value?.init && selectedInterface.value) {
|
||||
try {
|
||||
interfaceDetailModalRef.value.init({
|
||||
id: selectedInterface.value.id,
|
||||
interfaceId: selectedInterface.value.id,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('初始化接口明细组件失败:', error);
|
||||
}
|
||||
}
|
||||
}, activeContentTab.value === 'detail' ? 100 : 300);
|
||||
}
|
||||
}, { immediate: false });
|
||||
|
||||
@ -1422,7 +1447,7 @@ import { IconClose, IconEdit, IconPlus, IconSearch, IconMore, IconMinus, IconRef
|
||||
</div>
|
||||
<div class="tabs-extra">
|
||||
<a-button
|
||||
v-if="activeContentTab === 'mapping' || activeContentTab === 'detail'"
|
||||
v-if="activeContentTab === 'mapping'"
|
||||
size="small"
|
||||
type="primary"
|
||||
@click="handleTabSave"
|
||||
@ -1455,16 +1480,26 @@ import { IconClose, IconEdit, IconPlus, IconSearch, IconMore, IconMinus, IconRef
|
||||
:selected-interface="selectedInterface"
|
||||
:selected-service-provider="MapServiceProviderROW"
|
||||
:enable-defaults="true"
|
||||
:curl-to-import="curlToImport"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 接口明细内容 -->
|
||||
<div v-if="activeContentTab === 'detail' && selectedInterface" class="detail-content">
|
||||
<!-- 接口明细内容(v-show 保持挂载,便于切换至调试 tab 时获取 cURL) -->
|
||||
<div v-show="activeContentTab === 'detail' && selectedInterface" class="detail-content">
|
||||
<UpstreamInterfaceModal
|
||||
:key="`${selectedInterface?.id}-${detailRefreshKey}`"
|
||||
ref="interfaceDetailModalRef"
|
||||
@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>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user