All checks were successful
Build and Deploy Front / deploy (push) Successful in 6m0s
页面背景默认改为默认灰并同步启动屏;顶栏/登录头可回官网;线路卡片与话题面板顾问入口改为图标。 Co-authored-by: Cursor <cursoragent@cursor.com>
3180 lines
98 KiB
Vue
3180 lines
98 KiB
Vue
<script lang="ts" setup>
|
||
import { ref, reactive, computed, nextTick, watch, h, defineAsyncComponent } from 'vue';
|
||
import type { PropType } from 'vue';
|
||
import { IconDelete } from '@arco-design/web-vue/es/icon';
|
||
import { Message, Modal } from '@arco-design/web-vue';
|
||
import TablePro from '@/components/TablePro/index.vue';
|
||
import CodeMirror from '@/components/CodeMirror/index.vue';
|
||
import { useI18n } from 'vue-i18n';
|
||
import { random } from 'lodash';
|
||
import { $baseMessage } from '@/utils';
|
||
import JSON5 from 'json5';
|
||
import { validRulesEdit } from '@/views/api/upstreamInterface/config';
|
||
import { getDictionaryGetList } from '@/api/ApplicationServices/dictionary';
|
||
import { getInterfaceGetInterfaceById } from '@/api/InterfaceServices/interface';
|
||
import { getMethodText } from '@/views/api/upstreamInterface/utils';
|
||
import { useTabRowCounts } from '@/utils/useTabRowCounts';
|
||
import { prodApiOrigin } from '@/utils/rewriteBrowserApiPath';
|
||
|
||
/** 请求日志页含 Handsontable,仅打开「查看报文」时再加载 */
|
||
const RequestLog = defineAsyncComponent(() => import('@/views/api/requestLog/index.vue'));
|
||
|
||
const { t } = useI18n();
|
||
|
||
defineOptions({
|
||
name: 'ApiTest',
|
||
});
|
||
|
||
// 接收父组件传递的接口和服务商信息(可选)
|
||
const props = defineProps({
|
||
selectedInterface: {
|
||
type: Object as PropType<any>,
|
||
default: undefined,
|
||
},
|
||
selectedServiceProvider: {
|
||
type: Object as PropType<any>,
|
||
default: undefined,
|
||
},
|
||
enableDefaults: {
|
||
type: Boolean,
|
||
default: true, // 默认启用默认值设置
|
||
},
|
||
});
|
||
|
||
function getUpstreamInterfaceCode(): string {
|
||
return props.selectedInterface?.code || '';
|
||
}
|
||
|
||
// 映射的服务商接口 code(上游接口页面传入的 mapping 行通常在 serviceProviderCode 字段)
|
||
function getMappedServiceProviderInterfaceCode(): string {
|
||
return (
|
||
props.selectedServiceProvider?.serviceProviderCode ||
|
||
props.selectedServiceProvider?.code ||
|
||
''
|
||
);
|
||
}
|
||
|
||
// 导入 cURL 后:如果 query 里包含 standardCode / supplierCode,则按规则覆盖其值
|
||
function applyQueryCodeOverrides() {
|
||
if (!queryTableProps.data || queryTableProps.data.length === 0) return;
|
||
const standardCodeItem = queryTableProps.data.find((item) => item.name === 'standardCode');
|
||
const supplierCodeItem = queryTableProps.data.find((item) => item.name === 'supplierCode');
|
||
|
||
if (standardCodeItem) {
|
||
standardCodeItem.value = getUpstreamInterfaceCode();
|
||
}
|
||
if (supplierCodeItem) {
|
||
supplierCodeItem.value = getMappedServiceProviderInterfaceCode();
|
||
}
|
||
}
|
||
|
||
// 标记是否已从接口获取地址
|
||
const hasLoadedFromInterface = ref(false);
|
||
|
||
|
||
// HTTP 方法列表
|
||
const httpMethods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'];
|
||
const selectedMethod = ref('POST');
|
||
const apiUrl = ref('');
|
||
|
||
// 请求体 tabs(Query、Request 和 Header)
|
||
const requestTab = ref<'query' | 'request' | 'header'>('request');
|
||
|
||
const PANEL_LAYOUT_KEY = 'oms-api-test-panel-layout';
|
||
const panelLayout = ref<'horizontal' | 'vertical'>(
|
||
typeof localStorage !== 'undefined' && localStorage.getItem(PANEL_LAYOUT_KEY) === 'horizontal'
|
||
? 'horizontal'
|
||
: 'vertical',
|
||
);
|
||
function setPanelLayout(layout: 'horizontal' | 'vertical') {
|
||
panelLayout.value = layout;
|
||
try {
|
||
localStorage.setItem(PANEL_LAYOUT_KEY, layout);
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
function onPanelLayoutSwitch(checked: string | number | boolean) {
|
||
setPanelLayout(checked ? 'vertical' : 'horizontal');
|
||
}
|
||
|
||
// 返回体 tabs
|
||
const responseTab = ref<'response' | 'curl'>('response');
|
||
|
||
// Body 相关配置(需要在 watch 之前定义)
|
||
const bodyConfig = reactive({
|
||
bodyType: 'none',
|
||
body: '',
|
||
formData: [] as any[],
|
||
urlEncoded: [] as any[],
|
||
rawBody: '',
|
||
});
|
||
|
||
// 参数类型列表
|
||
const ParameterTypeList = ref<any[]>([]);
|
||
|
||
// 加载参数类型列表
|
||
async function loadParameterTypeList() {
|
||
try {
|
||
const response: any = await getDictionaryGetList({
|
||
type: 'ParameterType',
|
||
});
|
||
ParameterTypeList.value = response?.data || response || [];
|
||
} catch {
|
||
// 静默忽略加载失败
|
||
}
|
||
}
|
||
|
||
// 初始化时加载参数类型列表
|
||
loadParameterTypeList();
|
||
|
||
// 初始化默认参数
|
||
function initDefaultParams() {
|
||
// 设置 Header 默认值
|
||
if (!headerTableProps.data || headerTableProps.data.length === 0) {
|
||
headerTableProps.data = [
|
||
{
|
||
id: random(0, 2099999999),
|
||
name: 'appkey',
|
||
type: 1, // string类型
|
||
value: 'W2507080',
|
||
description: '应用密钥'
|
||
},
|
||
{
|
||
id: random(0, 2099999999),
|
||
name: 'appSecret',
|
||
type: 1, // string类型
|
||
value: 'G1rA80mAfGovH7j53t5bIH425M4vKreT',
|
||
description: '应用密钥'
|
||
},
|
||
{
|
||
id: random(0, 2099999999),
|
||
name: 'followName',
|
||
type: 1, // string类型
|
||
value: '0',
|
||
description: '跟踪名称'
|
||
}
|
||
];
|
||
}
|
||
|
||
// 设置 Query 默认值
|
||
if (!queryTableProps.data || queryTableProps.data.length === 0) {
|
||
const interfaceCode = getUpstreamInterfaceCode();
|
||
const serviceProviderCode = getMappedServiceProviderInterfaceCode();
|
||
|
||
queryTableProps.data = [
|
||
{
|
||
id: random(0, 2099999999),
|
||
name: 'standardCode',
|
||
type: 1, // string类型
|
||
value: interfaceCode,
|
||
description: '标准代码'
|
||
},
|
||
{
|
||
id: random(0, 2099999999),
|
||
name: 'supplierCode',
|
||
type: 1, // string类型
|
||
value: serviceProviderCode,
|
||
description: '供应商代码'
|
||
}
|
||
];
|
||
}
|
||
}
|
||
|
||
// 初始化默认参数(移到所有变量声明之后)
|
||
// initDefaultParams(); // 暂时注释,稍后在合适位置调用
|
||
|
||
// 将JSON对象扁平化为键值对数组的函数
|
||
function flattenJsonToKeyValuePairs(obj: any, prefix: string = '', result: any[] = []): any[] {
|
||
if (obj === null || obj === undefined) {
|
||
return result;
|
||
}
|
||
|
||
if (typeof obj === 'object' && !Array.isArray(obj)) {
|
||
// 对象类型,递归处理
|
||
Object.keys(obj).forEach(key => {
|
||
const newKey = prefix ? `${prefix}.${key}` : key;
|
||
flattenJsonToKeyValuePairs(obj[key], newKey, result);
|
||
});
|
||
} else if (Array.isArray(obj)) {
|
||
// 数组类型,转换为索引形式
|
||
obj.forEach((item, index) => {
|
||
const newKey = `${prefix}[${index}]`;
|
||
if (typeof item === 'object') {
|
||
flattenJsonToKeyValuePairs(item, newKey, result);
|
||
} else {
|
||
result.push({
|
||
key: newKey,
|
||
value: item,
|
||
description: `数组项 ${index}`
|
||
});
|
||
}
|
||
});
|
||
} else {
|
||
// 基本类型
|
||
result.push({
|
||
key: prefix,
|
||
value: obj,
|
||
description: ''
|
||
});
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
// 请求发送前去除 // 和 /* */ 注释,返回可用的 JSON 字符串
|
||
function stripCommentsForRequest(bodyStr: string): string {
|
||
if (!bodyStr || typeof bodyStr !== 'string') return bodyStr || '';
|
||
// 先尝试直接解析(用户编辑的、已含真实换行的内容)
|
||
try {
|
||
const parsed = JSON5.parse(bodyStr);
|
||
return JSON.stringify(parsed);
|
||
} catch {
|
||
// 解析失败时,处理后端返回的转义格式(\n \t \" 等),再解析
|
||
try {
|
||
const processed = bodyStr
|
||
.replace(/\\r\\n/g, '\n')
|
||
.replace(/\\n/g, '\n')
|
||
.replace(/\\r/g, '\r')
|
||
.replace(/\\t/g, '\t')
|
||
.replace(/\\\\/g, '\\')
|
||
.replace(/\\"/g, '"');
|
||
const parsed = JSON5.parse(processed);
|
||
return JSON.stringify(parsed);
|
||
} catch {
|
||
return bodyStr;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 处理 inParams 数据的专用函数
|
||
function processInParamsData(inParams: any): string {
|
||
if (!inParams) return '';
|
||
|
||
// 如果是对象,直接格式化
|
||
if (typeof inParams === 'object') {
|
||
return JSON.stringify(inParams, null, 2);
|
||
}
|
||
|
||
// 如果不是字符串,返回原值
|
||
if (typeof inParams !== 'string') {
|
||
return String(inParams);
|
||
}
|
||
|
||
try {
|
||
// 步骤1: 处理转义字符 - 将字面量的转义序列转换为实际字符
|
||
let processed = inParams;
|
||
|
||
// 按顺序处理转义字符(重要:顺序不能错)
|
||
processed = processed.replace(/\\r\\n/g, '\n'); // Windows 换行符转义 -> 实际换行
|
||
processed = processed.replace(/\\n/g, '\n'); // Unix 换行符转义 -> 实际换行
|
||
processed = processed.replace(/\\r/g, '\r'); // 回车符转义 -> 实际回车
|
||
processed = processed.replace(/\\t/g, '\t'); // 制表符转义 -> 实际制表符
|
||
processed = processed.replace(/\\\\/g, '\\'); // 反斜杠转义 -> 实际反斜杠
|
||
processed = processed.replace(/\\"/g, '"'); // 双引号转义 -> 实际双引号
|
||
|
||
// 步骤2: 使用 JSON5 解析(支持单引号、尾逗号、注释等宽松语法)
|
||
const parsedObject = JSON5.parse(processed);
|
||
|
||
// 步骤3: 格式化为美观的 JSON 字符串(2 空格缩进)
|
||
return JSON.stringify(parsedObject, null, 2);
|
||
|
||
} catch {
|
||
// 如果处理失败,返回原始数据
|
||
return inParams;
|
||
}
|
||
}
|
||
|
||
// 获取参数类型名称
|
||
function getParameterTypeName(type: any) {
|
||
const paramType = ParameterTypeList.value.find(item => item.key === type);
|
||
return paramType ? paramType.value : type;
|
||
}
|
||
|
||
// Query 参数表格配置(使用 InterfaceDetail 的配置)
|
||
const queryTableRef = ref();
|
||
const queryTableProps = reactive({
|
||
columns: [
|
||
{
|
||
field: 'name',
|
||
title: '参数名',
|
||
editRender: { name: '$input', props: { placeholder: '请输入参数名' } },
|
||
slots: { default: 'name', edit: 'name_edit' },
|
||
},
|
||
{
|
||
field: 'type',
|
||
title: '参数类型',
|
||
editRender: {},
|
||
slots: { default: 'type', edit: 'type_edit' },
|
||
},
|
||
{
|
||
field: 'value',
|
||
title: '参数值',
|
||
editRender: { name: '$input', props: { placeholder: '请输入参数值' } },
|
||
slots: { default: 'value', edit: 'value_edit' },
|
||
},
|
||
{
|
||
field: 'description',
|
||
title: '参数描述',
|
||
editRender: { name: '$input', props: { placeholder: '请输入描述' } },
|
||
slots: { default: 'description', edit: 'description_edit' },
|
||
},
|
||
{
|
||
field: 'action',
|
||
title: '操作',
|
||
fixed: 'right',
|
||
align: 'center',
|
||
width: 100,
|
||
slots: { default: 'action' },
|
||
},
|
||
],
|
||
isCellSort: false,
|
||
showToolbar: false,
|
||
isColumnsDrag: false,
|
||
showPager: false,
|
||
tableConfig: {
|
||
editConfig: {
|
||
trigger: 'click',
|
||
mode: 'row',
|
||
showStatus: true,
|
||
autoClear: false,
|
||
},
|
||
},
|
||
showIndex: false,
|
||
data: [] as any[],
|
||
minHeight: 150,
|
||
keepSource: true,
|
||
size: 'small',
|
||
useForm: false,
|
||
autoHeight: false,
|
||
height: '100%',
|
||
editRules: validRulesEdit.value,
|
||
toolbarConfig: {
|
||
setting: false
|
||
},
|
||
});
|
||
|
||
// 判断行是否处于编辑状态(Query)
|
||
function isQueryRowEditing(row: any): boolean {
|
||
if (!row || !queryTableRef.value) return false;
|
||
try {
|
||
return queryTableRef.value.isEditByRow(row);
|
||
} catch (error) {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
// 新增 Query 参数
|
||
async function addNewQueryParam() {
|
||
try {
|
||
const record = {
|
||
id: random(0, 2099999999),
|
||
name: '',
|
||
type: 1,
|
||
description: '',
|
||
};
|
||
|
||
await nextTick();
|
||
const { row: newRow } = await queryTableRef.value!.addRow(record);
|
||
await nextTick();
|
||
await queryTableRef.value?.editRowEvent(newRow);
|
||
refreshTabCounts();
|
||
} catch {
|
||
// 静默忽略
|
||
}
|
||
}
|
||
|
||
// 删除 Query 参数行
|
||
async function deleteQueryRow(row: any) {
|
||
await queryTableRef.value?.removeRow(row);
|
||
refreshTabCounts();
|
||
}
|
||
|
||
// Request 参数表格配置(与 Query 相同结构)
|
||
const requestTableRef = ref();
|
||
const requestTableProps = reactive({
|
||
columns: [
|
||
{
|
||
field: 'name',
|
||
title: '参数名',
|
||
editRender: { name: '$input', props: { placeholder: '请输入参数名' } },
|
||
slots: { default: 'name', edit: 'name_edit' },
|
||
},
|
||
{
|
||
field: 'type',
|
||
title: '参数类型',
|
||
editRender: {},
|
||
slots: { default: 'type', edit: 'type_edit' },
|
||
},
|
||
{
|
||
field: 'value',
|
||
title: '参数值',
|
||
editRender: { name: '$input', props: { placeholder: '请输入参数值' } },
|
||
slots: { default: 'value', edit: 'value_edit' },
|
||
},
|
||
{
|
||
field: 'description',
|
||
title: '参数描述',
|
||
editRender: { name: '$input', props: { placeholder: '请输入描述' } },
|
||
slots: { default: 'description', edit: 'description_edit' },
|
||
},
|
||
{
|
||
field: 'action',
|
||
title: '操作',
|
||
fixed: 'right',
|
||
align: 'center',
|
||
width: 100,
|
||
slots: { default: 'action' },
|
||
},
|
||
],
|
||
showToolbar: false,
|
||
showPager: false,
|
||
tableConfig: {
|
||
editConfig: {
|
||
trigger: 'click',
|
||
mode: 'row',
|
||
showStatus: true,
|
||
autoClear: false,
|
||
},
|
||
},
|
||
showIndex: false,
|
||
data: [] as any[],
|
||
minHeight: 200,
|
||
keepSource: true,
|
||
size: 'small',
|
||
useForm: false,
|
||
autoHeight: false,
|
||
height: '100%',
|
||
});
|
||
|
||
// Header 参数表格配置(使用 Query 的配置)
|
||
const headerTableRef = ref();
|
||
const headerTableProps = reactive({
|
||
columns: [
|
||
{
|
||
field: 'name',
|
||
title: '参数名',
|
||
editRender: { name: '$input', props: { placeholder: '请输入参数名' } },
|
||
slots: { default: 'name', edit: 'name_edit' },
|
||
},
|
||
{
|
||
field: 'type',
|
||
title: '参数类型',
|
||
editRender: {},
|
||
slots: { default: 'type', edit: 'type_edit' },
|
||
},
|
||
{
|
||
field: 'value',
|
||
title: '参数值',
|
||
editRender: { name: '$input', props: { placeholder: '请输入参数值' } },
|
||
slots: { default: 'value', edit: 'value_edit' },
|
||
},
|
||
{
|
||
field: 'description',
|
||
title: '参数描述',
|
||
editRender: { name: '$input', props: { placeholder: '请输入描述' } },
|
||
slots: { default: 'description', edit: 'description_edit' },
|
||
},
|
||
{
|
||
field: 'action',
|
||
title: '操作',
|
||
fixed: 'right',
|
||
align: 'center',
|
||
width: 100,
|
||
slots: { default: 'action' },
|
||
},
|
||
],
|
||
isCellSort: false,
|
||
showToolbar: false,
|
||
isColumnsDrag: false,
|
||
showPager: false,
|
||
tableConfig: {
|
||
editConfig: {
|
||
trigger: 'click',
|
||
mode: 'row',
|
||
showStatus: true,
|
||
autoClear: false,
|
||
},
|
||
},
|
||
showIndex: false,
|
||
data: [] as any[],
|
||
minHeight: 150,
|
||
keepSource: true,
|
||
size: 'small',
|
||
useForm: false,
|
||
autoHeight: false,
|
||
height: '100%',
|
||
editRules: validRulesEdit.value,
|
||
toolbarConfig: {
|
||
setting: false
|
||
},
|
||
});
|
||
|
||
// 判断行是否处于编辑状态(Header)
|
||
function isHeaderRowEditing(row: any): boolean {
|
||
if (!row || !headerTableRef.value) return false;
|
||
try {
|
||
return headerTableRef.value.isEditByRow(row);
|
||
} catch (error) {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
// 新增 Header 参数
|
||
async function addNewHeaderParam() {
|
||
try {
|
||
const record = {
|
||
id: random(0, 2099999999),
|
||
name: '',
|
||
type: 1,
|
||
description: '',
|
||
};
|
||
|
||
await nextTick();
|
||
const { row: newRow } = await headerTableRef.value!.addRow(record);
|
||
await nextTick();
|
||
await headerTableRef.value?.editRowEvent(newRow);
|
||
refreshTabCounts();
|
||
} catch {
|
||
// 静默忽略
|
||
}
|
||
}
|
||
|
||
// 删除 Header 参数行
|
||
async function deleteHeaderRow(row: any) {
|
||
await headerTableRef.value?.removeRow(row);
|
||
refreshTabCounts();
|
||
}
|
||
|
||
// 响应数据
|
||
const responseData = ref('');
|
||
const curlCommand = ref('');
|
||
const isLoading = ref(false);
|
||
const response = ref<any>(null);
|
||
|
||
// 格式化响应状态信息
|
||
const responseStatusText = computed(() => {
|
||
if (!response.value) return '';
|
||
const { status, duration } = response.value;
|
||
if (status === 0) {
|
||
return `请求失败,耗时: ${duration}ms`;
|
||
}
|
||
const isSuccess = status >= 200 && status < 300;
|
||
return `${isSuccess ? '请求成功' : '请求失败'}!状态码: ${status},耗时: ${duration}ms`;
|
||
});
|
||
|
||
// x-www-form-urlencoded 表格配置(使用 Query 样式)
|
||
const urlEncodedTableRef = ref();
|
||
const urlEncodedTableProps = reactive({
|
||
columns: [
|
||
{
|
||
field: 'name',
|
||
title: '参数名',
|
||
editRender: { name: '$input', props: { placeholder: '请输入参数名' } },
|
||
slots: { default: 'name', edit: 'name_edit' },
|
||
},
|
||
{
|
||
field: 'type',
|
||
title: '参数类型',
|
||
editRender: {},
|
||
slots: { default: 'type', edit: 'type_edit' },
|
||
},
|
||
{
|
||
field: 'value',
|
||
title: '参数值',
|
||
editRender: { name: '$input', props: { placeholder: '请输入参数值' } },
|
||
slots: { default: 'value', edit: 'value_edit' },
|
||
},
|
||
{
|
||
field: 'description',
|
||
title: '参数描述',
|
||
editRender: { name: '$input', props: { placeholder: '请输入描述' } },
|
||
slots: { default: 'description', edit: 'description_edit' },
|
||
},
|
||
{
|
||
field: 'action',
|
||
title: '操作',
|
||
fixed: 'right',
|
||
align: 'center',
|
||
width: 120,
|
||
slots: { default: 'action' },
|
||
},
|
||
],
|
||
isCellSort: false,
|
||
showToolbar: false,
|
||
isColumnsDrag: false,
|
||
showPager: false,
|
||
tableConfig: {
|
||
editConfig: {
|
||
trigger: 'click',
|
||
mode: 'row',
|
||
showStatus: true,
|
||
autoClear: false,
|
||
},
|
||
},
|
||
showIndex: false,
|
||
data: [] as any[],
|
||
minHeight: 150,
|
||
keepSource: true,
|
||
size: 'small',
|
||
useForm: false,
|
||
autoHeight: false,
|
||
height: '100%',
|
||
editRules: validRulesEdit.value,
|
||
toolbarConfig: {
|
||
setting: false
|
||
},
|
||
});
|
||
|
||
// form-data 表格配置(使用 Query 样式)
|
||
const formDataTableRef = ref();
|
||
const formDataTableProps = reactive({
|
||
columns: [
|
||
{
|
||
field: 'name',
|
||
title: '参数名',
|
||
editRender: { name: '$input', props: { placeholder: '请输入参数名' } },
|
||
slots: { default: 'name', edit: 'name_edit' },
|
||
},
|
||
{
|
||
field: 'type',
|
||
title: '参数类型',
|
||
editRender: {},
|
||
slots: { default: 'type', edit: 'type_edit' },
|
||
},
|
||
{
|
||
field: 'value',
|
||
title: '参数值',
|
||
editRender: { name: '$input', props: { placeholder: '请输入参数值' } },
|
||
slots: { default: 'value', edit: 'value_edit' },
|
||
},
|
||
{
|
||
field: 'description',
|
||
title: '参数描述',
|
||
editRender: { name: '$input', props: { placeholder: '请输入描述' } },
|
||
slots: { default: 'description', edit: 'description_edit' },
|
||
},
|
||
{
|
||
field: 'action',
|
||
title: '操作',
|
||
fixed: 'right',
|
||
align: 'center',
|
||
width: 120,
|
||
slots: { default: 'action' },
|
||
},
|
||
],
|
||
isCellSort: false,
|
||
showToolbar: false,
|
||
isColumnsDrag: false,
|
||
showPager: false,
|
||
tableConfig: {
|
||
editConfig: {
|
||
trigger: 'click',
|
||
mode: 'row',
|
||
showStatus: true,
|
||
autoClear: false,
|
||
},
|
||
},
|
||
showIndex: false,
|
||
data: [] as any[],
|
||
minHeight: 150,
|
||
keepSource: true,
|
||
size: 'small',
|
||
useForm: false,
|
||
autoHeight: false,
|
||
height: '100%',
|
||
editRules: validRulesEdit.value,
|
||
toolbarConfig: {
|
||
setting: false
|
||
},
|
||
});
|
||
|
||
const { refresh: refreshTabCounts, countFromTable, countTextBody } = useTabRowCounts();
|
||
|
||
const queryTabCount = computed(() =>
|
||
countFromTable(queryTableRef, () => queryTableProps.data),
|
||
);
|
||
const headerTabCount = computed(() =>
|
||
countFromTable(headerTableRef, () => headerTableProps.data),
|
||
);
|
||
const bodyTabCount = computed(() => {
|
||
const type = bodyConfig.bodyType;
|
||
if (type === 'x-www-form-urlencoded') {
|
||
return countFromTable(urlEncodedTableRef, () => urlEncodedTableProps.data);
|
||
}
|
||
if (type === 'form-data') {
|
||
return countFromTable(formDataTableRef, () => formDataTableProps.data);
|
||
}
|
||
if (type === 'json' || type === 'xml') {
|
||
return countTextBody(bodyConfig.body);
|
||
}
|
||
return 0;
|
||
});
|
||
|
||
const requestTabItems = computed(() => [
|
||
{ key: 'query', label: `Query (${queryTabCount.value})` },
|
||
{ key: 'request', label: `Body (${bodyTabCount.value})` },
|
||
{ key: 'header', label: `Header (${headerTabCount.value})` },
|
||
]);
|
||
|
||
const responseTabItems = [
|
||
{ key: 'response', label: 'Response' },
|
||
{ key: 'curl', label: 'cURL' },
|
||
];
|
||
|
||
const bodyTypeItems = [
|
||
{ key: 'none', label: 'none' },
|
||
{ key: 'json', label: 'json' },
|
||
{ key: 'xml', label: 'xml' },
|
||
{ key: 'x-www-form-urlencoded', label: 'x-www-form-urlencoded' },
|
||
{ key: 'form-data', label: 'form-data' },
|
||
];
|
||
|
||
/** 空态提示(展示在类型行正中) */
|
||
const requestEmptyTip = computed(() => {
|
||
if (requestTab.value === 'query' && queryTabCount.value === 0) {
|
||
return '暂无 Query 参数,可点击右侧「新增」';
|
||
}
|
||
if (requestTab.value === 'header' && headerTabCount.value === 0) {
|
||
return '暂无 Header 参数,可点击右侧「新增」';
|
||
}
|
||
if (requestTab.value === 'request') {
|
||
if (bodyConfig.bodyType === 'none') return '当前为 none,无请求体';
|
||
if (
|
||
bodyConfig.bodyType === 'x-www-form-urlencoded'
|
||
&& countFromTable(urlEncodedTableRef, () => urlEncodedTableProps.data) === 0
|
||
) {
|
||
return '暂无表单字段,可点击右侧「新增」';
|
||
}
|
||
if (
|
||
bodyConfig.bodyType === 'form-data'
|
||
&& countFromTable(formDataTableRef, () => formDataTableProps.data) === 0
|
||
) {
|
||
return '暂无 FormData 字段,可点击右侧「新增」';
|
||
}
|
||
}
|
||
return '';
|
||
});
|
||
|
||
// 初始化默认参数(在所有变量声明之后调用)
|
||
if (props.enableDefaults) {
|
||
initDefaultParams();
|
||
}
|
||
|
||
// 监听接口变化,直接使用传递的接口数据
|
||
watch(() => props.selectedInterface, async (newInterface, oldInterface) => {
|
||
if (newInterface && newInterface !== oldInterface) {
|
||
// 0. 设置默认参数(如果启用默认值)
|
||
if (props.enableDefaults) {
|
||
initDefaultParams();
|
||
}
|
||
|
||
// 1. 切换到Request tab显示demo数据
|
||
requestTab.value = 'request';
|
||
|
||
// 延时确保tab切换完成
|
||
nextTick(() => {
|
||
// 这里不需要await,直接在nextTick回调中执行
|
||
});
|
||
|
||
// 1. 设置请求方式(从接口对象中读取)
|
||
if (newInterface.requestMethod !== undefined && newInterface.requestMethod !== null) {
|
||
const methodValue = newInterface.requestMethod;
|
||
|
||
// 如果已经是字符串形式的 HTTP 方法(GET, POST等),直接使用
|
||
if (typeof methodValue === 'string' && ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'].includes(methodValue.toUpperCase())) {
|
||
selectedMethod.value = methodValue.toUpperCase();
|
||
} else {
|
||
// requestMethod 是数字(字典key),需要转换为对应的文本值
|
||
const methodNum = Number(methodValue);
|
||
if (!isNaN(methodNum)) {
|
||
const methodText = getMethodText(methodNum as any);
|
||
if (methodText && methodText !== '未知') {
|
||
selectedMethod.value = methodText;
|
||
} else {
|
||
// 如果转换失败,尝试直接使用原值并转为大写
|
||
selectedMethod.value = String(methodValue).toUpperCase();
|
||
}
|
||
} else {
|
||
// 无法转换为数字,直接使用原值
|
||
selectedMethod.value = String(methodValue).toUpperCase();
|
||
}
|
||
}
|
||
}
|
||
|
||
// 2. 设置请求地址(从接口对象中读取)
|
||
if (newInterface.requestAddress) {
|
||
apiUrl.value = String(newInterface.requestAddress);
|
||
hasLoadedFromInterface.value = true;
|
||
}
|
||
|
||
// 3. 设置请求类型(从接口对象中读取 contentType)
|
||
let newBodyType = 'none';
|
||
if (newInterface.contentType !== undefined && newInterface.contentType !== null) {
|
||
const contentTypeNum = Number(newInterface.contentType);
|
||
switch (contentTypeNum) {
|
||
case 1:
|
||
newBodyType = 'xml';
|
||
break;
|
||
case 2:
|
||
newBodyType = 'x-www-form-urlencoded';
|
||
break;
|
||
case 3:
|
||
newBodyType = 'json';
|
||
break;
|
||
case 4:
|
||
newBodyType = 'form-data';
|
||
break;
|
||
default:
|
||
newBodyType = 'raw';
|
||
}
|
||
} else {
|
||
newBodyType = 'json';
|
||
}
|
||
|
||
bodyConfig.bodyType = newBodyType;
|
||
|
||
// 强制UI更新 - 多次尝试确保更新生效
|
||
nextTick(() => {
|
||
if (bodyConfig.bodyType !== newBodyType) {
|
||
bodyConfig.bodyType = newBodyType;
|
||
}
|
||
});
|
||
|
||
// 4. 不再从接口明细的 inParams/curl 示例加载 body 数据,body 仅通过「导入cURL」或「调试」按钮导入的 cURL 的 --data-raw 显示
|
||
|
||
// 5. 设置 Header 和 Query 的默认值(只在启用默认值且为空时设置)
|
||
if (props.enableDefaults && (!headerTableProps.data || headerTableProps.data.length === 0)) {
|
||
headerTableProps.data = [
|
||
{
|
||
id: random(0, 2099999999),
|
||
name: 'appkey',
|
||
type: 1, // string类型
|
||
value: 'W2507080',
|
||
description: '应用密钥'
|
||
},
|
||
{
|
||
id: random(0, 2099999999),
|
||
name: 'appSecret',
|
||
type: 1, // string类型
|
||
value: 'G1rA80mAfGovH7j53t5bIH425M4vKreT',
|
||
description: '应用密钥'
|
||
},
|
||
{
|
||
id: random(0, 2099999999),
|
||
name: 'followName',
|
||
type: 1, // string类型
|
||
value: '0',
|
||
description: '跟踪名称'
|
||
}
|
||
];
|
||
}
|
||
|
||
// 设置 Query 默认值(只在启用默认值且为空时设置)
|
||
if (props.enableDefaults && (!queryTableProps.data || queryTableProps.data.length === 0)) {
|
||
const interfaceCode = newInterface.code || '';
|
||
const serviceProviderCode = props.selectedServiceProvider?.serviceProviderCode || '';
|
||
|
||
queryTableProps.data = [
|
||
{
|
||
id: random(0, 2099999999),
|
||
name: 'standardCode',
|
||
type: 1, // string类型
|
||
value: interfaceCode,
|
||
description: '标准代码'
|
||
},
|
||
{
|
||
id: random(0, 2099999999),
|
||
name: 'supplierCode',
|
||
type: 1, // string类型
|
||
value: serviceProviderCode,
|
||
description: '供应商代码'
|
||
}
|
||
];
|
||
} else if (props.enableDefaults) {
|
||
// 如果已有数据且启用默认值,更新动态值
|
||
const standardCodeItem = queryTableProps.data.find(item => item.name === 'standardCode');
|
||
const supplierCodeItem = queryTableProps.data.find(item => item.name === 'supplierCode');
|
||
|
||
const interfaceCode = newInterface.code || '';
|
||
const serviceProviderCode = props.selectedServiceProvider?.serviceProviderCode || '';
|
||
|
||
if (standardCodeItem) {
|
||
standardCodeItem.value = interfaceCode;
|
||
}
|
||
if (supplierCodeItem) {
|
||
supplierCodeItem.value = serviceProviderCode;
|
||
}
|
||
}
|
||
}
|
||
}, { immediate: true });
|
||
|
||
// 监听服务商变化,更新supplierCode(只有启用默认值时)
|
||
watch(() => props.selectedServiceProvider?.serviceProviderCode, (newCode) => {
|
||
if (props.enableDefaults && newCode && queryTableProps.data) {
|
||
const supplierCodeItem = queryTableProps.data.find(item => item.name === 'supplierCode');
|
||
if (supplierCodeItem) {
|
||
supplierCodeItem.value = newCode;
|
||
}
|
||
}
|
||
}, { immediate: true });
|
||
|
||
// 判断行是否处于编辑状态(urlEncoded)
|
||
function isUrlEncodedRowEditing(row: any): boolean {
|
||
if (!row || !urlEncodedTableRef.value) return false;
|
||
try {
|
||
return urlEncodedTableRef.value.isEditByRow(row);
|
||
} catch (error) {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
// 判断行是否处于编辑状态(formData)
|
||
function isFormDataRowEditing(row: any): boolean {
|
||
if (!row || !formDataTableRef.value) return false;
|
||
try {
|
||
return formDataTableRef.value.isEditByRow(row);
|
||
} catch (error) {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
|
||
// 解析并导入 cURL 命令(可被外部调用)
|
||
function parseCurlCommand(curlCommand: string) {
|
||
try {
|
||
// 清理命令,但保留数据部分的多行结构
|
||
let cleanCurl = curlCommand.trim();
|
||
|
||
// 先处理多行命令的连接:Linux/Mac 用 \,Windows 用 ^
|
||
cleanCurl = cleanCurl.replace(/\\\s*\r?\n\s*/g, ' ');
|
||
cleanCurl = cleanCurl.replace(/\^\s*\r?\n\s*/g, ' ');
|
||
|
||
// 移除多余的空格,但保留数据部分
|
||
cleanCurl = cleanCurl.replace(/\s+/g, ' ');
|
||
|
||
// 解析HTTP方法
|
||
let method = 'GET';
|
||
const methodMatch = cleanCurl.match(/-X\s+(\w+)|--request\s+(\w+)/i);
|
||
if (methodMatch) {
|
||
method = (methodMatch[1] || methodMatch[2]).toUpperCase();
|
||
} else if (cleanCurl.includes('--location')) {
|
||
// 如果有--location但没有-X,可能是GET
|
||
method = 'GET';
|
||
}
|
||
selectedMethod.value = method;
|
||
|
||
// 解析URL - 支持单引号和双引号包围的URL
|
||
const urlMatch = cleanCurl.match(/(?:-X\s+\w+\s+)?['"]?((?:https?|http):\/\/[^'"\s]+)['"]?/);
|
||
if (urlMatch) {
|
||
const url = urlMatch[1];
|
||
try {
|
||
// 分离URL和查询参数
|
||
const urlObj = new URL(url);
|
||
apiUrl.value = `${urlObj.protocol}//${urlObj.host}${urlObj.pathname}${urlObj.search}`;
|
||
hasLoadedFromInterface.value = true;
|
||
|
||
// 解析查询参数
|
||
if (urlObj.searchParams.toString()) {
|
||
const queryParams: any[] = [];
|
||
urlObj.searchParams.forEach((value, key) => {
|
||
queryParams.push({
|
||
id: random(0, 2099999999),
|
||
name: key,
|
||
type: 1, // string
|
||
value: value,
|
||
description: ''
|
||
});
|
||
});
|
||
queryTableProps.data = queryParams;
|
||
applyQueryCodeOverrides();
|
||
}
|
||
} catch {
|
||
// 如果URL解析失败,直接使用原URL
|
||
apiUrl.value = url;
|
||
hasLoadedFromInterface.value = true;
|
||
}
|
||
}
|
||
|
||
// 解析Headers - 改进的解析逻辑
|
||
const headerRegex = /-H\s+['"]([^:]+):\s*([^'"]*(?:\\.[^'"]*)*)['"]|--header\s+['"]([^:]+):\s*([^'"]*(?:\\.[^'"]*)*)['"]/g;
|
||
const headers: any[] = [];
|
||
let headerMatch;
|
||
|
||
while ((headerMatch = headerRegex.exec(cleanCurl)) !== null) {
|
||
const name = headerMatch[1] || headerMatch[3];
|
||
const value = headerMatch[2] || headerMatch[4];
|
||
|
||
if (name) {
|
||
// 过滤掉Content-Type,因为后面单独处理
|
||
if (name.toLowerCase() !== 'content-type') {
|
||
headers.push({
|
||
id: random(0, 2099999999),
|
||
name: name.trim(),
|
||
type: 1, // string
|
||
value: (value || '').trim(),
|
||
description: ''
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
const pushHeaderIfMissing = (name: string, value: string) => {
|
||
const n = String(name || '').trim();
|
||
const v = String(value || '').trim();
|
||
if (!n || n.toLowerCase() === 'content-type') return;
|
||
if (headers.some((h) => String(h.name) === n && String(h.value) === v)) return;
|
||
headers.push({
|
||
id: random(0, 2099999999),
|
||
name: n,
|
||
type: 1, // string
|
||
value: v,
|
||
description: ''
|
||
});
|
||
};
|
||
|
||
// 兜底解析:-H k:v(不带引号)
|
||
// 适用于 value 不包含空格的常见情况;复杂 header 仍建议加引号。
|
||
const unquotedHeaderRegex = /(?:-H|--header)\s+([A-Za-z0-9._-]+)\s*:\s*([^\s\\]+)/g;
|
||
let unquotedHeaderMatch: RegExpExecArray | null;
|
||
while ((unquotedHeaderMatch = unquotedHeaderRegex.exec(cleanCurl)) !== null) {
|
||
pushHeaderIfMissing(unquotedHeaderMatch[1], unquotedHeaderMatch[2]);
|
||
}
|
||
|
||
if (headers.length > 0) {
|
||
headerTableProps.data = headers;
|
||
}
|
||
|
||
// 解析请求体
|
||
let bodyData = '';
|
||
let contentType = '';
|
||
|
||
// 检查Content-Type
|
||
const contentTypeMatch =
|
||
// -H "content-type:application/json"
|
||
cleanCurl.match(/-H\s+['"]content-type:\s*([^'"]+)['"]/i) ||
|
||
cleanCurl.match(/--header\s+['"]content-type:\s*([^'"]+)['"]/i) ||
|
||
// -H content-type:application/json(不带引号)
|
||
cleanCurl.match(/-H\s+content-type\s*:\s*([^\s\\]+)/i) ||
|
||
cleanCurl.match(/--header\s+content-type\s*:\s*([^\s\\]+)/i);
|
||
if (contentTypeMatch) {
|
||
contentType = contentTypeMatch[1].toLowerCase();
|
||
}
|
||
|
||
// 解析数据 - 改进的匹配逻辑
|
||
|
||
// 先处理 --data-urlencode(支持多个)
|
||
const urlEncodeItems: string[] = [];
|
||
// 匹配:--data-urlencode 'a=b' 或 --data-urlencode "a=b"
|
||
const dataUrlencodeQuotedRegex = /--data-urlencode\s+(['"])(.*?)\1/g;
|
||
let duMatch: RegExpExecArray | null;
|
||
while ((duMatch = dataUrlencodeQuotedRegex.exec(cleanCurl)) !== null) {
|
||
urlEncodeItems.push(duMatch[2]);
|
||
}
|
||
// 匹配未加引号的:--data-urlencode a=b(直到下一个 flag 或结束)
|
||
if (urlEncodeItems.length === 0) {
|
||
const dataUrlencodeUnquotedRegex = /--data-urlencode\s+([^\s][^]*?)(?=\s--|\s-$|$)/g;
|
||
let du2Match: RegExpExecArray | null;
|
||
while ((du2Match = dataUrlencodeUnquotedRegex.exec(cleanCurl)) !== null) {
|
||
urlEncodeItems.push(du2Match[1].trim());
|
||
}
|
||
}
|
||
|
||
// 查找--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];
|
||
}
|
||
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;
|
||
}
|
||
|
||
// 查找 -d "...." / -d '....'(你的场景经常是 -d " { ... } ")
|
||
// 如果 -d 用双引号包裹,但 body 内部包含未转义的双引号,
|
||
// 常规按引号截取会过早截断;因此这里优先按 JSON 结构提取。
|
||
function extractDashDValue(str: string): string | null {
|
||
const dashMatch = /(?:^|\s)-d\s+/i.exec(str);
|
||
if (!dashMatch || typeof dashMatch.index !== 'number') return null;
|
||
const start = dashMatch.index + dashMatch[0].length;
|
||
const part = str.substring(start).trim();
|
||
if (!part) return null;
|
||
|
||
const firstObjIdx = part.indexOf('{');
|
||
const lastObjIdx = part.lastIndexOf('}');
|
||
if (firstObjIdx !== -1 && lastObjIdx !== -1 && lastObjIdx > firstObjIdx) {
|
||
return part.substring(firstObjIdx, lastObjIdx + 1).trim();
|
||
}
|
||
|
||
const firstArrIdx = part.indexOf('[');
|
||
const lastArrIdx = part.lastIndexOf(']');
|
||
if (firstArrIdx !== -1 && lastArrIdx !== -1 && lastArrIdx > firstArrIdx) {
|
||
return part.substring(firstArrIdx, lastArrIdx + 1).trim();
|
||
}
|
||
|
||
return null;
|
||
}
|
||
const extracted = extractDataRawValue(cleanCurl);
|
||
if (extracted !== null) {
|
||
bodyData = extracted.replace(/\\"/g, '"');
|
||
}
|
||
|
||
if (!bodyData) {
|
||
const extractedDashD = extractDashDValue(cleanCurl);
|
||
if (extractedDashD) {
|
||
bodyData = extractedDashD;
|
||
}
|
||
}
|
||
|
||
// 若仍未解析到 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();
|
||
if (dataPart.startsWith("'") || dataPart.startsWith('"')) {
|
||
const quote = dataPart.charAt(0);
|
||
const endQuoteIndex = dataPart.indexOf(quote, 1);
|
||
if (endQuoteIndex !== -1) {
|
||
bodyData = dataPart.substring(1, endQuoteIndex);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 如果检测到任何 data 参数,但未显式指定 method,则 curl 实际上会变成 POST
|
||
const hasAnyData =
|
||
urlEncodeItems.length > 0 ||
|
||
cleanCurl.includes('--data-raw') ||
|
||
cleanCurl.includes('--data ') ||
|
||
cleanCurl.includes('--data=') ||
|
||
cleanCurl.includes('-d ') ||
|
||
cleanCurl.includes('-d"') ||
|
||
cleanCurl.includes("-d'") ||
|
||
cleanCurl.includes('--data-urlencode');
|
||
if (!methodMatch && hasAnyData) {
|
||
selectedMethod.value = 'POST';
|
||
}
|
||
|
||
if (urlEncodeItems.length > 0) {
|
||
// --data-urlencode 默认就是 x-www-form-urlencoded
|
||
bodyConfig.bodyType = 'x-www-form-urlencoded';
|
||
|
||
const formData: any[] = [];
|
||
urlEncodeItems.forEach((pair) => {
|
||
// 常见形式:key=value
|
||
const eqIndex = pair.indexOf('=');
|
||
if (eqIndex === -1) {
|
||
// 兜底:无法拆分时塞到 raw
|
||
formData.push({
|
||
id: random(0, 2099999999),
|
||
name: pair,
|
||
type: 1,
|
||
value: '',
|
||
description: ''
|
||
});
|
||
return;
|
||
}
|
||
const key = pair.substring(0, eqIndex);
|
||
const value = pair.substring(eqIndex + 1);
|
||
formData.push({
|
||
id: random(0, 2099999999),
|
||
name: key,
|
||
type: 1,
|
||
value: value,
|
||
description: ''
|
||
});
|
||
});
|
||
|
||
urlEncodedTableProps.data = formData;
|
||
} else if (bodyData) {
|
||
// 根据 Content-Type 或内容推断 bodyType
|
||
let inferredType: 'json' | 'xml' | 'x-www-form-urlencoded' | 'form-data' | 'raw' = 'raw';
|
||
if (contentType.includes('application/json')) {
|
||
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);
|
||
} else if (inferredType === 'xml') {
|
||
bodyConfig.bodyType = 'xml';
|
||
bodyConfig.body = bodyData;
|
||
} else if (inferredType === 'x-www-form-urlencoded') {
|
||
bodyConfig.bodyType = 'x-www-form-urlencoded';
|
||
try {
|
||
const formData: any[] = [];
|
||
const params = new URLSearchParams(bodyData);
|
||
params.forEach((value, key) => {
|
||
formData.push({
|
||
id: random(0, 2099999999),
|
||
name: key,
|
||
type: 1,
|
||
value: value,
|
||
description: ''
|
||
});
|
||
});
|
||
urlEncodedTableProps.data = formData;
|
||
} catch {
|
||
bodyConfig.bodyType = 'raw';
|
||
bodyConfig.rawBody = bodyData;
|
||
}
|
||
} else if (inferredType === 'form-data') {
|
||
bodyConfig.bodyType = 'form-data';
|
||
bodyConfig.body = bodyData;
|
||
} else {
|
||
bodyConfig.bodyType = 'raw';
|
||
bodyConfig.rawBody = bodyData;
|
||
}
|
||
} else {
|
||
bodyConfig.bodyType = 'none';
|
||
}
|
||
|
||
// 切换到Request tab
|
||
requestTab.value = 'request';
|
||
|
||
Message.success('cURL命令导入成功!');
|
||
|
||
// 兜底再覆盖一次(防止后续解析步骤改动 query)
|
||
applyQueryCodeOverrides();
|
||
refreshTabCounts();
|
||
|
||
} catch (error) {
|
||
Message.error(`cURL命令解析失败: ${error instanceof Error ? error.message : String(error)}`);
|
||
}
|
||
}
|
||
|
||
// 导入 cURL(打开 Modal 让用户粘贴)
|
||
const importCurl = () => {
|
||
const curlInput = ref('');
|
||
Modal.confirm({
|
||
title: '导入 cURL 命令',
|
||
content: () => h('div', [
|
||
h('p', { style: 'margin-bottom: 16px' }, '请粘贴完整的 cURL 命令:'),
|
||
h('textarea', {
|
||
value: curlInput.value,
|
||
onInput: (e: any) => curlInput.value = e.target.value,
|
||
style: {
|
||
width: '100%',
|
||
height: '200px',
|
||
padding: '8px',
|
||
border: '1px solid var(--color-border-2)',
|
||
borderRadius: '4px',
|
||
fontFamily: 'monospace',
|
||
fontSize: '12px'
|
||
},
|
||
placeholder: `curl --location --request POST "https://api.example.com/endpoint" \\
|
||
--header "Content-Type: application/json" \\
|
||
--header "Authorization: Bearer token" \\
|
||
--data-raw '{"key": "value"}'`
|
||
})
|
||
]),
|
||
okText: '导入',
|
||
cancelText: '取消',
|
||
width: 600,
|
||
onOk: () => {
|
||
if (!curlInput.value.trim()) {
|
||
Message.warning('请输入 cURL 命令');
|
||
return false;
|
||
}
|
||
parseCurlCommand(curlInput.value.trim());
|
||
return true;
|
||
},
|
||
onCancel: () => {
|
||
// Modal.confirm() 返回的对象没有 destroy 方法,直接关闭即可
|
||
}
|
||
});
|
||
};
|
||
|
||
/**
|
||
* 开发态:把已知外域 URL 改写到 Vite 代理,避免浏览器 CORS。
|
||
* - api.nanafa.com / api(.test).ideship.com → /nanafa/...
|
||
* - 旧 itfx IP → /api/...
|
||
* - lsp.globex.cn → /external/...
|
||
* 生产/测试构建:nanafa 旧址改写到当前环境 API 根(方案 A)
|
||
*/
|
||
function rewriteDebuggerUrlForProxy(rawUrl: string): string {
|
||
const url = String(rawUrl || '').trim();
|
||
if (!url) return url;
|
||
const apiOrigin = prodApiOrigin();
|
||
|
||
// http(s)://api.nanafa.com/... 或 api(.test).ideship.com/...
|
||
if (/^https?:\/\/api\.(?:nanafa|ideship)\.com(?::\d+)?(\/|$)/i.test(url)
|
||
|| /^https?:\/\/api\.test\.ideship\.com(?::\d+)?(\/|$)/i.test(url)) {
|
||
if (import.meta.env.DEV) {
|
||
return url
|
||
.replace(/^https?:\/\/api\.nanafa\.com(?::\d+)?/i, '/nanafa')
|
||
.replace(/^https?:\/\/api\.test\.ideship\.com(?::\d+)?/i, '/nanafa')
|
||
.replace(/^https?:\/\/api\.ideship\.com(?::\d+)?/i, '/nanafa');
|
||
}
|
||
return url
|
||
.replace(/^https?:\/\/api\.nanafa\.com(?::\d+)?/i, apiOrigin)
|
||
.replace(/^https?:\/\/api\.test\.ideship\.com(?::\d+)?/i, apiOrigin)
|
||
.replace(/^https?:\/\/api\.ideship\.com(?::\d+)?/i, apiOrigin);
|
||
}
|
||
|
||
// 旧 itfx 直连 IP
|
||
if (url.startsWith('http://43.139.210.35:8888')) {
|
||
if (import.meta.env.DEV) {
|
||
return url.replace('http://43.139.210.35:8888', '/api');
|
||
}
|
||
return url.replace('http://43.139.210.35:8888', `${apiOrigin}/itfx`);
|
||
}
|
||
|
||
// 示例外部服务商
|
||
if (url.startsWith('https://lsp.globex.cn/') || url === 'https://lsp.globex.cn') {
|
||
return url.replace('https://lsp.globex.cn', '/external');
|
||
}
|
||
|
||
return url;
|
||
}
|
||
|
||
// 发送请求(参考上游接口的实现)
|
||
const sendRequest = async () => {
|
||
if (!apiUrl.value.trim()) {
|
||
$baseMessage('请输入API地址', 'warning');
|
||
return;
|
||
}
|
||
|
||
isLoading.value = true;
|
||
const startTime = Date.now();
|
||
let requestUrl = '';
|
||
|
||
try {
|
||
// 构建URL参数 - 从表格数据中获取
|
||
const urlParams = new URLSearchParams();
|
||
const queryTableData = queryTableRef.value?.getTableData?.()?.fullData || queryTableRef.value?.xGrid?.getTableData()?.fullData || queryTableProps.data || [];
|
||
queryTableData.forEach((item: any) => {
|
||
if (item.name && item.value) {
|
||
urlParams.append(item.name, item.value);
|
||
}
|
||
});
|
||
|
||
const url = urlParams.toString()
|
||
? `${apiUrl.value}?${urlParams.toString()}`
|
||
: apiUrl.value;
|
||
|
||
// 构建请求头 - 从表格数据中获取
|
||
const headers: Record<string, string> = {};
|
||
const headerTableData = headerTableRef.value?.getTableData?.()?.fullData || headerTableRef.value?.xGrid?.getTableData()?.fullData || headerTableProps.data || [];
|
||
|
||
// 浏览器会禁止设置部分“unsafe headers”,如果放进 fetch headers 里可能直接失败
|
||
const forbiddenHeaderNames = new Set<string>([
|
||
'user-agent',
|
||
'referer',
|
||
'origin',
|
||
'host',
|
||
'content-length',
|
||
'connection',
|
||
// chrome client hints(从 curl 导入时经常会带)
|
||
'sec-ch-ua',
|
||
'sec-ch-ua-mobile',
|
||
'sec-ch-ua-platform',
|
||
]);
|
||
|
||
headerTableData.forEach((item: any) => {
|
||
if (item.name) {
|
||
const name = String(item.name || '').trim();
|
||
const lower = name.toLowerCase();
|
||
if (forbiddenHeaderNames.has(lower)) return;
|
||
|
||
// Header 参数暂时只使用 name,value 需要后续添加
|
||
headers[name] = item.value || '';
|
||
}
|
||
});
|
||
|
||
// 构建请求体
|
||
let body: string | FormData | URLSearchParams | null = null;
|
||
if (['POST', 'PUT', 'PATCH'].includes(selectedMethod.value)) {
|
||
if (bodyConfig.bodyType === 'json') {
|
||
body = stripCommentsForRequest(bodyConfig.body);
|
||
headers['Content-Type'] = 'application/json';
|
||
} else if (bodyConfig.bodyType === 'xml') {
|
||
body = stripCommentsForRequest(bodyConfig.body);
|
||
headers['Content-Type'] = 'application/xml';
|
||
} else if (bodyConfig.bodyType === 'x-www-form-urlencoded') {
|
||
const usp = new URLSearchParams();
|
||
const urlEncodedTableData = urlEncodedTableRef.value?.getTableData?.()?.fullData || urlEncodedTableRef.value?.xGrid?.getTableData()?.fullData || urlEncodedTableProps.data || [];
|
||
urlEncodedTableData.forEach((field: any) => {
|
||
if (field.name && field.value) {
|
||
usp.append(field.name, field.value);
|
||
}
|
||
});
|
||
body = usp;
|
||
headers['Content-Type'] = 'application/x-www-form-urlencoded;charset=UTF-8';
|
||
} else if (bodyConfig.bodyType === 'form-data') {
|
||
const formData = new FormData();
|
||
const formDataTableData = formDataTableRef.value?.getTableData?.()?.fullData || formDataTableRef.value?.xGrid?.getTableData()?.fullData || formDataTableProps.data || [];
|
||
formDataTableData.forEach((field: any) => {
|
||
if (field.name && field.value) {
|
||
formData.append(field.name, field.value);
|
||
}
|
||
});
|
||
body = formData;
|
||
delete headers['Content-Type'];
|
||
} else if (bodyConfig.bodyType === 'raw') {
|
||
body = stripCommentsForRequest(bodyConfig.rawBody);
|
||
}
|
||
}
|
||
|
||
// 确保GET请求不设置body
|
||
if (['GET', 'HEAD', 'OPTIONS'].includes(selectedMethod.value)) {
|
||
body = null;
|
||
delete headers['Content-Type'];
|
||
}
|
||
|
||
// 发送请求
|
||
const fetchOptions: RequestInit = {
|
||
method: selectedMethod.value,
|
||
headers,
|
||
};
|
||
|
||
// 只有非GET请求才设置body
|
||
if (body !== null && !['GET', 'HEAD', 'OPTIONS'].includes(selectedMethod.value)) {
|
||
fetchOptions.body = body instanceof FormData ? body : body;
|
||
} else {
|
||
fetchOptions.body = undefined;
|
||
}
|
||
|
||
// 开发环境把已知外域改写到 Vite 代理,避免浏览器 CORS
|
||
requestUrl = rewriteDebuggerUrlForProxy(url);
|
||
|
||
// 仍为绝对跨域 URL 时提示(未覆盖的域名)
|
||
try {
|
||
if (requestUrl.startsWith('http')) {
|
||
const targetOrigin = new URL(requestUrl).origin;
|
||
const currentOrigin = window.location.origin;
|
||
if (targetOrigin !== currentOrigin) {
|
||
Message.warning(`检测到跨域请求,浏览器可能因 CORS 拒绝:${targetOrigin}`);
|
||
}
|
||
}
|
||
} catch {
|
||
// ignore
|
||
}
|
||
|
||
// 添加超时处理
|
||
const controller = new AbortController();
|
||
const timeoutId = setTimeout(() => {
|
||
controller.abort();
|
||
}, 30000); // 30秒超时
|
||
|
||
const res = await fetch(requestUrl, {
|
||
...fetchOptions,
|
||
signal: controller.signal
|
||
});
|
||
|
||
clearTimeout(timeoutId);
|
||
|
||
const responseText = await res.text();
|
||
|
||
const endTime = Date.now();
|
||
const duration = endTime - startTime;
|
||
|
||
// 设置响应数据
|
||
response.value = {
|
||
status: res.status,
|
||
statusText: res.statusText,
|
||
data: responseText,
|
||
duration,
|
||
size: responseText.length
|
||
};
|
||
|
||
// 格式化响应数据(尝试 JSON 格式化)
|
||
try {
|
||
const jsonData = JSON.parse(responseText);
|
||
responseData.value = JSON.stringify(jsonData, null, 2);
|
||
} catch (e) {
|
||
responseData.value = responseText;
|
||
}
|
||
|
||
// 生成 cURL 命令
|
||
generateCurlCommand({});
|
||
|
||
// 切换到 Response tab
|
||
responseTab.value = 'response';
|
||
|
||
$baseMessage(`请求成功!状态码: ${res.status},耗时: ${duration}ms`, 'success');
|
||
} catch (error) {
|
||
if (error instanceof Error) {
|
||
if (error.name === 'AbortError') {
|
||
$baseMessage('请求超时(30秒)', 'error');
|
||
} else if (error.message.includes('Failed to fetch')) {
|
||
$baseMessage(`网络连接失败/跨域 CORS 拒绝请求:${requestUrl}`, 'error');
|
||
} else {
|
||
$baseMessage(`请求失败: ${error.message}`, 'error');
|
||
}
|
||
} else {
|
||
$baseMessage('请求失败: 未知错误', 'error');
|
||
}
|
||
|
||
const endTime = Date.now();
|
||
const duration = endTime - startTime;
|
||
|
||
response.value = {
|
||
status: 0,
|
||
statusText: 'Error',
|
||
data: `请求失败: ${error instanceof Error ? error.message : '未知错误'}`,
|
||
duration,
|
||
size: 0,
|
||
};
|
||
|
||
responseData.value = `请求失败: ${error instanceof Error ? error.message : '未知错误'}`;
|
||
} finally {
|
||
isLoading.value = false;
|
||
}
|
||
};
|
||
|
||
/** cURL 输出:form 字段值若已 URL 编码则先解码,避免 --data-urlencode 二次编码 */
|
||
function decodeFormFieldValueForCurl(value: string): string {
|
||
const raw = String(value ?? '');
|
||
if (!raw.includes('%')) return raw;
|
||
try {
|
||
return decodeURIComponent(raw);
|
||
} catch {
|
||
return raw;
|
||
}
|
||
}
|
||
|
||
function escapeCurlSingleQuoted(value: string): string {
|
||
return String(value ?? '').replace(/'/g, `'\\''`);
|
||
}
|
||
|
||
function getBaseUrlWithoutSearch(url: string): string {
|
||
try {
|
||
const u = new URL(url);
|
||
return `${u.origin}${u.pathname}`;
|
||
} catch {
|
||
const idx = url.indexOf('?');
|
||
return idx >= 0 ? url.slice(0, idx) : url;
|
||
}
|
||
}
|
||
|
||
// 生成 cURL 命令(Apifox 风格:form 用 --data-urlencode,避免 query/body 重复)
|
||
const generateCurlCommand = (_config: any) => {
|
||
if (!apiUrl.value) {
|
||
curlCommand.value = '';
|
||
return;
|
||
}
|
||
|
||
const lineEnd = ' \\';
|
||
const method = selectedMethod.value.toUpperCase();
|
||
const baseUrl = getBaseUrlWithoutSearch(apiUrl.value);
|
||
|
||
const queryTableData =
|
||
queryTableRef.value?.getTableData?.()?.fullData ||
|
||
queryTableRef.value?.xGrid?.getTableData()?.fullData ||
|
||
queryTableProps.data ||
|
||
[];
|
||
const headerTableData =
|
||
headerTableRef.value?.getTableData?.()?.fullData ||
|
||
headerTableRef.value?.xGrid?.getTableData()?.fullData ||
|
||
headerTableProps.data ||
|
||
[];
|
||
const urlEncodedTableData =
|
||
urlEncodedTableRef.value?.getTableData?.()?.fullData ||
|
||
urlEncodedTableRef.value?.xGrid?.getTableData()?.fullData ||
|
||
urlEncodedTableProps.data ||
|
||
[];
|
||
const formDataTableData =
|
||
formDataTableRef.value?.getTableData?.()?.fullData ||
|
||
formDataTableRef.value?.xGrid?.getTableData()?.fullData ||
|
||
formDataTableProps.data ||
|
||
[];
|
||
|
||
const isFormUrlEncoded = bodyConfig.bodyType === 'x-www-form-urlencoded';
|
||
const bodyFieldNames = new Set(
|
||
urlEncodedTableData.filter((f: any) => f.name).map((f: any) => f.name),
|
||
);
|
||
|
||
// form-urlencoded 时,与 body 同名的参数不应再拼到 URL(避免 ?sign=... 与 body 重复)
|
||
const urlQueryItems = queryTableData.filter((item: any) => {
|
||
if (!item.name || item.value === undefined || item.value === null || item.value === '') {
|
||
return false;
|
||
}
|
||
if (isFormUrlEncoded && bodyFieldNames.has(item.name)) return false;
|
||
return true;
|
||
});
|
||
|
||
let urlForCurl = baseUrl;
|
||
if (urlQueryItems.length > 0) {
|
||
const qs = urlQueryItems
|
||
.map(
|
||
(item: any) =>
|
||
`${encodeURIComponent(item.name)}=${encodeURIComponent(item.value)}`,
|
||
)
|
||
.join('&');
|
||
urlForCurl = `${baseUrl}?${qs}`;
|
||
}
|
||
|
||
let curl = `curl --location '${escapeCurlSingleQuoted(urlForCurl)}'`;
|
||
|
||
const hasFormBody =
|
||
isFormUrlEncoded &&
|
||
urlEncodedTableData.some((f: any) => f.name && f.value !== undefined && f.value !== null && f.value !== '');
|
||
const hasJsonOrXmlBody =
|
||
(bodyConfig.bodyType === 'json' || bodyConfig.bodyType === 'xml') && bodyConfig.body;
|
||
const hasRawBody = bodyConfig.bodyType === 'raw' && bodyConfig.rawBody;
|
||
const hasMultipartBody =
|
||
bodyConfig.bodyType === 'form-data' &&
|
||
formDataTableData.some((f: any) => f.name && f.value);
|
||
|
||
// POST 且带 body 时不写 --request(与 Apifox 导出一致);其余方法显式声明
|
||
if (method !== 'GET' && !(method === 'POST' && (hasFormBody || hasJsonOrXmlBody || hasRawBody || hasMultipartBody))) {
|
||
curl += `${lineEnd}\n--request ${method}`;
|
||
}
|
||
|
||
headerTableData.forEach((item: any) => {
|
||
if (item.name) {
|
||
curl += `${lineEnd}\n--header '${escapeCurlSingleQuoted(item.name)}: ${escapeCurlSingleQuoted(item.value || '')}'`;
|
||
}
|
||
});
|
||
|
||
if (['POST', 'PUT', 'PATCH'].includes(selectedMethod.value)) {
|
||
if (bodyConfig.bodyType === 'json' && bodyConfig.body) {
|
||
const bodyForCurl = stripCommentsForRequest(bodyConfig.body);
|
||
curl += `${lineEnd}\n--data-raw '${escapeCurlSingleQuoted(bodyForCurl)}'`;
|
||
} else if (bodyConfig.bodyType === 'xml' && bodyConfig.body) {
|
||
const bodyForCurl = stripCommentsForRequest(bodyConfig.body);
|
||
curl += `${lineEnd}\n--data-raw '${escapeCurlSingleQuoted(bodyForCurl)}'`;
|
||
} else if (isFormUrlEncoded) {
|
||
urlEncodedTableData
|
||
.filter((f: any) => f.name)
|
||
.forEach((f: any) => {
|
||
const decoded = decodeFormFieldValueForCurl(String(f.value ?? ''));
|
||
const pair = `${f.name}=${decoded}`;
|
||
curl += `${lineEnd}\n--data-urlencode '${escapeCurlSingleQuoted(pair)}'`;
|
||
});
|
||
} else if (bodyConfig.bodyType === 'form-data') {
|
||
formDataTableData
|
||
.filter((f: any) => f.name && f.value)
|
||
.forEach((f: any) => {
|
||
curl += `${lineEnd}\n--form '${escapeCurlSingleQuoted(`${f.name}=${f.value}`)}'`;
|
||
});
|
||
} else if (bodyConfig.bodyType === 'raw' && bodyConfig.rawBody) {
|
||
const bodyForCurl = stripCommentsForRequest(bodyConfig.rawBody);
|
||
curl += `${lineEnd}\n--data-raw '${escapeCurlSingleQuoted(bodyForCurl)}'`;
|
||
}
|
||
}
|
||
|
||
curlCommand.value = curl;
|
||
};
|
||
|
||
// 参数类型选项
|
||
const paramTypes = ['string', 'number', 'boolean', 'object', 'array'];
|
||
|
||
// 复制返回体内容
|
||
const copyResponseContent = async () => {
|
||
let content = '';
|
||
if (responseTab.value === 'response') {
|
||
content = responseData.value;
|
||
} else if (responseTab.value === 'curl') {
|
||
content = curlCommand.value;
|
||
}
|
||
|
||
if (!content) {
|
||
Message.warning('暂无内容可复制');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
await navigator.clipboard.writeText(content);
|
||
Message.success('已复制到剪贴板');
|
||
} catch (error) {
|
||
// 降级方案:使用传统方法
|
||
const textArea = document.createElement('textarea');
|
||
textArea.value = content;
|
||
textArea.style.position = 'fixed';
|
||
textArea.style.left = '-999999px';
|
||
document.body.appendChild(textArea);
|
||
textArea.select();
|
||
try {
|
||
document.execCommand('copy');
|
||
Message.success('已复制到剪贴板');
|
||
} catch (err) {
|
||
Message.error('复制失败');
|
||
}
|
||
document.body.removeChild(textArea);
|
||
}
|
||
};
|
||
|
||
/** 从响应 JSON 中递归取 rqId(兼容大小写 / requestId) */
|
||
function extractRqIdFromResponse(text: string): string {
|
||
const raw = String(text || '').trim();
|
||
if (!raw) return '';
|
||
|
||
const pick = (obj: any): string => {
|
||
if (obj == null || typeof obj !== 'object') return '';
|
||
const preferred = ['rqId', 'RqId', 'RQId', 'requestId', 'RequestId'];
|
||
for (const key of preferred) {
|
||
if (obj[key] != null && String(obj[key]).trim()) {
|
||
return String(obj[key]).trim();
|
||
}
|
||
}
|
||
for (const [k, v] of Object.entries(obj)) {
|
||
if (/^(rqid|requestid)$/i.test(k) && v != null && String(v).trim()) {
|
||
return String(v).trim();
|
||
}
|
||
}
|
||
for (const v of Object.values(obj)) {
|
||
if (v && typeof v === 'object') {
|
||
const hit = pick(v);
|
||
if (hit) return hit;
|
||
}
|
||
}
|
||
return '';
|
||
};
|
||
|
||
try {
|
||
const hit = pick(JSON.parse(raw));
|
||
if (hit) return hit;
|
||
} catch {
|
||
/* fall through */
|
||
}
|
||
|
||
const m =
|
||
raw.match(/"rqId"\s*:\s*"([^"]+)"/i) ||
|
||
raw.match(/'rqId'\s*:\s*'([^']+)'/i) ||
|
||
raw.match(/"requestId"\s*:\s*"([^"]+)"/i);
|
||
return m?.[1]?.trim() || '';
|
||
}
|
||
|
||
const requestLogVisible = ref(false);
|
||
const viewPacketRqId = ref('');
|
||
|
||
function openRequestLogByRqId() {
|
||
const rqId = extractRqIdFromResponse(responseData.value);
|
||
if (!rqId) {
|
||
Message.warning('响应中未找到 rqId,无法查看报文');
|
||
return;
|
||
}
|
||
viewPacketRqId.value = rqId;
|
||
requestLogVisible.value = true;
|
||
}
|
||
|
||
// Body 表单字段管理(使用 Query 样式)
|
||
async function addUrlEncodedField() {
|
||
try {
|
||
const record = {
|
||
id: random(0, 2099999999),
|
||
name: '',
|
||
type: 1,
|
||
value: '',
|
||
description: '',
|
||
};
|
||
|
||
await nextTick();
|
||
const { row: newRow } = await urlEncodedTableRef.value!.addRow(record);
|
||
await nextTick();
|
||
await urlEncodedTableRef.value?.editRowEvent(newRow);
|
||
refreshTabCounts();
|
||
} catch {
|
||
// 静默忽略
|
||
}
|
||
}
|
||
|
||
async function deleteUrlEncodedRow(row: any) {
|
||
await urlEncodedTableRef.value?.removeRow(row);
|
||
refreshTabCounts();
|
||
}
|
||
|
||
async function addFormField() {
|
||
try {
|
||
const record = {
|
||
id: random(0, 2099999999),
|
||
name: '',
|
||
type: 1,
|
||
value: '',
|
||
description: '',
|
||
};
|
||
|
||
await nextTick();
|
||
const { row: newRow } = await formDataTableRef.value!.addRow(record);
|
||
await nextTick();
|
||
await formDataTableRef.value?.editRowEvent(newRow);
|
||
refreshTabCounts();
|
||
} catch {
|
||
// 静默忽略
|
||
}
|
||
}
|
||
|
||
async function deleteFormDataRow(row: any) {
|
||
await formDataTableRef.value?.removeRow(row);
|
||
refreshTabCounts();
|
||
}
|
||
|
||
// 格式化请求体内容
|
||
const formatBody = () => {
|
||
if (bodyConfig.bodyType === 'json' && bodyConfig.body) {
|
||
try {
|
||
const parsed = JSON.parse(bodyConfig.body);
|
||
bodyConfig.body = JSON.stringify(parsed, null, 2);
|
||
Message.success('JSON 格式化成功');
|
||
} catch (error) {
|
||
Message.error('JSON 格式错误,无法格式化');
|
||
}
|
||
} else if (bodyConfig.bodyType === 'xml' && bodyConfig.body) {
|
||
try {
|
||
// 简单的 XML 格式化(使用 DOMParser)
|
||
const parser = new DOMParser();
|
||
const xmlDoc = parser.parseFromString(bodyConfig.body, 'text/xml');
|
||
if (xmlDoc.documentElement.nodeName === 'parsererror') {
|
||
Message.error('XML 格式错误,无法格式化');
|
||
return;
|
||
}
|
||
const serializer = new XMLSerializer();
|
||
const formatted = serializer.serializeToString(xmlDoc);
|
||
// 使用简单的缩进格式化
|
||
bodyConfig.body = formatXml(formatted);
|
||
Message.success('XML 格式化成功');
|
||
} catch (error) {
|
||
Message.error('XML 格式错误,无法格式化');
|
||
}
|
||
} else {
|
||
Message.warning('当前内容类型不支持格式化');
|
||
}
|
||
};
|
||
|
||
// 复制请求体内容(json/xml)
|
||
const copyRequestBodyContent = async () => {
|
||
const content = bodyConfig.body || '';
|
||
if (!content) {
|
||
Message.warning('暂无内容可复制');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
await navigator.clipboard.writeText(content);
|
||
Message.success('已复制到剪贴板');
|
||
} catch (error) {
|
||
// 降级方案:使用传统方法
|
||
const textArea = document.createElement('textarea');
|
||
textArea.value = content;
|
||
textArea.style.position = 'fixed';
|
||
textArea.style.left = '-999999px';
|
||
document.body.appendChild(textArea);
|
||
textArea.select();
|
||
try {
|
||
document.execCommand('copy');
|
||
Message.success('已复制到剪贴板');
|
||
} catch (err) {
|
||
Message.error('复制失败');
|
||
}
|
||
document.body.removeChild(textArea);
|
||
}
|
||
};
|
||
|
||
// XML 格式化辅助函数
|
||
function formatXml(xml: string): string {
|
||
let formatted = '';
|
||
let indent = '';
|
||
const tab = ' ';
|
||
xml.split(/>\s*</).forEach((node) => {
|
||
if (node.match(/^\/\w/)) indent = indent.substring(tab.length);
|
||
formatted += indent + '<' + node + '>\r\n';
|
||
if (node.match(/^<?\w[^>]*[^\/]$/) && !node.startsWith('<?xml')) indent += tab;
|
||
});
|
||
return formatted.substring(1, formatted.length - 3);
|
||
}
|
||
|
||
defineExpose({
|
||
importCurlFromString: (curl: string) => {
|
||
if (curl && curl.trim()) parseCurlCommand(curl.trim());
|
||
},
|
||
});
|
||
</script>
|
||
|
||
<template>
|
||
<div class="api-test-container">
|
||
<!-- 顶部控制栏 -->
|
||
<div class="api-test-header">
|
||
<a-select
|
||
v-model="selectedMethod"
|
||
:class="['method-select', `method-${String(selectedMethod || '').toLowerCase()}`]"
|
||
:style="{ width: '120px' }"
|
||
placeholder="请求方法"
|
||
>
|
||
<a-option
|
||
v-for="method in httpMethods"
|
||
:key="method"
|
||
:value="method"
|
||
:class="`method-${method.toLowerCase()}`"
|
||
>
|
||
{{ method }}
|
||
</a-option>
|
||
</a-select>
|
||
|
||
<a-input
|
||
v-model="apiUrl"
|
||
class="url-input"
|
||
placeholder="接口路径"
|
||
/>
|
||
|
||
<div class="header-actions">
|
||
<a-button size="small" @click="importCurl">
|
||
导入 cURL
|
||
</a-button>
|
||
<a-button
|
||
size="small"
|
||
:loading="isLoading"
|
||
:disabled="isLoading"
|
||
@click="sendRequest"
|
||
>
|
||
发送
|
||
</a-button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 主体内容区域 -->
|
||
<div class="api-test-body" :class="`layout-${panelLayout}`">
|
||
<!-- 左侧:请求体 -->
|
||
<div class="api-test-request">
|
||
<!-- Request 容器(使用 InterfaceDetail 的结构,包含 Query、Request、Header) -->
|
||
<div class="detail-section section-request">
|
||
<div class="section-header">
|
||
<SlidingTabs
|
||
v-model="requestTab"
|
||
class="request-header-tabs"
|
||
size="mini"
|
||
:items="requestTabItems"
|
||
aria-label="Request tabs"
|
||
/>
|
||
<div class="section-header-right">
|
||
<div class="section-actions">
|
||
<a-button
|
||
v-if="requestTab === 'query'"
|
||
size="small"
|
||
@click="addNewQueryParam"
|
||
>
|
||
新增
|
||
</a-button>
|
||
<a-button
|
||
v-if="requestTab === 'request' && bodyConfig.bodyType === 'x-www-form-urlencoded'"
|
||
size="small"
|
||
@click="addUrlEncodedField"
|
||
>
|
||
新增
|
||
</a-button>
|
||
<a-button
|
||
v-if="requestTab === 'request' && bodyConfig.bodyType === 'form-data'"
|
||
size="small"
|
||
@click="addFormField"
|
||
>
|
||
新增
|
||
</a-button>
|
||
<a-button
|
||
v-if="requestTab === 'header'"
|
||
size="small"
|
||
@click="addNewHeaderParam"
|
||
>
|
||
新增
|
||
</a-button>
|
||
</div>
|
||
<div class="header-switches">
|
||
<a-switch
|
||
class="layout-switch"
|
||
:model-value="panelLayout === 'vertical'"
|
||
checked-text="上下"
|
||
unchecked-text="左右"
|
||
title="切换请求 / 响应布局"
|
||
@change="onPanelLayoutSwitch"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="section-content">
|
||
<div
|
||
v-if="requestTab === 'request' || requestEmptyTip"
|
||
class="body-type-selector"
|
||
>
|
||
<SlidingTabs
|
||
v-if="requestTab === 'request'"
|
||
v-model="bodyConfig.bodyType"
|
||
size="mini"
|
||
class="body-type-tabs"
|
||
:items="bodyTypeItems"
|
||
aria-label="Body type"
|
||
/>
|
||
<div
|
||
v-if="requestEmptyTip"
|
||
class="content-type-empty-tip"
|
||
:title="requestEmptyTip"
|
||
>
|
||
{{ requestEmptyTip }}
|
||
</div>
|
||
<div
|
||
v-if="requestTab === 'request' && (bodyConfig.bodyType === 'json' || bodyConfig.bodyType === 'xml')"
|
||
class="body-actions"
|
||
>
|
||
<a-button size="small" @click="copyRequestBodyContent">复制</a-button>
|
||
<a-button size="small" @click="formatBody">格式化</a-button>
|
||
</div>
|
||
</div>
|
||
<!-- Query tab 内容 -->
|
||
<div v-if="requestTab === 'query'" class="table-content">
|
||
<TablePro
|
||
ref="queryTableRef"
|
||
v-bind="queryTableProps"
|
||
:row-config="{ isHover: true }"
|
||
:mouse-config="{
|
||
area: false,
|
||
}"
|
||
class="query-params-table"
|
||
>
|
||
<template #name="{ row }">
|
||
<template v-if="isQueryRowEditing(row)">
|
||
<a-input
|
||
v-model="row.name"
|
||
allow-clear
|
||
placeholder="请输入参数名"
|
||
/>
|
||
</template>
|
||
<span v-else>{{ row.name || '-' }}</span>
|
||
</template>
|
||
|
||
<template #name_edit="{ row }">
|
||
<a-input
|
||
v-model="row.name"
|
||
allow-clear
|
||
placeholder="请输入参数名"
|
||
/>
|
||
</template>
|
||
|
||
<template #type="{ row }">
|
||
<template v-if="isQueryRowEditing(row)">
|
||
<a-select
|
||
v-model="row.type"
|
||
placeholder="请选择类型"
|
||
>
|
||
<a-option
|
||
v-for="option in ParameterTypeList"
|
||
:key="option.key"
|
||
:value="option.key"
|
||
>
|
||
{{ option.value }}
|
||
</a-option>
|
||
</a-select>
|
||
</template>
|
||
<span v-else>{{ getParameterTypeName(row.type) || '-' }}</span>
|
||
</template>
|
||
|
||
<template #type_edit="{ row }">
|
||
<a-select
|
||
v-model="row.type"
|
||
placeholder="请选择类型"
|
||
>
|
||
<a-option
|
||
v-for="option in ParameterTypeList"
|
||
:key="option.key"
|
||
:value="option.key"
|
||
>
|
||
{{ option.value }}
|
||
</a-option>
|
||
</a-select>
|
||
</template>
|
||
|
||
<template #value="{ row }">
|
||
<template v-if="isQueryRowEditing(row)">
|
||
<a-input
|
||
v-model="row.value"
|
||
allow-clear
|
||
placeholder="请输入参数值"
|
||
/>
|
||
</template>
|
||
<span v-else>{{ row.value || '-' }}</span>
|
||
</template>
|
||
|
||
<template #value_edit="{ row }">
|
||
<a-input
|
||
v-model="row.value"
|
||
allow-clear
|
||
placeholder="请输入参数值"
|
||
/>
|
||
</template>
|
||
|
||
<template #description="{ row }">
|
||
<template v-if="isQueryRowEditing(row)">
|
||
<a-input
|
||
v-model="row.description"
|
||
allow-clear
|
||
placeholder="请输入描述"
|
||
/>
|
||
</template>
|
||
<span v-else>{{ row.description || '-' }}</span>
|
||
</template>
|
||
|
||
<template #description_edit="{ row }">
|
||
<a-input
|
||
v-model="row.description"
|
||
allow-clear
|
||
placeholder="请输入描述"
|
||
/>
|
||
</template>
|
||
|
||
<template #action="{ row }">
|
||
<a-space>
|
||
<a-button type="text" class="row-action-btn" title="删除" @click="deleteQueryRow(row)">
|
||
<IconDelete />
|
||
</a-button>
|
||
</a-space>
|
||
</template>
|
||
</TablePro>
|
||
</div>
|
||
|
||
<!-- Request tab 内容(复制自上游接口的 body tab) -->
|
||
<div v-if="requestTab === 'request'" class="config-content">
|
||
<div v-if="bodyConfig.bodyType === 'json'" class="body-editor">
|
||
<CodeMirror
|
||
v-model="bodyConfig.body"
|
||
language="json"
|
||
:style="{ height: '100%', width: '100%' }"
|
||
/>
|
||
</div>
|
||
|
||
<div v-else-if="bodyConfig.bodyType === 'xml'" class="body-editor">
|
||
<CodeMirror
|
||
v-model="bodyConfig.body"
|
||
language="xml"
|
||
:style="{ height: '100%', width: '100%' }"
|
||
/>
|
||
</div>
|
||
|
||
<div v-else-if="bodyConfig.bodyType === 'x-www-form-urlencoded'" class="table-content">
|
||
<TablePro
|
||
ref="urlEncodedTableRef"
|
||
v-bind="urlEncodedTableProps"
|
||
:row-config="{ isHover: true }"
|
||
:mouse-config="{
|
||
area: false,
|
||
}"
|
||
class="query-params-table"
|
||
>
|
||
<template #name="{ row }">
|
||
<template v-if="isUrlEncodedRowEditing(row)">
|
||
<a-input
|
||
v-model="row.name"
|
||
allow-clear
|
||
placeholder="请输入参数名"
|
||
/>
|
||
</template>
|
||
<span v-else>{{ row.name || '-' }}</span>
|
||
</template>
|
||
|
||
<template #name_edit="{ row }">
|
||
<a-input
|
||
v-model="row.name"
|
||
allow-clear
|
||
placeholder="请输入参数名"
|
||
/>
|
||
</template>
|
||
|
||
<template #type="{ row }">
|
||
<template v-if="isUrlEncodedRowEditing(row)">
|
||
<a-select
|
||
v-model="row.type"
|
||
placeholder="请选择类型"
|
||
>
|
||
<a-option
|
||
v-for="option in ParameterTypeList"
|
||
:key="option.key"
|
||
:value="option.key"
|
||
>
|
||
{{ option.value }}
|
||
</a-option>
|
||
</a-select>
|
||
</template>
|
||
<span v-else>{{ getParameterTypeName(row.type) || '-' }}</span>
|
||
</template>
|
||
|
||
<template #type_edit="{ row }">
|
||
<a-select
|
||
v-model="row.type"
|
||
placeholder="请选择类型"
|
||
>
|
||
<a-option
|
||
v-for="option in ParameterTypeList"
|
||
:key="option.key"
|
||
:value="option.key"
|
||
>
|
||
{{ option.value }}
|
||
</a-option>
|
||
</a-select>
|
||
</template>
|
||
|
||
<template #value="{ row }">
|
||
<template v-if="isUrlEncodedRowEditing(row)">
|
||
<a-input
|
||
v-model="row.value"
|
||
allow-clear
|
||
placeholder="请输入参数值"
|
||
/>
|
||
</template>
|
||
<span v-else>{{ row.value || '-' }}</span>
|
||
</template>
|
||
|
||
<template #value_edit="{ row }">
|
||
<a-input
|
||
v-model="row.value"
|
||
allow-clear
|
||
placeholder="请输入参数值"
|
||
/>
|
||
</template>
|
||
|
||
<template #description="{ row }">
|
||
<template v-if="isUrlEncodedRowEditing(row)">
|
||
<a-input
|
||
v-model="row.description"
|
||
allow-clear
|
||
placeholder="请输入描述"
|
||
/>
|
||
</template>
|
||
<span v-else>{{ row.description || '-' }}</span>
|
||
</template>
|
||
|
||
<template #description_edit="{ row }">
|
||
<a-input
|
||
v-model="row.description"
|
||
allow-clear
|
||
placeholder="请输入描述"
|
||
/>
|
||
</template>
|
||
|
||
<template #action="{ row }">
|
||
<a-space>
|
||
<a-button type="text" class="row-action-btn" title="删除" @click="deleteUrlEncodedRow(row)">
|
||
<IconDelete />
|
||
</a-button>
|
||
</a-space>
|
||
</template>
|
||
</TablePro>
|
||
</div>
|
||
|
||
<div v-else-if="bodyConfig.bodyType === 'form-data'" class="table-content">
|
||
<TablePro
|
||
ref="formDataTableRef"
|
||
v-bind="formDataTableProps"
|
||
:row-config="{ isHover: true }"
|
||
:mouse-config="{
|
||
area: false,
|
||
}"
|
||
class="query-params-table"
|
||
>
|
||
<template #name="{ row }">
|
||
<template v-if="isFormDataRowEditing(row)">
|
||
<a-input
|
||
v-model="row.name"
|
||
allow-clear
|
||
placeholder="请输入参数名"
|
||
/>
|
||
</template>
|
||
<span v-else>{{ row.name || '-' }}</span>
|
||
</template>
|
||
|
||
<template #name_edit="{ row }">
|
||
<a-input
|
||
v-model="row.name"
|
||
allow-clear
|
||
placeholder="请输入参数名"
|
||
/>
|
||
</template>
|
||
|
||
<template #type="{ row }">
|
||
<template v-if="isFormDataRowEditing(row)">
|
||
<a-select
|
||
v-model="row.type"
|
||
placeholder="请选择类型"
|
||
>
|
||
<a-option
|
||
v-for="option in ParameterTypeList"
|
||
:key="option.key"
|
||
:value="option.key"
|
||
>
|
||
{{ option.value }}
|
||
</a-option>
|
||
</a-select>
|
||
</template>
|
||
<span v-else>{{ getParameterTypeName(row.type) || '-' }}</span>
|
||
</template>
|
||
|
||
<template #type_edit="{ row }">
|
||
<a-select
|
||
v-model="row.type"
|
||
placeholder="请选择类型"
|
||
>
|
||
<a-option
|
||
v-for="option in ParameterTypeList"
|
||
:key="option.key"
|
||
:value="option.key"
|
||
>
|
||
{{ option.value }}
|
||
</a-option>
|
||
</a-select>
|
||
</template>
|
||
|
||
<template #value="{ row }">
|
||
<template v-if="isFormDataRowEditing(row)">
|
||
<a-input
|
||
v-model="row.value"
|
||
allow-clear
|
||
placeholder="请输入参数值"
|
||
/>
|
||
</template>
|
||
<span v-else>{{ row.value || '-' }}</span>
|
||
</template>
|
||
|
||
<template #value_edit="{ row }">
|
||
<a-input
|
||
v-model="row.value"
|
||
allow-clear
|
||
placeholder="请输入参数值"
|
||
/>
|
||
</template>
|
||
|
||
<template #description="{ row }">
|
||
<template v-if="isFormDataRowEditing(row)">
|
||
<a-input
|
||
v-model="row.description"
|
||
allow-clear
|
||
placeholder="请输入描述"
|
||
/>
|
||
</template>
|
||
<span v-else>{{ row.description || '-' }}</span>
|
||
</template>
|
||
|
||
<template #description_edit="{ row }">
|
||
<a-input
|
||
v-model="row.description"
|
||
allow-clear
|
||
placeholder="请输入描述"
|
||
/>
|
||
</template>
|
||
|
||
<template #action="{ row }">
|
||
<a-space>
|
||
<a-button type="text" class="row-action-btn" title="删除" @click="deleteFormDataRow(row)">
|
||
<IconDelete />
|
||
</a-button>
|
||
</a-space>
|
||
</template>
|
||
</TablePro>
|
||
</div>
|
||
|
||
<div v-else-if="bodyConfig.bodyType === 'raw'" class="raw-editor">
|
||
<a-textarea
|
||
v-model="bodyConfig.rawBody"
|
||
placeholder="请输入JSON或其他格式的请求体"
|
||
:rows="10"
|
||
class="raw-textarea"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Header tab 内容(使用 Query 的样式) -->
|
||
<div v-if="requestTab === 'header'" class="table-content">
|
||
<TablePro
|
||
ref="headerTableRef"
|
||
v-bind="headerTableProps"
|
||
:row-config="{ isHover: true }"
|
||
:mouse-config="{
|
||
area: false,
|
||
}"
|
||
class="query-params-table"
|
||
>
|
||
<template #name="{ row }">
|
||
<template v-if="isHeaderRowEditing(row)">
|
||
<a-input
|
||
v-model="row.name"
|
||
allow-clear
|
||
placeholder="请输入参数名"
|
||
/>
|
||
</template>
|
||
<span v-else>{{ row.name || '-' }}</span>
|
||
</template>
|
||
|
||
<template #name_edit="{ row }">
|
||
<a-input
|
||
v-model="row.name"
|
||
allow-clear
|
||
placeholder="请输入参数名"
|
||
/>
|
||
</template>
|
||
|
||
<template #type="{ row }">
|
||
<template v-if="isHeaderRowEditing(row)">
|
||
<a-select
|
||
v-model="row.type"
|
||
placeholder="请选择类型"
|
||
>
|
||
<a-option
|
||
v-for="option in ParameterTypeList"
|
||
:key="option.key"
|
||
:value="option.key"
|
||
>
|
||
{{ option.value }}
|
||
</a-option>
|
||
</a-select>
|
||
</template>
|
||
<span v-else>{{ getParameterTypeName(row.type) || '-' }}</span>
|
||
</template>
|
||
|
||
<template #type_edit="{ row }">
|
||
<a-select
|
||
v-model="row.type"
|
||
placeholder="请选择类型"
|
||
>
|
||
<a-option
|
||
v-for="option in ParameterTypeList"
|
||
:key="option.key"
|
||
:value="option.key"
|
||
>
|
||
{{ option.value }}
|
||
</a-option>
|
||
</a-select>
|
||
</template>
|
||
|
||
<template #value="{ row }">
|
||
<template v-if="isHeaderRowEditing(row)">
|
||
<a-input
|
||
v-model="row.value"
|
||
allow-clear
|
||
placeholder="请输入参数值"
|
||
/>
|
||
</template>
|
||
<span v-else>{{ row.value || '-' }}</span>
|
||
</template>
|
||
|
||
<template #value_edit="{ row }">
|
||
<a-input
|
||
v-model="row.value"
|
||
allow-clear
|
||
placeholder="请输入参数值"
|
||
/>
|
||
</template>
|
||
|
||
<template #description="{ row }">
|
||
<template v-if="isHeaderRowEditing(row)">
|
||
<a-input
|
||
v-model="row.description"
|
||
allow-clear
|
||
placeholder="请输入描述"
|
||
/>
|
||
</template>
|
||
<span v-else>{{ row.description || '-' }}</span>
|
||
</template>
|
||
|
||
<template #description_edit="{ row }">
|
||
<a-input
|
||
v-model="row.description"
|
||
allow-clear
|
||
placeholder="请输入描述"
|
||
/>
|
||
</template>
|
||
|
||
<template #action="{ row }">
|
||
<a-space>
|
||
<a-button type="text" class="row-action-btn" title="删除" @click="deleteHeaderRow(row)">
|
||
<IconDelete />
|
||
</a-button>
|
||
</a-space>
|
||
</template>
|
||
</TablePro>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 右侧:返回体(使用请求体的样式) -->
|
||
<div class="detail-section section-response">
|
||
<div class="section-header">
|
||
<SlidingTabs
|
||
v-model="responseTab"
|
||
class="request-header-tabs"
|
||
size="mini"
|
||
:items="responseTabItems"
|
||
aria-label="Response tabs"
|
||
/>
|
||
<div class="response-status" v-if="responseStatusText">
|
||
<span :class="response?.status >= 200 && response?.status < 300 ? 'status-success' : 'status-error'">
|
||
{{ responseStatusText }}
|
||
</span>
|
||
</div>
|
||
<div class="section-actions">
|
||
<a-button
|
||
size="small"
|
||
@click="copyResponseContent"
|
||
>
|
||
复制
|
||
</a-button>
|
||
<a-button
|
||
size="small"
|
||
:disabled="!responseData"
|
||
@click="openRequestLogByRqId"
|
||
>
|
||
查看报文
|
||
</a-button>
|
||
</div>
|
||
</div>
|
||
<div class="section-content">
|
||
<!-- Response tab 内容 -->
|
||
<div v-if="responseTab === 'response'" class="response-container">
|
||
<CodeMirror
|
||
v-model="responseData"
|
||
language="json"
|
||
:read-only="true"
|
||
:style="{ height: '100%', width: '100%' }"
|
||
/>
|
||
</div>
|
||
|
||
<!-- cURL tab 内容 -->
|
||
<div v-if="responseTab === 'curl'" class="response-container">
|
||
<CodeMirror
|
||
v-model="curlCommand"
|
||
language="shell"
|
||
:read-only="true"
|
||
:style="{ height: '100%', width: '100%' }"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<a-modal
|
||
v-model:visible="requestLogVisible"
|
||
title="查看报文"
|
||
:width="'92vw'"
|
||
:footer="false"
|
||
unmount-on-close
|
||
class="api-test-request-log-modal"
|
||
:body-style="{ padding: '12px 16px 16px' }"
|
||
>
|
||
<div class="request-log-modal-tip">
|
||
已用响应中的 <code>rqId</code> 预填请求日志并查询:{{ viewPacketRqId }}
|
||
</div>
|
||
<RequestLog
|
||
v-if="requestLogVisible"
|
||
:key="viewPacketRqId"
|
||
:initial-request-id="viewPacketRqId"
|
||
embedded
|
||
/>
|
||
</a-modal>
|
||
</template>
|
||
|
||
<style lang="less" scoped>
|
||
.api-test-container {
|
||
display: flex;
|
||
flex-direction: column;
|
||
height: 100%;
|
||
padding: 16px;
|
||
background: var(--app-card-bg);
|
||
overflow: hidden;
|
||
box-sizing: border-box;
|
||
|
||
:deep(.arco-btn) {
|
||
border-radius: 4px;
|
||
}
|
||
|
||
:deep(.arco-btn-primary) {
|
||
background-color: var(--app-primary-color);
|
||
border-color: var(--app-primary-color);
|
||
|
||
&:hover,
|
||
&:focus {
|
||
background-color: var(--app-primary-color);
|
||
border-color: var(--app-primary-color);
|
||
opacity: 0.9;
|
||
}
|
||
}
|
||
|
||
:deep(.arco-input-wrapper:focus-within),
|
||
:deep(.arco-select-view-focus),
|
||
:deep(.arco-textarea-focus) {
|
||
border-color: var(--app-primary-color);
|
||
box-shadow: 0 0 0 2px color-mix(in srgb, var(--app-primary-color) 14%, transparent);
|
||
}
|
||
|
||
:deep(.arco-input-wrapper .arco-input),
|
||
:deep(.arco-input-wrapper .arco-input:focus) {
|
||
border: none !important;
|
||
box-shadow: none !important;
|
||
outline: none !important;
|
||
}
|
||
}
|
||
|
||
.api-test-header {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
margin-bottom: 16px;
|
||
padding: 12px 16px;
|
||
background: var(--app-card-bg, #fff);
|
||
border-radius: 8px;
|
||
border: 1px solid color-mix(in srgb, var(--color-border-2) 40%, transparent);
|
||
flex-shrink: 0;
|
||
box-sizing: border-box;
|
||
min-height: 56px;
|
||
|
||
:deep(.arco-btn) {
|
||
height: 32px;
|
||
}
|
||
|
||
:deep(.arco-input-wrapper),
|
||
:deep(.arco-select-view-single) {
|
||
height: 32px;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
:deep(.arco-input) {
|
||
height: 30px;
|
||
line-height: 30px;
|
||
}
|
||
|
||
:deep(.arco-select-view-value),
|
||
:deep(.arco-select-view-input) {
|
||
line-height: 30px;
|
||
}
|
||
}
|
||
|
||
.url-input {
|
||
flex: 1;
|
||
min-width: 0;
|
||
height: 32px;
|
||
}
|
||
|
||
.header-actions {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: var(--app-button-gap, 8px);
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.api-test-body {
|
||
display: flex;
|
||
flex: 1;
|
||
gap: 16px;
|
||
min-height: 0;
|
||
overflow: hidden;
|
||
box-sizing: border-box;
|
||
|
||
&.layout-horizontal {
|
||
flex-direction: row;
|
||
}
|
||
|
||
&.layout-vertical {
|
||
flex-direction: column;
|
||
|
||
.api-test-request,
|
||
.section-response {
|
||
flex: 1;
|
||
min-height: 0;
|
||
height: auto;
|
||
}
|
||
}
|
||
}
|
||
|
||
.api-test-request {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
min-width: 0;
|
||
height: 100%;
|
||
overflow: hidden;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.detail-section {
|
||
display: flex;
|
||
flex-direction: column;
|
||
background-color: var(--app-card-bg, #fff);
|
||
border: 1px solid color-mix(in srgb, var(--color-border-2) 40%, transparent);
|
||
border-radius: 8px;
|
||
overflow: hidden;
|
||
min-height: 0;
|
||
}
|
||
|
||
.section-header {
|
||
position: relative;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 8px 14px;
|
||
border-bottom: 1px solid color-mix(in srgb, var(--color-border-2) 35%, transparent);
|
||
background: var(--app-card-bg, #fff);
|
||
flex-shrink: 0;
|
||
min-height: 40px;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.section-header-right {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
flex-shrink: 0;
|
||
margin-left: auto;
|
||
}
|
||
|
||
.header-switches {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: flex-end;
|
||
gap: 10px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.layout-switch {
|
||
flex-shrink: 0;
|
||
min-width: 58px !important;
|
||
width: auto;
|
||
height: 28px !important;
|
||
line-height: 28px !important;
|
||
border-radius: 14px !important;
|
||
padding: 0;
|
||
background-color: var(--color-fill-3) !important;
|
||
|
||
&:hover {
|
||
background-color: var(--color-fill-4) !important;
|
||
}
|
||
|
||
:deep(.arco-switch-handle) {
|
||
top: 4px;
|
||
left: 4px;
|
||
width: 20px;
|
||
height: 20px;
|
||
background-color: #fff;
|
||
}
|
||
|
||
:deep(.arco-switch-text-holder) {
|
||
margin: 0 10px 0 30px;
|
||
font-size: 12px;
|
||
font-weight: 400;
|
||
}
|
||
|
||
:deep(.arco-switch-text) {
|
||
left: 30px;
|
||
font-size: 12px;
|
||
font-weight: 400;
|
||
color: var(--color-text-2) !important;
|
||
}
|
||
|
||
:deep(&.arco-switch-checked) {
|
||
background-color: var(--color-fill-3) !important;
|
||
}
|
||
|
||
:deep(&.arco-switch-checked .arco-switch-handle) {
|
||
left: calc(100% - 24px);
|
||
}
|
||
|
||
:deep(&.arco-switch-checked .arco-switch-text-holder) {
|
||
margin: 0 30px 0 10px;
|
||
}
|
||
|
||
:deep(&.arco-switch-checked .arco-switch-text) {
|
||
left: 10px;
|
||
color: var(--color-text-2) !important;
|
||
}
|
||
}
|
||
|
||
.section-title {
|
||
font-weight: 600;
|
||
font-size: 14px;
|
||
color: var(--color-text-1);
|
||
line-height: 24px;
|
||
}
|
||
|
||
.section-actions {
|
||
display: flex;
|
||
gap: var(--app-button-gap, 8px);
|
||
align-items: center;
|
||
min-height: 24px;
|
||
}
|
||
|
||
.request-log-modal-tip {
|
||
margin-bottom: 8px;
|
||
font-size: 12px;
|
||
color: var(--color-text-2);
|
||
|
||
code {
|
||
padding: 0 4px;
|
||
color: var(--app-primary-color);
|
||
}
|
||
}
|
||
|
||
.response-status {
|
||
flex: 1;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
margin: 0 12px;
|
||
font-size: 12px;
|
||
|
||
.status-success {
|
||
color: var(--color-success-6, #00b42a);
|
||
font-weight: 500;
|
||
}
|
||
|
||
.status-error {
|
||
color: var(--color-danger-6, #f53f3f);
|
||
font-weight: 500;
|
||
}
|
||
}
|
||
|
||
.section-content {
|
||
padding: 16px;
|
||
flex: 1;
|
||
min-height: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.config-content {
|
||
padding: 0;
|
||
flex: 1;
|
||
overflow-y: auto;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.table-content {
|
||
flex: 1;
|
||
min-height: 0;
|
||
overflow: auto;
|
||
}
|
||
|
||
.body-type-selector {
|
||
position: relative;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: var(--app-button-gap, 8px);
|
||
padding: 0 0 10px var(--vxe-table-cell-padding-left, 10px);
|
||
margin-bottom: 0;
|
||
min-height: 28px;
|
||
box-sizing: border-box;
|
||
background: transparent;
|
||
flex-shrink: 0;
|
||
border: none;
|
||
|
||
.content-type-empty-tip {
|
||
position: absolute;
|
||
left: 50%;
|
||
top: 50%;
|
||
transform: translate(-50%, -55%);
|
||
z-index: 1;
|
||
max-width: min(46%, 360px);
|
||
padding: 0 8px;
|
||
font-size: 12px;
|
||
line-height: 20px;
|
||
color: var(--color-text-3);
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
pointer-events: none;
|
||
text-align: center;
|
||
}
|
||
|
||
.body-actions {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
flex-shrink: 0;
|
||
margin-left: auto;
|
||
}
|
||
|
||
.body-type-tabs {
|
||
flex-shrink: 0;
|
||
}
|
||
}
|
||
|
||
.body-editor {
|
||
flex: 1;
|
||
border: 1px solid color-mix(in srgb, var(--color-border-2) 40%, transparent);
|
||
border-radius: 4px;
|
||
overflow: hidden;
|
||
display: flex;
|
||
flex-direction: column;
|
||
min-height: 280px;
|
||
}
|
||
|
||
.raw-editor {
|
||
.raw-textarea {
|
||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||
font-size: 12px;
|
||
line-height: 1.5;
|
||
resize: both;
|
||
height: 400px;
|
||
}
|
||
}
|
||
|
||
.query-params-table {
|
||
height: 100%;
|
||
min-height: 0;
|
||
|
||
:deep(.vxe-table) {
|
||
height: 100%;
|
||
--vxe-table-border-color: color-mix(in srgb, var(--color-border-2) 42%, transparent);
|
||
border-color: color-mix(in srgb, var(--color-border-2) 42%, transparent) !important;
|
||
}
|
||
|
||
:deep(.vxe-table--body-wrapper) {
|
||
max-height: 100%;
|
||
overflow-y: auto;
|
||
}
|
||
}
|
||
|
||
// 列表:表头白底 + 边框弱化(仅本页)
|
||
:deep(.vxe-table) {
|
||
--vxe-table-border-color: color-mix(in srgb, var(--color-border-2) 42%, transparent);
|
||
border-color: color-mix(in srgb, var(--color-border-2) 42%, transparent) !important;
|
||
}
|
||
|
||
:deep(.vxe-table--header-wrapper),
|
||
:deep(.vxe-table--header),
|
||
:deep(.vxe-header--row),
|
||
:deep(.vxe-header--column) {
|
||
background-color: var(--app-card-bg, #fff) !important;
|
||
}
|
||
|
||
:deep(.vxe-header--column),
|
||
:deep(.vxe-body--column),
|
||
:deep(.vxe-table--border-line) {
|
||
border-color: color-mix(in srgb, var(--color-border-2) 42%, transparent) !important;
|
||
}
|
||
|
||
:deep(.vxe-table--header-border-line) {
|
||
border-color: color-mix(in srgb, var(--color-border-2) 42%, transparent) !important;
|
||
}
|
||
|
||
.section-request {
|
||
flex: 1;
|
||
min-height: 0;
|
||
height: 100%;
|
||
}
|
||
|
||
.request-header-tabs {
|
||
flex: 0 1 auto;
|
||
width: fit-content;
|
||
max-width: 100%;
|
||
}
|
||
|
||
.section-response {
|
||
flex: 1;
|
||
min-height: 0;
|
||
height: 100%;
|
||
}
|
||
|
||
.table-container {
|
||
display: flex;
|
||
flex-direction: column;
|
||
flex: 1;
|
||
min-height: 0;
|
||
max-height: 100%;
|
||
padding: 12px;
|
||
overflow: hidden;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.table-toolbar {
|
||
margin-bottom: 12px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.response-container {
|
||
flex: 1;
|
||
min-height: 0;
|
||
height: 100%;
|
||
overflow: hidden;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.method-select {
|
||
flex-shrink: 0;
|
||
|
||
:deep(.arco-select-view) {
|
||
font-weight: 600;
|
||
}
|
||
|
||
&.method-get {
|
||
:deep(.arco-select-view),
|
||
:deep(.arco-select-view-single),
|
||
:deep(.arco-select-view-value),
|
||
:deep(.arco-select-view-single .arco-select-view-value) {
|
||
color: #10b981 !important;
|
||
}
|
||
}
|
||
|
||
&.method-post {
|
||
:deep(.arco-select-view),
|
||
:deep(.arco-select-view-single),
|
||
:deep(.arco-select-view-value),
|
||
:deep(.arco-select-view-single .arco-select-view-value) {
|
||
color: #f59e0b !important;
|
||
}
|
||
}
|
||
|
||
&.method-delete {
|
||
:deep(.arco-select-view),
|
||
:deep(.arco-select-view-single),
|
||
:deep(.arco-select-view-value),
|
||
:deep(.arco-select-view-single .arco-select-view-value) {
|
||
color: #ef4444 !important;
|
||
}
|
||
}
|
||
|
||
&.method-put,
|
||
&.method-patch,
|
||
&.method-head,
|
||
&.method-options {
|
||
:deep(.arco-select-view),
|
||
:deep(.arco-select-view-single),
|
||
:deep(.arco-select-view-value),
|
||
:deep(.arco-select-view-single .arco-select-view-value) {
|
||
color: #6b7280 !important;
|
||
}
|
||
}
|
||
}
|
||
|
||
.row-action-btn {
|
||
color: var(--color-text-3) !important;
|
||
|
||
&:hover,
|
||
&:focus {
|
||
color: var(--color-text-1) !important;
|
||
background-color: var(--color-fill-2) !important;
|
||
}
|
||
}
|
||
</style>
|
||
|
||
<style lang="less">
|
||
/* 下拉挂到 body,需全局样式恢复请求方式颜色 */
|
||
.method-select.method-get {
|
||
.arco-select-view,
|
||
.arco-select-view-single,
|
||
.arco-select-view-value {
|
||
color: #10b981 !important;
|
||
font-weight: 700 !important;
|
||
}
|
||
}
|
||
|
||
.method-select.method-post {
|
||
.arco-select-view,
|
||
.arco-select-view-single,
|
||
.arco-select-view-value {
|
||
color: #f59e0b !important;
|
||
font-weight: 700 !important;
|
||
}
|
||
}
|
||
|
||
.method-select.method-delete {
|
||
.arco-select-view,
|
||
.arco-select-view-single,
|
||
.arco-select-view-value {
|
||
color: #ef4444 !important;
|
||
font-weight: 700 !important;
|
||
}
|
||
}
|
||
|
||
.method-select.method-put,
|
||
.method-select.method-patch,
|
||
.method-select.method-head,
|
||
.method-select.method-options {
|
||
.arco-select-view,
|
||
.arco-select-view-single,
|
||
.arco-select-view-value {
|
||
color: #6b7280 !important;
|
||
font-weight: 700 !important;
|
||
}
|
||
}
|
||
|
||
.arco-select-dropdown {
|
||
.arco-select-option {
|
||
&.method-get {
|
||
color: #10b981 !important;
|
||
font-weight: 600;
|
||
|
||
&.arco-select-option-selected {
|
||
font-weight: 700;
|
||
}
|
||
}
|
||
|
||
&.method-post {
|
||
color: #f59e0b !important;
|
||
font-weight: 600;
|
||
|
||
&.arco-select-option-selected {
|
||
font-weight: 700;
|
||
}
|
||
}
|
||
|
||
&.method-delete {
|
||
color: #ef4444 !important;
|
||
font-weight: 600;
|
||
|
||
&.arco-select-option-selected {
|
||
font-weight: 700;
|
||
}
|
||
}
|
||
|
||
&.method-put,
|
||
&.method-patch,
|
||
&.method-head,
|
||
&.method-options {
|
||
color: #6b7280 !important;
|
||
|
||
&.arco-select-option-selected {
|
||
font-weight: 700;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
</style>
|