ApiCraft-Web
项目介绍
ApiCraft-Web 是一个轻量级的 API 测试工具,提供了简洁直观的界面,帮助开发者快速测试和调试 HTTP 接口。
功能特点
- 支持多种 HTTP 请求方法(GET、POST、PUT、DELETE)
- 可配置请求参数(Query Parameters)
- 可配置请求头(Headers)
- JSON 格式的请求体编辑器,支持语法高亮和格式化
- 美观的响应数据展示
- 显示响应状态码、响应时间和数据大小
- 跨域支持
技术栈
前端
- Vue 3
- TailwindCSS
- CodeMirror 6
- Axios
后端
- Spring Boot 2.3.4
- RestTemplate
- Lombok
快速开始
环境要求
- Node.js 14+
- JDK 8+
- Maven 3+
安装和运行
- 克隆项目
git clone https://gitee.com/anxwefndu/ApiCraft-Web.git
cd ApiCraft-Web
- 启动后端服务
cd SpringBoot
mvn spring-boot:run
- 启动前端服务
cd code
npm install
npm run serve
- 访问应用
打开浏览器访问:http://localhost:8081
使用说明
-
发送请求
- 选择请求方法(GET、POST、PUT、DELETE)
- 输入目标 URL
- 根据需要添加查询参数、请求头和请求体
- 点击"发送请求"按钮
-
查看响应
- 响应状态码
- 响应时间
- 数据大小
- 格式化的响应数据
开发计划
- 支持更多请求方法
- 请求历史记录
- 接口集合管理
- 环境变量配置
- 响应数据导出
- 暗色主题支持
源码下载
ApiCraft-Web
演示截图
1.系统首页
2.接口请求
核心源码
code/src/App.vue
<script setup>
import {ref, reactive} from 'vue';
import axios from 'axios';
import Message from '@/utils/message';
import JsonEditor from '@/components/JsonEditor.vue';// 请求方法
const method = ref('GET');// 请求URL
const url = ref('');// 当前选中的参数类型标签
const activeTab = ref('params'); // params, headers, body// 请求参数
const requestData = reactive({params: [{ key: '', value: '' }],headers: [{ key: 'Content-Type', value: 'application/json' },],body: ''
});// 响应数据
const response = reactive({status: '',time: '',size: '',data: null,loading: false
});// 添加参数
const addParam = () => {requestData.params.push({ key: '', value: '' });
};// 删除参数
const removeParam = (index) => {requestData.params.splice(index, 1);
};// 添加请求头
const addHeader = () => {requestData.headers.push({ key: '', value: '' });
};// 删除请求头
const removeHeader = (index) => {requestData.headers.splice(index, 1);
};// 切换参数类型标签
const switchTab = (tab) => {activeTab.value = tab;
};const jsonEditor = ref();
const hasJsonError = ref(false);const formatJsonBody = () => {jsonEditor.value?.formatJson();
};const handleJsonError = (error) => {hasJsonError.value = !!error;
};// 发送请求
const sendRequest = async () => {if (!url.value) {Message.warning('请输入请求URL');return;}if (hasJsonError.value) {Message.error('请求体 JSON 格式错误');return;}response.loading = true;try {// 构建请求参数const queryParams = {};const headers = {};requestData.params.forEach(param => {if (param.key && param.value) {queryParams[param.key] = param.value;}});requestData.headers.forEach(header => {if (header.key && header.value) {headers[header.key] = header.value;}});const requestBody = activeTab.value === 'body' ? requestData.body : null;// 发送请求到后端代理const result = await axios.post('http://localhost:8080/api/proxy', {url: url.value,method: method.value,headers: headers,queryParams: queryParams,body: requestBody});// 更新响应数据response.status = `${result.data.status} ${result.data.status === 200 ? 'OK' : ''}`;response.time = `${result.data.responseTime}ms`;response.size = result.data.contentLength;response.data = result.data.data;Message.success('请求成功');} catch (error) {Message.error(error.message || '请求失败');response.status = '500 Error';response.data = error.message;} finally {response.loading = false;}
};// 添加格式化响应数据的函数
const formatResponseData = (data) => {if (typeof data === 'string') {try {// 尝试解析字符串为 JSONreturn JSON.stringify(JSON.parse(data), null, 2);} catch {// 如果不是 JSON 字符串,直接返回原始字符串,去掉多余的引号return data.replace(/^"|"$/g, '');}}// 如果是对象,格式化为 JSONreturn JSON.stringify(data, null, 2);
};
</script><template><div class="bg-gray-50" style="width: 100%; height: 100%"><div class="w-[1200px] mx-auto"><nav class="h-16 bg-white shadow flex items-center justify-between px-8"><div class="flex items-center space-x-2"><span class="text-2xl font-['Pacifico'] text-primary">logo</span><span class="text-lg font-medium">API工具</span></div><button class="w-10 h-10 rounded-button flex items-center justify-center hover:bg-gray-100 transition-colors"><i class="fas fa-sun text-gray-600"></i></button></nav><main class="py-12"><div class="mx-auto"><div class="bg-white rounded-lg shadow p-8"><h2 class="text-lg font-semibold mb-4">接口测试</h2><div class="space-y-4"><div class="grid grid-cols-1 md:grid-cols-4 gap-4"><div><label class="block text-sm font-medium text-gray-700 mb-1">请求方法</label><div class="relative"><select v-model="method"class="block w-full pl-3 pr-10 py-2 text-base border border-gray-300 focus:outline-none focus:ring-primary focus:border-primary rounded-button"><option>GET</option><option>POST</option><option>PUT</option><option>DELETE</option></select></div></div><div class="md:col-span-3"><label class="block text-sm font-medium text-gray-700 mb-1">请求URL</label><div class="flex"><input type="text" v-model="url"class="flex-1 min-w-0 block w-full px-3 py-2 rounded-l-button border border-gray-300 focus:outline-none focus:ring-primary focus:border-primary"><buttonclass="bg-primary hover:bg-blue-600 text-white px-4 py-2 rounded-r-button text-sm font-medium" @click="sendRequest">发送</button></div></div></div><div><label class="block text-sm font-medium text-gray-700 mb-1">请求参数</label><div class="overflow-hidden border border-gray-300 rounded-button"><div class="bg-gray-50 px-4 py-2 border-b border-gray-300"><div class="flex items-center space-x-4"><button class="text-sm font-medium text-gray-500" :class="activeTab === 'params' ? ' text-primary ' : ''" @click="switchTab('params')">Query Params</button><button class="text-sm font-medium text-gray-500" :class="activeTab === 'headers' ? ' text-primary ' : ''" @click="switchTab('headers')">Headers</button><button class="text-sm font-medium text-gray-500" :class="activeTab === 'body' ? ' text-primary ' : ''" @click="switchTab('body')">Body</button></div></div><div class="bg-white"><div class="space-y-3 p-4" v-show="activeTab === 'params'"><template v-for="(param, index) in requestData.params" :key="index"><div class="grid grid-cols-12 gap-4 items-center"><div class="col-span-3"><input type="text" v-model="param.key" placeholder="参数名"class="block w-full px-3 py-2 border border-gray-300 rounded-button focus:outline-none focus:ring-primary focus:border-primary"></div><div class="col-span-8"><input type="text" v-model="param.value" placeholder="参数值"class="block w-full px-3 py-2 border border-gray-300 rounded-button focus:outline-none focus:ring-primary focus:border-primary"></div><div class="col-span-1"><button class="text-gray-500 hover:text-gray-700" @click="removeParam(index)"><i class="fas fa-trash fa-icon"></i></button></div></div></template><button class="text-sm text-primary hover:text-blue-600 flex items-center space-x-1"><i class="fas fa-plus fa-icon"></i><span @click="addParam">添加参数</span></button></div><div class="space-y-3 p-4" v-show="activeTab === 'headers'"><template v-for="(header, index) in requestData.headers" :key="index"><div class="grid grid-cols-12 gap-4 items-center"><div class="col-span-3"><input type="text" v-model="header.key" placeholder="参数名"class="block w-full px-3 py-2 border border-gray-300 rounded-button focus:outline-none focus:ring-primary focus:border-primary"></div><div class="col-span-8"><input type="text" v-model="header.value" placeholder="参数值"class="block w-full px-3 py-2 border border-gray-300 rounded-button focus:outline-none focus:ring-primary focus:border-primary"></div><div class="col-span-1"><button class="text-gray-500 hover:text-gray-700" @click="removeHeader(index)"><i class="fas fa-trash fa-icon"></i></button></div></div></template><button class="text-sm text-primary hover:text-blue-600 flex items-center space-x-1"><i class="fas fa-plus fa-icon"></i><span @click="addHeader">添加请求头</span></button></div><div class="space-y-3" v-if="activeTab === 'body'"><JsonEditorv-model="requestData.body"height="500px"ref="jsonEditor"@error="handleJsonError"/><div class="flex justify-end space-x-2" style="margin-bottom: 0.75rem; margin-right: 0.75rem"><button@click="formatJsonBody"class="text-sm text-primary hover:text-blue-600 flex items-center space-x-1"><i class="fas fa-code fa-icon"></i><span>格式化</span></button></div></div></div></div></div><div><label class="block text-sm font-medium text-gray-700 mb-1">响应结果</label><div v-if="response.data" class="border border-gray-300 rounded-button overflow-hidden"><div class="bg-gray-50 px-4 py-2 border-b border-gray-300 flex items-center justify-between"><div class="flex items-center space-x-2"><span class="text-sm font-medium">状态: {{ response.status }}</span><span class="text-sm text-gray-500">时间: {{ response.time }}</span><span class="text-sm text-gray-500">大小: {{ response.size }}</span></div><button @click="sendRequest" class="text-sm text-primary hover:text-blue-600"><i class="fas fa-redo fa-icon mr-1"></i><span>重新请求</span></button></div><div class="bg-white p-4"><pre class="whitespace-pre-wrap">{{ formatResponseData(response.data) }}</pre></div></div></div></div></div><div class="mt-12 bg-white rounded-lg shadow p-8"><h2 class="text-xl font-medium mb-6">使用说明</h2><div class="space-y-4 text-gray-600"><div class="flex items-start space-x-3"><div class="w-6 h-6 rounded-full bg-primary/10 flex items-center justify-center flex-shrink-0 mt-0.5"><i class="fas fa-check text-sm text-primary"></i></div><p>在请求 URL 输入框中输入完整的 API 地址,选择对应的请求方法(GET、POST、PUT、DELETE)</p></div><div class="flex items-start space-x-3"><div class="w-6 h-6 rounded-full bg-primary/10 flex items-center justify-center flex-shrink-0 mt-0.5"><i class="fas fa-check text-sm text-primary"></i></div><p>在请求参数区域可以设置 Query 参数、Headers 以及请求体(Body),支持多个参数的添加和删除</p></div><div class="flex items-start space-x-3"><div class="w-6 h-6 rounded-full bg-primary/10 flex items-center justify-center flex-shrink-0 mt-0.5"><i class="fas fa-check text-sm text-primary"></i></div><p>发送请求后,可以在响应结果区域查看状态码、响应时间、数据大小等信息,支持重新发送请求</p></div><div class="flex items-start space-x-3"><div class="w-6 h-6 rounded-full bg-primary/10 flex items-center justify-center flex-shrink-0 mt-0.5"><i class="fas fa-check text-sm text-primary"></i></div><p>响应数据会以格式化的 JSON 形式展示,方便查看和分析接口返回的数据结构</p></div></div></div></div></main></div></div>
</template><style>
body {min-height: 100vh;
}#app {min-height: 100vh;
}body::-webkit-scrollbar {width: 15px;
}body::-webkit-scrollbar-track {background: #f1f5f9;border-radius: 8px;
}body::-webkit-scrollbar-thumb {background: #6366f1;border-radius: 8px;border: 2px solid #f1f5f9;
}body::-webkit-scrollbar-thumb:hover {background: #4f46e5;
}
</style>
SpringBoot/src/main/java/com/boot/service/ApiService.java
package com.boot.service;import com.boot.model.ApiRequest;
import com.boot.model.ApiResponse;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;import java.util.Map;@Service
public class ApiService {private final RestTemplate restTemplate;public ApiService() {this.restTemplate = new RestTemplate();}public ApiResponse executeRequest(ApiRequest request) {long startTime = System.currentTimeMillis();ApiResponse response = new ApiResponse();try {// 构建请求头HttpHeaders headers = new HttpHeaders();if (request.getHeaders() != null) {request.getHeaders().forEach(headers::add);}// 构建URL(包含查询参数)UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(request.getUrl());if (request.getQueryParams() != null) {for (Map.Entry<String, String> entry : request.getQueryParams().entrySet()) {builder.queryParam(entry.getKey(), entry.getValue());}}// 构建请求实体HttpEntity<?> httpEntity = new HttpEntity<>(request.getBody(), headers);// 不要对 URL 组件进行编码String finalUrl = builder.build(false).toUri().toString();// 修改执行请求部分,使用 String.class 接收响应ResponseEntity<String> responseEntity = restTemplate.exchange(finalUrl,HttpMethod.valueOf(request.getMethod().toUpperCase()),httpEntity,String.class);// 设置响应信息response.setStatus(responseEntity.getStatusCodeValue());// 尝试将响应转换为 JSON 对象String responseBody = responseEntity.getBody();try {ObjectMapper mapper = new ObjectMapper();Object jsonData = mapper.readValue(responseBody, Object.class);response.setData(jsonData);} catch (JsonProcessingException e) {// 如果不是 JSON 格式,直接返回字符串response.setData(responseBody);}response.setResponseTime(System.currentTimeMillis() - startTime);response.setContentLength(responseEntity.getHeaders().getContentLength() != -1? responseEntity.getHeaders().getContentLength() + " bytes": "unknown");} catch (Exception e) {e.printStackTrace();response.setStatus(500);String message = e.getMessage();if (message.contains("[")) {response.setData(message.substring(message.indexOf("[")));}response.setResponseTime(System.currentTimeMillis() - startTime);}return response;}
}