工具完成
This commit is contained in:
432
src/components/tools/HttpTester.vue
Normal file
432
src/components/tools/HttpTester.vue
Normal file
@ -0,0 +1,432 @@
|
||||
<template>
|
||||
<div class="space-y-6">
|
||||
<!-- 工具栏 -->
|
||||
<div class="card p-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button
|
||||
@click="sendRequest"
|
||||
:disabled="!requestUrl.trim() || isLoading"
|
||||
class="btn-primary flex items-center space-x-2"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
:icon="isLoading ? ['fas', 'spinner'] : ['fas', 'paper-plane']"
|
||||
:class="isLoading && 'animate-spin'"
|
||||
/>
|
||||
<span>发送请求</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
@click="clearAll"
|
||||
class="btn-secondary flex items-center space-x-2"
|
||||
>
|
||||
<FontAwesomeIcon :icon="['fas', 'trash']" />
|
||||
<span>清空</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 xl:grid-cols-2 gap-6">
|
||||
<!-- 左侧:请求配置 -->
|
||||
<div class="space-y-6">
|
||||
<!-- 请求URL和方法 -->
|
||||
<div class="card p-4">
|
||||
<h3 class="text-lg font-semibold text-primary mb-4">请求配置</h3>
|
||||
|
||||
<div class="space-y-3">
|
||||
<div class="flex space-x-2">
|
||||
<select v-model="requestMethod" class="select-input w-32">
|
||||
<option v-for="method in httpMethods" :key="method" :value="method">
|
||||
{{ method }}
|
||||
</option>
|
||||
</select>
|
||||
<input
|
||||
v-model="requestUrl"
|
||||
type="url"
|
||||
placeholder="https://api.example.com/users"
|
||||
class="input-field flex-1"
|
||||
@keyup.enter="sendRequest"
|
||||
>
|
||||
</div>
|
||||
|
||||
<!-- 快速URL -->
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button
|
||||
v-for="quickUrl in quickUrls"
|
||||
:key="quickUrl.name"
|
||||
@click="setQuickUrl(quickUrl.url)"
|
||||
class="px-3 py-1 text-xs rounded bg-block hover:bg-block-hover text-secondary hover:text-primary transition-colors"
|
||||
>
|
||||
{{ quickUrl.name }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 请求头配置 -->
|
||||
<div class="card p-4">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<h4 class="font-medium text-secondary">请求头</h4>
|
||||
<button @click="addHeader" class="btn-small">
|
||||
<FontAwesomeIcon :icon="['fas', 'plus']" class="mr-1" />
|
||||
添加头部
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<div
|
||||
v-for="(header, index) in requestHeaders"
|
||||
:key="index"
|
||||
class="flex space-x-2"
|
||||
>
|
||||
<input
|
||||
v-model="header.key"
|
||||
type="text"
|
||||
placeholder="Header Name"
|
||||
class="input-field flex-1"
|
||||
>
|
||||
<input
|
||||
v-model="header.value"
|
||||
type="text"
|
||||
placeholder="Header Value"
|
||||
class="input-field flex-1"
|
||||
>
|
||||
<button
|
||||
@click="removeHeader(index)"
|
||||
class="p-2 text-error hover:bg-error/10 rounded transition-colors"
|
||||
>
|
||||
<FontAwesomeIcon :icon="['fas', 'times']" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 请求体 -->
|
||||
<div v-if="['POST', 'PUT', 'PATCH'].includes(requestMethod)" class="card p-4">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<h4 class="font-medium text-secondary">请求体</h4>
|
||||
<select v-model="requestBodyType" class="select-input w-32">
|
||||
<option value="json">JSON</option>
|
||||
<option value="text">Text</option>
|
||||
<option value="form">Form</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<textarea
|
||||
v-model="requestBody"
|
||||
:placeholder="getBodyPlaceholder()"
|
||||
class="textarea-field h-40 font-mono text-sm"
|
||||
/>
|
||||
|
||||
<div v-if="requestBodyType === 'json'" class="flex justify-between items-center mt-2">
|
||||
<button @click="formatJsonBody" class="btn-small">
|
||||
<FontAwesomeIcon :icon="['fas', 'code']" class="mr-1" />
|
||||
格式化
|
||||
</button>
|
||||
<div class="text-xs text-secondary">
|
||||
{{ requestBody.length }} 字符
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧:响应结果 -->
|
||||
<div class="card p-4 min-h-[600px]">
|
||||
<h3 class="text-lg font-semibold text-primary mb-4">响应结果</h3>
|
||||
|
||||
<div v-if="isLoading" class="flex flex-col items-center justify-center h-96">
|
||||
<FontAwesomeIcon :icon="['fas', 'spinner']" class="text-4xl text-primary animate-spin mb-4" />
|
||||
<p class="text-secondary">正在发送请求...</p>
|
||||
</div>
|
||||
|
||||
<div v-else-if="lastResponse" class="space-y-4">
|
||||
<!-- 响应状态栏 -->
|
||||
<div class="flex items-center justify-between p-3 rounded-lg bg-secondary/10">
|
||||
<div class="flex items-center space-x-3">
|
||||
<span :class="[
|
||||
'px-3 py-1 text-sm font-medium rounded',
|
||||
getStatusColor(lastResponse.status)
|
||||
]">
|
||||
{{ lastResponse.status }} {{ lastResponse.statusText }}
|
||||
</span>
|
||||
<span class="text-sm text-secondary">
|
||||
{{ formatResponseSize(lastResponse.size) }} | {{ lastResponse.time }}ms
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<button @click="copyResponseContent" class="btn-small">
|
||||
<FontAwesomeIcon :icon="copied ? ['fas', 'check'] : ['fas', 'copy']" class="mr-1" />
|
||||
{{ copied ? '已复制' : '复制' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 响应内容 -->
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm text-secondary">响应内容</span>
|
||||
<button
|
||||
v-if="isJsonResponse"
|
||||
@click="toggleJsonFormat"
|
||||
class="btn-small"
|
||||
>
|
||||
<FontAwesomeIcon :icon="['fas', 'code']" class="mr-1" />
|
||||
{{ jsonFormatted ? '原始' : '格式化' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="relative">
|
||||
<pre class="bg-secondary/10 p-4 rounded-lg text-sm font-mono overflow-auto max-h-96 whitespace-pre-wrap">{{ formattedResponseData }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="flex flex-col items-center justify-center h-96 text-tertiary">
|
||||
<FontAwesomeIcon :icon="['fas', 'paper-plane']" class="text-6xl mb-4 opacity-50" />
|
||||
<p class="text-lg">暂无响应</p>
|
||||
<p class="text-sm">点击发送请求按钮开始测试</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 错误提示 -->
|
||||
<div v-if="errorMessage" class="card p-4 bg-error/10 border-error/20">
|
||||
<div class="flex items-center space-x-2 text-error">
|
||||
<FontAwesomeIcon :icon="['fas', 'exclamation-triangle']" />
|
||||
<span>{{ errorMessage }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
// 响应式状态
|
||||
const requestUrl = ref('https://jsonplaceholder.typicode.com/posts/1')
|
||||
const requestMethod = ref('GET')
|
||||
const requestHeaders = ref([
|
||||
{ key: 'Content-Type', value: 'application/json' }
|
||||
])
|
||||
const requestBody = ref('')
|
||||
const requestBodyType = ref('json')
|
||||
|
||||
const isLoading = ref(false)
|
||||
const lastResponse = ref(null)
|
||||
const errorMessage = ref('')
|
||||
const copied = ref(false)
|
||||
const jsonFormatted = ref(true)
|
||||
|
||||
// HTTP方法列表
|
||||
const httpMethods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS']
|
||||
|
||||
// 快速URL列表
|
||||
const quickUrls = [
|
||||
{ name: 'JSONPlaceholder', url: 'https://jsonplaceholder.typicode.com/posts/1' },
|
||||
{ name: 'GitHub API', url: 'https://api.github.com/users/octocat' },
|
||||
{ name: 'HTTPBin', url: 'https://httpbin.org/get' }
|
||||
]
|
||||
|
||||
// 计算属性
|
||||
const isJsonResponse = computed(() => {
|
||||
if (!lastResponse.value) return false
|
||||
const contentType = lastResponse.value.headers['content-type'] || ''
|
||||
return contentType.includes('application/json') ||
|
||||
(typeof lastResponse.value.data === 'object' && lastResponse.value.data !== null)
|
||||
})
|
||||
|
||||
const formattedResponseData = computed(() => {
|
||||
if (!lastResponse.value) return ''
|
||||
|
||||
if (isJsonResponse.value && jsonFormatted.value) {
|
||||
try {
|
||||
return JSON.stringify(lastResponse.value.data, null, 2)
|
||||
} catch {
|
||||
return String(lastResponse.value.data)
|
||||
}
|
||||
}
|
||||
|
||||
return typeof lastResponse.value.data === 'string'
|
||||
? lastResponse.value.data
|
||||
: JSON.stringify(lastResponse.value.data)
|
||||
})
|
||||
|
||||
// 请求头管理
|
||||
const addHeader = () => {
|
||||
requestHeaders.value.push({ key: '', value: '' })
|
||||
}
|
||||
|
||||
const removeHeader = (index: number) => {
|
||||
requestHeaders.value.splice(index, 1)
|
||||
}
|
||||
|
||||
// 工具函数
|
||||
const getBodyPlaceholder = () => {
|
||||
switch (requestBodyType.value) {
|
||||
case 'json':
|
||||
return '{\n "key": "value"\n}'
|
||||
default:
|
||||
return '请输入请求体内容...'
|
||||
}
|
||||
}
|
||||
|
||||
const formatJsonBody = () => {
|
||||
try {
|
||||
const parsed = JSON.parse(requestBody.value)
|
||||
requestBody.value = JSON.stringify(parsed, null, 2)
|
||||
} catch (error) {
|
||||
console.error('JSON格式化失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const setQuickUrl = (url: string) => {
|
||||
requestUrl.value = url
|
||||
}
|
||||
|
||||
const getStatusColor = (status: number) => {
|
||||
if (status >= 200 && status < 300) return 'bg-success/20 text-success'
|
||||
if (status >= 300 && status < 400) return 'bg-warning/20 text-warning'
|
||||
if (status >= 400 && status < 500) return 'bg-error/20 text-error'
|
||||
if (status >= 500) return 'bg-error/30 text-error'
|
||||
return 'bg-secondary/20 text-secondary'
|
||||
}
|
||||
|
||||
const formatResponseSize = (bytes: number) => {
|
||||
if (bytes < 1024) return `${bytes} B`
|
||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
|
||||
return `${(bytes / 1024 / 1024).toFixed(1)} MB`
|
||||
}
|
||||
|
||||
// 发送请求
|
||||
const sendRequest = async () => {
|
||||
if (!requestUrl.value.trim()) return
|
||||
|
||||
isLoading.value = true
|
||||
errorMessage.value = ''
|
||||
lastResponse.value = null
|
||||
|
||||
try {
|
||||
const startTime = performance.now()
|
||||
|
||||
// 准备请求头
|
||||
const headers: Record<string, string> = {}
|
||||
requestHeaders.value.forEach(h => {
|
||||
if (h.key.trim() && h.value.trim()) {
|
||||
headers[h.key] = h.value
|
||||
}
|
||||
})
|
||||
|
||||
// 准备请求体
|
||||
let body: string | undefined
|
||||
if (['POST', 'PUT', 'PATCH'].includes(requestMethod.value)) {
|
||||
body = requestBody.value
|
||||
}
|
||||
|
||||
// 发送请求
|
||||
const response = await fetch(requestUrl.value, {
|
||||
method: requestMethod.value,
|
||||
headers,
|
||||
body
|
||||
})
|
||||
|
||||
const endTime = performance.now()
|
||||
const responseTime = Math.round(endTime - startTime)
|
||||
|
||||
// 获取响应头
|
||||
const responseHeaders: Record<string, string> = {}
|
||||
response.headers.forEach((value, key) => {
|
||||
responseHeaders[key] = value
|
||||
})
|
||||
|
||||
// 解析响应体
|
||||
let responseData: any
|
||||
const contentType = response.headers.get('content-type') || ''
|
||||
|
||||
if (contentType.includes('application/json')) {
|
||||
responseData = await response.json()
|
||||
} else {
|
||||
responseData = await response.text()
|
||||
}
|
||||
|
||||
// 计算响应大小
|
||||
const responseText = typeof responseData === 'string' ? responseData : JSON.stringify(responseData)
|
||||
const responseSize = new Blob([responseText]).size
|
||||
|
||||
lastResponse.value = {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
headers: responseHeaders,
|
||||
data: responseData,
|
||||
time: responseTime,
|
||||
size: responseSize
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
errorMessage.value = `请求失败: ${(error as Error).message}`
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 其他功能
|
||||
const clearAll = () => {
|
||||
requestUrl.value = 'https://jsonplaceholder.typicode.com/posts/1'
|
||||
requestMethod.value = 'GET'
|
||||
requestHeaders.value = [{ key: 'Content-Type', value: 'application/json' }]
|
||||
requestBody.value = ''
|
||||
lastResponse.value = null
|
||||
errorMessage.value = ''
|
||||
}
|
||||
|
||||
const copyResponseContent = async () => {
|
||||
if (!lastResponse.value) return
|
||||
|
||||
try {
|
||||
const textToCopy = formattedResponseData.value
|
||||
await navigator.clipboard.writeText(textToCopy)
|
||||
copied.value = true
|
||||
setTimeout(() => {
|
||||
copied.value = false
|
||||
}, 2000)
|
||||
} catch (error) {
|
||||
console.error('复制失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const toggleJsonFormat = () => {
|
||||
jsonFormatted.value = !jsonFormatted.value
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.select-input {
|
||||
padding: 0.5rem 0.75rem;
|
||||
border-radius: 0.375rem;
|
||||
border: 1px solid rgba(var(--color-primary), 0.2);
|
||||
background-color: rgb(var(--color-bg-card));
|
||||
color: rgb(var(--color-text-primary));
|
||||
outline: none;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.select-input:focus {
|
||||
border-color: rgb(var(--color-primary));
|
||||
box-shadow: 0 0 0 2px rgba(var(--color-primary), 0.2);
|
||||
}
|
||||
|
||||
.btn-small {
|
||||
padding: 0.25rem 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
border-radius: 0.375rem;
|
||||
background-color: rgb(var(--color-bg-secondary));
|
||||
color: rgb(var(--color-primary-light));
|
||||
border: 1px solid rgb(var(--color-primary));
|
||||
transition: all 0.2s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-small:hover {
|
||||
background-color: rgba(var(--color-primary), 0.1);
|
||||
border-color: rgb(var(--color-primary-hover));
|
||||
}
|
||||
</style>
|
Reference in New Issue
Block a user