Files
tyapi-frontend/src/pages/api/ApiDebugger.vue
2026-01-09 15:58:17 +08:00

1501 lines
56 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="debugger-page flex flex-col h-screen bg-gray-50 overflow-hidden">
<!-- 主要内容区域 -->
<div class="debugger-main flex gap-3 flex-1 p-3 overflow-hidden min-h-0">
<!-- 左侧产品选择区域 -->
<div class="panel-left bg-white rounded-lg shadow-sm overflow-hidden flex flex-col h-full w-72 flex-shrink-0">
<div class="p-3 flex flex-col h-full">
<div class="flex justify-between items-center mb-3 flex-wrap gap-2 flex-shrink-0">
<h3 class="text-base font-semibold text-gray-800 m-0">产品选择</h3>
<el-input v-model="searchKeyword" placeholder="搜索产品..." clearable size="default"
class="w-full min-w-[180px]">
<template #prefix>
<svg class="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
</svg>
</template>
</el-input>
</div>
<div class="flex-1 overflow-y-auto mt-0">
<div v-if="productsLoading" class="p-3">
<el-skeleton :rows="3" animated />
</div>
<div v-else-if="userProducts.length === 0" class="flex items-center justify-center h-24 text-gray-400">
<el-empty description="暂无订阅产品">
<template #description>
<div class="text-center">
<p class="text-gray-500 mb-2">您还没有订阅任何产品</p>
<p class="text-gray-400 text-sm">请先订阅产品后再使用调试功能</p>
</div>
</template>
</el-empty>
</div>
<div v-else class="flex flex-col gap-1.5">
<div v-for="product in filteredProducts" :key="product.id"
class="p-2.5 border border-gray-200 rounded cursor-pointer transition-all duration-150 flex justify-between items-start bg-gray-50 hover:border-indigo-400 hover:bg-gray-100 hover:-translate-y-px hover:shadow-sm"
:class="{ 'border-indigo-400 bg-gray-100 shadow-sm': selectedProduct?.id === product.id }"
@click="selectProduct(product)">
<div class="flex-1">
<h4 class="text-sm font-semibold text-gray-800 m-0 mb-1">{{ product.name }}</h4>
<p class="text-xs text-gray-500 m-0 mb-1 font-mono">{{ product.code }}</p>
<div class="flex items-center gap-1">
<span class="text-xs text-gray-500">价格:</span>
<span class="text-xs font-semibold text-red-600">¥{{ product.price }}</span>
</div>
</div>
<div class="mt-0.5">
<el-tag type="success" size="small">已订阅</el-tag>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 中间调试配置区域 -->
<div class="panel-center bg-white rounded-lg shadow-sm overflow-hidden flex flex-col h-full flex-1 min-w-0">
<div class="p-3 flex flex-col h-full">
<div class="flex justify-between items-center mb-3 flex-wrap gap-2 flex-shrink-0 actions-header">
<h3 class="text-base font-semibold text-gray-800 m-0">调试配置</h3>
<div class="actions-bar flex gap-1.5 items-center flex-wrap">
<el-button @click="goToProductDetail" size="default" type="primary" plain>
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z">
</path>
</svg>
产品文档
</el-button>
<el-button @click="showExampleCode" size="default" type="success" plain>
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"></path>
</svg>
示例代码
</el-button>
<el-button @click="resetForm" size="default" plain>
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15">
</path>
</svg>
重置
</el-button>
<el-button type="warning" size="default" @click="handleDebug" :loading="debugging" :disabled="!canDebug"
class="bg-gradient-to-r from-orange-500 to-red-500 border-none text-white font-semibold px-4 py-2 h-8 text-sm shadow-lg transition-all duration-200 hover:-translate-y-px hover:shadow-xl disabled:bg-gray-300 disabled:transform-none disabled:shadow-none disabled:cursor-not-allowed">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8">
</path>
</svg>
发起请求
</el-button>
</div>
</div>
<div v-if="!selectedProduct" class="flex items-center justify-center h-24 text-gray-400">
<el-empty description="请先选择左侧产品开始配置" />
</div>
<div v-else class="p-3 flex-1 overflow-y-auto">
<!-- 产品基本信息和API密钥 -->
<div class="grid grid-cols-2 gap-3 mb-3">
<div class="bg-white rounded-lg border border-gray-200">
<div class="px-3 py-2 bg-gray-50 border-b border-gray-200">
<h4 class="text-sm font-semibold text-gray-800 m-0">产品信息</h4>
</div>
<div class="p-3 grid grid-cols-2 gap-2">
<div class="flex flex-col gap-0.5">
<span class="font-medium text-gray-500 text-xs mb-0.5">产品名称</span>
<span class="text-gray-800 font-medium text-xs break-all">{{ selectedProduct.name }}</span>
</div>
<div class="flex flex-col gap-0.5">
<span class="font-medium text-gray-500 text-xs mb-0.5">产品代码</span>
<span class="font-mono bg-blue-50 px-1.5 py-0.5 rounded text-blue-700 text-xs">{{
selectedProduct.code
}}</span>
</div>
<div class="flex flex-col gap-0.5">
<span class="font-medium text-gray-500 text-xs mb-0.5">请求方法</span>
<span class="text-gray-800 font-medium text-xs break-all">{{ apiConfig?.method || 'POST' }}</span>
</div>
<div class="flex flex-col gap-0.5 col-span-2">
<span class="font-medium text-gray-500 text-xs mb-0.5">接口地址</span>
<span class="font-mono bg-gray-100 px-1.5 py-0.5 rounded text-gray-500 text-xs break-all">{{
getRequestUrl() }}</span>
</div>
</div>
</div>
<div class="bg-white rounded-lg border border-gray-200">
<div class="px-3 py-2 bg-gray-50 border-b border-gray-200">
<h4 class="text-sm font-semibold text-gray-800 m-0">API 密钥</h4>
</div>
<div class="p-3 flex flex-col gap-2">
<div class="flex flex-col gap-1">
<span class="font-medium text-gray-500 text-xs">Access ID</span>
<div class="flex gap-1 items-center">
<el-input v-model="debugForm.accessId" size="default" class="flex-1" placeholder="请输入Access ID" />
<el-button type="info" size="default" @click="copyToClipboard(debugForm.accessId)">
复制
</el-button>
</div>
</div>
<div class="flex flex-col gap-1">
<span class="font-medium text-gray-500 text-xs">Secret Key</span>
<div class="flex gap-1 items-center">
<el-input v-model="debugForm.secretKey" type="password" size="default" class="flex-1"
placeholder="请输入Secret Key" show-password />
<el-button type="info" size="default" @click="copyToClipboard(debugForm.secretKey)">
复制
</el-button>
</div>
</div>
</div>
</div>
</div>
<!-- 请求参数配置和参数加密对比 - 横向并排 -->
<div class="grid grid-cols-2 gap-3 mb-3">
<!-- 请求参数配置 -->
<div v-if="apiConfig" class="bg-white rounded-lg border border-gray-200">
<div class="px-3 py-2 bg-gray-50 border-b border-gray-200">
<h4 class="text-sm font-semibold text-gray-800 m-0">请求参数</h4>
</div>
<!-- 动态表单 -->
<div v-if="formFields.length > 0" class="p-3 space-y-2">
<div v-for="field in formFields" :key="field.name"
class="py-2 border-b border-gray-100 last:border-b-0 hover:bg-gray-50 hover:px-2 hover:-mx-2 hover:rounded transition-all duration-200">
<label class="block text-sm font-medium text-gray-700 mb-1">
{{ field.label }}
<span v-if="field.required" class="text-red-500 ml-1">*</span>
</label>
<!-- 图片上传字段photo_data -->
<div v-if="field.name === 'photo_data' || field.name === 'vlphoto_data' && field.type === 'textarea'" class="space-y-2">
<div class="flex gap-2 mb-2">
<el-upload
:auto-upload="false"
:show-file-list="false"
accept="image/jpeg,image/jpg,image/png,image/bmp"
:on-change="(file) => handleImageUpload(file, field.name)"
class="flex-1">
<el-button type="primary" size="small">
<i class="el-icon-upload"></i> 上传图片JPG/BMP/PNG
</el-button>
</el-upload>
<el-button v-if="formData[field.name]" type="danger" size="small" @click="clearImageData(field.name)">
清空
</el-button>
</div>
<el-input
v-model="formData[field.name]"
type="textarea"
:rows="6"
:placeholder="field.placeholder || field.example"
size="default"
class="w-full mb-1"
:show-word-limit="false" />
</div>
<!-- 普通文本域 -->
<el-input
v-else-if="field.type === 'textarea'"
v-model="formData[field.name]"
type="textarea"
:rows="4"
:placeholder="field.placeholder || field.example"
size="default"
class="w-full mb-1" />
<!-- 普通输入框 -->
<el-input
v-else
v-model="formData[field.name]"
:placeholder="field.placeholder || field.example"
:type="field.type"
size="default"
class="w-full mb-1" />
<div class="mt-1 text-xs text-gray-500 space-y-0.5">
<p v-if="field.description" class="text-gray-600">{{ field.description }}</p>
<p v-if="field.example && field.name !== 'photo_data'" class="text-blue-600 font-medium">示例: {{ field.example }}</p>
<p v-if="field.validation" class="text-orange-600 font-medium">验证: {{ field.validation }}</p>
</div>
</div>
</div>
<!-- 备选JSON输入模式 -->
<div v-else class="p-3">
<div class="flex items-center gap-2 mb-2">
<span class="text-xs text-gray-600">请输入JSON格式的请求参数</span>
<el-button type="info" size="small" @click="loadExampleParams" class="text-xs">
加载示例
</el-button>
<el-button type="warning" size="small" @click="validateJsonParams" class="text-xs">
验证JSON
</el-button>
</div>
<el-input v-model="debugForm.params" type="textarea" :rows="5"
placeholder='请输入JSON格式参数例如{"param1": "value1", "param2": "value2"}'
class="font-mono text-xs leading-relaxed [&_.el-textarea__inner]:font-mono [&_.el-textarea__inner]:text-xs [&_.el-textarea__inner]:leading-relaxed [&_.el-textarea__inner]:p-2.5 [&_.el-textarea__inner]:border-none [&_.el-textarea__inner]:bg-gray-50 [&_.el-textarea__inner]:text-gray-800 [&_.el-textarea__inner]:focus:bg-white [&_.el-textarea__inner]:focus:ring-2 [&_.el-textarea__inner]:focus:ring-blue-500/20"
:class="{ '[&_.el-textarea__inner]:border-red-500 [&_.el-textarea__inner]:ring-red-500/20': hasJsonError }"
@input="checkJsonFormat" />
<div class="mt-2 text-xs text-gray-500">
<p>💡 提示请确保JSON格式正确必填字段已填写</p>
</div>
</div>
<!-- 移除开始调试按钮区域 -->
</div>
<!-- 参数加密对比 - 一直显示 -->
<div class="bg-white rounded-lg border border-gray-200">
<div class="px-3 py-2 bg-gray-50 border-b border-gray-200">
<h4 class="text-sm font-semibold text-gray-800 m-0">参数加密与请求体构建</h4>
</div>
<div class="p-3 space-y-3">
<div class="p-3 bg-blue-50 rounded border border-blue-200">
<div class="flex items-start gap-2">
<svg class="w-5 h-5 text-blue-600 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor"
viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<div class="text-sm text-blue-800">
<p class="font-medium mb-1">加密流程说明</p>
<ol class="list-decimal list-inside space-y-1 text-xs">
<li>用户输入的JSON参数 AES-CBC加密 Base64编码</li>
<li>Base64字符串放入请求体的 <code class="bg-blue-100 px-1 rounded">data</code> 字段</li>
<li>最终HTTP请求体格式<code class="bg-blue-100 px-1 rounded">{"data": "base64字符串"}</code></li>
</ol>
<div class="mt-2">
<el-button type="primary" size="small" @click="showEncryptionDetails = true" class="text-xs">
<svg class="w-3 h-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
查看加密详情
</el-button>
</div>
</div>
</div>
</div>
<div class="flex justify-between items-center flex-wrap gap-2">
<p class="text-xs text-gray-500 mr-2 flex-1 min-w-[180px]">点击下方按钮对参数进行加密查看最终请求体</p>
<el-button type="primary" size="default" @click="encryptParams" :loading="encrypting">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z">
</path>
</svg>
加密参数
</el-button>
</div>
<!-- 原始参数 -->
<div class="p-3 bg-gray-50 rounded border border-gray-200">
<h5 class="text-xs font-semibold text-gray-800 m-0 mb-1.5 pb-0.5 border-b border-gray-200">原始参数
(JSON)
</h5>
<pre
class="bg-gray-100 p-2 rounded text-xs overflow-x-auto max-h-20 m-0 border border-gray-200 font-mono text-gray-800">
{{ JSON.stringify(debugForm.params, null, 2) }}</pre>
<p class="text-xs text-gray-500 text-center mt-1">用户输入的原始参数</p>
</div>
<!-- 最终请求体 -->
<div class="p-3 bg-green-50 rounded border border-green-200">
<h5 class="text-xs font-semibold text-green-800 m-0 mb-1.5 pb-0.5 border-b border-green-200">最终请求体
(HTTP)
</h5>
<pre
class="bg-gray-100 p-2 rounded text-xs overflow-x-auto max-h-20 m-0 border border-green-200 font-mono text-green-800">
{{ getFinalRequestBody() }}</pre>
<p class="text-xs text-green-600 text-center font-medium mt-1">实际发送给API的HTTP请求体</p>
<el-button type="success" size="default" @click="copyToClipboard(getFinalRequestBody())"
class="w-full mt-2">
复制请求体
</el-button>
</div>
</div>
</div>
</div>
<!-- 移除原来的调试操作按钮区域 -->
</div>
</div>
</div>
<!-- 右侧调试结果区域 -->
<div class="flex flex-col gap-3 h-full w-96 flex-shrink-0">
<!-- 调试结果 -->
<div class="bg-white rounded-lg shadow-sm overflow-hidden flex flex-col h-full">
<div class="p-3 flex flex-col h-full">
<div
class="flex justify-between items-center mb-3 pb-1.5 border-b border-gray-200 flex-wrap gap-2 flex-shrink-0">
<h3 class="text-base font-semibold text-gray-800 m-0">调试结果</h3>
<el-button v-if="debugResult" @click="debugResult = null" size="default" plain>
清空
</el-button>
</div>
<div v-if="!debugResult" class="flex items-center justify-center h-24 text-gray-400">
<el-empty description="调试结果将在这里显示" />
</div>
<div v-else class="p-3 flex-1 overflow-y-auto">
<div class="flex justify-between items-center mb-3 pb-1.5 border-b border-gray-200 flex-wrap gap-2">
<div class="text-base font-semibold text-gray-800">
<el-tag :type="debugResult.success ? 'success' : 'danger'" size="default">
{{ debugResult.success ? '调试成功' : '调试失败' }}
</el-tag>
</div>
<div class="flex gap-2 text-xs text-gray-500 flex-wrap">
<span class="flex items-center gap-1">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
{{ debugResult.responseTime }}
</span>
<span v-if="debugResult.transactionId" class="flex items-center gap-1">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z">
</path>
</svg>
{{ debugResult.transactionId }}
</span>
</div>
</div>
<div class="mt-3">
<h4 class="text-sm font-semibold text-gray-800 mb-1.5 pb-0.5 border-b border-gray-200">请求信息</h4>
<div class="bg-white rounded border border-gray-200 p-2 mb-3">
<h5 class="text-xs font-semibold text-gray-700 mb-1">请求体</h5>
<pre
class="bg-gray-100 p-2 rounded text-xs overflow-x-auto m-0 mb-1.5 max-h-32 border border-gray-200 font-mono text-gray-800">
{{ JSON.stringify(getUserVisibleRequestBody(debugResult.requestBody), null, 2) }}</pre>
<el-button type="info" size="default"
@click="copyToClipboard(JSON.stringify(getUserVisibleRequestBody(debugResult.requestBody), null, 2))"
class="w-full">
复制请求体
</el-button>
</div>
<h4 class="text-sm font-semibold text-gray-800 mb-1.5 pb-0.5 border-b border-gray-200">响应内容</h4>
<div class="bg-white rounded border border-gray-200 p-2">
<pre
class="bg-gray-100 p-2 rounded text-xs overflow-x-auto m-0 mb-1.5 max-h-45 border border-gray-200 font-mono text-gray-800">
{{ JSON.stringify(debugResult.response, null, 2) }}</pre>
<el-button type="info" size="default"
@click="copyToClipboard(JSON.stringify(debugResult.response, null, 2))" class="w-full">
复制响应
</el-button>
<!-- 解密后的内容 -->
<div v-if="decryptedData" class="mt-3 pt-3 border-t border-gray-200">
<h5 class="text-xs font-semibold text-gray-700 mb-1">解密后的内容</h5>
<pre
class="bg-green-50 p-2 rounded text-xs overflow-x-auto m-0 mb-1.5 max-h-45 border border-green-200 font-mono text-green-800"
:key="`decrypted-${Date.now()}`">
{{ JSON.stringify(decryptedData, null, 2) }}</pre>
<el-button type="success" size="default"
@click="copyToClipboard(JSON.stringify(decryptedData, null, 2))" class="w-full">
复制解密内容
</el-button>
</div>
<!-- 解密加载状态 -->
<div v-else-if="debugging && debugResult && debugResult.success && debugResult.response?.data?.data"
class="mt-3 pt-3 border-t border-gray-200">
<div class="flex items-center gap-2 text-sm text-gray-500">
<svg class="animate-spin h-4 w-4 text-gray-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<span>正在解密响应数据...</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 加密详情弹窗 -->
<el-dialog v-model="showEncryptionDetails" title="AES-CBC 加密机制详解" width="800px" :close-on-click-modal="false"
:close-on-press-escape="true">
<div class="space-y-6">
<!-- 加密和解密机制 -->
<div class="bg-gray-50 p-5 rounded-lg border border-gray-200">
<h3 class="text-lg font-semibold text-gray-800 mb-3">加密和解密机制</h3>
<p class="text-sm text-gray-700 leading-relaxed">
账户获得的密钥Access Key是一个 16 进制字符串使用 AES-128 加密算法
</p>
</div>
<!-- 加密过程 -->
<div class="bg-gray-50 p-5 rounded-lg border border-gray-200">
<h4 class="text-base font-semibold text-gray-800 mb-4">加密过程</h4>
<ul class="space-y-3 text-sm text-gray-700">
<li class="flex items-start gap-3">
<span class="bg-gray-300 text-gray-700 px-2.5 py-1 rounded text-xs font-mono font-semibold">1</span>
<span><strong>加密模式</strong>AES-CBC 模式</span>
</li>
<li class="flex items-start gap-3">
<span class="bg-gray-300 text-gray-700 px-2.5 py-1 rounded text-xs font-mono font-semibold">2</span>
<span><strong>密钥长度</strong>128 16 字节</span>
</li>
<li class="flex items-start gap-3">
<span class="bg-gray-300 text-gray-700 px-2.5 py-1 rounded text-xs font-mono font-semibold">3</span>
<span><strong>填充方式</strong>PKCS7 填充</span>
</li>
<li class="flex items-start gap-3">
<span class="bg-gray-300 text-gray-700 px-2.5 py-1 rounded text-xs font-mono font-semibold">4</span>
<span><strong>IV初始化向量</strong>IV 长度为 16 字节128 每次加密时随机生成</span>
</li>
<li class="flex items-start gap-3">
<span class="bg-gray-300 text-gray-700 px-2.5 py-1 rounded text-xs font-mono font-semibold">5</span>
<span><strong>数据拼接</strong>加密后 IV 和密文拼接在一起进行传输</span>
</li>
<li class="flex items-start gap-3">
<span class="bg-gray-300 text-gray-700 px-2.5 py-1 rounded text-xs font-mono font-semibold">6</span>
<span><strong>Base64编码</strong>最后将拼接了 IV 的密文通过 Base64 编码方便在网络或文件中传输</span>
</li>
</ul>
</div>
<!-- 解密过程 -->
<div class="bg-gray-50 p-5 rounded-lg border border-gray-200">
<h4 class="text-base font-semibold text-gray-800 mb-4">解密过程</h4>
<ul class="space-y-3 text-sm text-gray-700">
<li class="flex items-start gap-3">
<span class="bg-gray-300 text-gray-700 px-2.5 py-1 rounded text-xs font-mono font-semibold">1</span>
<span><strong>Base64解码</strong>解密时首先从 Base64 解码后的数据中提取前 16 字节作为 IV</span>
</li>
<li class="flex items-start gap-3">
<span class="bg-gray-300 text-gray-700 px-2.5 py-1 rounded text-xs font-mono font-semibold">2</span>
<span><strong>AES解密</strong>然后使用提取的 IV通过 AES-CBC 模式解密剩余部分的密文</span>
</li>
<li class="flex items-start gap-3">
<span class="bg-gray-300 text-gray-700 px-2.5 py-1 rounded text-xs font-mono font-semibold">3</span>
<span><strong>去除填充</strong>解密后去除 PKCS7 填充即可得到原始明文</span>
</li>
</ul>
</div>
</div>
<template #footer>
<div class="flex justify-end">
<el-button @click="showEncryptionDetails = false" type="primary">
关闭
</el-button>
</div>
</template>
</el-dialog>
<!-- 示例代码弹窗 -->
<el-dialog v-model="showExampleCodeDialog" title="示例代码" width="1000px" :close-on-click-modal="false"
:close-on-press-escape="true" class="example-code-dialog">
<div class="h-[600px]">
<CodeDisplay :product-code="selectedProduct?.code || ''" />
</div>
<template #footer>
<div class="flex justify-end">
<el-button @click="showExampleCodeDialog = false" type="primary">
关闭
</el-button>
</div>
</template>
</el-dialog>
</template>
<style scoped>
/* 响应式优化 */
@media (max-width: 1024px) {
.debugger-page {
height: auto;
min-height: 100vh;
overflow: auto;
}
.debugger-main {
flex-direction: column;
overflow: visible;
}
.panel-left {
width: 100%;
max-height: 360px;
overflow: auto;
}
.panel-center {
width: 100%;
min-height: auto;
overflow: visible;
}
.panel-center .grid.grid-cols-2 {
grid-template-columns: 1fr;
}
.panel-center .grid.grid-cols-2 > * {
min-width: 0;
}
}
@media (max-width: 768px) {
.debugger-main {
gap: 12px;
padding: 12px;
}
.panel-left {
max-height: 320px;
}
.panel-center {
padding: 0;
}
.actions-header {
align-items: flex-start;
}
/* 按钮区域:两列折行,避免超出屏幕 */
.actions-bar {
width: 100%;
justify-content: flex-start;
gap: 8px;
}
.actions-bar .el-button {
flex: 1 1 48%;
min-width: 140px;
}
/* 主要按钮独占一行,保证可点 */
.actions-bar .el-button:last-child {
flex: 1 1 100%;
}
/* 操作按钮区域允许换行且间距更紧凑 */
.panel-center .flex-wrap {
gap: 8px;
}
/* 表单输入区域保持可读性 */
.panel-center :deep(.el-input),
.panel-center :deep(.el-select) {
width: 100%;
}
}
</style>
<script setup>
import { apiApi, apiKeysApi, consoleApi, formConfigApi, productApi, subscriptionApi } from '@/api'
import { ElMessage } from 'element-plus'
import { marked } from 'marked'
import { computed, nextTick, onMounted, reactive, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import CodeDisplay from '@/components/common/CodeDisplay.vue'
const router = useRouter()
const route = useRoute()
// 响应式数据
const loading = ref(false)
const productsLoading = ref(false)
const debugging = ref(false)
const encrypting = ref(false)
const autoSelecting = ref(false) // 新增:自动选择状态
const isSelectingProduct = ref(false) // 防止重复选择产品的标志
const lastSelectedProductId = ref(null) // 记录最后选择的产品ID
const userProducts = ref([])
const apiConfig = ref(null)
const selectedProduct = ref(null)
const searchKeyword = ref('')
const encryptedData = ref('')
const activeTab = ref('basic_info') // 控制文档内容的Tab
const productDocumentation = ref(null) // 存储产品文档
const showEncryptionDetails = ref(false) // 新增:加密详情弹窗状态
const showExampleCodeDialog = ref(false) // 新增:示例代码弹窗状态
const hasJsonError = ref(false) // JSON格式错误状态
const decryptedData = ref(null) // 解密后的数据
// 新增:动态表单相关数据
const formFields = ref([])
const formData = ref({})
// 表单引用
const paramFormRef = ref(null)
// 调试表单
const debugForm = reactive({
accessId: '',
secretKey: '',
params: {}
})
// 调试结果
const debugResult = ref(null)
// 参数验证规则 - 现在直接验证JSON格式
const validateJsonParams = () => {
try {
if (typeof debugForm.params === 'string') {
JSON.parse(debugForm.params)
}
hasJsonError.value = false
ElMessage.success('JSON格式正确')
return true
} catch (error) {
hasJsonError.value = true
ElMessage.error('JSON格式错误' + error.message)
return false
}
}
// 实时JSON格式检查
const checkJsonFormat = () => {
if (!debugForm.params || typeof debugForm.params !== 'string') {
hasJsonError.value = false
return
}
try {
JSON.parse(debugForm.params)
hasJsonError.value = false
} catch (error) {
hasJsonError.value = true
}
}
// 计算属性
const filteredProducts = computed(() => {
if (!searchKeyword.value) {
return userProducts.value
}
return userProducts.value.filter(product =>
product.name.toLowerCase().includes(searchKeyword.value.toLowerCase()) ||
product.code.toLowerCase().includes(searchKeyword.value.toLowerCase())
)
})
const canDebug = computed(() => {
if (!selectedProduct.value || !debugForm.accessId || !debugForm.secretKey) {
return false
}
if (!apiConfig.value || !apiConfig.value.request_params) {
return false
}
// 如果有动态表单字段,检查必填字段是否填写
if (formFields.value.length > 0) {
const hasRequiredFields = formFields.value.some(field => field.required)
if (hasRequiredFields) {
// 检查所有必填字段是否已填写
return formFields.value.every(field => !field.required || formData.value[field.name])
}
// 如果没有必填字段,只要有字段就可以调试
return formFields.value.length > 0
}
// 如果没有动态表单回退到JSON验证
return !hasJsonError.value && debugForm.params && typeof debugForm.params === 'string' && debugForm.params.trim() !== ''
})
// 生命周期
onMounted(async () => {
await loadApiKeys()
await loadUserProducts()
// 自动选择产品的逻辑已在 loadUserProducts 中处理
console.log("route", route.params)
})
// 监听路由参数变化,自动选择产品
watch(
() => route.query.productId || route.params.productId,
async (newProductId, oldProductId) => {
// 防止重复触发如果产品ID没有变化不执行
if (newProductId === oldProductId) {
return
}
// 如果正在选择产品,不重复执行
if (isSelectingProduct.value) {
return
}
if (newProductId && userProducts.value.length > 0) {
await autoSelectProduct(newProductId)
} else if (!newProductId && userProducts.value.length > 0 && !selectedProduct.value) {
// 如果没有指定产品且当前没有选中产品,选择第一个
await selectProduct(userProducts.value[0])
}
}
)
// 自动选择产品
const autoSelectProduct = async (productId) => {
// 防止重复执行
if (isSelectingProduct.value) {
console.log('正在选择产品,跳过重复请求')
return
}
// 如果已经选择了相同的产品,不重复选择
if (lastSelectedProductId.value === productId && selectedProduct.value) {
console.log('产品已选择,跳过重复选择:', productId)
return
}
// 如果用户产品列表为空,等待加载完成
if (!userProducts.value.length) {
console.log('等待用户产品列表加载完成...')
// 等待一段时间后重试
await new Promise(resolve => setTimeout(resolve, 1000))
// 如果仍然为空,说明加载失败
if (!userProducts.value.length) {
console.warn('用户产品列表加载失败或为空')
ElMessage.warning('产品列表加载失败,请刷新页面重试')
return
}
}
// 查找目标产品
const targetProduct = userProducts.value.find(p =>
p.id == productId ||
p.product_id == productId ||
p.code === productId
)
if (targetProduct) {
console.log('自动选择产品:', targetProduct)
isSelectingProduct.value = true
try {
await selectProduct(targetProduct)
lastSelectedProductId.value = productId
} finally {
isSelectingProduct.value = false
}
} else {
console.warn('未找到指定的产品:', productId)
ElMessage.warning(`未找到产品ID为 ${productId} 的产品,请手动选择`)
}
}
// 加载用户订阅的产品
const loadUserProducts = async () => {
productsLoading.value = true
try {
const response = await subscriptionApi.getMySubscriptions({
page: 1,
page_size: 1000
})
if (response.success && response.data?.items) {
userProducts.value = response.data.items.map(item => ({
id: item.id,
product_id: item.product_id || item.product?.id, // 确保获取product_id
name: item.product?.name || '未知产品',
code: item.product?.code || '',
price: item.price || 0
}))
console.log("route.params", route)
// 检查是否有query或params参数指定产品
const productId = route.query.productId || route.params.productId
if (productId && userProducts.value.length > 0) {
await nextTick()
// 使用 autoSelectProduct 会自动处理防重复逻辑
await autoSelectProduct(productId)
} else if (userProducts.value.length > 0 && !selectedProduct.value) {
// 没有指定产品时,默认选择第一个(仅在未选择产品时)
autoSelecting.value = true
isSelectingProduct.value = true
try {
await selectProduct(userProducts.value[0])
lastSelectedProductId.value = userProducts.value[0].id || userProducts.value[0].product_id
} finally {
isSelectingProduct.value = false
autoSelecting.value = false
}
}
} else {
// 如果没有订阅产品,显示提示信息
userProducts.value = []
ElMessage.info('您还没有订阅任何产品,请先订阅产品后再使用调试功能')
}
} catch (error) {
console.error('加载用户产品失败:', error)
ElMessage.error('加载用户产品失败,请稍后重试')
userProducts.value = []
} finally {
productsLoading.value = false
}
}
// 加载API密钥
const loadApiKeys = async () => {
try {
const response = await apiKeysApi.getUserApiKeys()
if (response.success && response.data) {
debugForm.accessId = response.data.access_id || ''
debugForm.secretKey = response.data.secret_key || ''
}
} catch (error) {
console.error('加载API密钥失败:', error)
ElMessage.error('加载API密钥失败')
}
}
// 选择产品
const selectProduct = async (product) => {
// 防止重复选择相同产品
const productId = product.product_id || product.id
if (selectedProduct.value &&
(selectedProduct.value.id === productId || selectedProduct.value.product_id === productId) &&
!isSelectingProduct.value) {
console.log('产品已选择,跳过重复加载:', productId)
return
}
// 确保API密钥已经加载
if (!debugForm.accessId || !debugForm.secretKey) {
ElMessage.warning('正在加载API密钥请稍候...')
return
}
selectedProduct.value = product
// 重置参数
debugForm.params = {}
debugResult.value = null
encryptedData.value = null
decryptedData.value = null
activeTab.value = 'basic_info' // 重置Tab
productDocumentation.value = null // 重置文档
// 加载产品详情和API配置
await loadProductDetail(product.product_id || product.id)
// 新增:加载表单配置
await loadFormConfig(product.code)
}
// 新增:加载表单配置
const loadFormConfig = async (apiCode) => {
try {
const response = await formConfigApi.getFormConfig(apiCode)
if (response.success && response.data) {
formFields.value = response.data.fields
// 初始化表单数据
formData.value = {}
formFields.value.forEach(field => {
formData.value[field.name] = ''
})
}
} catch (error) {
console.error('加载表单配置失败:', error)
// 如果加载失败清空表单字段回退到JSON输入模式
formFields.value = []
formData.value = {}
}
}
// 处理图片上传并转换为base64
const handleImageUpload = (file, fieldName) => {
const fileObj = file.raw || file
// 验证文件类型
const allowedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/bmp']
if (!allowedTypes.includes(fileObj.type)) {
ElMessage.error('只支持 JPG、BMP、PNG 格式的图片')
return false
}
// 验证文件大小限制为5MB
const maxSize = 5 * 1024 * 1024 // 5MB
if (fileObj.size > maxSize) {
ElMessage.error('图片大小不能超过 5MB')
return false
}
// 读取文件并转换为base64
const reader = new FileReader()
reader.onload = (e) => {
const base64String = e.target.result
// 移除 data:image/xxx;base64, 前缀只保留纯base64数据
const base64Data = base64String.includes(',') ? base64String.split(',')[1] : base64String
formData.value[fieldName] = base64Data
ElMessage.success('图片上传成功已转换为base64')
}
reader.onerror = () => {
ElMessage.error('图片读取失败,请重试')
}
reader.readAsDataURL(fileObj)
return false // 阻止自动上传
}
// 清空图片数据
const clearImageData = (fieldName) => {
formData.value[fieldName] = ''
ElMessage.success('已清空图片数据')
}
// 加载示例参数
const loadExampleParams = () => {
if (!apiConfig.value?.request_params) {
ElMessage.warning('暂无API配置信息')
return
}
const exampleParams = {}
apiConfig.value.request_params.forEach(param => {
if (param.example) {
exampleParams[param.field] = param.example
} else {
// 根据类型设置默认示例值
switch (param.type) {
case 'string':
exampleParams[param.field] = '示例值'
break
case 'number':
exampleParams[param.field] = 123
break
case 'boolean':
exampleParams[param.field] = true
break
case 'password':
exampleParams[param.field] = 'password123'
break
default:
exampleParams[param.field] = '示例值'
}
}
})
debugForm.params = JSON.stringify(exampleParams, null, 2)
hasJsonError.value = false
ElMessage.success('已加载示例参数')
}
// 加载产品详情
const loadProductDetail = async (productId) => {
try {
// 添加 with_document 参数获取文档信息
const response = await productApi.getProductDetail(productId, { with_document: true })
if (response.success && response.data) {
const productData = response.data
// 构建API配置
apiConfig.value = {
request_params: productData.api_config?.request_params || [],
response_fields: productData.api_config?.response_fields || [],
response_example: productData.api_config?.response_example || {}
}
// 初始化参数表单
if (apiConfig.value.request_params && apiConfig.value.request_params.length > 0) {
// 使用JSON文本输入初始化为空字符串
debugForm.params = ''
// 同时更新动态表单数据
formFields.value = apiConfig.value.request_params.map(param => ({
name: param.name,
label: param.name,
field: param.field,
type: param.type,
required: param.required,
description: param.description,
example: param.example,
validation: param.validation
}))
formData.value = {} // 清空表单数据
} else {
// 如果没有API配置使用默认配置
apiConfig.value = {
request_params: [
{
field: 'param1',
name: '参数1',
type: 'text',
required: true,
description: '请输入参数1',
example: '示例值1'
}
],
response_fields: [
{
name: '状态码',
path: 'code',
type: 'integer',
description: '响应状态码',
required: true,
example: '0'
}
],
response_example: {
code: 0,
message: "请求成功",
data: {}
}
}
debugForm.params = ''
formFields.value = [] // 清空动态表单
formData.value = {} // 清空表单数据
}
// 保存文档内容
productDocumentation.value = {
basic_info: productData.documentation?.basic_info || '',
request_params: productData.documentation?.request_params || '',
response_fields: productData.documentation?.response_fields || '',
response_example: productData.documentation?.response_example || '',
error_codes: productData.documentation?.error_codes || ''
}
}
} catch (error) {
console.error('加载产品详情失败:', error)
ElMessage.error('加载产品详情失败')
// 如果加载失败,使用默认配置
apiConfig.value = {
request_params: [
{
field: 'param1',
name: '参数1',
type: 'text',
required: true,
description: '请输入参数1',
example: '示例值1'
}
],
response_fields: [
{
name: '状态码',
path: 'code',
type: 'integer',
description: '响应状态码',
required: true,
example: '0'
}
],
response_example: {
code: 0,
message: "请求成功",
data: {}
}
}
debugForm.params = { param1: '' }
formFields.value = [] // 清空动态表单
formData.value = {} // 清空表单数据
productDocumentation.value = null // 清除文档
}
}
// 搜索处理
const handleSearch = () => {
// 搜索逻辑已在计算属性中处理
}
// 获取请求URL
const getRequestUrl = () => {
if (!selectedProduct.value) return ''
const baseUrl = import.meta.env.VITE_API_URL
return `${baseUrl}/api/v1/${selectedProduct.value.code}`
}
// 根据字段类型转换数据
const convertFieldTypes = (data) => {
if (!formFields.value || formFields.value.length === 0) {
return data
}
const processedData = { ...data }
formFields.value.forEach(field => {
const value = processedData[field.name]
// 如果字段值为空字符串、null 或 undefined跳过转换
if (value === '' || value === null || value === undefined) {
return
}
// 根据字段类型进行转换
if (field.type === 'number') {
// 将字符串转换为数字(整数)
const numValue = parseInt(value, 10)
if (!isNaN(numValue)) {
processedData[field.name] = numValue
}
}
})
return processedData
}
// 加密参数
const encryptParams = async () => {
if (!canDebug.value) {
ElMessage.warning('请先填写完整的必填参数')
return
}
encrypting.value = true
try {
// 使用后端加密接口
const encryptedResult = await encryptWithAES(debugForm.params, debugForm.secretKey)
console.log('encryptedResult', encryptedResult)
if (encryptedResult) {
encryptedData.value = encryptedResult
ElMessage.success('参数加密成功')
} else {
throw new Error('加密失败')
}
} catch (error) {
console.error('加密失败:', error)
ElMessage.error('参数加密失败')
} finally {
encrypting.value = false
}
}
// 调用后端加密接口
const encryptWithAES = async (data, secretKey) => {
try {
console.log('开始调用后端加密接口,参数:', data, '密钥:', secretKey)
// 解析JSON字符串如果是字符串
let parsedData = typeof data === 'string' ? JSON.parse(data) : data
// 根据字段类型进行类型转换
parsedData = convertFieldTypes(parsedData)
// 使用项目的标准API调用方式传递密钥参数
const result = await apiApi.encryptParams(parsedData, secretKey)
console.log('加密接口响应数据:', result)
// 根据项目的响应结构result 就是 EncryptResponse
if (!result || !result.data?.encrypted_data) {
throw new Error('加密响应数据格式错误')
}
console.log('加密成功,返回数据:', result.data.encrypted_data)
return result.data.encrypted_data
} catch (error) {
console.error('调用后端加密接口失败:', error)
throw new Error('加密失败: ' + error.message)
}
}
// 复制到剪贴板
const copyToClipboard = async (text) => {
if (!text) return
try {
await navigator.clipboard.writeText(text)
ElMessage.success('复制成功')
} catch (error) {
console.error('复制失败:', error)
ElMessage.error('复制失败')
}
}
// 重置表单
const resetForm = () => {
selectedProduct.value = null
debugForm.params = ''
hasJsonError.value = false
debugResult.value = null
apiConfig.value = null
encryptedData.value = null
decryptedData.value = null // 重置解密数据
activeTab.value = 'basic_info' // 重置Tab
productDocumentation.value = null // 重置文档
formFields.value = [] // 清空动态表单
formData.value = {} // 清空表单数据
showExampleCodeDialog.value = false // 重置示例代码弹窗状态
showEncryptionDetails.value = false // 重置加密详情弹窗状态
}
// 开始调试
const handleDebug = async () => {
if (!canDebug.value) return
// 新增:验证动态表单
if (formFields.value.length > 0) {
const errors = []
formFields.value.forEach(field => {
if (field.required && !formData.value[field.name]) {
errors.push(`${field.label}是必填字段`)
}
})
if (errors.length > 0) {
ElMessage.error(errors.join('、'))
return
}
// 将表单数据转换为JSON格式并根据字段类型进行类型转换
const processedData = convertFieldTypes(formData.value)
debugForm.params = JSON.stringify(processedData, null, 2)
} else {
// 原有的JSON验证逻辑
if (!validateJsonParams()) {
return
}
}
debugging.value = true
// 清空之前的调试结果确保UI实时更新
debugResult.value = null
decryptedData.value = null
await nextTick() // 确保DOM更新
const startTime = new Date()
try {
// 1. 解析JSON参数
let parsedParams
try {
parsedParams = JSON.parse(debugForm.params)
} catch (error) {
ElMessage.error('JSON格式错误无法解析参数')
return
}
// 2. 加密参数
const encryptedParams = await encryptWithAES(parsedParams, debugForm.secretKey)
if (!encryptedParams) {
ElMessage.error('参数加密失败')
return
}
// 更新加密数据,用于显示最终请求体
encryptedData.value = encryptedParams
// 3. 构建请求体
const requestBody = {
data: encryptedParams,
options: {
is_debug: true // 标识为调试调用
}
}
// 4. 调用API
try {
console.log('准备调用产品API:', selectedProduct.value.code)
const responseData = await consoleApi.callProductAPI(selectedProduct.value.code, requestBody, debugForm.accessId)
console.log('产品API调用成功:', responseData)
const endTime = new Date()
// 5. 保存调试结果 - 立即更新确保UI实时显示
debugResult.value = createDebugResult(
selectedProduct.value,
requestBody,
encryptedParams,
responseData,
startTime,
endTime,
responseData.success && responseData.data?.code === 0
)
// 确保DOM更新后再进行解密操作
await nextTick()
// 6. 如果响应成功且包含加密数据,自动解密
console.log('responseData', responseData)
if (responseData.success && responseData.data?.code === 0 && responseData.data?.data && typeof responseData.data.data === 'string') {
try {
const decryptResult = await apiApi.decryptParams(
responseData.data.data,
debugForm.secretKey
)
if (decryptResult.success) {
// 使用 nextTick 确保响应式更新
decryptedData.value = decryptResult.data
await nextTick()
ElMessage.success('调试完成,数据已自动解密')
} else {
ElMessage.warning('调试完成,但数据解密失败:' + (decryptResult.message || '未知错误'))
}
} catch (decryptError) {
console.error('自动解密失败:', decryptError)
ElMessage.warning('调试完成,但数据解密失败:' + (decryptError.message || '未知错误'))
}
} else {
ElMessage.success('调试完成')
}
} catch (apiError) {
console.error('API调用失败:', apiError)
console.error('错误详情:', {
message: apiError.message,
stack: apiError.stack,
name: apiError.name
})
const apiEndTime = new Date()
// 构建错误响应结构,保持与成功响应一致
const errorResponse = {
success: false,
data: {
code: -1,
message: apiError.message || 'API调用失败',
transaction_id: '',
data: null
},
message: '请求失败'
}
debugResult.value = createDebugResult(
selectedProduct.value,
requestBody,
encryptedParams,
errorResponse,
startTime,
apiEndTime,
false
)
// 确保DOM更新
await nextTick()
ElMessage.error('API调用失败' + apiError.message)
}
} catch (error) {
console.error('调试失败:', error)
ElMessage.error('调试失败,请检查网络连接')
const endTime = new Date()
debugResult.value = createDebugResult(
selectedProduct.value,
{},
'',
{ error: error.message },
startTime,
endTime,
false
)
// 确保DOM更新
await nextTick()
} finally {
debugging.value = false
}
}
// 渲染Markdown
const renderMarkdown = (markdown) => {
if (!markdown) {
return ''
}
return marked(markdown)
}
// 跳转到产品详情页面
const goToProductDetail = () => {
if (selectedProduct.value) {
// 在新标签页中打开产品详情页面
const productId = selectedProduct.value.product_id || selectedProduct.value.id
const url = `/products/${productId}`
window.open(url, '_blank')
} else {
ElMessage.warning('请先选择一个产品')
}
}
// 显示示例代码弹窗
const showExampleCode = () => {
if (!selectedProduct.value) {
ElMessage.warning('请先选择一个产品')
return
}
showExampleCodeDialog.value = true
}
// 调用产品API进行调试
// 现在使用 consoleApi.callProductAPI此函数已删除
// 创建调试结果对象
const createDebugResult = (product, requestBody, encryptedParams, responseData, startTime, endTime, success = true) => {
return {
productName: product.name,
apiCode: product.code,
transactionId: responseData?.data?.transaction_id || '',
requestTime: startTime.toLocaleString('zh-CN'),
responseTime: endTime.toLocaleString('zh-CN'),
success: success,
requestBody: requestBody,
encryptedParams: encryptedParams,
response: responseData
}
}
// 获取最终请求体(用户可见版本,不显示调试标识)
const getFinalRequestBody = () => {
if (!encryptedData.value) {
return JSON.stringify({ data: "请先加密参数" }, null, 2)
}
const requestBody = {
data: encryptedData.value
}
return JSON.stringify(requestBody, null, 2)
}
// 获取用户可见的请求体(过滤掉调试标识)
const getUserVisibleRequestBody = (requestBody) => {
if (!requestBody) return {}
// 创建请求体的副本,移除调试标识
const visibleBody = { ...requestBody }
// 如果存在options且包含is_debug则移除
if (visibleBody.options && visibleBody.options.is_debug !== undefined) {
delete visibleBody.options.is_debug
// 如果options为空对象则完全移除options
if (Object.keys(visibleBody.options).length === 0) {
delete visibleBody.options
}
}
return visibleBody
}
</script>