Trae Assistant
优化
03b3117
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>HTTP 接口调试工作室 | HTTP Request Studio</title>
<!-- Local Static Assets -->
<script src="/static/js/vue.js"></script>
<script src="/static/js/tailwind.js"></script>
<link href="/static/css/prism.css" rel="stylesheet">
<script src="/static/js/prism.js"></script>
<script src="/static/js/prism-json.js"></script>
<style>
body { font-family: 'Inter', system-ui, -apple-system, sans-serif; }
.scrollbar-hide::-webkit-scrollbar { display: none; }
.scrollbar-hide { -ms-overflow-style: none; scrollbar-width: none; }
/* Custom Scrollbar for panels */
::-webkit-scrollbar { width: 8px; height: 8px; }
::-webkit-scrollbar-track { background: #1e293b; }
::-webkit-scrollbar-thumb { background: #475569; border-radius: 4px; }
::-webkit-scrollbar-thumb:hover { background: #64748b; }
.code-editor {
font-family: 'Fira Code', monospace;
tab-size: 2;
}
/* Glassmorphism utils */
.glass {
background: rgba(30, 41, 59, 0.7);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
}
</style>
</head>
<body class="bg-slate-900 text-slate-200 h-screen overflow-hidden flex flex-col">
<div id="app" class="w-full h-full flex flex-col">
<!-- Header -->
<header class="h-16 bg-gradient-to-r from-slate-900 via-slate-800 to-slate-900 border-b border-slate-700 flex items-center px-6 justify-between shrink-0 shadow-lg z-10">
<div class="flex items-center space-x-4">
<div class="bg-gradient-to-br from-blue-500 to-indigo-600 w-10 h-10 rounded-lg shadow-lg flex items-center justify-center transform transition hover:scale-105">
<!-- Bolt Icon -->
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6 text-white">
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 13.5l10.5-11.25L12 10.5h8.25L9.75 21.75 12 13.5H3.75z" />
</svg>
</div>
<div>
<h1 class="font-bold text-xl text-white tracking-wide bg-clip-text text-transparent bg-gradient-to-r from-white to-slate-400">HTTP Request Studio</h1>
<p class="text-[10px] text-slate-500 uppercase tracking-widest font-semibold">Pro Edition</p>
</div>
</div>
<div class="flex items-center space-x-4 text-sm">
<!-- Demo Dropdown -->
<div class="relative group">
<button class="text-slate-400 hover:text-white transition flex items-center bg-slate-800/50 hover:bg-slate-700 px-3 py-1.5 rounded-full border border-slate-700/50">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4 mr-1.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 18v-5.25m0 0a6.01 6.01 0 001.5-.189m-1.5.189a6.01 6.01 0 01-1.5-.189m3.75 2.625v-8.25m0 0V3.75m0 3.75a6.01 6.01 0 01-1.5.189m1.5-.189a6.01 6.01 0 011.5.189m-6 2.25h.008v.008H12v-.008zM12 15h.008v.008H12V15zm0 2.25h.008v.008H12v-.008zM9.75 15h.008v.008H9.75V15zm0 2.25h.008v.008H9.75v-.008zM7.5 15h.008v.008H7.5V15zm0 2.25h.008v.008H7.5v-.008zm6.75-4.5h.008v.008h-.008v-.008zm0 2.25h.008v.008h-.008V15zm0 2.25h.008v.008h-.008v-.008zm2.25-4.5h.008v.008H16.5v-.008zm0 2.25h.008v.008H16.5V15z" />
</svg>
加载示例
</button>
<div class="absolute right-0 mt-2 w-48 bg-slate-800 border border-slate-700 rounded-lg shadow-xl hidden group-hover:block z-50 overflow-hidden">
<a href="#" @click.prevent="loadDemo('get')" class="block px-4 py-2 hover:bg-slate-700 text-slate-300 text-xs">GET JSON Request</a>
<a href="#" @click.prevent="loadDemo('post')" class="block px-4 py-2 hover:bg-slate-700 text-slate-300 text-xs">POST JSON Data</a>
<a href="#" @click.prevent="loadDemo('image')" class="block px-4 py-2 hover:bg-slate-700 text-slate-300 text-xs">GET Image Binary</a>
<a href="#" @click.prevent="loadDemo('404')" class="block px-4 py-2 hover:bg-slate-700 text-slate-300 text-xs">404 Error Test</a>
</div>
</div>
<div class="h-4 w-px bg-slate-700"></div>
<!-- Import cURL -->
<button @click="showImportModal = true" class="text-slate-400 hover:text-white transition flex items-center px-2">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4 mr-1.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5" />
</svg>
导入 cURL
</button>
<div class="h-4 w-px bg-slate-700"></div>
<span v-if="loading" class="text-blue-400 flex items-center animate-pulse">
<svg class="animate-spin -ml-1 mr-2 h-4 w-4 text-blue-400" 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>
<a href="https://huggingface.co/spaces/duqing26/http-request-studio" target="_blank" class="hover:text-white transition opacity-70 hover:opacity-100">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
<path stroke-linecap="round" stroke-linejoin="round" d="M13.5 6H5.25A2.25 2.25 0 003 8.25v10.5A2.25 2.25 0 005.25 21h10.5A2.25 2.25 0 0018 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25" />
</svg>
</a>
</div>
</header>
<!-- Main Content -->
<div class="flex-1 flex overflow-hidden">
<!-- Sidebar (History) -->
<div class="w-64 bg-slate-900 border-r border-slate-800 flex flex-col shrink-0 transition-all duration-300" :class="{'w-0 opacity-0': !showSidebar}">
<div class="p-3 border-b border-slate-800 flex justify-between items-center bg-slate-800/20">
<span class="font-semibold text-slate-400 text-xs uppercase tracking-wider">History</span>
<button @click="clearHistory" class="text-xs text-slate-500 hover:text-red-400 p-1 rounded hover:bg-slate-800 transition" title="清空历史">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
<path stroke-linecap="round" stroke-linejoin="round" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0" />
</svg>
</button>
</div>
<div class="flex-1 overflow-y-auto p-2 space-y-2 custom-scrollbar">
<div v-if="history.length === 0" class="text-center text-slate-700 py-10 text-xs flex flex-col items-center">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-8 h-8 mb-2 opacity-50">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
暂无记录
</div>
<div v-for="(item, index) in history" :key="index"
@click="loadHistory(item)"
class="group p-3 rounded-lg hover:bg-slate-800 cursor-pointer border border-transparent hover:border-slate-700 transition-all relative">
<div class="flex items-center justify-between mb-1">
<span :class="getMethodColor(item.method)" class="text-[10px] font-bold px-1.5 py-0.5 rounded bg-opacity-10 w-auto">${ item.method }</span>
<span class="text-[10px] text-slate-500">${ formatDate(item.timestamp) }</span>
</div>
<div class="text-xs text-slate-300 truncate font-mono mb-1 opacity-80 group-hover:opacity-100" :title="item.url">${ item.url }</div>
<div class="flex items-center justify-between mt-1">
<div class="flex items-center space-x-2">
<span v-if="item.status" :class="getStatusColor(item.status)" class="text-[10px] px-1.5 py-0.5 rounded-full bg-opacity-20 font-medium">
${ item.status }
</span>
<span class="text-[10px] text-slate-600">${ item.duration }ms</span>
</div>
</div>
</div>
</div>
</div>
<!-- Workspace -->
<div class="flex-1 flex flex-col min-w-0 bg-slate-900 relative">
<!-- Toggle Sidebar Button -->
<button @click="showSidebar = !showSidebar" class="absolute top-1/2 left-0 -translate-y-1/2 z-10 bg-slate-800 border border-slate-700 text-slate-400 p-1 rounded-r shadow hover:text-white" style="width: 16px; height: 32px; display: flex; align-items: center; justify-content: center;">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-3 h-3">
<path v-if="showSidebar" stroke-linecap="round" stroke-linejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5" />
<path v-else stroke-linecap="round" stroke-linejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" />
</svg>
</button>
<!-- Request Bar -->
<div class="p-4 border-b border-slate-800 bg-slate-800/30 backdrop-blur-sm z-10">
<div class="flex space-x-2">
<select v-model="currentRequest.method" class="bg-slate-800 border border-slate-600 text-white text-sm rounded-lg px-3 py-2.5 focus:outline-none focus:border-blue-500 font-bold w-28 transition shadow-sm hover:border-slate-500">
<option value="GET">GET</option>
<option value="POST">POST</option>
<option value="PUT">PUT</option>
<option value="DELETE">DELETE</option>
<option value="PATCH">PATCH</option>
<option value="HEAD">HEAD</option>
<option value="OPTIONS">OPTIONS</option>
</select>
<div class="flex-1 relative group">
<input type="text" v-model="currentRequest.url"
placeholder="输入 URL (例如: https://httpbin.org/get)"
@keyup.enter="sendRequest"
class="w-full bg-slate-800 border border-slate-600 text-white text-sm rounded-lg px-4 py-2.5 focus:outline-none focus:border-blue-500 font-mono transition shadow-sm hover:border-slate-500">
<div class="absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none text-slate-600 group-focus-within:text-blue-500 transition">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
<path stroke-linecap="round" stroke-linejoin="round" d="M13.19 8.688a4.5 4.5 0 011.242 7.244l-4.5 4.5a4.5 4.5 0 01-6.364-6.364l1.757-1.757m13.35-.622l1.757-1.757a4.5 4.5 0 00-6.364-6.364l-4.5 4.5a4.5 4.5 0 001.242 7.244" />
</svg>
</div>
</div>
<button @click="sendRequest" :disabled="loading"
class="bg-blue-600 hover:bg-blue-500 text-white px-8 py-2.5 rounded-lg font-medium transition shadow-lg shadow-blue-900/50 flex items-center disabled:opacity-50 disabled:cursor-not-allowed disabled:shadow-none transform active:scale-95">
<svg v-if="!loading" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4 mr-2">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 12L3.269 3.126A59.768 59.768 0 0121.485 12 59.77 59.77 0 013.27 20.876L5.999 12zm0 0h7.5" />
</svg>
<svg v-else class="animate-spin -ml-1 mr-2 h-4 w-4 text-white" 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>
发送
</button>
</div>
</div>
<!-- Main Split Pane (Request Config / Response) -->
<div class="flex-1 flex flex-col md:flex-row overflow-hidden">
<!-- Request Config (Left/Top) -->
<div class="flex-1 flex flex-col border-r border-slate-800 min-w-[300px]">
<!-- Tabs -->
<div class="flex border-b border-slate-800 bg-slate-900">
<button v-for="tab in ['Params', 'Headers', 'Body', 'Auth']"
@click="activeReqTab = tab"
:class="activeReqTab === tab ? 'text-blue-400 border-b-2 border-blue-400 bg-slate-800/50' : 'text-slate-400 hover:text-slate-200 hover:bg-slate-800/30'"
class="px-6 py-3 text-sm font-medium transition flex items-center">
${ tab }
<span v-if="tab === 'Params' && paramsCount > 0" class="ml-2 text-[10px] bg-blue-900 text-blue-300 px-1.5 py-0.5 rounded-full font-bold">${ paramsCount }</span>
<span v-if="tab === 'Headers' && headersCount > 0" class="ml-2 text-[10px] bg-blue-900 text-blue-300 px-1.5 py-0.5 rounded-full font-bold">${ headersCount }</span>
</button>
</div>
<!-- Tab Content -->
<div class="flex-1 overflow-y-auto p-4 bg-slate-900">
<!-- Params Tab -->
<div v-if="activeReqTab === 'Params'" class="animate-fade-in">
<div class="space-y-3">
<div class="flex justify-between items-center mb-2">
<div class="text-xs text-slate-500 uppercase tracking-wider font-semibold">Query Parameters</div>
<button @click="addParam" class="text-xs text-blue-400 hover:text-blue-300 flex items-center bg-slate-800 px-2 py-1 rounded hover:bg-slate-700 transition">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-3 h-3 mr-1">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
</svg>
添加参数
</button>
</div>
<div v-if="currentRequest.params.length === 0" class="text-center text-slate-600 py-8 text-sm italic">
没有查询参数
</div>
<div v-for="(param, idx) in currentRequest.params" :key="idx" class="flex space-x-2 group items-center">
<div class="pt-0">
<input type="checkbox" v-model="param.active" class="rounded bg-slate-700 border-slate-600 text-blue-500 focus:ring-offset-slate-900">
</div>
<input type="text" v-model="param.key" placeholder="Key" class="flex-1 bg-slate-800 border border-slate-700 rounded px-3 py-1.5 text-sm text-slate-300 focus:border-blue-500 outline-none transition placeholder-slate-600">
<input type="text" v-model="param.value" placeholder="Value" class="flex-1 bg-slate-800 border border-slate-700 rounded px-3 py-1.5 text-sm text-slate-300 focus:border-blue-500 outline-none transition placeholder-slate-600">
<button @click="removeParam(idx)" class="text-slate-600 hover:text-red-400 opacity-0 group-hover:opacity-100 px-2 transition">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</div>
</div>
<!-- Headers Tab -->
<div v-if="activeReqTab === 'Headers'" class="animate-fade-in">
<div class="space-y-3">
<div class="flex justify-between items-center mb-2">
<div class="text-xs text-slate-500 uppercase tracking-wider font-semibold">Request Headers</div>
<button @click="addHeader" class="text-xs text-blue-400 hover:text-blue-300 flex items-center bg-slate-800 px-2 py-1 rounded hover:bg-slate-700 transition">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-3 h-3 mr-1">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
</svg>
添加 Header
</button>
</div>
<div v-for="(header, idx) in currentRequest.headers" :key="idx" class="flex space-x-2 group items-center">
<div class="pt-0">
<input type="checkbox" v-model="header.active" class="rounded bg-slate-700 border-slate-600 text-blue-500 focus:ring-offset-slate-900">
</div>
<input type="text" v-model="header.key" placeholder="Key" class="flex-1 bg-slate-800 border border-slate-700 rounded px-3 py-1.5 text-sm text-slate-300 focus:border-blue-500 outline-none transition placeholder-slate-600 font-mono text-xs">
<input type="text" v-model="header.value" placeholder="Value" class="flex-1 bg-slate-800 border border-slate-700 rounded px-3 py-1.5 text-sm text-slate-300 focus:border-blue-500 outline-none transition placeholder-slate-600">
<button @click="removeHeader(idx)" class="text-slate-600 hover:text-red-400 opacity-0 group-hover:opacity-100 px-2 transition">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</div>
</div>
<!-- Body Tab -->
<div v-if="activeReqTab === 'Body'" class="animate-fade-in h-full flex flex-col">
<div class="flex items-center space-x-6 mb-4 px-1">
<label class="flex items-center space-x-2 cursor-pointer group">
<input type="radio" v-model="currentRequest.bodyType" value="none" class="text-blue-500 focus:ring-blue-500 bg-slate-700 border-slate-600 focus:ring-offset-slate-900">
<span class="text-sm text-slate-400 group-hover:text-slate-200 transition">None</span>
</label>
<label class="flex items-center space-x-2 cursor-pointer group">
<input type="radio" v-model="currentRequest.bodyType" value="json" class="text-blue-500 focus:ring-blue-500 bg-slate-700 border-slate-600 focus:ring-offset-slate-900">
<span class="text-sm text-slate-400 group-hover:text-slate-200 transition">JSON</span>
</label>
<label class="flex items-center space-x-2 cursor-pointer group">
<input type="radio" v-model="currentRequest.bodyType" value="text" class="text-blue-500 focus:ring-blue-500 bg-slate-700 border-slate-600 focus:ring-offset-slate-900">
<span class="text-sm text-slate-400 group-hover:text-slate-200 transition">Text</span>
</label>
</div>
<div v-if="currentRequest.bodyType === 'json'" class="flex-1 relative border border-slate-700 rounded-lg overflow-hidden">
<button @click="formatBody" class="absolute top-2 right-2 z-10 text-xs bg-slate-800 hover:bg-slate-700 text-slate-300 px-2 py-1 rounded border border-slate-600 transition opacity-70 hover:opacity-100">
Format JSON
</button>
<textarea v-model="currentRequest.bodyContent"
class="w-full h-full bg-slate-800 p-4 font-mono text-sm text-green-400 focus:border-blue-500 outline-none resize-none code-editor"
placeholder="{ 'key': 'value' }"></textarea>
</div>
<div v-if="currentRequest.bodyType === 'text'" class="flex-1 border border-slate-700 rounded-lg overflow-hidden">
<textarea v-model="currentRequest.bodyContent"
class="w-full h-full bg-slate-800 p-4 font-mono text-sm text-slate-300 focus:border-blue-500 outline-none resize-none"
placeholder="Raw text content..."></textarea>
</div>
<div v-if="currentRequest.bodyType === 'none'" class="flex-1 flex flex-col items-center justify-center text-slate-600 italic border border-dashed border-slate-800 rounded-lg">
<span class="mb-2">This request has no body</span>
<span class="text-xs opacity-50">Select JSON or Text to add content</span>
</div>
</div>
<!-- Auth Tab (Simple) -->
<div v-if="activeReqTab === 'Auth'" class="animate-fade-in">
<div class="bg-slate-800/50 p-6 rounded-lg border border-slate-700 text-center">
<div class="text-sm text-slate-400 mb-4">目前仅支持通过 Headers 添加认证信息。</div>
<button @click="addAuthHeader" class="bg-blue-600 hover:bg-blue-500 text-white px-4 py-2 rounded text-sm transition shadow-lg shadow-blue-900/30">
添加 Bearer Token Header
</button>
<p class="text-xs text-slate-500 mt-4">更多认证方式(Basic Auth, OAuth2)正在开发中...</p>
</div>
</div>
</div>
</div>
<!-- Response (Right/Bottom) -->
<div class="flex-1 flex flex-col min-w-[300px] bg-slate-900 border-l border-slate-800">
<div v-if="response" class="h-full flex flex-col">
<!-- Response Meta -->
<div class="px-4 py-3 border-b border-slate-800 bg-slate-800/20 flex justify-between items-center">
<div class="flex items-center space-x-6">
<span class="text-xs font-medium text-slate-400 uppercase tracking-wider">Status</span>
<span :class="getStatusColor(response.status)" class="font-bold text-sm px-2 py-0.5 rounded-full">${ response.status } ${ response.status_text }</span>
<div class="h-4 w-px bg-slate-700"></div>
<span class="text-xs font-medium text-slate-400 uppercase tracking-wider">Time</span>
<span class="text-sm text-slate-200 font-mono">${ response.duration }ms</span>
<div class="h-4 w-px bg-slate-700"></div>
<span class="text-xs font-medium text-slate-400 uppercase tracking-wider">Size</span>
<span class="text-sm text-slate-200 font-mono">${ formatSize(response.size) }</span>
</div>
<div class="flex space-x-2">
<button @click="copyResponse" class="text-slate-400 hover:text-white text-xs bg-slate-800 px-2 py-1 rounded border border-slate-700 transition flex items-center" title="复制结果">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-3 h-3 mr-1">
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 01-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 011.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 00-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 01-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 00-3.375-3.375h-1.5" />
</svg>
Copy
</button>
</div>
</div>
<!-- Response Body -->
<div class="flex-1 overflow-hidden relative group bg-slate-900">
<div v-if="response.is_binary" class="w-full h-full flex flex-col items-center justify-center p-4 overflow-auto">
<div v-if="response.content_type && response.content_type.startsWith('image/')" class="text-center">
<img :src="`data:${response.content_type};base64,${response.data}`" class="max-w-full max-h-[70vh] shadow-2xl rounded-lg border border-slate-700 bg-[url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyMCIgaGVpZ2h0PSIyMCIgZmlsbD0iIzMzMyIgZmlsbC1vcGFjaXR5PSIwLjEiPjxwYXRoIGQ9Ik0wIDBoMTB2MTBIMHptMTAgMTBoMTB2MTBIMTB6Ii8+PC9zdmc+')] bg-repeat" />
<div class="mt-4 px-3 py-1 bg-slate-800 rounded-full inline-block text-xs text-slate-400 border border-slate-700 font-mono">${ response.content_type }</div>
</div>
<div v-else class="text-center">
<div class="w-20 h-20 bg-slate-800 rounded-full flex items-center justify-center mx-auto mb-4 border border-slate-700">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-10 h-10 text-slate-500">
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m2.25 0H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" />
</svg>
</div>
<p class="text-slate-300 font-medium text-lg">Binary Content</p>
<p class="text-xs text-slate-500 mt-1 font-mono">${ response.content_type }</p>
<p class="text-xs text-slate-600 mt-4 bg-slate-800/50 px-4 py-2 rounded">Preview unavailable for this file type</p>
</div>
</div>
<div v-else class="w-full h-full relative">
<pre v-if="response.is_json" class="w-full h-full p-4 overflow-auto text-sm font-mono language-json custom-scrollbar"><code class="language-json">${ formatResponseData(response.data) }</code></pre>
<pre v-else class="w-full h-full p-4 overflow-auto text-sm font-mono text-slate-300 whitespace-pre-wrap custom-scrollbar">${ response.data }</pre>
</div>
</div>
</div>
<div v-else class="flex-1 flex flex-col items-center justify-center text-slate-600">
<div class="w-24 h-24 bg-slate-800/50 rounded-full flex items-center justify-center mb-6">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-12 h-12 opacity-30">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 12L3.269 3.126A59.768 59.768 0 0121.485 12 59.77 59.77 0 013.27 20.876L5.999 12zm0 0h7.5" />
</svg>
</div>
<p class="text-lg font-medium text-slate-500">Ready to Request</p>
<p class="text-sm text-slate-600 mt-2">Enter URL and click Send to see the response</p>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Import Modal -->
<div v-if="showImportModal" class="fixed inset-0 bg-black/70 backdrop-blur-sm flex items-center justify-center z-50">
<div class="bg-slate-800 border border-slate-700 rounded-lg shadow-2xl w-[600px] max-w-full m-4 p-6 animate-scale-in">
<h3 class="text-lg font-bold text-white mb-4">Import cURL</h3>
<textarea v-model="curlInput" class="w-full h-40 bg-slate-900 border border-slate-700 rounded p-3 font-mono text-sm text-slate-300 focus:border-blue-500 outline-none resize-none mb-4" placeholder="Paste cURL command here..."></textarea>
<div class="flex justify-end space-x-3">
<button @click="showImportModal = false" class="px-4 py-2 rounded text-slate-400 hover:text-white hover:bg-slate-700 transition text-sm">Cancel</button>
<button @click="importCurl" class="px-4 py-2 rounded bg-blue-600 hover:bg-blue-500 text-white font-medium transition text-sm">Import</button>
</div>
</div>
</div>
<script>
const { createApp, ref, computed, onMounted, nextTick } = Vue;
createApp({
delimiters: ['${', '}'],
setup() {
const loading = ref(false);
const activeReqTab = ref('Params');
const history = ref([]);
const response = ref(null);
const showSidebar = ref(true);
const showImportModal = ref(false);
const curlInput = ref('');
const currentRequest = ref({
method: 'GET',
url: 'https://httpbin.org/get',
params: [{ key: 'hello', value: 'world', active: true }],
headers: [
{ key: 'User-Agent', value: 'HttpRequestStudio/1.0', active: true },
{ key: 'Accept', value: 'application/json', active: true }
],
bodyType: 'none',
bodyContent: ''
});
// Load history from local storage
onMounted(() => {
const saved = localStorage.getItem('http-studio-history');
if (saved) {
history.value = JSON.parse(saved);
}
// Add default header if empty
if (currentRequest.value.headers.length === 0 || (currentRequest.value.headers.length === 1 && !currentRequest.value.headers[0].key)) {
currentRequest.value.headers = [
{ key: 'User-Agent', value: 'HttpRequestStudio/1.0', active: true },
{ key: 'Accept', value: '*/*', active: true }
];
}
});
const paramsCount = computed(() => currentRequest.value.params.filter(p => p.key && p.active).length);
const headersCount = computed(() => currentRequest.value.headers.filter(h => h.key && h.active).length);
const addParam = () => currentRequest.value.params.push({ key: '', value: '', active: true });
const removeParam = (idx) => currentRequest.value.params.splice(idx, 1);
const addHeader = () => currentRequest.value.headers.push({ key: '', value: '', active: true });
const removeHeader = (idx) => currentRequest.value.headers.splice(idx, 1);
const addAuthHeader = () => {
currentRequest.value.headers.push({ key: 'Authorization', value: 'Bearer ', active: true });
activeReqTab.value = 'Headers';
};
const clearHistory = () => {
if(confirm('确定清空所有历史记录吗?')) {
history.value = [];
localStorage.removeItem('http-studio-history');
}
};
const formatBody = () => {
try {
const obj = JSON.parse(currentRequest.value.bodyContent);
currentRequest.value.bodyContent = JSON.stringify(obj, null, 2);
} catch (e) {
alert('JSON 格式无效');
}
};
const formatResponseData = (data) => {
if (typeof data === 'object') {
return JSON.stringify(data, null, 2);
}
return data;
};
const getMethodColor = (method) => {
const colors = {
'GET': 'text-green-400',
'POST': 'text-yellow-400',
'PUT': 'text-blue-400',
'DELETE': 'text-red-400',
'PATCH': 'text-purple-400'
};
return colors[method] || 'text-slate-400';
};
const getStatusColor = (status) => {
if (status >= 200 && status < 300) return 'text-green-400 bg-green-900/50';
if (status >= 300 && status < 400) return 'text-yellow-400 bg-yellow-900/50';
if (status >= 400 && status < 500) return 'text-orange-400 bg-orange-900/50';
if (status >= 500) return 'text-red-400 bg-red-900/50';
return 'text-slate-400 bg-slate-800';
};
const formatSize = (bytes) => {
if (bytes === undefined || bytes === null) return '0 B';
if (bytes < 1024) return bytes + ' B';
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(2) + ' KB';
return (bytes / (1024 * 1024)).toFixed(2) + ' MB';
};
const formatDate = (ts) => {
return new Date(ts).toLocaleTimeString();
};
const loadDemo = (type) => {
const demos = {
'get': {
method: 'GET',
url: 'https://httpbin.org/get',
params: [{ key: 'demo_param', value: 'hello_world', active: true }],
headers: [{ key: 'Accept', value: 'application/json', active: true }],
bodyType: 'none',
bodyContent: ''
},
'post': {
method: 'POST',
url: 'https://httpbin.org/post',
params: [],
headers: [
{ key: 'Content-Type', value: 'application/json', active: true },
{ key: 'X-Custom-Header', value: 'demo-value', active: true }
],
bodyType: 'json',
bodyContent: '{\n "name": "HTTP Request Studio",\n "description": "Powerful API Client",\n "active": true\n}'
},
'image': {
method: 'GET',
url: 'https://httpbin.org/image/png',
params: [],
headers: [],
bodyType: 'none',
bodyContent: ''
},
'404': {
method: 'GET',
url: 'https://httpbin.org/status/404',
params: [],
headers: [],
bodyType: 'none',
bodyContent: ''
}
};
const demo = demos[type];
if (demo) {
currentRequest.value = JSON.parse(JSON.stringify(demo));
// Add default user agent
currentRequest.value.headers.push({ key: 'User-Agent', value: 'HttpRequestStudio/1.0', active: true });
}
};
const loadHistory = (item) => {
currentRequest.value.method = item.method;
currentRequest.value.url = item.url;
if (item.headers) {
currentRequest.value.headers = Array.isArray(item.headers) ? item.headers :
Object.entries(item.headers).map(([k, v]) => ({ key: k, value: v, active: true }));
}
if (item.params) {
currentRequest.value.params = Array.isArray(item.params) ? item.params :
Object.entries(item.params).map(([k, v]) => ({ key: k, value: v, active: true }));
}
if (item.body) {
currentRequest.value.bodyContent = item.body;
try {
JSON.parse(item.body);
currentRequest.value.bodyType = 'json';
} catch {
currentRequest.value.bodyType = 'text';
}
} else {
currentRequest.value.bodyType = 'none';
currentRequest.value.bodyContent = '';
}
};
const copyResponse = () => {
if (!response.value) return;
const content = response.value.is_json
? JSON.stringify(response.value.data, null, 2)
: response.value.data;
navigator.clipboard.writeText(content).then(() => {
alert('已复制到剪贴板');
});
};
const importCurl = () => {
const curl = curlInput.value.trim();
if (!curl) return;
// Basic cURL parser
// Note: This is a simplified parser
try {
const methodMatch = curl.match(/-X\s+([A-Z]+)/) || curl.match(/--request\s+([A-Z]+)/);
const method = methodMatch ? methodMatch[1] : 'GET';
// Extract URL (naively)
const urlMatch = curl.match(/['"](http[s]?:\/\/[^'"]+)['"]/) || curl.match(/(http[s]?:\/\/[^\s]+)/);
const url = urlMatch ? urlMatch[1] : '';
if (!url) {
alert('无法解析 URL');
return;
}
// Extract Headers
const headers = [];
const headerRegex = /-H\s+['"]([^'"]+)['"]/g;
let match;
while ((match = headerRegex.exec(curl)) !== null) {
const [key, value] = match[1].split(/:\s*(.*)/);
headers.push({ key: key.trim(), value: value.trim(), active: true });
}
// Extract Data
const dataMatch = curl.match(/-d\s+['"]([^'"]+)['"]/) || curl.match(/--data\s+['"]([^'"]+)['"]/) || curl.match(/--data-raw\s+['"]([^'"]+)['"]/);
const body = dataMatch ? dataMatch[1] : '';
currentRequest.value = {
method,
url,
headers: headers.length ? headers : [{ key: 'User-Agent', value: 'HttpRequestStudio/1.0', active: true }],
params: [],
bodyType: body ? (isValidJson(body) ? 'json' : 'text') : 'none',
bodyContent: body
};
showImportModal.value = false;
curlInput.value = '';
} catch (e) {
alert('解析 cURL 失败: ' + e.message);
}
};
const isValidJson = (str) => {
try { JSON.parse(str); return true; } catch { return false; }
};
const sendRequest = async () => {
if (!currentRequest.value.url) {
alert('请输入 URL');
return;
}
loading.value = true;
response.value = null;
// Prepare payload
const headers = {};
currentRequest.value.headers.forEach(h => {
if (h.active && h.key) headers[h.key] = h.value;
});
const params = {};
currentRequest.value.params.forEach(p => {
if (p.active && p.key) params[p.key] = p.value;
});
// Set Content-Type if JSON body
if (currentRequest.value.bodyType === 'json') {
if (!headers['Content-Type']) headers['Content-Type'] = 'application/json';
}
const payload = {
method: currentRequest.value.method,
url: currentRequest.value.url,
params: params,
headers: headers,
body: currentRequest.value.bodyType !== 'none' ? currentRequest.value.bodyContent : null
};
try {
const res = await fetch('/api/request', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
});
const data = await res.json();
if (data.error) {
alert('请求失败: ' + data.error);
response.value = {
status: 0,
status_text: 'Error',
duration: 0,
size: 0,
data: data.error,
is_json: false
};
} else {
response.value = data;
// Save to history
const historyItem = {
...currentRequest.value,
timestamp: Date.now(),
status: data.status,
duration: data.duration
};
// Remove deep clone of headers/params for storage to save space, but here we keep structure
// Limit history size
history.value.unshift(historyItem);
if (history.value.length > 50) history.value.pop();
localStorage.setItem('http-studio-history', JSON.stringify(history.value));
}
} catch (e) {
alert('网络错误: ' + e.message);
} finally {
loading.value = false;
nextTick(() => {
if(window.Prism) Prism.highlightAll();
});
}
};
return {
loading,
activeReqTab,
history,
response,
currentRequest,
paramsCount,
headersCount,
showSidebar,
showImportModal,
curlInput,
addParam,
removeParam,
addHeader,
removeHeader,
addAuthHeader,
clearHistory,
formatBody,
formatResponseData,
getMethodColor,
getStatusColor,
formatSize,
formatDate,
sendRequest,
loadDemo,
loadHistory,
copyResponse,
importCurl
};
}
}).mount('#app');
</script>
</body>
</html>