bug修复

This commit is contained in:
2025-12-22 23:40:47 +08:00
parent 7d53a059d7
commit 1fefd98d74
19 changed files with 1339 additions and 534 deletions
+11
View File
@@ -26,5 +26,16 @@ export const menuConfig: MenuItem[] = [
path: '/aiconfig',
title: 'AI配置管理',
icon: 'Setting'
},
{
path: '/tools',
title: '开发工具',
icon: 'Tools',
children: [
{
path: '/tools/api-tester',
title: 'API接口调用'
}
]
}
]
+14
View File
@@ -63,6 +63,20 @@ const routes: RouteRecordRaw[] = [
}
]
},
{
path: '/tools',
component: Layout,
redirect: '/tools/api-tester',
meta: { title: '开发工具', icon: 'Tools' },
children: [
{
path: 'api-tester',
name: 'ApiTester',
component: () => import('@/views/tools/ApiTester.vue'),
meta: { title: 'API接口调用' }
}
]
},
{
path: '/:pathMatch(.*)*',
name: 'NotFound',
+659
View File
@@ -0,0 +1,659 @@
<template>
<div class="api-tester">
<h2 class="page-title">API接口调用</h2>
<!-- 请求配置区域 -->
<el-card class="request-card">
<template #header>
<div class="card-header">
<span>请求配置</span>
<el-button type="primary" @click="sendRequest" :loading="loading">
发送请求
</el-button>
</div>
</template>
<!-- 请求方法和URL -->
<el-form :model="requestForm" label-width="100px">
<el-row :gutter="20">
<el-col :span="4">
<el-form-item label="请求方法">
<el-select v-model="requestForm.method" style="width: 100%">
<el-option label="GET" value="GET" />
<el-option label="POST" value="POST" />
<el-option label="PUT" value="PUT" />
<el-option label="DELETE" value="DELETE" />
<el-option label="PATCH" value="PATCH" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="20">
<el-form-item label="接口路径">
<el-input
v-model="requestForm.url"
placeholder="请输入接口路径,如:/api/user-profile/migrateLifeEvents"
clearable
>
<template #prepend>{{ baseUrl }}</template>
</el-input>
</el-form-item>
</el-col>
</el-row>
</el-form>
<!-- 参数配置标签页 -->
<el-tabs v-model="activeTab" type="border-card">
<!-- Query参数 -->
<el-tab-pane label="Query参数" name="query">
<div class="params-header">
<span class="params-tip">URL查询参数?key=value&key2=value2</span>
<el-button type="primary" size="small" @click="addQueryParam">
添加参数
</el-button>
</div>
<el-table :data="requestForm.queryParams" border size="small">
<el-table-column label="参数名" min-width="150">
<template #default="{ row }">
<el-input v-model="row.key" placeholder="参数名" size="small" />
</template>
</el-table-column>
<el-table-column label="参数值" min-width="200">
<template #default="{ row }">
<el-input v-model="row.value" placeholder="参数值" size="small" />
</template>
</el-table-column>
<el-table-column label="描述" min-width="150">
<template #default="{ row }">
<el-input v-model="row.description" placeholder="描述(可选)" size="small" />
</template>
</el-table-column>
<el-table-column label="操作" width="80" align="center">
<template #default="{ $index }">
<el-button type="danger" link size="small" @click="removeQueryParam($index)">
删除
</el-button>
</template>
</el-table-column>
</el-table>
</el-tab-pane>
<!-- 请求头 -->
<el-tab-pane label="请求头" name="headers">
<div class="params-header">
<span class="params-tip">自定义请求头Authorization已自动添加</span>
<el-button type="primary" size="small" @click="addHeader">
添加请求头
</el-button>
</div>
<el-table :data="requestForm.headers" border size="small">
<el-table-column label="Header名" min-width="150">
<template #default="{ row }">
<el-input v-model="row.key" placeholder="Header名" size="small" />
</template>
</el-table-column>
<el-table-column label="Header值" min-width="200">
<template #default="{ row }">
<el-input v-model="row.value" placeholder="Header值" size="small" />
</template>
</el-table-column>
<el-table-column label="描述" min-width="150">
<template #default="{ row }">
<el-input v-model="row.description" placeholder="描述(可选)" size="small" />
</template>
</el-table-column>
<el-table-column label="操作" width="80" align="center">
<template #default="{ $index }">
<el-button type="danger" link size="small" @click="removeHeader($index)">
删除
</el-button>
</template>
</el-table-column>
</el-table>
</el-tab-pane>
<!-- 请求体 -->
<el-tab-pane label="请求体" name="body">
<div class="params-header">
<span class="params-tip">请求体内容JSON格式</span>
<div class="body-actions">
<el-button type="success" size="small" @click="formatRequestBody">
格式化JSON
</el-button>
<el-button type="warning" size="small" @click="compressRequestBody">
压缩JSON
</el-button>
<el-button size="small" @click="clearRequestBody">
清空
</el-button>
</div>
</div>
<el-input
v-model="requestForm.body"
type="textarea"
:rows="10"
placeholder='请输入JSON格式的请求体,如:{"key": "value"}'
class="body-textarea"
/>
<div v-if="bodyError" class="body-error">
<el-alert :title="bodyError" type="error" :closable="false" />
</div>
</el-tab-pane>
</el-tabs>
</el-card>
<!-- 响应结果区域 -->
<el-card class="response-card" v-if="responseData">
<template #header>
<div class="card-header">
<span>响应结果</span>
<div class="response-info">
<el-tag :type="getStatusType(responseData.status)" size="small">
状态码: {{ responseData.status }}
</el-tag>
<el-tag type="info" size="small">
耗时: {{ responseData.duration }}ms
</el-tag>
<el-button type="primary" size="small" @click="copyResponse">
复制响应
</el-button>
</div>
</div>
</template>
<el-tabs v-model="responseTab" type="border-card">
<!-- 响应体 -->
<el-tab-pane label="响应体" name="body">
<div class="response-body">
<pre class="json-display">{{ formatJson(responseData.data) }}</pre>
</div>
</el-tab-pane>
<!-- 响应头 -->
<el-tab-pane label="响应头" name="headers">
<el-table :data="responseHeaders" border size="small">
<el-table-column prop="key" label="Header名" min-width="200" />
<el-table-column prop="value" label="Header值" min-width="300" />
</el-table>
</el-tab-pane>
</el-tabs>
</el-card>
<!-- 历史记录区域 -->
<el-card class="history-card">
<template #header>
<div class="card-header">
<span>请求历史最近10条</span>
<el-button type="danger" size="small" @click="clearHistory" :disabled="history.length === 0">
清空历史
</el-button>
</div>
</template>
<el-table :data="history" border size="small" v-if="history.length > 0">
<el-table-column label="方法" width="80" align="center">
<template #default="{ row }">
<el-tag :type="getMethodType(row.method)" size="small">
{{ row.method }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="url" label="接口路径" min-width="300" show-overflow-tooltip />
<el-table-column label="状态" width="100" align="center">
<template #default="{ row }">
<el-tag :type="getStatusType(row.status)" size="small">
{{ row.status }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="duration" label="耗时" width="100" align="center">
<template #default="{ row }">
{{ row.duration }}ms
</template>
</el-table-column>
<el-table-column prop="time" label="时间" width="180" />
<el-table-column label="操作" width="120" align="center">
<template #default="{ row }">
<el-button type="primary" link size="small" @click="loadFromHistory(row)">
加载
</el-button>
</template>
</el-table-column>
</el-table>
<el-empty v-else description="暂无请求历史" />
</el-card>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, computed } from 'vue'
import { ElMessage } from 'element-plus'
import axios from 'axios'
/**
* 参数项接口
*/
interface ParamItem {
key: string
value: string
description: string
}
/**
* 请求表单接口
*/
interface RequestForm {
method: string
url: string
queryParams: ParamItem[]
headers: ParamItem[]
body: string
}
/**
* 响应数据接口
*/
interface ResponseData {
status: number
data: any
headers: Record<string, string>
duration: number
}
/**
* 历史记录接口
*/
interface HistoryItem {
method: string
url: string
status: number
duration: number
time: string
queryParams: ParamItem[]
headers: ParamItem[]
body: string
}
// 基础URL
const baseUrl = computed(() => import.meta.env.VITE_APP_BASE_API || '')
// 加载状态
const loading = ref(false)
// 当前激活的标签页
const activeTab = ref('query')
const responseTab = ref('body')
// 请求表单
const requestForm = reactive<RequestForm>({
method: 'GET',
url: '',
queryParams: [],
headers: [],
body: ''
})
// 响应数据
const responseData = ref<ResponseData | null>(null)
// 请求体错误信息
const bodyError = ref('')
// 历史记录
const history = ref<HistoryItem[]>([])
// 响应头列表
const responseHeaders = computed(() => {
if (!responseData.value?.headers) return []
return Object.entries(responseData.value.headers).map(([key, value]) => ({
key,
value: String(value)
}))
})
/**
* 添加Query参数
*/
const addQueryParam = () => {
requestForm.queryParams.push({ key: '', value: '', description: '' })
}
/**
* 移除Query参数
*/
const removeQueryParam = (index: number) => {
requestForm.queryParams.splice(index, 1)
}
/**
* 添加请求头
*/
const addHeader = () => {
requestForm.headers.push({ key: '', value: '', description: '' })
}
/**
* 移除请求头
*/
const removeHeader = (index: number) => {
requestForm.headers.splice(index, 1)
}
/**
* 格式化请求体JSON
*/
const formatRequestBody = () => {
if (!requestForm.body.trim()) return
try {
const parsed = JSON.parse(requestForm.body)
requestForm.body = JSON.stringify(parsed, null, 2)
bodyError.value = ''
ElMessage.success('格式化成功')
} catch (e) {
bodyError.value = 'JSON格式错误: ' + (e as Error).message
ElMessage.error('JSON格式错误')
}
}
/**
* 压缩请求体JSON
*/
const compressRequestBody = () => {
if (!requestForm.body.trim()) return
try {
const parsed = JSON.parse(requestForm.body)
requestForm.body = JSON.stringify(parsed)
bodyError.value = ''
ElMessage.success('压缩成功')
} catch (e) {
bodyError.value = 'JSON格式错误: ' + (e as Error).message
ElMessage.error('JSON格式错误')
}
}
/**
* 清空请求体
*/
const clearRequestBody = () => {
requestForm.body = ''
bodyError.value = ''
}
/**
* 格式化JSON显示
*/
const formatJson = (data: any): string => {
try {
return JSON.stringify(data, null, 2)
} catch {
return String(data)
}
}
/**
* 获取状态码对应的标签类型
*/
const getStatusType = (status: number): string => {
if (status >= 200 && status < 300) return 'success'
if (status >= 400 && status < 500) return 'warning'
if (status >= 500) return 'danger'
return 'info'
}
/**
* 获取请求方法对应的标签类型
*/
const getMethodType = (method: string): string => {
const types: Record<string, string> = {
GET: 'success',
POST: 'primary',
PUT: 'warning',
DELETE: 'danger',
PATCH: 'info'
}
return types[method] || 'info'
}
/**
* 发送请求
*/
const sendRequest = async () => {
if (!requestForm.url.trim()) {
ElMessage.warning('请输入接口路径')
return
}
// 验证请求体JSON格式
if (requestForm.body.trim() && ['POST', 'PUT', 'PATCH'].includes(requestForm.method)) {
try {
JSON.parse(requestForm.body)
bodyError.value = ''
} catch (e) {
bodyError.value = 'JSON格式错误: ' + (e as Error).message
ElMessage.error('请求体JSON格式错误')
return
}
}
loading.value = true
const startTime = Date.now()
try {
// 获取token
const token = localStorage.getItem('adminToken')
// 构建请求头
const headers: Record<string, string> = {
'Content-Type': 'application/json'
}
if (token) {
headers['Authorization'] = `Bearer ${token}`
}
// 添加自定义请求头
requestForm.headers.forEach(h => {
if (h.key.trim()) {
headers[h.key] = h.value
}
})
// 构建Query参数
const params: Record<string, string> = {}
requestForm.queryParams.forEach(p => {
if (p.key.trim()) {
params[p.key] = p.value
}
})
// 构建请求体
let data = undefined
if (requestForm.body.trim() && ['POST', 'PUT', 'PATCH'].includes(requestForm.method)) {
data = JSON.parse(requestForm.body)
}
// 发送请求
const response = await axios({
method: requestForm.method.toLowerCase(),
url: baseUrl.value + requestForm.url,
headers,
params,
data,
validateStatus: () => true // 不抛出HTTP错误
})
const duration = Date.now() - startTime
// 保存响应数据
responseData.value = {
status: response.status,
data: response.data,
headers: response.headers as Record<string, string>,
duration
}
// 添加到历史记录
addToHistory({
method: requestForm.method,
url: requestForm.url,
status: response.status,
duration,
time: new Date().toLocaleString(),
queryParams: [...requestForm.queryParams],
headers: [...requestForm.headers],
body: requestForm.body
})
if (response.status >= 200 && response.status < 300) {
ElMessage.success(`请求成功 (${response.status})`)
} else {
ElMessage.warning(`请求完成,状态码: ${response.status}`)
}
} catch (error: any) {
const duration = Date.now() - startTime
responseData.value = {
status: 0,
data: { error: error.message || '请求失败' },
headers: {},
duration
}
ElMessage.error('请求失败: ' + (error.message || '未知错误'))
} finally {
loading.value = false
}
}
/**
* 添加到历史记录
*/
const addToHistory = (item: HistoryItem) => {
history.value.unshift(item)
// 只保留最近10条
if (history.value.length > 10) {
history.value = history.value.slice(0, 10)
}
// 保存到localStorage
localStorage.setItem('apiTesterHistory', JSON.stringify(history.value))
}
/**
* 从历史记录加载
*/
const loadFromHistory = (item: HistoryItem) => {
requestForm.method = item.method
requestForm.url = item.url
requestForm.queryParams = item.queryParams ? [...item.queryParams] : []
requestForm.headers = item.headers ? [...item.headers] : []
requestForm.body = item.body || ''
ElMessage.success('已加载历史请求')
}
/**
* 清空历史记录
*/
const clearHistory = () => {
history.value = []
localStorage.removeItem('apiTesterHistory')
ElMessage.success('历史记录已清空')
}
/**
* 复制响应内容
*/
const copyResponse = async () => {
if (!responseData.value) return
try {
const text = formatJson(responseData.value.data)
await navigator.clipboard.writeText(text)
ElMessage.success('已复制到剪贴板')
} catch {
ElMessage.error('复制失败')
}
}
// 初始化时加载历史记录
const initHistory = () => {
const saved = localStorage.getItem('apiTesterHistory')
if (saved) {
try {
history.value = JSON.parse(saved)
} catch {
history.value = []
}
}
}
initHistory()
</script>
<style scoped lang="scss">
.api-tester {
.page-title {
margin-bottom: 20px;
font-size: 24px;
color: #333;
}
.request-card,
.response-card,
.history-card {
margin-bottom: 20px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.params-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
.params-tip {
color: #909399;
font-size: 12px;
}
.body-actions {
display: flex;
gap: 8px;
}
}
.body-textarea {
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
:deep(.el-textarea__inner) {
font-family: inherit;
line-height: 1.5;
}
}
.body-error {
margin-top: 10px;
}
.response-info {
display: flex;
align-items: center;
gap: 10px;
}
.response-body {
max-height: 500px;
overflow: auto;
background-color: #f5f7fa;
border-radius: 4px;
padding: 10px;
}
.json-display {
margin: 0;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 13px;
line-height: 1.5;
white-space: pre-wrap;
word-break: break-all;
}
}
</style>