This commit is contained in:
Wyle.Gong-巩文昕
2025-04-23 11:21:08 +08:00
parent fc643727f3
commit 7a1aae1e2f
135 changed files with 221483 additions and 1 deletions
+666
View File
@@ -0,0 +1,666 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Stage管理</title>
<link rel="stylesheet" href="/ui/assets/common.css" />
<style>
body {
font-family: "Microsoft YaHei", sans-serif;
padding: 20px;
font-size: 12px;
margin: 0;
background-color: #f5f5f5;
}
.container {
max-width: 1200px;
margin: 0 auto;
background-color: #fff;
padding: 20px;
border-radius: 5px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.title {
font-size: 24px;
font-weight: bold;
}
.btn {
padding: 8px 16px;
background-color: #1890ff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.3s;
font-size: 12px;
}
.btn:hover {
background-color: #40a9ff;
}
.btn-danger {
background-color: #ff4d4f;
}
.btn-danger:hover {
background-color: #ff7875;
}
.breadcrumb {
margin-bottom: 20px;
font-size: 14px;
color: #666;
}
.breadcrumb a {
color: #1890ff;
text-decoration: none;
}
.breadcrumb a:hover {
text-decoration: underline;
}
.table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
.table th,
.table td {
padding: 12px 15px;
text-align: left;
border-bottom: 1px solid #e8e8e8;
}
.table th {
background-color: #fafafa;
font-weight: 500;
}
.table tr:hover {
background-color: #f5f5f5;
}
.action-cell {
display: flex;
gap: 8px;
}
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
z-index: 1000;
justify-content: center;
align-items: center;
}
.modal-content {
background-color: white;
padding: 24px;
border-radius: 4px;
width: 400px;
max-width: 90%;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.modal-title {
font-size: 18px;
font-weight: bold;
}
.close {
font-size: 24px;
cursor: pointer;
}
.form-group {
margin-bottom: 16px;
}
.form-label {
display: block;
margin-bottom: 8px;
font-weight: 500;
}
.form-input {
width: 100%;
padding: 8px 12px;
border: 1px solid #d9d9d9;
border-radius: 4px;
box-sizing: border-box;
}
.form-actions {
display: flex;
justify-content: flex-end;
gap: 8px;
margin-top: 24px;
}
.loading {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(255, 255, 255, 0.7);
display: flex;
justify-content: center;
align-items: center;
z-index: 2000;
}
.spinner {
border: 4px solid #f3f3f3;
border-top: 4px solid #1890ff;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.empty-state {
text-align: center;
padding: 40px;
color: #999;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<!-- <h1 class="title">Stage管理</h1> -->
<div class="breadcrumb">
<a href="/project_list">返回 Project 管理</a> / Stage管理
</div>
<button class="btn" id="add-stage-btn">添加Stage</button>
</div>
<div id="stage-list">
<table class="table">
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>关联树节点</th>
<!-- <th>描述</th> -->
<th>操作</th>
</tr>
</thead>
<tbody id="stage-table-body">
<!-- 数据将通过JavaScript动态加载 -->
</tbody>
</table>
<div id="empty-state" class="empty-state" style="display: none">
<p>暂无Stage数据,请添加新Stage</p>
</div>
</div>
</div>
<!-- 添加/编辑Stage的模态框 -->
<div class="modal" id="stage-modal">
<div class="modal-content">
<div class="modal-header">
<h2 class="modal-title" id="modal-title">添加Stage</h2>
<span class="close" id="close-modal">&times;</span>
</div>
<form id="stage-form">
<input type="hidden" id="stage-id" value="" />
<div class="form-group">
<label class="form-label" for="stage-name">名称</label>
<input
type="text"
class="form-input"
id="stage-name"
value=""
required
/>
</div>
<div class="form-group">
<label class="form-label" for="stage-tree-id">关联树节点</label>
<select class="form-input" id="stage-tree-id" required>
<option value="">请选择树节点</option>
<!-- 树节点选项将通过JavaScript动态加载 -->
</select>
</div>
<div class="form-actions">
<button type="button" class="btn" id="cancel-btn">取消</button>
<button type="submit" class="btn" id="save-btn">保存</button>
</div>
</form>
</div>
</div>
<!-- 确认删除的模态框 -->
<div class="modal" id="confirm-modal">
<div class="modal-content">
<div class="modal-header">
<h2 class="modal-title">确认删除</h2>
<span class="close" id="close-confirm-modal">&times;</span>
</div>
<p>
确定要删除此Stage吗?此操作将同时删除该Stage下的所有图形节点和连接,且不可恢复。
</p>
<div class="form-actions">
<button type="button" class="btn" id="cancel-delete-btn">取消</button>
<button type="button" class="btn btn-danger" id="confirm-delete-btn">
删除
</button>
</div>
</div>
</div>
<!-- 加载指示器 -->
<div class="loading" id="loading" style="display: none">
<div class="spinner"></div>
</div>
<script>
// 基础URL
const API_BASE_URL = "http://127.0.0.1:5002/api";
// DOM元素
const stageTableBody = document.getElementById("stage-table-body");
const emptyState = document.getElementById("empty-state");
const addStageBtn = document.getElementById("add-stage-btn");
const stageModal = document.getElementById("stage-modal");
const modalTitle = document.getElementById("modal-title");
const closeModalBtn = document.getElementById("close-modal");
const stageForm = document.getElementById("stage-form");
const stageIdInput = document.getElementById("stage-id");
const stageNameInput = document.getElementById("stage-name");
const stageTreeIdSelect = document.getElementById("stage-tree-id");
const cancelBtn = document.getElementById("cancel-btn");
const confirmModal = document.getElementById("confirm-modal");
const closeConfirmModal = document.getElementById("close-confirm-modal");
const cancelDeleteBtn = document.getElementById("cancel-delete-btn");
const confirmDeleteBtn = document.getElementById("confirm-delete-btn");
const loading = document.getElementById("loading");
const urlParams = new URLSearchParams(window.location.search);
// 当前要删除的StageID
let currentDeleteId = null;
// 存储树节点数据
let treeNodes = [];
let projectId = urlParams.get("project_id");
// 页面加载时获取Stage列表和树节点列表
function init() {
fetchTreeNodes();
fetchStages();
}
init();
// 添加Stage按钮点击事件
addStageBtn.addEventListener("click", () => {
modalTitle.textContent = "添加Stage";
stageForm.reset();
stageIdInput.value = "";
openModal(stageModal);
});
// 关闭模态框
closeModalBtn.addEventListener("click", () => closeModal(stageModal));
cancelBtn.addEventListener("click", () => closeModal(stageModal));
// 关闭确认删除模态框
closeConfirmModal.addEventListener("click", () =>
closeModal(confirmModal)
);
cancelDeleteBtn.addEventListener("click", () => closeModal(confirmModal));
// 确认删除
confirmDeleteBtn.addEventListener("click", deleteStage);
// 表单提交
stageForm.addEventListener("submit", handleFormSubmit);
// 获取所有Stage
async function fetchStages() {
showLoading();
try {
const response = await fetch(
`${API_BASE_URL}/project/stage/${projectId}/`
);
if (!response.ok) {
throw new Error(`HTTP error ${response.status}`);
}
const result = await response.json();
if (result.code === 0 && Array.isArray(result.data)) {
renderStageList(result.data);
} else {
showError("获取Stage列表失败: " + (result.message || "未知错误"));
}
} catch (error) {
console.error("获取Stage列表错误:", error);
showError("获取Stage列表错误: " + error.message);
} finally {
hideLoading();
}
}
async function fetchTreeNodes() {
showLoading();
try {
const response = await fetch(
`${API_BASE_URL}/project/demand/${projectId}`
);
if (!response.ok) {
throw new Error(`HTTP error ${response.status}`);
}
console.log("fetch nodes", response);
const result = await response.json();
if (result.code === 0 && Array.isArray(result.data)) {
treeNodes = result.data;
populateTreeSelect();
} else {
showError("获取树节点列表失败: " + (result.message || "未知错误"));
}
} catch (error) {
console.error("获取树节点列表错误:", error);
showError("获取树节点列表错误: " + error.message);
} finally {
hideLoading();
}
}
// 加载Stage数据用于编辑
// 填充树节点选择框
function populateTreeSelect() {
stageTreeIdSelect.innerHTML = '<option value="">请选择树节点</option>';
// 添加虚拟根节点选项
/* const rootOption = document.createElement("option");
rootOption.value = "root"; // 使用特殊值"root"表示虚拟根节点
rootOption.textContent = "所有节点 (虚拟根节点)";
stageTreeIdSelect.appendChild(rootOption);
*/
treeNodes.forEach((node) => {
const option = document.createElement("option");
option.value = node.id;
option.textContent = `${node.name} (Level: ${node.level})`;
stageTreeIdSelect.appendChild(option);
});
}
function renderStageList(stages) {
stageTableBody.innerHTML = "";
if (stages.length === 0) {
emptyState.style.display = "block";
return;
}
emptyState.style.display = "none";
stages.forEach((stage) => {
const row = document.createElement("tr");
// 截断ID以便显示
const shortId = stage.id.substring(0, 8) + "...";
// 查找关联的树节点名称
let treeNodeName = "未关联";
if (stage.demand_id) {
if (stage.demand_id === "root") {
treeNodeName = "所有节点 (虚拟根节点)";
} else {
console.log("demand_id: ", stage.demand_id);
console.log(treeNodes);
const treeNode = treeNodes.find(
(node) => node.id === stage.demand_id
);
if (treeNode) {
treeNodeName = `${treeNode.name} (Level: ${treeNode.level})`;
} else {
treeNodeName = `ID: ${stage.demand_id.substring(0, 8)}...`;
}
}
}
row.innerHTML = `
<td title="${stage.id}">${shortId}</td>
<td>${stage.name || "未命名"}</td>
<td>${treeNodeName}</td>
<td class="action-cell">
<button class="btn" data-id="${
stage.id
}" data-action="view-graph">查看Graph</button>
<button class="btn" data-id="${
stage.id
}" data-action="view-endpoint">查看Endpoint</button>
<button class="btn" data-id="${
stage.id
}" data-action="edit">编辑</button>
<button class="btn btn-danger" data-id="${
stage.id
}" data-action="delete">删除</button>
</td>
`;
stageTableBody.appendChild(row);
});
// 添加按钮事件监听
document.querySelectorAll("[data-action]").forEach((button) => {
button.addEventListener("click", handleActionClick);
});
}
// 处理操作按钮点击
async function handleActionClick(event) {
console.log("action click");
const action = event.target.getAttribute("data-action");
const stageId = event.target.getAttribute("data-id");
switch (action) {
case "view-graph":
// 跳转到图形页面
window.location.href = `/project/stage/stageGraph?project_id=${projectId}&stage_id=${stageId}`;
break;
case "view-endpoint":
// 跳转到Endpoint管理页面
window.location.href = `/project/stage/endpoint_list?project_id=${projectId}&stage_id=${stageId}`;
break;
case "edit":
await loadStageForEdit(stageId);
break;
case "delete":
openDeleteConfirmation(stageId);
break;
}
}
// 加载Stage数据用于编辑
async function loadStageForEdit(stageId) {
showLoading();
try {
const response = await fetch(
`${API_BASE_URL}/project/stage/${project_id}/${stageId}/`
);
if (!response.ok) {
throw new Error(`HTTP error ${response.status}`);
}
console.log("get");
const result = await response.json();
if (result.code === 0 && result.data) {
const stage = result.data.stage;
// 填充表单
console.log(stageIdInput);
stageIdInput.value = stage.id;
stageNameInput.value = stage.name || "";
stageTreeIdSelect.value = stage.demand_id || "";
//stageLevelInput.value = stage.level || 0;
//stageDescriptionInput.innerHTML = stage.description || "";
// 更新模态框标题
modalTitle.textContent = "编辑Stage";
// 打开模态框
openModal(stageModal);
} else {
showError("获取Stage详情失败: " + (result.message || "未知错误"));
}
} catch (error) {
console.error("获取Stage详情错误:", error);
showError("获取Stage详情错误: " + error.message);
} finally {
hideLoading();
}
}
async function handleFormSubmit(event) {
event.preventDefault();
const stageId = stageIdInput.value;
const isEdit = !!stageId;
const stageData = {
name: stageNameInput.value,
project_id: projectId,
demand_id: stageTreeIdSelect.value,
};
showLoading();
try {
const url = isEdit
? `${API_BASE_URL}/project/stage/${projectId}/${stageId}/`
: `${API_BASE_URL}/project/stage/`;
const method = isEdit ? "PATCH" : "POST";
const response = await fetch(url, {
method: method,
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(stageData),
});
if (!response.ok) {
throw new Error(`HTTP error ${response.status}`);
}
const result = await response.json();
if (result.code === 0) {
showMessage(isEdit ? "Stage更新成功" : "Stage创建成功");
closeModal(stageModal);
fetchStages(); // 刷新列表
} else {
showError(
(isEdit ? "更新" : "创建") +
"Stage失败: " +
(result.message || "未知错误")
);
}
} catch (error) {
console.error(isEdit ? "更新Stage错误:" : "创建Stage错误:", error);
showError((isEdit ? "更新" : "创建") + "Stage错误: " + error.message);
} finally {
hideLoading();
}
}
// 打开删除确认对话框
function openDeleteConfirmation(stageId) {
currentDeleteId = stageId;
openModal(confirmModal);
}
// 删除Stage
async function deleteStage() {
if (!currentDeleteId) return;
showLoading();
try {
const response = await fetch(
`${API_BASE_URL}/project/stage/${project_id}/${currentDeleteId}/`,
{
method: "DELETE",
}
);
if (!response.ok) {
throw new Error(`HTTP error ${response.status}`);
}
const result = await response.json();
if (result.code === 0) {
showMessage("Stage删除成功");
closeModal(confirmModal);
fetchStages(); // 刷新列表
} else {
showError("删除Stage失败: " + (result.message || "未知错误"));
}
} catch (error) {
console.error("删除Stage错误:", error);
showError("删除Stage错误: " + error.message);
} finally {
hideLoading();
currentDeleteId = null;
}
}
// 工具函数:打开模态框
function openModal(modal) {
modal.style.display = "flex";
}
// 工具函数:关闭模态框
function closeModal(modal) {
modal.style.display = "none";
}
// 工具函数:显示加载中
function showLoading() {
loading.style.display = "flex";
}
// 工具函数:隐藏加载中
function hideLoading() {
loading.style.display = "none";
}
// 工具函数:显示消息
function showMessage(message) {
alert(message); // 简单实现,可以替换为更友好的通知
}
// 工具函数:显示错误
function showError(message) {
alert("错误: " + message); // 简单实现,可以替换为更友好的通知
}
</script>
</body>
</html>
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+726
View File
@@ -0,0 +1,726 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Endpoint管理</title>
<link rel="stylesheet" href="/assets/common.css" />
<style>
body {
font-family: "Microsoft YaHei", sans-serif;
padding: 20px;
margin: 0;
background-color: #f5f5f5;
}
.container {
max-width: 1200px;
margin: 0 auto;
background-color: #fff;
padding: 20px;
border-radius: 5px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.title {
font-size: 24px;
font-weight: bold;
}
.btn {
padding: 8px 16px;
background-color: #1890ff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.3s;
}
.btn:hover {
background-color: #40a9ff;
}
.btn-danger {
background-color: #ff4d4f;
}
.btn-danger:hover {
background-color: #ff7875;
}
.btn-warning {
background-color: #fa8c16;
}
.btn-warning:hover {
background-color: #ffa940;
}
.btn-back {
margin-right: 10px;
}
.table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
.table th,
.table td {
padding: 12px 15px;
text-align: left;
border-bottom: 1px solid #e8e8e8;
}
.table th {
background-color: #fafafa;
font-weight: 500;
}
.table tr:hover {
background-color: #f5f5f5;
}
.action-cell {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
z-index: 1000;
justify-content: center;
align-items: center;
}
.modal-content {
background-color: white;
padding: 24px;
border-radius: 4px;
width: 500px;
max-width: 90%;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.modal-title {
font-size: 18px;
font-weight: bold;
}
.close {
font-size: 24px;
cursor: pointer;
}
.form-group {
margin-bottom: 16px;
}
.form-label {
display: block;
margin-bottom: 8px;
font-weight: 500;
}
.form-input,
.form-select,
.form-textarea {
width: 100%;
padding: 8px 12px;
border: 1px solid #d9d9d9;
border-radius: 4px;
box-sizing: border-box;
}
.form-textarea {
resize: vertical;
min-height: 80px;
}
.form-actions {
display: flex;
justify-content: flex-end;
gap: 8px;
margin-top: 24px;
}
.notification {
padding: 12px 15px;
margin-bottom: 20px;
border-radius: 4px;
display: none;
}
.success {
background-color: #f6ffed;
border: 1px solid #b7eb8f;
color: #52c41a;
}
.error {
background-color: #fff2f0;
border: 1px solid #ffccc7;
color: #ff4d4f;
}
.loading {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(255, 255, 255, 0.7);
display: flex;
justify-content: center;
align-items: center;
z-index: 2000;
}
.spinner {
border: 4px solid #f3f3f3;
border-top: 4px solid #1890ff;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.empty-state {
text-align: center;
padding: 40px;
color: #999;
}
.breadcrumb {
margin-bottom: 20px;
font-size: 14px;
color: #666;
}
.breadcrumb a {
color: #1890ff;
text-decoration: none;
}
.breadcrumb a:hover {
text-decoration: underline;
}
</style>
</head>
<body>
<div class="container">
<div class="breadcrumb">
<a href="javascript:void(0);" onclick="history.back()"
>返回 Stage 管理</a
>
/ Endpoint管理
</div>
<div class="header">
<h1 class="title">Endpoint管理</h1>
<div>
<button class="btn" id="createBtn">添加 Endpoint</button>
</div>
</div>
<div id="notification" class="notification"></div>
<div id="loading" class="loading" style="display: none">
<div class="spinner"></div>
</div>
<div id="endpointsTable" class="table-container">
<table class="table">
<thead>
<tr>
<th>ID</th>
<th>名称</th>
<th>URL</th>
<th>请求方法</th>
<th>操作</th>
</tr>
</thead>
<tbody id="endpointsList"></tbody>
</table>
</div>
<div id="emptyState" class="empty-state" style="display: none">
<p>暂无 Endpoint 数据,请添加新 Endpoint</p>
</div>
<!-- 创建/编辑 Modal -->
<div id="endpointModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2 class="modal-title" id="modalTitle">添加 Endpoint</h2>
<span class="close" id="closeModal">&times;</span>
</div>
<form id="endpointForm">
<input type="hidden" id="endpointId" />
<div class="form-group">
<label class="form-label" for="name">名称:</label>
<input type="text" class="form-input" id="name" required />
</div>
<div class="form-group">
<label class="form-label" for="url">URL:</label>
<input type="text" class="form-input" id="url" required />
</div>
<div class="form-group">
<label class="form-label" for="method">请求方法:</label>
<select class="form-select" id="method" required>
<option value="GET">GET</option>
<option value="POST">POST</option>
<option value="PUT">PUT</option>
<option value="PATCH">PATCH</option>
<option value="DELETE">DELETE</option>
</select>
</div>
<div class="form-group">
<label class="form-label" for="description">描述:</label>
<textarea
class="form-textarea"
id="description"
rows="3"
></textarea>
</div>
<div class="form-actions">
<button type="button" class="btn" id="cancelBtn">取消</button>
<button type="submit" class="btn" id="saveBtn">保存</button>
</div>
</form>
</div>
</div>
<!-- 查看详情 Modal -->
<div id="viewModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2 class="modal-title">Endpoint详情</h2>
<span class="close" id="closeViewModal">&times;</span>
</div>
<div id="endpointDetails"></div>
<div class="form-actions">
<button type="button" class="btn" id="closeViewBtn">关闭</button>
</div>
</div>
</div>
<!-- 确认删除 Modal -->
<div id="confirmModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2 class="modal-title">确认删除</h2>
<span class="close" id="closeConfirmModal">&times;</span>
</div>
<p>确定要删除此Endpoint吗?此操作不可恢复。</p>
<div class="form-actions">
<button type="button" class="btn" id="cancelDeleteBtn">取消</button>
<button type="button" class="btn btn-danger" id="confirmDeleteBtn">
删除
</button>
</div>
</div>
</div>
</div>
<script>
// 基础API URL
const API_BASE_URL =
"http://127.0.0.1:5002/api/project/stage/tools/endpoint";
// 从URL获取参数
const urlParams = new URLSearchParams(window.location.search);
const projectId = urlParams.get("project_id");
const stageId = urlParams.get("stage_id");
// DOM 元素
const endpointsList = document.getElementById("endpointsList");
const emptyState = document.getElementById("emptyState");
const endpointsTable = document.getElementById("endpointsTable");
const createBtn = document.getElementById("createBtn");
const notification = document.getElementById("notification");
const loading = document.getElementById("loading");
// Modal 元素
const endpointModal = document.getElementById("endpointModal");
const viewModal = document.getElementById("viewModal");
const confirmModal = document.getElementById("confirmModal");
const modalTitle = document.getElementById("modalTitle");
const endpointForm = document.getElementById("endpointForm");
const endpointIdInput = document.getElementById("endpointId");
const nameInput = document.getElementById("name");
const urlInput = document.getElementById("url");
const methodInput = document.getElementById("method");
const descriptionInput = document.getElementById("description");
const endpointDetails = document.getElementById("endpointDetails");
// 按钮元素
const closeModal = document.getElementById("closeModal");
const closeViewModal = document.getElementById("closeViewModal");
const closeConfirmModal = document.getElementById("closeConfirmModal");
const cancelBtn = document.getElementById("cancelBtn");
const closeViewBtn = document.getElementById("closeViewBtn");
const cancelDeleteBtn = document.getElementById("cancelDeleteBtn");
const confirmDeleteBtn = document.getElementById("confirmDeleteBtn");
// 当前要删除的ID
let currentDeleteId = null;
// 页面加载时检查参数并获取数据
if (!projectId || !stageId) {
showNotification(
"缺少必要的项目ID或Stage ID,请返回Stage管理页面重试",
true
);
return;
}
fetchEndpoints();
// 添加按钮事件监听
createBtn.addEventListener("click", showCreateModal);
endpointForm.addEventListener("submit", handleFormSubmit);
// 关闭Modal的事件
closeModal.addEventListener("click", () => closeModalFunc(endpointModal));
closeViewModal.addEventListener("click", () => closeModalFunc(viewModal));
closeConfirmModal.addEventListener("click", () =>
closeModalFunc(confirmModal)
);
cancelBtn.addEventListener("click", () => closeModalFunc(endpointModal));
closeViewBtn.addEventListener("click", () => closeModalFunc(viewModal));
cancelDeleteBtn.addEventListener("click", () =>
closeModalFunc(confirmModal)
);
confirmDeleteBtn.addEventListener("click", deleteEndpoint);
// 获取Endpoints列表
async function fetchEndpoints() {
showLoading();
try {
const response = await fetch(
`${API_BASE_URL}/list/${projectId}/${stageId}/`
);
if (!response.ok) {
throw new Error(`Error: ${response.status}`);
}
const data = await response.json();
if (data.length === 0) {
endpointsTable.style.display = "none";
emptyState.style.display = "block";
} else {
renderEndpointsList(data.data);
endpointsTable.style.display = "block";
emptyState.style.display = "none";
}
} catch (error) {
showNotification(`获取Endpoint列表失败: ${error.message}`, true);
} finally {
hideLoading();
}
}
// 渲染Endpoints列表
function renderEndpointsList(endpoints) {
endpointsList.innerHTML = "";
endpoints.forEach((endpoint) => {
// 截取ID以便显示
const shortId = endpoint.id
? endpoint.id.substring(0, 8) + "..."
: "-";
const row = document.createElement("tr");
row.innerHTML = `
<td title="${endpoint.id}">${shortId}</td>
<td>${endpoint.name || "-"}</td>
<td>${endpoint.path || "-"}</td>
<td>${endpoint.method || "-"}</td>
<td class="action-cell">
<button class="btn" data-id="${
endpoint.id
}" data-action="view">查看</button>
<button class="btn" data-id="${
endpoint.id
}" data-action="edit">编辑</button>
<button class="btn btn-warning" data-id="${
endpoint.id
}" data-action="debug"
data-url="${endpoint.path || ""}" data-method="${
endpoint.method || "GET"
}">调试</button>
<button class="btn btn-danger" data-id="${
endpoint.id
}" data-action="delete">删除</button>
</td>
`;
endpointsList.appendChild(row);
});
// 为按钮添加事件
document.querySelectorAll("[data-action]").forEach((button) => {
button.addEventListener("click", handleActionClick);
});
}
// 处理操作按钮点击
async function handleActionClick(event) {
const action = event.target.getAttribute("data-action");
const id = event.target.getAttribute("data-id");
switch (action) {
case "view":
await viewEndpoint(id);
break;
case "edit":
await showEditModal(id);
break;
case "delete":
showDeleteConfirmation(id);
break;
case "debug":
const url = event.target.getAttribute("data-url");
const method = event.target.getAttribute("data-method");
goToDebugger(id, url, method);
break;
}
}
// 跳转到调试页面
function goToDebugger(id, url, method) {
window.location.href = `/project/stage/utils/endpoint_debug?project_id=${projectId}&stage_id=${stageId}&endpoint_id=${id}`;
}
// 显示创建Modal
function showCreateModal() {
endpointForm.reset();
endpointIdInput.value = "";
modalTitle.textContent = "添加 Endpoint";
openModalFunc(endpointModal);
}
// 显示编辑Modal
async function showEditModal(id) {
showLoading();
try {
const response = await fetch(`${API_BASE_URL}/${id}/`);
if (!response.ok) {
throw new Error(`Error: ${response.status}`);
}
const endpointT = await response.json();
const endpoint = endpointT.data;
endpointIdInput.value = endpoint.id;
nameInput.value = endpoint.name || "";
urlInput.value = endpoint.path || "";
methodInput.value = endpoint.method || "GET";
descriptionInput.value = endpoint.description || "";
modalTitle.textContent = "编辑 Endpoint";
openModalFunc(endpointModal);
} catch (error) {
showNotification(`获取Endpoint详情失败: ${error.message}`, true);
} finally {
hideLoading();
}
}
// 查看Endpoint详情
async function viewEndpoint(id) {
showLoading();
try {
const response = await fetch(`${API_BASE_URL}/${id}/`);
if (!response.ok) {
throw new Error(`Error: ${response.status}`);
}
const endpointT = await response.json();
const endpoint = endpointT.data;
// 格式化日期
const createdDate = endpoint.created_at
? new Date(endpoint.created_at).toLocaleString()
: "-";
const updatedDate = endpoint.updated_at
? new Date(endpoint.updated_at).toLocaleString()
: "-";
endpointDetails.innerHTML = `
<div class="form-group">
<label class="form-label">ID:</label>
<p>${endpoint.id}</p>
</div>
<div class="form-group">
<label class="form-label">名称:</label>
<p>${endpoint.name || "-"}</p>
</div>
<div class="form-group">
<label class="form-label">URL:</label>
<p>${endpoint.path || "-"}</p>
</div>
<div class="form-group">
<label class="form-label">请求方法:</label>
<p>${endpoint.method || "-"}</p>
</div>
<div class="form-group">
<label class="form-label">项目ID:</label>
<p>${endpoint.project_id}</p>
</div>
<div class="form-group">
<label class="form-label">Stage ID:</label>
<p>${endpoint.stage_id}</p>
</div>
<div class="form-group">
<label class="form-label">描述:</label>
<p>${endpoint.description || "-"}</p>
</div>
<div class="form-group">
<label class="form-label">创建时间:</label>
<p>${createdDate}</p>
</div>
<div class="form-group">
<label class="form-label">更新时间:</label>
<p>${updatedDate}</p>
</div>
`;
openModalFunc(viewModal);
} catch (error) {
showNotification(`获取Endpoint详情失败: ${error.message}`, true);
} finally {
hideLoading();
}
}
// 处理表单提交
async function handleFormSubmit(event) {
event.preventDefault();
const id = endpointIdInput.value;
const isUpdate = !!id;
const endpointData = {
name: nameInput.value.trim(),
url: urlInput.value.trim(),
method: methodInput.value,
project_id: projectId,
stage_id: stageId,
description: descriptionInput.value.trim(),
};
showLoading();
try {
const url = isUpdate ? `${API_BASE_URL}/${id}/` : API_BASE_URL;
const method = isUpdate ? "PATCH" : "POST";
const response = await fetch(url, {
method: method,
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(endpointData),
});
if (!response.ok) {
throw new Error(`Error: ${response.status}`);
}
closeModalFunc(endpointModal);
showNotification(`Endpoint ${isUpdate ? "更新" : "创建"}成功!`);
fetchEndpoints();
} catch (error) {
showNotification(
`${isUpdate ? "更新" : "创建"} Endpoint失败: ${error.message}`,
true
);
} finally {
hideLoading();
}
}
// 显示删除确认
function showDeleteConfirmation(id) {
currentDeleteId = id;
openModalFunc(confirmModal);
}
// 删除Endpoint
async function deleteEndpoint() {
if (!currentDeleteId) return;
showLoading();
try {
const response = await fetch(`${API_BASE_URL}/${currentDeleteId}/`, {
method: "DELETE",
});
if (!response.ok) {
throw new Error(`Error: ${response.status}`);
}
closeModalFunc(confirmModal);
showNotification("Endpoint删除成功!");
fetchEndpoints();
} catch (error) {
showNotification(`删除Endpoint失败: ${error.message}`, true);
} finally {
hideLoading();
currentDeleteId = null;
}
}
// 显示Modal
function openModalFunc(modal) {
modal.style.display = "flex";
}
// 关闭Modal
function closeModalFunc(modal) {
modal.style.display = "none";
}
// 显示通知
function showNotification(message, isError = false) {
notification.textContent = message;
notification.className = isError
? "notification error"
: "notification success";
notification.style.display = "block";
setTimeout(() => {
notification.style.display = "none";
}, 5000);
}
// 显示加载
function showLoading() {
loading.style.display = "flex";
}
// 隐藏加载
function hideLoading() {
loading.style.display = "none";
}
// 关闭Modal当点击外部
window.addEventListener("click", (event) => {
if (event.target === endpointModal) {
closeModalFunc(endpointModal);
}
if (event.target === viewModal) {
closeModalFunc(viewModal);
}
if (event.target === confirmModal) {
closeModalFunc(confirmModal);
}
});
</script>
</body>
</html>
File diff suppressed because it is too large Load Diff
+664
View File
@@ -0,0 +1,664 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>API Debugger</title>
<!-- Include Tailwind CSS via CDN -->
<!-- <script src="https://cdn.tailwindcss.com"></script> -->
</head>
<body class="bg-gray-100 text-gray-800">
<div id="notifications" class="fixed top-5 right-5 z-50"></div>
<div class="container mx-auto p-4 max-w-6xl">
<header
class="flex justify-between items-center py-4 mb-6 border-b border-gray-200"
>
<div class="flex items-center">
<span class="text-2xl mr-2">⚙️</span>
<h1 class="text-2xl font-bold text-blue-600">API Debugger</h1>
</div>
</header>
<div class="bg-white rounded-lg shadow-md p-5 mb-6">
<div class="flex flex-col md:flex-row mb-4">
<select
id="method-select"
class="md:w-32 w-full p-2.5 border border-gray-300 rounded-t md:rounded-tr-none md:rounded-l text-blue-600 font-bold mb-2 md:mb-0"
>
<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"
id="url-input"
placeholder="Enter request URL"
class="flex-grow p-2.5 border border-gray-300 md:rounded-none border-y md:border-l-0 md:border-r-0 mb-2 md:mb-0 outline-none"
/>
<button
id="send-btn"
class="p-2.5 bg-blue-600 text-white font-medium rounded-b md:rounded-bl-none md:rounded-r hover:bg-blue-700 transition"
>
Send
</button>
</div>
<!-- Headers Section -->
<div class="mb-4">
<div
class="bg-gray-100 p-3 flex justify-between items-center cursor-pointer rounded-t"
id="headers-accordion-header"
>
<span class="font-semibold">Headers</span>
<span></span>
</div>
<div
class="border border-gray-200 border-t-0 p-4 rounded-b"
id="headers-accordion-content"
>
<div id="headers-container">
<div class="flex justify-between items-center mb-3">
<h3 class="font-semibold text-lg">Request Headers</h3>
<button
id="add-header-btn"
class="px-3 py-1 bg-green-500 text-white rounded hover:bg-green-600 transition"
>
Add Header
</button>
</div>
<div class="header-row flex mb-2">
<input
type="text"
class="p-2 border border-gray-300 rounded-l flex-1"
placeholder="Header name"
value="Content-Type"
/>
<input
type="text"
class="p-2 border border-gray-300 border-l-0 flex-1"
placeholder="Header value"
value="application/json"
/>
<button
class="p-2 bg-red-500 text-white rounded-r hover:bg-red-600 transition"
>
×
</button>
</div>
</div>
</div>
</div>
<!-- Parameters Section -->
<div class="mb-4">
<div
class="bg-gray-100 p-3 flex justify-between items-center cursor-pointer rounded-t"
id="params-accordion-header"
>
<span class="font-semibold">Query Parameters</span>
<span></span>
</div>
<div
class="border border-gray-200 border-t-0 p-4 rounded-b hidden"
id="params-accordion-content"
>
<div id="params-container">
<div class="flex justify-between items-center mb-3">
<h3 class="font-semibold text-lg">Query Parameters</h3>
<button
id="add-param-btn"
class="px-3 py-1 bg-green-500 text-white rounded hover:bg-green-600 transition"
>
Add Parameter
</button>
</div>
<!-- Parameters will be added here -->
</div>
</div>
</div>
<!-- Request Body Section -->
<div class="mb-4">
<div
class="bg-gray-100 p-3 flex justify-between items-center cursor-pointer rounded-t"
id="body-accordion-header"
>
<span class="font-semibold">Request Body</span>
<span></span>
</div>
<div
class="border border-gray-200 border-t-0 p-4 rounded-b hidden"
id="body-accordion-content"
>
<div>
<div class="flex mb-3" id="body-type-selector">
<div
class="px-3 py-1.5 border border-gray-300 rounded-l cursor-pointer body-type-option bg-blue-600 text-white"
data-type="json"
>
JSON
</div>
<div
class="px-3 py-1.5 border-t border-b border-r border-gray-300 cursor-pointer body-type-option"
data-type="form"
>
Form Data
</div>
<div
class="px-3 py-1.5 border-t border-b border-r border-gray-300 cursor-pointer body-type-option"
data-type="x-www-form-urlencoded"
>
x-www-form-urlencoded
</div>
<div
class="px-3 py-1.5 border-t border-b border-r border-gray-300 rounded-r cursor-pointer body-type-option"
data-type="text"
>
Text
</div>
</div>
<textarea
id="body-editor"
class="w-full h-48 p-3 border border-gray-300 rounded font-mono resize-y"
placeholder="Enter request body here"
>
{\n "key": "value"\n}</textarea
>
<div id="form-data-container" class="hidden">
<div class="flex justify-between items-center mb-3 mt-4">
<h3 class="font-semibold text-lg">Form Data</h3>
<button
id="add-form-field-btn"
class="px-3 py-1 bg-green-500 text-white rounded hover:bg-green-600 transition"
>
Add Field
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Response Section -->
<div class="bg-white rounded-lg shadow-md p-5">
<div class="flex justify-between items-center mb-4">
<h3 class="font-semibold text-lg">Response</h3>
<div id="response-time" class="text-gray-600"></div>
</div>
<div
class="flex justify-between items-center mb-4 pb-3 border-b border-gray-200"
>
<div id="response-status" class="font-bold">No request sent yet</div>
<div id="response-size" class="text-gray-600"></div>
</div>
<pre
id="response-body"
class="bg-gray-800 text-white p-4 rounded overflow-auto max-h-96 font-mono"
>
Send a request to see the response</pre
>
<div class="flex justify-end mt-4 space-x-2">
<button
id="copy-response-btn"
class="px-3 py-1.5 bg-gray-700 text-white rounded hover:bg-gray-600 transition flex items-center"
>
📋 Copy
</button>
</div>
</div>
</div>
<script>
// DOM Elements
const methodSelect = document.getElementById("method-select");
const urlInput = document.getElementById("url-input");
const sendBtn = document.getElementById("send-btn");
const addHeaderBtn = document.getElementById("add-header-btn");
const headersContainer = document.getElementById("headers-container");
const addParamBtn = document.getElementById("add-param-btn");
const paramsContainer = document.getElementById("params-container");
const bodyTypeOptions = document.querySelectorAll(".body-type-option");
const bodyEditor = document.getElementById("body-editor");
const formDataContainer = document.getElementById("form-data-container");
const addFormFieldBtn = document.getElementById("add-form-field-btn");
const responseStatus = document.getElementById("response-status");
const responseTime = document.getElementById("response-time");
const responseSize = document.getElementById("response-size");
const responseBody = document.getElementById("response-body");
const copyResponseBtn = document.getElementById("copy-response-btn");
const notificationsContainer = document.getElementById("notifications");
// Accordion elements
const accordionHeaders = document.querySelectorAll(
'[id$="accordion-header"]'
);
const accordionContents = document.querySelectorAll(
'[id$="accordion-content"]'
);
// State management
let currentBodyType = "json";
let requestStartTime = 0;
let latestResponse = null;
// Accordion functionality
accordionHeaders.forEach((header) => {
header.addEventListener("click", () => {
const contentId = header.id.replace("-header", "-content");
const content = document.getElementById(contentId);
content.classList.toggle("hidden");
// Update the arrow icon
const arrow = header.querySelector("span:last-child");
if (!content.classList.contains("hidden")) {
arrow.textContent = "▼";
} else {
arrow.textContent = "▶";
}
});
});
// Request Headers
addHeaderBtn.addEventListener("click", addNewHeader);
// Add initial header row functionality
document
.querySelector(".header-row button")
.addEventListener("click", function () {
this.closest(".header-row").remove();
});
// Query Parameters
addParamBtn.addEventListener("click", addNewParam);
// Request Body Type Selection
bodyTypeOptions.forEach((option) => {
option.addEventListener("click", () => {
bodyTypeOptions.forEach((opt) => {
opt.classList.remove("bg-blue-600", "text-white");
});
option.classList.add("bg-blue-600", "text-white");
currentBodyType = option.getAttribute("data-type");
if (currentBodyType === "form") {
bodyEditor.classList.add("hidden");
formDataContainer.classList.remove("hidden");
} else {
bodyEditor.classList.remove("hidden");
formDataContainer.classList.add("hidden");
// Set appropriate placeholder based on body type
switch (currentBodyType) {
case "json":
bodyEditor.placeholder = "Enter JSON body here";
bodyEditor.value =
bodyEditor.value || '{\n "key": "value"\n}';
break;
case "x-www-form-urlencoded":
bodyEditor.placeholder = "key1=value1&key2=value2";
bodyEditor.value = bodyEditor.value || "key=value";
break;
case "text":
bodyEditor.placeholder = "Enter plain text body here";
bodyEditor.value = bodyEditor.value || "";
break;
}
}
});
});
// Form Data Fields
addFormFieldBtn.addEventListener("click", addNewFormField);
// Send Request
sendBtn.addEventListener("click", sendRequest);
// Response Actions
copyResponseBtn.addEventListener("click", copyResponseToClipboard);
/**
* Add a new header row to the headers container
*/
function addNewHeader() {
const headerRow = document.createElement("div");
headerRow.className = "header-row flex mb-2";
headerRow.innerHTML = `
<input type="text" class="p-2 border border-gray-300 rounded-l flex-1" placeholder="Header name">
<input type="text" class="p-2 border border-gray-300 border-l-0 flex-1" placeholder="Header value">
<button class="p-2 bg-red-500 text-white rounded-r hover:bg-red-600 transition">×</button>
`;
headersContainer.appendChild(headerRow);
// Add event listener to the remove button
headerRow
.querySelector("button")
.addEventListener("click", function () {
this.closest(".header-row").remove();
});
}
/**
* Add a new query parameter row
*/
function addNewParam() {
const paramRow = document.createElement("div");
paramRow.className = "param-row flex mb-2";
paramRow.innerHTML = `
<input type="text" class="p-2 border border-gray-300 rounded-l flex-1" placeholder="Parameter name">
<input type="text" class="p-2 border border-gray-300 border-l-0 flex-1" placeholder="Parameter value">
<button class="p-2 bg-red-500 text-white rounded-r hover:bg-red-600 transition">×</button>
`;
paramsContainer.appendChild(paramRow);
// Add event listener to the remove button
paramRow.querySelector("button").addEventListener("click", function () {
this.closest(".param-row").remove();
});
}
/**
* Add a new form field row for form data body type
*/
function addNewFormField() {
const formFieldRow = document.createElement("div");
formFieldRow.className = "param-row flex mb-2";
formFieldRow.innerHTML = `
<input type="text" class="p-2 border border-gray-300 rounded-l flex-1" placeholder="Field name">
<input type="text" class="p-2 border border-gray-300 border-l-0 flex-1" placeholder="Field value">
<button class="p-2 bg-red-500 text-white rounded-r hover:bg-red-600 transition">×</button>
`;
formDataContainer.appendChild(formFieldRow);
// Add event listener to the remove button
formFieldRow
.querySelector("button")
.addEventListener("click", function () {
this.closest(".param-row").remove();
});
}
/**
* Collect headers from the UI
* @returns {Object} - Headers object
*/
function getHeaders() {
const headers = {};
const headerRows = headersContainer.querySelectorAll(".header-row");
headerRows.forEach((row) => {
const key = row.querySelector("input:nth-child(1)").value.trim();
const value = row.querySelector("input:nth-child(2)").value.trim();
if (key && value) {
headers[key] = value;
}
});
return headers;
}
/**
* Build query string from parameters
* @returns {string} - Query string
*/
function buildQueryString() {
const params = [];
const paramRows = paramsContainer.querySelectorAll(".param-row");
paramRows.forEach((row) => {
const key = row.querySelector("input:nth-child(1)").value.trim();
const value = row.querySelector("input:nth-child(2)").value.trim();
if (key && value) {
params.push(
`${encodeURIComponent(key)}=${encodeURIComponent(value)}`
);
}
});
return params.length > 0 ? `?${params.join("&")}` : "";
}
/**
* Get request body based on the selected body type
* @returns {string|FormData|null} - Request body
*/
function getRequestBody() {
if (methodSelect.value === "GET" || methodSelect.value === "HEAD") {
return null;
}
switch (currentBodyType) {
case "json":
try {
// Try to parse JSON to validate it
JSON.parse(bodyEditor.value);
return bodyEditor.value;
} catch (error) {
showNotification(`Invalid JSON: ${error.message}`, "error");
return null;
}
case "form":
const formData = new FormData();
const formFields = formDataContainer.querySelectorAll(".param-row");
formFields.forEach((field) => {
const key = field
.querySelector("input:nth-child(1)")
.value.trim();
const value = field
.querySelector("input:nth-child(2)")
.value.trim();
if (key) {
formData.append(key, value);
}
});
return formData;
case "x-www-form-urlencoded":
return bodyEditor.value;
case "text":
return bodyEditor.value;
default:
return null;
}
}
/**
* Send the API request
*/
function sendRequest() {
// Get the URL
const url = urlInput.value;
console.log(url);
if (!url) {
showNotification("Please enter a URL", "error");
return;
}
// Add query string if there are query parameters
const queryString = buildQueryString();
const fullUrl = `${url}${queryString}`;
// Get request headers
const headers = getHeaders();
// Get request body based on body type
const body = getRequestBody();
if (body === null && currentBodyType === "json") {
return; // Stop if JSON is invalid
}
console.log(fullUrl);
// Set loading state
responseBody.textContent = "Loading...";
responseStatus.textContent = "Sending request...";
responseStatus.className = "font-bold";
responseSize.textContent = "";
requestStartTime = Date.now();
// Create fetch options
const fetchOptions = {
method: methodSelect.value,
headers: headers,
};
// Add body to request if it's not GET or HEAD
if (
methodSelect.value !== "GET" &&
methodSelect.value !== "HEAD" &&
body
) {
fetchOptions.body = body;
}
// Make the fetch request
fetch(fullUrl, fetchOptions)
.then((response) => {
const endTime = Date.now();
const responseTimeMs = endTime - requestStartTime;
responseTime.textContent = `Time: ${responseTimeMs}ms`;
// Set status
responseStatus.textContent = `Status: ${response.status} ${response.statusText}`;
if (response.ok) {
responseStatus.className = "font-bold text-green-600";
} else {
responseStatus.className = "font-bold text-red-600";
}
// Get response size
const contentLength = response.headers.get("content-length");
if (contentLength) {
const size = parseInt(contentLength, 10);
responseSize.textContent = `Size: ${formatBytes(size)}`;
}
// Store the response object for later use
latestResponse = response;
// Clone the response so we can use it multiple times
return response.text().then((text) => {
try {
// Try to parse as JSON
const json = JSON.parse(text);
return JSON.stringify(json, null, 2);
} catch (e) {
// If not JSON, return as text
return text;
}
});
})
.then((responseText) => {
// Display the response
responseBody.textContent = responseText;
})
.catch((error) => {
responseStatus.textContent = `Error: ${error.message}`;
responseStatus.className = "font-bold text-red-600";
responseBody.textContent = `Failed to fetch: ${error.message}`;
responseSize.textContent = "";
showNotification(`Request failed: ${error.message}`, "error");
});
}
/**
* Format bytes to a human-readable format
* @param {number} bytes - The size in bytes
* @returns {string} - Formatted size
*/
function formatBytes(bytes) {
if (bytes === 0) return "0 Bytes";
const k = 1024;
const sizes = ["Bytes", "KB", "MB", "GB", "TB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
}
/**
* Copy response to clipboard
*/
function copyResponseToClipboard() {
if (
responseBody.textContent.trim() ===
"Send a request to see the response" ||
responseBody.textContent.trim() === "Loading..."
) {
showNotification("No response to copy", "error");
return;
}
navigator.clipboard
.writeText(responseBody.textContent)
.then(() => {
showNotification("Response copied to clipboard", "success");
})
.catch((err) => {
showNotification("Failed to copy response", "error");
});
}
/**
* Show a notification
*/
function showNotification(message, type = "success") {
const notification = document.createElement("div");
notification.className = `mb-3 p-3 rounded-lg shadow-md border-l-4 ${
type === "success"
? "bg-green-100 border-green-500 text-green-800"
: "bg-red-100 border-red-500 text-red-800"
}`;
notification.innerHTML = `
<div class="flex justify-between items-center">
<div>${message}</div>
<button class="ml-4 text-gray-600 hover:text-gray-800">×</button>
</div>
`;
// Add to notifications container
notificationsContainer.appendChild(notification);
// Add event listener to close button
notification.querySelector("button").addEventListener("click", () => {
notification.remove();
});
// Auto-remove after 5 seconds
setTimeout(() => {
if (notification.parentNode) {
notification.remove();
}
}, 5000);
}
// Set up keyboard shortcuts
document.addEventListener("keydown", (event) => {
// Ctrl+Enter to send request
if ((event.ctrlKey || event.metaKey) && event.key === "Enter") {
sendRequest();
event.preventDefault();
}
});
// Initialize
document.addEventListener("DOMContentLoaded", () => {
// Open the headers section by default
const headersContent = document.getElementById(
"headers-accordion-content"
);
headersContent.classList.remove("hidden");
});
</script>
</body>
</html>
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff