<!DOCTYPE html>
|
<html lang="zh-CN">
|
|
<head>
|
<meta charset="UTF-8">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<title>API测试工具</title>
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/codemirror.min.css">
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/theme/dracula.min.css">
|
<style>
|
:root {
|
--primary-color: #ff6b6b;
|
--secondary-color: #48dbfb;
|
--dark-color: #2d3436;
|
--light-color: #f5f6fa;
|
--success-color: #1dd1a1;
|
--warning-color: #feca57;
|
--error-color: #ff6b6b;
|
--sidebar-width: 280px;
|
--header-height: 60px;
|
--footer-height: 40px;
|
}
|
|
* {
|
margin: 0;
|
padding: 0;
|
box-sizing: border-box;
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
}
|
|
body {
|
background-color: #1e272e;
|
color: #f5f6fa;
|
height: 100vh;
|
display: flex;
|
flex-direction: column;
|
}
|
|
/* 炫酷渐变背景 */
|
.gradient-bg {
|
position: fixed;
|
top: 0;
|
left: 0;
|
width: 100%;
|
height: 100%;
|
background: linear-gradient(-45deg, #1e272e, #2d3436, #3d3d3d, #2d3436);
|
background-size: 400% 400%;
|
animation: gradientBG 15s ease infinite;
|
z-index: -1;
|
}
|
|
@keyframes gradientBG {
|
0% {
|
background-position: 0% 50%;
|
}
|
|
50% {
|
background-position: 100% 50%;
|
}
|
|
100% {
|
background-position: 0% 50%;
|
}
|
}
|
|
/* 头部样式 */
|
.header {
|
height: var(--header-height);
|
background-color: rgba(45, 52, 54, 0.9);
|
display: flex;
|
align-items: center;
|
padding: 0 20px;
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
|
z-index: 100;
|
}
|
|
.logo {
|
display: flex;
|
align-items: center;
|
font-size: 22px;
|
font-weight: bold;
|
color: var(--primary-color);
|
}
|
|
.logo i {
|
margin-right: 10px;
|
font-size: 24px;
|
}
|
|
.header-actions {
|
margin-left: auto;
|
display: flex;
|
gap: 15px;
|
}
|
|
/* 主内容区 */
|
.main-container {
|
display: flex;
|
flex: 1;
|
overflow: hidden;
|
}
|
|
/* 侧边栏 */
|
.sidebar {
|
width: var(--sidebar-width);
|
background-color: rgba(45, 52, 54, 0.8);
|
border-right: 1px solid rgba(255, 255, 255, 0.1);
|
display: flex;
|
flex-direction: column;
|
transition: all 0.3s ease;
|
overflow-y: auto;
|
}
|
|
.sidebar-header {
|
padding: 15px;
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
display: flex;
|
justify-content: space-between;
|
align-items: center;
|
}
|
|
.sidebar-title {
|
font-size: 16px;
|
font-weight: 600;
|
color: var(--secondary-color);
|
}
|
|
.sidebar-content {
|
flex: 1;
|
padding: 10px 0;
|
overflow-y: auto;
|
}
|
|
/* 请求历史项 */
|
.history-item {
|
padding: 12px 15px;
|
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
cursor: pointer;
|
transition: all 0.2s ease;
|
display: flex;
|
align-items: center;
|
}
|
|
.history-item:hover {
|
background-color: rgba(255, 255, 255, 0.05);
|
}
|
|
.history-method {
|
width: 50px;
|
padding: 3px 6px;
|
border-radius: 3px;
|
font-size: 12px;
|
font-weight: bold;
|
text-align: center;
|
margin-right: 10px;
|
}
|
|
.history-method.get {
|
background-color: rgba(72, 219, 251, 0.2);
|
color: var(--secondary-color);
|
border: 1px solid var(--secondary-color);
|
}
|
|
.history-method.post {
|
background-color: rgba(29, 209, 161, 0.2);
|
color: var(--success-color);
|
border: 1px solid var(--success-color);
|
}
|
|
.history-method.put {
|
background-color: rgba(254, 202, 87, 0.2);
|
color: var(--warning-color);
|
border: 1px solid var(--warning-color);
|
}
|
|
.history-method.delete {
|
background-color: rgba(255, 107, 107, 0.2);
|
color: var(--error-color);
|
border: 1px solid var(--error-color);
|
}
|
|
.history-method.patch {
|
background-color: rgba(155, 89, 182, 0.2);
|
color: #9b59b6;
|
border: 1px solid #9b59b6;
|
}
|
|
.history-url {
|
flex: 1;
|
font-size: 13px;
|
white-space: nowrap;
|
overflow: hidden;
|
text-overflow: ellipsis;
|
}
|
|
/* 主工作区 */
|
.workspace {
|
flex: 1;
|
display: flex;
|
flex-direction: column;
|
overflow: hidden;
|
}
|
|
/* 请求控制面板 */
|
.request-panel {
|
padding: 20px;
|
background-color: rgba(45, 52, 54, 0.7);
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
}
|
|
.request-controls {
|
display: flex;
|
margin-bottom: 15px;
|
align-items: center;
|
}
|
|
/* 新增请求动画效果 */
|
.request-animation {
|
position: relative;
|
overflow: hidden;
|
}
|
|
.request-animation::after {
|
content: '';
|
position: absolute;
|
top: 0;
|
left: -100%;
|
width: 100%;
|
height: 100%;
|
background: linear-gradient(90deg,
|
transparent,
|
rgba(72, 219, 251, 0.4),
|
transparent);
|
animation: loading 1.5s infinite;
|
}
|
|
/* 响应状态动画 */
|
.response-status {
|
display: flex;
|
align-items: center;
|
}
|
|
.status-pulse {
|
width: 12px;
|
height: 12px;
|
border-radius: 50%;
|
margin-right: 8px;
|
animation: pulse 2s infinite;
|
}
|
|
@keyframes pulse {
|
0% {
|
box-shadow: 0 0 0 0 rgba(72, 219, 251, 0.7);
|
}
|
|
70% {
|
box-shadow: 0 0 0 10px rgba(72, 219, 251, 0);
|
}
|
|
100% {
|
box-shadow: 0 0 0 0 rgba(72, 219, 251, 0);
|
}
|
}
|
|
/* 更多功能面板样式 */
|
.advanced-panel {
|
margin-top: 15px;
|
padding: 15px;
|
background-color: rgba(0, 0, 0, 0.2);
|
border-radius: 4px;
|
display: none;
|
}
|
|
.advanced-panel.active {
|
display: block;
|
animation: fadeIn 0.3s ease;
|
}
|
|
@keyframes loading {
|
100% {
|
left: 100%;
|
}
|
}
|
|
.method-select {
|
width: 120px;
|
margin-right: 10px;
|
background-color: rgba(255, 255, 255, 0.1);
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
color: white;
|
padding: 10px 35px 10px 15px;
|
border-radius: 4px;
|
font-size: 14px;
|
appearance: none;
|
-webkit-appearance: none;
|
-moz-appearance: none;
|
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23ffffff'%3e%3cpath d='M7 10l5 5 5-5z'/%3e%3c/svg%3e");
|
background-repeat: no-repeat;
|
background-position: right 10px center;
|
background-size: 12px;
|
cursor: pointer;
|
transition: all 0.3s ease;
|
}
|
|
.method-select:focus {
|
background-color: rgba(45, 52, 54, 0.9);
|
border-color: var(--secondary-color);
|
outline: none;
|
box-shadow: 0 0 0 2px rgba(72, 219, 251, 0.3);
|
}
|
|
.method-select option {
|
background-color: #2d3436;
|
color: white;
|
padding: 10px;
|
}
|
|
.method-select option:checked {
|
background-color: rgba(72, 219, 251, 0.3);
|
}
|
|
@-moz-document url-prefix() {
|
.method-select {
|
color: white !important;
|
text-shadow: 0 0 0 white;
|
}
|
|
.method-select option {
|
background-color: #2d3436;
|
}
|
}
|
|
.url-input {
|
flex: 1;
|
padding: 10px 15px;
|
background-color: rgba(255, 255, 255, 0.1);
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
color: white;
|
border-radius: 4px;
|
font-size: 14px;
|
transition: all 0.3s ease;
|
}
|
|
.url-input:focus {
|
border-color: var(--secondary-color);
|
outline: none;
|
box-shadow: 0 0 0 2px rgba(72, 219, 251, 0.3);
|
}
|
|
.send-btn {
|
margin-left: 10px;
|
padding: 10px 20px;
|
background-color: var(--primary-color);
|
color: white;
|
border: none;
|
border-radius: 4px;
|
font-weight: bold;
|
cursor: pointer;
|
transition: all 0.2s ease;
|
display: flex;
|
align-items: center;
|
gap: 8px;
|
}
|
|
.send-btn i {
|
font-size: 16px;
|
}
|
|
.send-btn:hover {
|
background-color: #ff5252;
|
transform: translateY(-1px);
|
box-shadow: 0 4px 8px rgba(255, 107, 107, 0.3);
|
}
|
|
.send-btn:active {
|
transform: translateY(0);
|
box-shadow: none;
|
}
|
|
/* 标签页导航 */
|
.tabs {
|
display: flex;
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
margin-bottom: 15px;
|
}
|
|
.tab {
|
padding: 10px 20px;
|
cursor: pointer;
|
border-bottom: 2px solid transparent;
|
transition: all 0.2s ease;
|
font-size: 14px;
|
position: relative;
|
}
|
|
.tab.active {
|
border-bottom-color: var(--primary-color);
|
color: var(--primary-color);
|
}
|
|
.tab:hover:not(.active) {
|
background-color: rgba(255, 255, 255, 0.05);
|
}
|
|
.tab-badge {
|
position: absolute;
|
top: -5px;
|
right: 5px;
|
background-color: var(--secondary-color);
|
color: var(--dark-color);
|
border-radius: 50%;
|
width: 18px;
|
height: 18px;
|
font-size: 10px;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
}
|
|
/* 标签内容区 */
|
.tab-content {
|
display: none;
|
animation: fadeIn 0.3s ease;
|
}
|
|
@keyframes fadeIn {
|
from {
|
opacity: 0;
|
transform: translateY(5px);
|
}
|
|
to {
|
opacity: 1;
|
transform: translateY(0);
|
}
|
}
|
|
.tab-content.active {
|
display: block;
|
}
|
|
/* 参数表格样式 */
|
.params-table {
|
width: 100%;
|
border-collapse: collapse;
|
margin-bottom: 15px;
|
}
|
|
.params-table th,
|
.params-table td {
|
padding: 10px;
|
text-align: left;
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
}
|
|
.params-table th {
|
font-weight: 500;
|
color: var(--secondary-color);
|
font-size: 13px;
|
}
|
|
.params-table tr:last-child td {
|
border-bottom: none;
|
}
|
|
.params-table input {
|
width: 100%;
|
padding: 8px;
|
background-color: rgba(255, 255, 255, 0.1);
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
color: white;
|
border-radius: 3px;
|
font-size: 13px;
|
transition: all 0.2s ease;
|
}
|
|
.params-table input:focus {
|
border-color: var(--secondary-color);
|
outline: none;
|
box-shadow: 0 0 0 2px rgba(72, 219, 251, 0.2);
|
}
|
|
.params-table select {
|
width: 100%;
|
padding: 8px;
|
background-color: rgba(255, 255, 255, 0.1);
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
color: white;
|
border-radius: 3px;
|
font-size: 13px;
|
appearance: none;
|
-webkit-appearance: none;
|
-moz-appearance: none;
|
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23ffffff'%3e%3cpath d='M7 10l5 5 5-5z'/%3e%3c/svg%3e");
|
background-repeat: no-repeat;
|
background-position: right 8px center;
|
background-size: 10px;
|
cursor: pointer;
|
}
|
|
.params-actions {
|
display: flex;
|
gap: 5px;
|
}
|
|
.param-action-btn {
|
width: 28px;
|
height: 28px;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
background-color: rgba(255, 255, 255, 0.1);
|
border: none;
|
border-radius: 3px;
|
color: white;
|
cursor: pointer;
|
transition: all 0.2s ease;
|
}
|
|
.param-action-btn:hover {
|
background-color: rgba(255, 255, 255, 0.2);
|
}
|
|
.param-action-btn.delete:hover {
|
background-color: rgba(255, 107, 107, 0.3);
|
color: var(--error-color);
|
}
|
|
.add-param-btn {
|
margin-top: 5px;
|
padding: 8px 15px;
|
background-color: rgba(72, 219, 251, 0.2);
|
color: var(--secondary-color);
|
border: 1px dashed var(--secondary-color);
|
border-radius: 4px;
|
cursor: pointer;
|
transition: all 0.2s ease;
|
font-size: 13px;
|
display: inline-flex;
|
align-items: center;
|
gap: 8px;
|
}
|
|
.add-param-btn:hover {
|
background-color: rgba(72, 219, 251, 0.3);
|
}
|
|
/* 请求体编辑器 */
|
.body-editor {
|
width: 100%;
|
height: 200px;
|
background-color: rgba(0, 0, 0, 0.3);
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
border-radius: 4px;
|
padding: 10px;
|
color: white;
|
font-family: 'Courier New', Courier, monospace;
|
font-size: 14px;
|
resize: vertical;
|
transition: all 0.3s ease;
|
}
|
|
.body-editor:focus {
|
border-color: var(--secondary-color);
|
outline: none;
|
box-shadow: 0 0 0 2px rgba(72, 219, 251, 0.3);
|
}
|
|
.body-type-selector {
|
margin: 12px 0;
|
padding: 0;
|
}
|
|
.button-group {
|
display: flex;
|
flex-wrap: wrap;
|
gap: 6px;
|
background: transparent;
|
padding: 0;
|
}
|
|
.type-button {
|
position: relative;
|
cursor: pointer;
|
}
|
|
.type-button input[type="radio"] {
|
position: absolute;
|
opacity: 0;
|
width: 0;
|
height: 0;
|
}
|
|
.type-button span {
|
display: block;
|
padding: 6px 12px;
|
font-size: 13px;
|
color: rgba(255, 255, 255, 0.7);
|
background-color: rgba(255, 255, 255, 0.1);
|
border-radius: 4px;
|
transition: all 0.2s ease;
|
white-space: nowrap;
|
}
|
|
.type-button:hover span {
|
box-shadow: 0 0 0 2px rgba(72, 219, 251, 0.3);
|
}
|
|
.type-button input[type="radio"]:checked+span {
|
background-color: rgba(72, 219, 251, 0.3);
|
color: var(--secondary-color);
|
}
|
|
.type-button input[type="radio"]:focus+span {
|
outline: none;
|
box-shadow: 0 0 0 2px rgba(72, 219, 251, 0.3);
|
}
|
|
.body-type-btn {
|
padding: 6px 12px;
|
background-color: rgba(255, 255, 255, 0.1);
|
border: none;
|
border-radius: 3px;
|
color: rgba(255, 255, 255, 0.7);
|
cursor: pointer;
|
font-size: 13px;
|
transition: all 0.2s ease;
|
}
|
|
body-type-btn.active {
|
background-color: rgba(72, 219, 251, 0.3);
|
color: var(--secondary-color);
|
}
|
|
/* 响应区域 */
|
.response-panel {
|
flex: 1;
|
padding: 20px;
|
overflow-y: auto;
|
background-color: rgba(45, 52, 54, 0.5);
|
}
|
|
.response-header {
|
display: flex;
|
align-items: center;
|
margin-bottom: 20px;
|
padding-bottom: 10px;
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
}
|
|
.status-code {
|
padding: 5px 10px;
|
border-radius: 3px;
|
font-weight: bold;
|
margin-right: 15px;
|
font-size: 14px;
|
}
|
|
.status-code.success {
|
background-color: rgba(29, 209, 161, 0.2);
|
color: var(--success-color);
|
border: 1px solid var(--success-color);
|
}
|
|
.status-code.error {
|
background-color: rgba(255, 107, 107, 0.2);
|
color: var(--error-color);
|
border: 1px solid var(--error-color);
|
}
|
|
.response-time {
|
color: var(--secondary-color);
|
margin-right: 15px;
|
font-size: 13px;
|
}
|
|
.response-size {
|
color: var(--secondary-color);
|
font-size: 13px;
|
}
|
|
/* 响应标签页 */
|
.response-tabs {
|
display: flex;
|
margin-bottom: 15px;
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
}
|
|
.response-tab {
|
padding: 8px 15px;
|
cursor: pointer;
|
border-bottom: 2px solid transparent;
|
transition: all 0.2s ease;
|
font-size: 13px;
|
}
|
|
.response-tab.active {
|
border-bottom-color: var(--primary-color);
|
color: var(--primary-color);
|
}
|
|
/* 响应内容区域 */
|
.response-content {
|
background-color: rgba(0, 0, 0, 0.3);
|
border-radius: 5px;
|
padding: 15px;
|
font-family: 'Courier New', Courier, monospace;
|
font-size: 14px;
|
white-space: pre-wrap;
|
overflow-x: auto;
|
min-height: 200px;
|
max-height: 400px;
|
overflow-y: auto;
|
}
|
|
.response-content {
|
display: none;
|
animation: fadeIn 0.3s ease;
|
}
|
|
.response-content.active {
|
display: block;
|
}
|
|
/* JSON高亮样式 */
|
.json-key {
|
color: #48dbfb;
|
}
|
|
.json-string {
|
color: #1dd1a1;
|
}
|
|
.json-number {
|
color: #feca57;
|
}
|
|
.json-boolean {
|
color: #ff6b6b;
|
}
|
|
.json-null {
|
color: #a55eea;
|
}
|
|
/* 响应头表格 */
|
.headers-table {
|
width: 100%;
|
border-collapse: collapse;
|
margin-bottom: 20px;
|
}
|
|
.headers-table th,
|
.headers-table td {
|
padding: 8px 10px;
|
text-align: left;
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
font-size: 13px;
|
}
|
|
.headers-table th {
|
font-weight: 500;
|
color: var(--secondary-color);
|
}
|
|
/* 底部状态栏 */
|
.footer {
|
height: var(--footer-height);
|
background-color: rgba(45, 52, 54, 0.9);
|
display: flex;
|
align-items: center;
|
padding: 0 20px;
|
font-size: 13px;
|
color: rgba(255, 255, 255, 0.7);
|
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
}
|
|
.footer-item {
|
margin-right: 20px;
|
display: flex;
|
align-items: center;
|
}
|
|
.footer-item i {
|
margin-right: 5px;
|
font-size: 14px;
|
}
|
|
/* 加载动画 */
|
.loader {
|
display: inline-block;
|
width: 16px;
|
height: 16px;
|
border: 2px solid rgba(255, 255, 255, 0.3);
|
border-radius: 50%;
|
border-top-color: var(--secondary-color);
|
animation: spin 1s ease-in-out infinite;
|
margin-right: 8px;
|
}
|
|
@keyframes spin {
|
to {
|
transform: rotate(360deg);
|
}
|
}
|
|
/* 响应图片预览 */
|
.image-preview {
|
max-width: 100%;
|
max-height: 300px;
|
border-radius: 4px;
|
margin-top: 10px;
|
}
|
|
/* 响应HTML预览 */
|
.html-preview {
|
width: 100%;
|
height: 300px;
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
border-radius: 4px;
|
background-color: white;
|
}
|
|
/* 响应原始数据 */
|
.raw-response {
|
white-space: pre-wrap;
|
font-family: 'Courier New', Courier, monospace;
|
font-size: 14px;
|
}
|
|
/* 工具提示 */
|
.tooltip {
|
position: relative;
|
display: inline-block;
|
}
|
|
.tooltip .tooltip-text {
|
visibility: hidden;
|
width: 120px;
|
background-color: rgba(0, 0, 0, 0.8);
|
color: #fff;
|
text-align: center;
|
border-radius: 6px;
|
padding: 5px;
|
position: absolute;
|
z-index: 1;
|
bottom: 125%;
|
left: 50%;
|
margin-left: -60px;
|
opacity: 0;
|
transition: opacity 0.3s;
|
font-size: 12px;
|
}
|
|
.tooltip:hover .tooltip-text {
|
visibility: visible;
|
opacity: 1;
|
}
|
|
/* 响应cookie表格 */
|
.cookies-table {
|
width: 100%;
|
border-collapse: collapse;
|
margin-bottom: 20px;
|
}
|
|
.cookies-table th,
|
.cookies-table td {
|
padding: 8px 10px;
|
text-align: left;
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
font-size: 13px;
|
}
|
|
.cookies-table th {
|
font-weight: 500;
|
color: var(--secondary-color);
|
}
|
|
/* 响应测试结果 */
|
.test-results {
|
margin-top: 20px;
|
}
|
|
.test-result {
|
padding: 10px;
|
margin-bottom: 10px;
|
border-radius: 4px;
|
font-size: 13px;
|
}
|
|
.test-result.pass {
|
background-color: rgba(29, 209, 161, 0.2);
|
color: var(--success-color);
|
border-left: 3px solid var(--success-color);
|
}
|
|
.test-result.fail {
|
background-color: rgba(255, 107, 107, 0.2);
|
color: var(--error-color);
|
border-left: 3px solid var(--error-color);
|
}
|
|
.test-result-name {
|
font-weight: bold;
|
margin-bottom: 5px;
|
}
|
|
/* 暗色滚动条 */
|
::-webkit-scrollbar {
|
width: 8px;
|
height: 8px;
|
}
|
|
::-webkit-scrollbar-track {
|
background: rgba(255, 255, 255, 0.05);
|
}
|
|
::-webkit-scrollbar-thumb {
|
background: rgba(255, 255, 255, 0.2);
|
border-radius: 4px;
|
}
|
|
::-webkit-scrollbar-thumb:hover {
|
background: rgba(255, 255, 255, 0.3);
|
}
|
|
/* 响应式设计 */
|
@media (max-width: 768px) {
|
.main-container {
|
flex-direction: column;
|
}
|
|
.sidebar {
|
width: 100%;
|
height: 200px;
|
}
|
|
.request-controls {
|
flex-direction: column;
|
}
|
|
.method-select,
|
.url-input,
|
.send-btn {
|
width: 100%;
|
margin: 5px 0;
|
}
|
|
.response-content {
|
max-height: 300px;
|
}
|
}
|
|
/* 新增样式 */
|
.request-loading {
|
position: relative;
|
overflow: hidden;
|
}
|
|
.request-loading::after {
|
content: '';
|
position: absolute;
|
top: 0;
|
left: -100%;
|
width: 100%;
|
height: 100%;
|
background: linear-gradient(90deg,
|
transparent,
|
rgba(72, 219, 251, 0.4),
|
transparent);
|
animation: loading 1.5s infinite;
|
}
|
|
@keyframes loading {
|
100% {
|
left: 100%;
|
}
|
}
|
|
.error-message {
|
display: flex;
|
align-items: center;
|
gap: 10px;
|
color: #ff6b6b;
|
padding: 15px;
|
background-color: rgba(255, 107, 107, 0.1);
|
border-radius: 4px;
|
}
|
|
.error-message i {
|
font-size: 24px;
|
}
|
|
.cm-s-dracula span.cm-string {
|
color: #f1fa8c;
|
}
|
|
.cm-s-dracula span.cm-number {
|
color: #bd93f9;
|
}
|
|
.cm-s-dracula span.cm-key {
|
color: #ff79c6;
|
}
|
|
.cm-s-dracula span.cm-boolean {
|
color: #bd93f9;
|
}
|
|
.cm-s-dracula span.cm-null {
|
color: #bd93f9;
|
}
|
</style>
|
</head>
|
|
<body>
|
<div class="gradient-bg"></div>
|
<div class="header">
|
<div class="logo">
|
<i class="fas fa-bolt"></i>
|
<span>API测试工具</span>
|
</div>
|
<div class="header-actions">
|
<button id="new-request-btn" class="tooltip">
|
<i class="fas fa-plus"></i>
|
<span class="tooltip-text">新建请求</span>
|
</button>
|
<button id="save-request-btn" class="tooltip">
|
<i class="fas fa-save"></i>
|
<span class="tooltip-text">保存请求</span>
|
</button>
|
<button id="import-request-btn" class="tooltip">
|
<i class="fas fa-file-import"></i>
|
<span class="tooltip-text">导入请求</span>
|
</button>
|
<button id="export-request-btn" class="tooltip">
|
<i class="fas fa-file-export"></i>
|
<span class="tooltip-text">导出请求</span>
|
</button>
|
<button id="settings-btn" class="tooltip">
|
<i class="fas fa-cog"></i>
|
<span class="tooltip-text">设置</span>
|
</button>
|
</div>
|
</div>
|
|
<div class="main-container">
|
<!-- 侧边栏 - 请求历史 -->
|
<div class="sidebar">
|
<div class="sidebar-header">
|
<div class="sidebar-title">请求历史</div>
|
<button id="clear-history-btn" class="tooltip">
|
<i class="fas fa-trash"></i>
|
<span class="tooltip-text">清空历史</span>
|
</button>
|
</div>
|
<div class="sidebar-content" id="history-list">
|
<!-- 历史请求将在这里动态生成 -->
|
</div>
|
</div>
|
|
<!-- 主工作区 -->
|
<div class="workspace">
|
<!-- 请求面板 -->
|
<div class="request-panel">
|
<div class="request-controls">
|
<select class="method-select" id="method-select">
|
<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>
|
<input type="text" class="url-input" id="url-input"
|
placeholder="输入请求URL (例如: https://api.example.com/data)">
|
<button class="send-btn" id="send-btn">
|
<i class="fas fa-paper-plane"></i>
|
<span>发送</span>
|
</button>
|
</div>
|
|
<!-- 请求参数标签页 -->
|
<div class="tabs">
|
<div class="tab active" data-tab="params">参数</div>
|
<div class="tab" data-tab="headers">请求头</div>
|
<div class="tab" data-tab="body">请求体</div>
|
<div class="tab" data-tab="auth">认证</div>
|
<div class="tab" data-tab="tests">测试</div>
|
</div>
|
|
<!-- 参数标签内容 -->
|
<div class="tab-content active" id="params-tab">
|
<table class="params-table" id="params-table">
|
<thead>
|
<tr>
|
<th>参数名</th>
|
<th>值</th>
|
<th>描述</th>
|
<th>操作</th>
|
</tr>
|
</thead>
|
<tbody>
|
<tr>
|
<td><input type="text" placeholder="参数名"></td>
|
<td><input type="text" placeholder="值"></td>
|
<td><input type="text" placeholder="描述"></td>
|
<td class="params-actions">
|
<button class="param-action-btn delete"><i class="fas fa-trash"></i></button>
|
</td>
|
</tr>
|
</tbody>
|
</table>
|
<button class="add-param-btn" id="add-param-btn">
|
<i class="fas fa-plus"></i>
|
<span>添加参数</span>
|
</button>
|
</div>
|
|
<!-- 请求头标签内容 -->
|
<div class="tab-content" id="headers-tab">
|
<table class="params-table" id="headers-table">
|
<thead>
|
<tr>
|
<th>请求头名</th>
|
<th>值</th>
|
<th>描述</th>
|
<th>操作</th>
|
</tr>
|
</thead>
|
<tbody>
|
<tr>
|
<td><input type="text" placeholder="例如: Content-Type"></td>
|
<td><input type="text" placeholder="例如: application/json"></td>
|
<td><input type="text" placeholder="描述"></td>
|
<td class="params-actions">
|
<button class="param-action-btn delete"><i class="fas fa-trash"></i></button>
|
</td>
|
</tr>
|
</tbody>
|
</table>
|
<button class="add-param-btn" id="add-header-btn">
|
<i class="fas fa-plus"></i>
|
<span>添加请求头</span>
|
</button>
|
</div>
|
|
<!-- 请求体标签内容 -->
|
<div class="tab-content" id="body-tab">
|
<div class="body-type-selector">
|
<div class="button-group">
|
<label class="type-button">
|
<input type="radio" name="body-type" value="none" checked>
|
<span>无</span>
|
</label>
|
<label class="type-button">
|
<input type="radio" name="body-type" value="form-data">
|
<span>表单数据</span>
|
</label>
|
<label class="type-button">
|
<input type="radio" name="body-type" value="x-www-form-urlencoded">
|
<span>x-www-form-urlencoded</span>
|
</label>
|
<label class="type-button">
|
<input type="radio" name="body-type" value="raw">
|
<span>原始数据</span>
|
</label>
|
<label class="type-button">
|
<input type="radio" name="body-type" value="binary">
|
<span>二进制</span>
|
</label>
|
</div>
|
</div>
|
|
<!-- 表单数据 -->
|
<div class="body-content" id="form-data-content">
|
<table class="params-table">
|
<thead>
|
<tr>
|
<th>键</th>
|
<th>值</th>
|
<th>描述</th>
|
<th>操作</th>
|
</tr>
|
</thead>
|
<tbody>
|
<tr>
|
<td><input type="text" placeholder="键"></td>
|
<td><input type="text" placeholder="值"></td>
|
<td><input type="text" placeholder="描述"></td>
|
<td class="params-actions">
|
<button class="param-action-btn delete"><i class="fas fa-trash"></i></button>
|
</td>
|
</tr>
|
</tbody>
|
</table>
|
<button class="add-param-btn">
|
<i class="fas fa-plus"></i>
|
<span>添加表单数据</span>
|
</button>
|
</div>
|
|
<!-- 原始数据 -->
|
<div class="body-content" id="raw-content">
|
<textarea class="body-editor" id="body-editor" placeholder='例如: {"key": "value"}'></textarea>
|
</div>
|
</div>
|
|
<!-- 认证标签内容 -->
|
<div class="tab-content" id="auth-tab">
|
<select class="method-select" id="auth-type">
|
<option value="none">无认证</option>
|
<option value="basic">Basic Auth</option>
|
<option value="bearer">Bearer Token</option>
|
<option value="api-key">API Key</option>
|
<option value="oauth2">OAuth 2.0</option>
|
</select>
|
|
<div id="auth-fields">
|
<!-- 认证字段将根据选择的认证类型动态生成 -->
|
</div>
|
</div>
|
|
<!-- 测试标签内容 -->
|
<div class="tab-content" id="tests-tab">
|
<textarea class="body-editor" id="tests-editor" placeholder="// 编写JavaScript测试脚本
|
// 例如: pm.test('状态码是200', function() {
|
// pm.response.to.have.status(200);
|
// });"></textarea>
|
</div>
|
</div>
|
|
<!-- 响应面板 -->
|
<div class="response-panel" id="response-panel">
|
<div class="response-header">
|
<div class="status-code" id="status-code">等待请求...</div>
|
<div class="response-time" id="response-time"></div>
|
<div class="response-size" id="response-size"></div>
|
</div>
|
|
<div class="response-tabs">
|
<div class="response-tab active" data-response-tab="body">响应体</div>
|
<div class="response-tab" data-response-tab="headers">响应头</div>
|
<div class="response-tab" data-response-tab="cookies">Cookies</div>
|
<div class="response-tab" data-response-tab="tests">测试结果</div>
|
</div>
|
|
<div class="response-content active" id="response-body">
|
<pre id="response-body-content">发送请求以查看响应</pre>
|
</div>
|
|
<div class="response-content" id="response-headers">
|
<table class="headers-table" id="headers-table">
|
<thead>
|
<tr>
|
<th>名称</th>
|
<th>值</th>
|
</tr>
|
</thead>
|
<tbody>
|
<!-- 响应头将在这里动态生成 -->
|
</tbody>
|
</table>
|
</div>
|
|
<div class="response-content" id="response-cookies">
|
<table class="cookies-table" id="cookies-table">
|
<thead>
|
<tr>
|
<th>名称</th>
|
<th>值</th>
|
<th>域</th>
|
<th>路径</th>
|
<th>过期时间</th>
|
</tr>
|
</thead>
|
<tbody>
|
<!-- Cookies将在这里动态生成 -->
|
</tbody>
|
</table>
|
</div>
|
|
<div class="response-content" id="response-tests">
|
<div class="test-results" id="test-results">
|
<!-- 测试结果将在这里动态生成 -->
|
</div>
|
</div>
|
</div>
|
</div>
|
</div>
|
|
<!-- 底部状态栏 -->
|
<div class="footer">
|
<div class="footer-item">
|
<i class="fas fa-circle"></i>
|
<span id="connection-status">已连接</span>
|
</div>
|
<div class="footer-item">
|
<i class="fas fa-code-branch"></i>
|
<span>v1.0.0</span>
|
</div>
|
<div class="footer-item">
|
<i class="fas fa-info-circle"></i>
|
<span>API测试工具</span>
|
</div>
|
</div>
|
|
<!-- JavaScript 实现 -->
|
<script>
|
// JavaScript 实现
|
document.addEventListener('DOMContentLoaded', function () {
|
// 全局状态管理
|
const state = {
|
currentRequest: {
|
method: 'GET',
|
url: '',
|
params: [],
|
headers: [
|
{ key: 'Content-Type', value: 'application/json', enabled: true }
|
],
|
body: '{}',
|
auth: { type: 'none', basic: { username: '', password: '' }, bearer: { token: '' } },
|
tests: ''
|
},
|
requestHistory: JSON.parse(localStorage.getItem('requestHistory')) || [],
|
isRequestInProgress: false,
|
activeResponseTab: 'body',
|
activeRequestTab: 'params'
|
};
|
|
// DOM元素缓存
|
const elements = {
|
methodSelect: document.getElementById('method-select'),
|
urlInput: document.getElementById('url-input'),
|
sendBtn: document.getElementById('send-btn'),
|
saveBtn: document.getElementById('save-request-btn'),
|
historyList: document.getElementById('history-list'),
|
clearHistoryBtn: document.getElementById('clear-history-btn'),
|
statusCode: document.getElementById('status-code'),
|
responseTime: document.getElementById('response-time'),
|
responseSize: document.getElementById('response-size'),
|
responseBodyContent: document.getElementById('response-body-content'),
|
headersTableBody: document.querySelector('#headers-table tbody'),
|
cookiesTableBody: document.querySelector('#cookies-table tbody'),
|
testResults: document.getElementById('test-results'),
|
paramsTableBody: document.querySelector('#params-table tbody'),
|
headersTableBodyReq: document.querySelector('#headers-table tbody'),
|
bodyEditor: document.getElementById('body-editor'),
|
authTypeSelect: document.getElementById('auth-type'),
|
authBasicSection: document.getElementById('auth-basic'),
|
authBearerSection: document.getElementById('auth-bearer'),
|
testsEditor: document.getElementById('tests-editor')
|
};
|
|
// 初始化应用
|
init();
|
|
function init() {
|
renderRequestHistory();
|
setupUIEventListeners();
|
setupRequestTabs();
|
setupResponseTabs();
|
loadDefaultRequest();
|
setupBodyEditor();
|
setupTestsEditor();
|
}
|
|
// 1. 请求发送功能
|
async function sendRequest() {
|
if (state.isRequestInProgress) return;
|
|
try {
|
state.isRequestInProgress = true;
|
updateUIForRequestStart();
|
|
// 准备请求数据
|
const { url, method, headers, body } = prepareRequestData();
|
|
// 发送请求
|
const startTime = performance.now();
|
const response = await fetch(url, {
|
method,
|
headers: prepareRequestHeaders(headers),
|
body: method !== 'GET' ? body : null
|
});
|
const duration = (performance.now() - startTime).toFixed(2);
|
|
// 处理响应
|
await processResponse(response, duration);
|
|
// 保存到历史记录
|
saveToHistory();
|
|
} catch (error) {
|
handleRequestError(error);
|
} finally {
|
state.isRequestInProgress = false;
|
updateUIForRequestEnd();
|
}
|
}
|
|
function prepareRequestData() {
|
// 收集当前请求的所有数据
|
const method = elements.methodSelect.value;
|
let url = elements.urlInput.value.trim();
|
|
// 处理查询参数
|
const params = collectParams();
|
if (params.length > 0) {
|
const urlObj = new URL(url);
|
params.forEach(param => {
|
if (param.key && param.enabled) {
|
urlObj.searchParams.append(param.key, param.value);
|
}
|
});
|
url = urlObj.toString();
|
}
|
|
// 处理请求头
|
const headers = collectHeaders();
|
|
// 处理认证
|
handleAuthentication(headers);
|
|
// 处理请求体
|
const body = getRequestBody();
|
|
// 更新当前请求状态
|
state.currentRequest = {
|
...state.currentRequest,
|
method,
|
url,
|
params,
|
headers,
|
body
|
};
|
|
return { url, method, headers, body };
|
}
|
|
function prepareRequestHeaders(headers) {
|
const headerObj = {};
|
headers
|
.filter(h => h.enabled && h.key)
|
.forEach(h => {
|
headerObj[h.key] = h.value;
|
});
|
return headerObj;
|
}
|
|
function handleAuthentication(headers) {
|
const authType = elements.authTypeSelect.value;
|
|
if (authType === 'basic') {
|
const username = document.getElementById('basic-username').value;
|
const password = document.getElementById('basic-password').value;
|
if (username && password) {
|
const token = btoa(`${username}:${password}`);
|
headers.push({
|
key: 'Authorization',
|
value: `Basic ${token}`,
|
enabled: true
|
});
|
}
|
}
|
else if (authType === 'bearer') {
|
const token = document.getElementById('bearer-token').value;
|
if (token) {
|
headers.push({
|
key: 'Authorization',
|
value: `Bearer ${token}`,
|
enabled: true
|
});
|
}
|
}
|
}
|
|
// 2. 参数管理功能
|
function collectParams() {
|
const params = [];
|
const rows = elements.paramsTableBody.querySelectorAll('tr');
|
|
rows.forEach(row => {
|
const key = row.querySelector('.param-key').value.trim();
|
const value = row.querySelector('.param-value').value.trim();
|
const enabled = row.querySelector('.param-enabled').checked;
|
|
if (key) {
|
params.push({ key, value, enabled });
|
}
|
});
|
|
return params;
|
}
|
|
function collectHeaders() {
|
const headers = [];
|
const rows = elements.headersTableBodyReq.querySelectorAll('tr');
|
|
rows.forEach(row => {
|
const key = row.querySelector('.header-key').value.trim();
|
const value = row.querySelector('.header-value').value.trim();
|
const enabled = row.querySelector('.header-enabled').checked;
|
|
if (key) {
|
headers.push({ key, value, enabled });
|
}
|
});
|
|
return headers;
|
}
|
|
function getRequestBody() {
|
const bodyType = document.querySelector('.body-type-selector input:checked').value;
|
|
switch (bodyType) {
|
case 'json':
|
return elements.bodyEditor.value;
|
case 'form-data':
|
return prepareFormData();
|
case 'x-www-form-urlencoded':
|
return prepareUrlEncodedData();
|
default:
|
return '';
|
}
|
}
|
|
function prepareFormData() {
|
const formData = new FormData();
|
document.querySelectorAll('#form-data-table tbody tr').forEach(row => {
|
const key = row.querySelector('.form-data-key').value;
|
const value = row.querySelector('.form-data-value').value;
|
const type = row.querySelector('.form-data-type').value;
|
|
if (key) {
|
if (type === 'file') {
|
const file = row.querySelector('.form-data-file').files[0];
|
if (file) formData.append(key, file);
|
} else {
|
formData.append(key, value);
|
}
|
}
|
});
|
return formData;
|
}
|
|
function prepareUrlEncodedData() {
|
const params = new URLSearchParams();
|
document.querySelectorAll('#urlencoded-table tbody tr').forEach(row => {
|
const key = row.querySelector('.urlencoded-key').value;
|
const value = row.querySelector('.urlencoded-value').value;
|
if (key) params.append(key, value);
|
});
|
return params.toString();
|
}
|
|
// 3. 响应处理功能
|
async function processResponse(response, duration) {
|
// 更新响应状态
|
updateResponseStatus(response, duration);
|
|
// 处理响应数据
|
const contentType = response.headers.get('content-type') || '';
|
let responseData;
|
|
if (contentType.includes('application/json')) {
|
responseData = await response.json();
|
displayJsonResponse(responseData);
|
}
|
else if (contentType.includes('text/')) {
|
responseData = await response.text();
|
displayTextResponse(responseData);
|
}
|
else if (contentType.includes('image/')) {
|
responseData = await response.blob();
|
displayImageResponse(responseData);
|
}
|
else {
|
responseData = await response.text();
|
displayRawResponse(responseData);
|
}
|
|
// 显示响应头
|
displayResponseHeaders(response.headers);
|
|
// 显示Cookies
|
displayResponseCookies(response.headers);
|
|
// 计算并显示响应大小
|
displayResponseSize(response, responseData);
|
|
// 运行测试
|
runTests(response, responseData);
|
}
|
|
function updateResponseStatus(response, duration) {
|
elements.statusCode.className = response.ok ? 'status-code success' : 'status-code error';
|
elements.statusCode.textContent = `${response.status} ${response.statusText}`;
|
elements.responseTime.textContent = `${duration}ms`;
|
}
|
|
function displayJsonResponse(data) {
|
elements.responseBodyContent.innerHTML = syntaxHighlight(JSON.stringify(data, null, 2));
|
}
|
|
function displayTextResponse(text) {
|
elements.responseBodyContent.textContent = text;
|
}
|
|
function displayImageResponse(blob) {
|
const url = URL.createObjectURL(blob);
|
elements.responseBodyContent.innerHTML = `<img src="${url}" style="max-width: 100%;">`;
|
}
|
|
function displayRawResponse(data) {
|
elements.responseBodyContent.textContent = data;
|
}
|
|
// 4. 响应头显示功能
|
function displayResponseHeaders(headers) {
|
elements.headersTableBody.innerHTML = '';
|
|
headers.forEach((value, key) => {
|
const row = document.createElement('tr');
|
row.innerHTML = `
|
<td>${key}</td>
|
<td>${value}</td>
|
`;
|
elements.headersTableBody.appendChild(row);
|
});
|
}
|
|
// 5. Cookie显示功能
|
function displayResponseCookies(headers) {
|
elements.cookiesTableBody.innerHTML = '';
|
|
const cookieHeader = headers.get('set-cookie');
|
if (!cookieHeader) return;
|
|
// 解析多个Set-Cookie头
|
const cookies = Array.isArray(cookieHeader) ? cookieHeader : [cookieHeader];
|
|
cookies.forEach(cookie => {
|
const parts = cookie.split(';')[0].split('=');
|
const key = parts[0];
|
const value = parts.slice(1).join('=');
|
|
const row = document.createElement('tr');
|
row.innerHTML = `
|
<td>${key}</td>
|
<td>${value}</td>
|
`;
|
elements.cookiesTableBody.appendChild(row);
|
});
|
}
|
|
// 6. 响应大小计算
|
function displayResponseSize(response, responseData) {
|
let size = '0 B';
|
|
// 从Content-Length头获取大小
|
const contentLength = response.headers.get('content-length');
|
if (contentLength) {
|
size = formatBytes(parseInt(contentLength));
|
}
|
// 如果没有Content-Length,计算实际数据大小
|
else if (responseData) {
|
if (typeof responseData === 'string') {
|
size = formatBytes(new Blob([responseData]).size);
|
} else if (responseData instanceof Blob) {
|
size = formatBytes(responseData.size);
|
} else {
|
const jsonStr = JSON.stringify(responseData);
|
size = formatBytes(new Blob([jsonStr]).size);
|
}
|
}
|
|
elements.responseSize.textContent = size;
|
}
|
|
function formatBytes(bytes) {
|
if (bytes === 0) return '0 B';
|
|
const k = 1024;
|
const sizes = ['B', 'KB', 'MB', 'GB'];
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
}
|
|
// 7. 测试脚本执行
|
function runTests(response, responseData) {
|
const tests = elements.testsEditor.getValue();
|
if (!tests.trim()) return;
|
|
try {
|
// 创建测试环境
|
const testEnv = {
|
status: response.status,
|
statusText: response.statusText,
|
headers: Object.fromEntries(response.headers),
|
response: responseData,
|
tests: [],
|
assertions: 0,
|
failures: 0
|
};
|
|
// 添加测试工具函数
|
testEnv.expect = function (value) {
|
this.assertions++;
|
return {
|
to: {
|
be: expected => {
|
if (value !== expected) {
|
this.failures++;
|
this.tests.push({
|
passed: false,
|
message: `Expected ${value} to be ${expected}`
|
});
|
} else {
|
this.tests.push({
|
passed: true,
|
message: `Expected ${value} to be ${expected}`
|
});
|
}
|
},
|
contain: expected => {
|
if (!value.includes(expected)) {
|
this.failures++;
|
this.tests.push({
|
passed: false,
|
message: `Expected ${value} to contain ${expected}`
|
});
|
} else {
|
this.tests.push({
|
passed: true,
|
message: `Expected ${value} to contain ${expected}`
|
});
|
}
|
}
|
}
|
};
|
};
|
|
// 执行测试代码
|
new Function('pm', tests)(testEnv);
|
|
// 显示测试结果
|
displayTestResults(testEnv);
|
|
} catch (error) {
|
elements.testResults.innerHTML = `
|
<div class="test-error">
|
<i class="fas fa-exclamation-circle"></i>
|
Test script error: ${error.message}
|
</div>
|
`;
|
}
|
}
|
|
function displayTestResults(testEnv) {
|
let html = `
|
<div class="test-summary">
|
Tests: ${testEnv.tests.length},
|
Passed: ${testEnv.tests.length - testEnv.failures},
|
Failed: ${testEnv.failures}
|
</div>
|
`;
|
|
testEnv.tests.forEach(test => {
|
html += `
|
<div class="test-result ${test.passed ? 'passed' : 'failed'}">
|
<i class="fas ${test.passed ? 'fa-check-circle' : 'fa-times-circle'}"></i>
|
${test.message}
|
</div>
|
`;
|
});
|
|
elements.testResults.innerHTML = html;
|
}
|
|
// 8. 请求历史管理
|
function saveToHistory() {
|
// 检查是否已存在相同请求
|
const existingIndex = state.requestHistory.findIndex(
|
req => req.method === state.currentRequest.method &&
|
req.url === state.currentRequest.url
|
);
|
|
if (existingIndex >= 0) {
|
// 更新现有记录
|
state.requestHistory[existingIndex] = { ...state.currentRequest };
|
} else {
|
// 添加到历史记录
|
state.requestHistory.unshift({ ...state.currentRequest });
|
|
// 限制历史记录数量
|
if (state.requestHistory.length > 50) {
|
state.requestHistory.pop();
|
}
|
}
|
|
// 保存到本地存储
|
localStorage.setItem('requestHistory', JSON.stringify(state.requestHistory));
|
|
// 重新渲染历史列表
|
renderRequestHistory();
|
}
|
|
function renderRequestHistory() {
|
elements.historyList.innerHTML = '';
|
|
state.requestHistory.forEach((request, index) => {
|
const item = document.createElement('div');
|
item.className = 'history-item';
|
item.innerHTML = `
|
<span class="method ${request.method.toLowerCase()}">${request.method}</span>
|
<span class="url">${request.url}</span>
|
<button class="load-btn" data-index="${index}">
|
<i class="fas fa-upload"></i>
|
</button>
|
<button class="delete-btn" data-index="${index}">
|
<i class="fas fa-trash"></i>
|
</button>
|
`;
|
|
elements.historyList.appendChild(item);
|
});
|
|
// 添加历史项事件监听
|
document.querySelectorAll('.history-item .load-btn').forEach(btn => {
|
btn.addEventListener('click', () => loadFromHistory(parseInt(btn.dataset.index)));
|
});
|
|
document.querySelectorAll('.history-item .delete-btn').forEach(btn => {
|
btn.addEventListener('click', (e) => {
|
e.stopPropagation();
|
deleteFromHistory(parseInt(btn.dataset.index));
|
});
|
});
|
}
|
|
function loadFromHistory(index) {
|
const request = state.requestHistory[index];
|
if (!request) return;
|
|
// 更新UI
|
elements.methodSelect.value = request.method;
|
elements.urlInput.value = request.url;
|
|
// 加载参数
|
renderParamsTable(request.params);
|
|
// 加载请求头
|
renderHeadersTable(request.headers);
|
|
// 加载请求体
|
elements.bodyEditor.setValue(request.body || '');
|
|
// 更新当前请求状态
|
state.currentRequest = { ...request };
|
}
|
|
function deleteFromHistory(index) {
|
state.requestHistory.splice(index, 1);
|
localStorage.setItem('requestHistory', JSON.stringify(state.requestHistory));
|
renderRequestHistory();
|
}
|
|
// 9. UI交互功能
|
function setupUIEventListeners() {
|
// 发送请求按钮
|
elements.sendBtn.addEventListener('click', sendRequest);
|
|
// 保存请求按钮
|
elements.saveBtn.addEventListener('click', saveToHistory);
|
|
// 清空历史按钮
|
elements.clearHistoryBtn.addEventListener('click', () => {
|
state.requestHistory = [];
|
localStorage.removeItem('requestHistory');
|
renderRequestHistory();
|
});
|
|
// 认证类型切换
|
elements.authTypeSelect.addEventListener('change', () => {
|
updateAuthUI();
|
});
|
|
// 请求体类型切换
|
document.querySelectorAll('.body-type-selector input').forEach(radio => {
|
radio.addEventListener('change', () => {
|
updateBodyTypeUI();
|
});
|
});
|
}
|
|
function updateAuthUI() {
|
const authType = elements.authTypeSelect.value;
|
|
// 隐藏所有认证部分
|
elements.authBasicSection.style.display = 'none';
|
elements.authBearerSection.style.display = 'none';
|
|
// 显示选中的认证部分
|
if (authType === 'basic') {
|
elements.authBasicSection.style.display = 'block';
|
} else if (authType === 'bearer') {
|
elements.authBearerSection.style.display = 'block';
|
}
|
}
|
|
function updateBodyTypeUI() {
|
const bodyType = document.querySelector('.body-type-selector input:checked').value;
|
|
// 隐藏所有body编辑器
|
document.querySelectorAll('.body-editor-container').forEach(el => {
|
el.style.display = 'none';
|
});
|
|
// 显示选中的body编辑器
|
const activeEditor = document.getElementById(`${bodyType}-editor`);
|
if (activeEditor) {
|
activeEditor.style.display = 'block';
|
|
// 如果是JSON编辑器,重新格式化内容
|
if (bodyType === 'json') {
|
try {
|
const json = JSON.parse(elements.bodyEditor.getValue());
|
elements.bodyEditor.setValue(JSON.stringify(json, null, 2));
|
} catch (e) {
|
// 不是有效的JSON,保持原样
|
}
|
}
|
}
|
}
|
|
// 10. 表格行操作功能
|
function setupTableRowOperations() {
|
// 参数表格操作
|
setupDynamicTable('#params-table', 'params', {
|
keyClass: 'param-key',
|
valueClass: 'param-value',
|
enabledClass: 'param-enabled'
|
});
|
|
// 请求头表格操作
|
setupDynamicTable('#headers-table', 'headers', {
|
keyClass: 'header-key',
|
valueClass: 'header-value',
|
enabledClass: 'header-enabled'
|
});
|
|
// FormData表格操作
|
setupDynamicTable('#form-data-table', 'form-data', {
|
keyClass: 'form-data-key',
|
valueClass: 'form-data-value',
|
typeClass: 'form-data-type'
|
});
|
|
// URL编码表格操作
|
setupDynamicTable('#urlencoded-table', 'urlencoded', {
|
keyClass: 'urlencoded-key',
|
valueClass: 'urlencoded-value'
|
});
|
}
|
|
function setupDynamicTable(tableId, type, classes) {
|
const table = document.querySelector(tableId);
|
const tbody = table.querySelector('tbody');
|
const addBtn = table.querySelector('.add-row-btn');
|
|
// 添加行
|
addBtn.addEventListener('click', () => {
|
addTableRow(tbody, type, classes);
|
});
|
|
// 初始化行
|
if (tbody.children.length === 0) {
|
addTableRow(tbody, type, classes);
|
}
|
|
// 行内删除按钮
|
tbody.addEventListener('click', (e) => {
|
if (e.target.classList.contains('delete-row-btn')) {
|
const row = e.target.closest('tr');
|
if (row && tbody.children.length > 1) {
|
row.remove();
|
}
|
}
|
});
|
}
|
|
function addTableRow(tbody, type, classes) {
|
const row = document.createElement('tr');
|
|
let rowHtml = `
|
<td>
|
<input type="text" class="${classes.keyClass}" placeholder="Key">
|
</td>
|
<td>
|
<input type="text" class="${classes.valueClass}" placeholder="Value">
|
</td>
|
`;
|
|
// 添加启用/禁用开关
|
if (classes.enabledClass) {
|
rowHtml += `
|
<td>
|
<label class="switch">
|
<input type="checkbox" class="${classes.enabledClass}" checked>
|
<span class="slider"></span>
|
</label>
|
</td>
|
`;
|
}
|
|
// 添加类型选择器 (用于FormData)
|
if (classes.typeClass) {
|
rowHtml += `
|
<td>
|
<select class="${classes.typeClass}">
|
<option value="text">Text</option>
|
<option value="file">File</option>
|
</select>
|
</td>
|
`;
|
}
|
|
// 添加删除按钮
|
rowHtml += `
|
<td>
|
<button class="delete-row-btn">
|
<i class="fas fa-trash"></i>
|
</button>
|
</td>
|
`;
|
|
row.innerHTML = rowHtml;
|
tbody.appendChild(row);
|
|
// 处理文件选择器的显示/隐藏
|
if (classes.typeClass) {
|
const typeSelect = row.querySelector(`.${classes.typeClass}`);
|
const valueInput = row.querySelector(`.${classes.valueClass}`);
|
const fileInput = document.createElement('input');
|
fileInput.type = 'file';
|
fileInput.className = 'form-data-file';
|
fileInput.style.display = 'none';
|
|
valueInput.parentNode.insertBefore(fileInput, valueInput.nextSibling);
|
|
typeSelect.addEventListener('change', () => {
|
if (typeSelect.value === 'file') {
|
valueInput.style.display = 'none';
|
fileInput.style.display = 'block';
|
} else {
|
valueInput.style.display = 'block';
|
fileInput.style.display = 'none';
|
}
|
});
|
}
|
}
|
|
// 11. 请求和响应标签页切换
|
function setupRequestTabs() {
|
const tabs = document.querySelectorAll('.tab');
|
tabs.forEach(tab => {
|
tab.addEventListener('click', () => {
|
tabs.forEach(t => t.classList.remove('active'));
|
tab.classList.add('active');
|
|
const tabId = tab.getAttribute('data-tab');
|
document.querySelectorAll('.tab-content').forEach(content => {
|
content.classList.remove('active');
|
});
|
document.getElementById(`${tabId}-tab`).classList.add('active');
|
|
// 如果是Body标签,刷新编辑器
|
if (tabId === 'body') {
|
setTimeout(() => {
|
elements.bodyEditor.refresh();
|
}, 0);
|
}
|
});
|
});
|
}
|
|
function setupResponseTabs() {
|
const responseTabs = document.querySelectorAll('.response-tab');
|
responseTabs.forEach(tab => {
|
tab.addEventListener('click', () => {
|
responseTabs.forEach(t => t.classList.remove('active'));
|
tab.classList.add('active');
|
|
const tabId = tab.getAttribute('data-response-tab');
|
document.querySelectorAll('#response-panel .response-content').forEach(content => {
|
content.classList.remove('active');
|
});
|
document.getElementById(`response-${tabId}`).classList.add('active');
|
});
|
});
|
}
|
|
// 12. 编辑器初始化
|
function setupBodyEditor() {
|
var a = document.getElementById('body-editor');
|
elements.bodyEditor = CodeMirror.fromTextArea(
|
document.getElementById('body-editor'),
|
{
|
mode: 'application/json',
|
lineNumbers: true,
|
lineWrapping: true,
|
indentUnit: 2,
|
tabSize: 2,
|
theme: 'dracula',
|
autoCloseBrackets: true,
|
matchBrackets: true,
|
extraKeys: {
|
'Ctrl-Enter': sendRequest,
|
'Cmd-Enter': sendRequest
|
}
|
}
|
);
|
|
// 初始内容
|
elements.bodyEditor.setValue('{\n \n}');
|
}
|
|
function setupTestsEditor() {
|
elements.testsEditor = CodeMirror.fromTextArea(
|
document.getElementById('tests-editor'),
|
{
|
mode: 'javascript',
|
lineNumbers: true,
|
lineWrapping: true,
|
indentUnit: 2,
|
tabSize: 2,
|
theme: 'dracula',
|
autoCloseBrackets: true,
|
matchBrackets: true
|
}
|
);
|
|
// 初始内容
|
elements.testsEditor.setValue('// 使用pm对象编写测试\n// 示例:\npm.expect(pm.response.status).to.be(200);');
|
}
|
|
// 13. 加载默认请求
|
function loadDefaultRequest() {
|
// 设置默认方法
|
elements.methodSelect.value = 'GET';
|
|
// 设置默认URL
|
elements.urlInput.value = '';
|
|
// 添加示例请求头
|
renderHeadersTable([
|
|
]);
|
|
// 添加示例参数
|
renderParamsTable([
|
|
]);
|
}
|
|
function renderHeadersTable(headers) {
|
const tbody = elements.headersTableBodyReq;
|
tbody.innerHTML = '';
|
|
headers.forEach(header => {
|
addTableRow(tbody, 'headers', {
|
keyClass: 'header-key',
|
valueClass: 'header-value',
|
enabledClass: 'header-enabled'
|
});
|
|
const lastRow = tbody.lastChild;
|
lastRow.querySelector('.header-key').value = header.key;
|
lastRow.querySelector('.header-value').value = header.value;
|
lastRow.querySelector('.header-enabled').checked = header.enabled;
|
});
|
}
|
|
function renderParamsTable(params) {
|
const tbody = elements.paramsTableBody;
|
tbody.innerHTML = '';
|
|
params.forEach(param => {
|
addTableRow(tbody, 'params', {
|
keyClass: 'param-key',
|
valueClass: 'param-value',
|
enabledClass: 'param-enabled'
|
});
|
|
const lastRow = tbody.lastChild;
|
lastRow.querySelector('.param-key').value = param.key;
|
lastRow.querySelector('.param-value').value = param.value;
|
lastRow.querySelector('.param-enabled').checked = param.enabled;
|
});
|
|
// 确保至少有一行
|
if (tbody.children.length === 0) {
|
addTableRow(tbody, 'params', {
|
keyClass: 'param-key',
|
valueClass: 'param-value',
|
enabledClass: 'param-enabled'
|
});
|
}
|
}
|
|
// 14. 请求状态管理
|
function updateUIForRequestStart() {
|
// 禁用发送按钮
|
elements.sendBtn.disabled = true;
|
elements.sendBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 请求中...';
|
|
// 添加加载动画
|
document.querySelector('.request-panel').classList.add('request-loading');
|
|
// 清空之前的响应
|
elements.responseBodyContent.innerHTML = '';
|
elements.headersTableBody.innerHTML = '';
|
elements.cookiesTableBody.innerHTML = '';
|
elements.testResults.innerHTML = '';
|
elements.statusCode.textContent = '--';
|
elements.responseTime.textContent = '--';
|
elements.responseSize.textContent = '--';
|
}
|
|
function updateUIForRequestEnd() {
|
// 恢复发送按钮
|
elements.sendBtn.disabled = false;
|
elements.sendBtn.innerHTML = '<i class="fas fa-paper-plane"></i> 发送';
|
|
// 移除加载动画
|
document.querySelector('.request-panel').classList.remove('request-loading');
|
}
|
|
// 15. 错误处理
|
function handleRequestError(error) {
|
console.error('请求失败:', error);
|
|
// 更新UI显示错误
|
elements.statusCode.className = 'status-code error';
|
elements.statusCode.textContent = 'Error';
|
elements.responseTime.textContent = '--';
|
elements.responseSize.textContent = '--';
|
|
// 显示错误信息
|
elements.responseBodyContent.innerHTML = `
|
<div class="error-message">
|
<i class="fas fa-exclamation-triangle"></i>
|
<div>
|
<h4>请求失败</h4>
|
<p>${error.message || '未知错误'}</p>
|
</div>
|
</div>
|
`;
|
|
// 显示在测试结果区域
|
elements.testResults.innerHTML = `
|
<div class="test-error">
|
<i class="fas fa-exclamation-circle"></i>
|
Request failed: ${error.message}
|
</div>
|
`;
|
}
|
|
// 16. 辅助函数
|
function syntaxHighlight(json) {
|
if (typeof json !== 'string') {
|
json = JSON.stringify(json, null, 2);
|
}
|
|
json = json.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
return json.replace(
|
/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g,
|
function (match) {
|
let cls = 'number';
|
if (/^"/.test(match)) {
|
if (/:$/.test(match)) {
|
cls = 'key';
|
} else {
|
cls = 'string';
|
}
|
} else if (/true|false/.test(match)) {
|
cls = 'boolean';
|
} else if (/null/.test(match)) {
|
cls = 'null';
|
}
|
return '<span class="' + cls + '">' + match + '</span>';
|
}
|
);
|
}
|
|
// 17. 快捷键支持
|
function setupKeyboardShortcuts() {
|
// Ctrl+Enter 发送请求
|
document.addEventListener('keydown', (e) => {
|
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
|
e.preventDefault();
|
sendRequest();
|
}
|
});
|
|
// URL输入框回车发送请求
|
elements.urlInput.addEventListener('keydown', (e) => {
|
if (e.key === 'Enter') {
|
sendRequest();
|
}
|
});
|
}
|
|
// 18. 初始化完成
|
console.log('HTTP请求工具已初始化');
|
});
|
|
</script>
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/codemirror.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/mode/javascript/javascript.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/mode/xml/xml.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/addon/edit/matchbrackets.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/addon/edit/closebrackets.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/addon/selection/active-line.min.js"></script>
|
|
</body>
|
|
</html>
|