2025-11-24 16:06:44 +08:00
|
|
|
|
<template>
|
2025-12-09 17:13:29 +08:00
|
|
|
|
<div class="debugger-page flex flex-col h-screen bg-gray-50 overflow-hidden">
|
2025-11-24 16:06:44 +08:00
|
|
|
|
<!-- 主要内容区域 -->
|
2025-12-09 17:13:29 +08:00
|
|
|
|
<div class="debugger-main flex gap-3 flex-1 p-3 overflow-hidden min-h-0">
|
2025-11-24 16:06:44 +08:00
|
|
|
|
<!-- 左侧产品选择区域 -->
|
2025-12-09 17:13:29 +08:00
|
|
|
|
<div class="panel-left bg-white rounded-lg shadow-sm overflow-hidden flex flex-col h-full w-72 flex-shrink-0">
|
2025-11-24 16:06:44 +08:00
|
|
|
|
<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>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 中间调试配置区域 -->
|
2025-12-09 17:13:29 +08:00
|
|
|
|
<div class="panel-center bg-white rounded-lg shadow-sm overflow-hidden flex flex-col h-full flex-1 min-w-0">
|
2025-11-24 16:06:44 +08:00
|
|
|
|
<div class="p-3 flex flex-col h-full">
|
2025-12-09 17:13:29 +08:00
|
|
|
|
<div class="flex justify-between items-center mb-3 flex-wrap gap-2 flex-shrink-0 actions-header">
|
2025-11-24 16:06:44 +08:00
|
|
|
|
<h3 class="text-base font-semibold text-gray-800 m-0">调试配置</h3>
|
2025-12-09 17:13:29 +08:00
|
|
|
|
<div class="actions-bar flex gap-1.5 items-center flex-wrap">
|
2025-11-24 16:06:44 +08:00
|
|
|
|
<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) -->
|
2026-01-09 15:58:17 +08:00
|
|
|
|
<div v-if="field.name === 'photo_data' || field.name === 'vlphoto_data' && field.type === 'textarea'" class="space-y-2">
|
2025-11-24 16:06:44 +08:00
|
|
|
|
<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>
|
2025-12-09 17:13:29 +08:00
|
|
|
|
/* 响应式优化 */
|
|
|
|
|
|
@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%;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-11-24 16:06:44 +08:00
|
|
|
|
</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>
|