Files
tyapi-frontend/src/pages/api/ApiDebugger.vue

1422 lines
55 KiB
Vue
Raw Normal View History

2025-11-24 16:06:44 +08:00
<template>
<div class="flex flex-col h-screen bg-gray-50 overflow-hidden">
<!-- 主要内容区域 -->
<div class="flex gap-3 flex-1 p-3 overflow-hidden min-h-0">
<!-- 左侧产品选择区域 -->
<div class="bg-white rounded-lg shadow-sm overflow-hidden flex flex-col h-full w-64 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="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">
<h3 class="text-base font-semibold text-gray-800 m-0">调试配置</h3>
<div class="flex gap-1.5 items-center">
<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.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
2025-12-04 12:44:54 +08:00
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()}`">
2025-11-24 16:06:44 +08:00
{{ 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>
2025-12-04 12:44:54 +08:00
<!-- 解密加载状态 -->
<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>
2025-11-24 16:06:44 +08:00
</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>
/* 所有样式已使用Tailwind CSS实现 */
</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) // 新增:自动选择状态
2025-12-04 12:44:54 +08:00
const isSelectingProduct = ref(false) // 防止重复选择产品的标志
const lastSelectedProductId = ref(null) // 记录最后选择的产品ID
2025-11-24 16:06:44 +08:00
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(
2025-12-04 12:44:54 +08:00
() => route.query.productId || route.params.productId,
async (newProductId, oldProductId) => {
// 防止重复触发如果产品ID没有变化不执行
if (newProductId === oldProductId) {
return
}
// 如果正在选择产品,不重复执行
if (isSelectingProduct.value) {
return
}
2025-11-24 16:06:44 +08:00
if (newProductId && userProducts.value.length > 0) {
await autoSelectProduct(newProductId)
2025-12-04 12:44:54 +08:00
} else if (!newProductId && userProducts.value.length > 0 && !selectedProduct.value) {
// 如果没有指定产品且当前没有选中产品,选择第一个
2025-11-24 16:06:44 +08:00
await selectProduct(userProducts.value[0])
}
}
)
// 自动选择产品
const autoSelectProduct = async (productId) => {
2025-12-04 12:44:54 +08:00
// 防止重复执行
if (isSelectingProduct.value) {
console.log('正在选择产品,跳过重复请求')
return
}
// 如果已经选择了相同的产品,不重复选择
if (lastSelectedProductId.value === productId && selectedProduct.value) {
console.log('产品已选择,跳过重复选择:', productId)
return
}
2025-11-24 16:06:44 +08:00
// 如果用户产品列表为空,等待加载完成
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)
2025-12-04 12:44:54 +08:00
isSelectingProduct.value = true
try {
await selectProduct(targetProduct)
lastSelectedProductId.value = productId
} finally {
isSelectingProduct.value = false
}
2025-11-24 16:06:44 +08:00
} else {
console.warn('未找到指定的产品:', productId)
ElMessage.warning(`未找到产品ID为 ${productId} 的产品,请手动选择`)
}
}
// 加载用户订阅的产品
const loadUserProducts = async () => {
productsLoading.value = true
try {
const response = await subscriptionApi.getMySubscriptions({
page: 1,
2025-12-04 12:44:54 +08:00
page_size: 1000
2025-11-24 16:06:44 +08:00
})
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)
2025-12-04 12:44:54 +08:00
// 检查是否有query或params参数指定产品
const productId = route.query.productId || route.params.productId
if (productId && userProducts.value.length > 0) {
2025-11-24 16:06:44 +08:00
await nextTick()
2025-12-04 12:44:54 +08:00
// 使用 autoSelectProduct 会自动处理防重复逻辑
await autoSelectProduct(productId)
} else if (userProducts.value.length > 0 && !selectedProduct.value) {
// 没有指定产品时,默认选择第一个(仅在未选择产品时)
2025-11-24 16:06:44 +08:00
autoSelecting.value = true
2025-12-04 12:44:54 +08:00
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
}
2025-11-24 16:06:44 +08:00
}
} 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) => {
2025-12-04 12:44:54 +08:00
// 防止重复选择相同产品
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
}
2025-11-24 16:06:44 +08:00
// 确保API密钥已经加载
if (!debugForm.accessId || !debugForm.secretKey) {
ElMessage.warning('正在加载API密钥请稍候...')
return
}
selectedProduct.value = product
// 重置参数
debugForm.params = {}
debugResult.value = null
encryptedData.value = null
2025-12-04 12:44:54 +08:00
decryptedData.value = null
2025-11-24 16:06:44 +08:00
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}`
}
2025-12-05 14:59:37 +08:00
// 根据字段类型转换数据
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
}
2025-11-24 16:06:44 +08:00
// 加密参数
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)
2025-12-05 14:59:37 +08:00
// 解析JSON字符串如果是字符串
let parsedData = typeof data === 'string' ? JSON.parse(data) : data
// 根据字段类型进行类型转换
parsedData = convertFieldTypes(parsedData)
2025-11-24 16:06:44 +08:00
// 使用项目的标准API调用方式传递密钥参数
2025-12-05 14:59:37 +08:00
const result = await apiApi.encryptParams(parsedData, secretKey)
2025-11-24 16:06:44 +08:00
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
}
2025-12-05 14:59:37 +08:00
// 将表单数据转换为JSON格式并根据字段类型进行类型转换
const processedData = convertFieldTypes(formData.value)
debugForm.params = JSON.stringify(processedData, null, 2)
2025-11-24 16:06:44 +08:00
} else {
// 原有的JSON验证逻辑
if (!validateJsonParams()) {
return
}
}
debugging.value = true
2025-12-04 12:44:54 +08:00
// 清空之前的调试结果确保UI实时更新
debugResult.value = null
decryptedData.value = null
await nextTick() // 确保DOM更新
2025-11-24 16:06:44 +08:00
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()
2025-12-04 12:44:54 +08:00
// 5. 保存调试结果 - 立即更新确保UI实时显示
2025-11-24 16:06:44 +08:00
debugResult.value = createDebugResult(
selectedProduct.value,
requestBody,
encryptedParams,
responseData,
startTime,
endTime,
responseData.success && responseData.data?.code === 0
)
2025-12-04 12:44:54 +08:00
// 确保DOM更新后再进行解密操作
await nextTick()
2025-11-24 16:06:44 +08:00
// 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) {
2025-12-04 12:44:54 +08:00
// 使用 nextTick 确保响应式更新
2025-11-24 16:06:44 +08:00
decryptedData.value = decryptResult.data
2025-12-04 12:44:54 +08:00
await nextTick()
2025-11-24 16:06:44 +08:00
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
)
2025-12-04 12:44:54 +08:00
// 确保DOM更新
await nextTick()
2025-11-24 16:06:44 +08:00
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
)
2025-12-04 12:44:54 +08:00
// 确保DOM更新
await nextTick()
2025-11-24 16:06:44 +08:00
} 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>