@ghini/kit
Version:
js practical tools to assist efficient development
654 lines (574 loc) • 19.4 kB
HTML
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>API 参数调试器</title>
<style>
/* CSS 样式:保持紧凑并易读 */
html,
body {
height: 100%;
}
body {
font-family: sans-serif;
margin: 0;
background-color: #f7f7f7;
color: #333;
}
.container {
max-width: 100%;
margin: auto;
background: #fff;
padding: 10px;
border-radius: 6px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
/* 顶部控制栏 */
.controls {
display: flex;
align-items: center;
gap: 5px;
padding-bottom: 8px;
border-bottom: 1px solid #eee;
margin-bottom: 8px;
}
.controls input[type="checkbox"] {
width: 16px;
height: 16px;
cursor: pointer;
}
.controls button {
padding: 5px 10px;
border: 1px solid #ccc;
border-radius: 4px;
cursor: pointer;
background-color: #f0f0f0;
font-size: 13px;
line-height: 1.2;
}
.controls button:hover {
background-color: #e0e0e0;
}
.btn-add {
font-size: 16px;
}
.btn-merge {
font-weight: bold;
}
.btn-init {
background-color: #fff3e0;
border-color: #ffb74d;
color: #e65100;
}
.btn-init:hover {
background-color: #ffe0b2;
}
/* 参数列表行 */
#param-list .param-row {
display: flex;
align-items: center;
gap: 5px;
margin-bottom: 8px;
cursor: grab;
transition: transform 0.2s, box-shadow 0.2s;
}
#param-list .param-row:active {
cursor: grabbing;
}
#param-list .param-row.dragging {
opacity: 0.5;
transform: scale(0.98);
cursor: grabbing;
}
#param-list .param-row.drag-over-top {
box-shadow: 0 -2px 0 0 #4CAF50;
}
#param-list .param-row.drag-over-bottom {
box-shadow: 0 2px 0 0 #4CAF50;
}
#param-list .param-row:last-child {
margin-bottom: 0;
}
#param-list .param-row input[type="checkbox"] {
width: 16px;
height: 16px;
flex-shrink: 0;
}
#param-list .param-row input[type="text"] {
flex-grow: 1;
padding: 6px 8px;
border: 1px solid #ddd;
border-radius: 4px;
font-family: ui-monospace, Consolas, Menlo, Monaco, monospace;
font-size: 13px;
cursor: text;
}
/* 紧凑按钮样式 */
#param-list .param-row .btn-group {
display: flex;
gap: 2px;
}
#param-list .param-row button {
padding: 4px 6px;
border: 1px solid #ccc;
border-radius: 3px;
cursor: pointer;
font-size: 11px;
transition: all 0.2s;
}
#param-list .param-row .btn-clone {
background-color: #e3f2fd;
border-color: #90caf9;
color: #1976d2;
}
#param-list .param-row .btn-clone:hover {
background-color: #bbdefb;
}
#param-list .param-row .btn-delete {
background-color: #ffebee;
border-color: #ef9a9a;
color: #c62828;
}
#param-list .param-row .btn-delete:hover {
background-color: #ffcdd2;
}
#param-list .param-row .btn-split {
background-color: #f5f5f5;
border-color: #bdbdbd;
}
#param-list .param-row .btn-split:hover {
background-color: #e0e0e0;
}
/* 分隔线样式 */
.separator-row {
display: flex;
align-items: center;
margin: 4px 0;
height: 20px;
position: relative;
cursor: grab;
transition: transform 0.2s, box-shadow 0.2s;
}
.separator-row:active {
cursor: grabbing;
}
.separator-row::before {
content: "";
position: absolute;
left: 0;
right: 0;
top: 50%;
height: 2px;
background-color: #e0e0e0;
transform: translateY(-50%);
}
.separator-row.dragging {
opacity: 0.5;
cursor: grabbing;
}
.separator-row.drag-over-top {
box-shadow: 0 -2px 0 0 #4CAF50;
}
.separator-row.drag-over-bottom {
box-shadow: 0 2px 0 0 #4CAF50;
}
.separator-label {
background: #fff;
padding: 0 10px;
position: relative;
z-index: 1;
margin-left: 10px;
display: flex;
align-items: center;
gap: 5px;
}
.separator-label span {
color: #333;
font-size: 14px;
font-weight: 600;
letter-spacing: 0.5px;
padding: 3px 6px;
border-radius: 3px;
cursor: text;
}
.separator-label input {
border: 1px solid #ddd;
padding: 3px 6px;
font-size: 14px;
font-weight: 600;
border-radius: 3px;
outline-color: #4CAF50;
}
.separator-label button {
padding: 2px 6px;
border: 1px solid #ccc;
border-radius: 3px;
cursor: pointer;
font-size: 10px;
background-color: #f5f5f5;
opacity: 0.7;
transition: opacity 0.2s;
}
.separator-label button:hover {
background-color: #e0e0e0;
opacity: 1;
}
/* API 端点面板 */
.api-panel {
padding-bottom: 8px;
border-bottom: 1px solid #eee;
margin-bottom: 8px;
}
#api-buttons-container button {
margin: 0 5px 5px 0;
padding: 5px 10px;
border: 1px solid #90caf9;
background-color: #e3f2fd;
border-radius: 4px;
cursor: pointer;
font-size: 13px;
}
#api-buttons-container button:hover {
background-color: #bbdefb;
}
.api-method {
font-size: 0.8em;
color: #888;
margin-right: 4px;
}
</style>
</head>
<body>
<div class="container">
<div id="api-endpoints" class="api-panel">
{{api-buttons-container}}
</div>
<div class="controls">
<input type="checkbox" id="select-all" title="智能选择/取消">
<button class="btn-add" onclick="addParamRow()" title="添加新行">➕</button>
<input type="radio" name="radio_main">并列
<input type="radio" name="radio_main">融合
<button class="btn-merge" onclick="console.log(getMergedParams())">控制台输出参数</button>
<button class="btn-batch-delete" onclick="batchDelete()">批量删除选中</button>
<button class="btn-init" onclick="initializeExample()">初始化参数</button>
</div>
<div id="param-list">
</div>
</div>
<script>
const paramList = document.getElementById("param-list");
const selectAllCheckbox = document.getElementById("select-all");
const STORAGE_KEY = "apidev-" + location.host;
// ---- 拖拽功能 ----
let draggedElement = null;
function getDragAfterElement(y) {
const draggableElements = [...paramList.children].filter(child => child !== draggedElement);
return draggableElements.reduce((closest, child) => {
const box = child.getBoundingClientRect();
const offset = y - box.top - box.height / 2;
if (offset < 0 && offset > closest.offset) {
return { offset: offset, element: child };
} else {
return closest;
}
}, { offset: Number.NEGATIVE_INFINITY }).element;
}
document.body.addEventListener('dragover', e => {
e.preventDefault();
if (!draggedElement) return;
const afterElement = getDragAfterElement(e.clientY);
[...paramList.children].forEach(child => {
child.classList.remove('drag-over-top', 'drag-over-bottom');
});
if (paramList.children.length === 1 && paramList.children[0] === draggedElement) return;
if (afterElement == null) {
const lastChild = [...paramList.children].filter(c => c !== draggedElement).pop();
if (lastChild) lastChild.classList.add('drag-over-bottom');
} else {
afterElement.classList.add('drag-over-top');
}
});
document.body.addEventListener('drop', e => {
e.preventDefault();
if (!draggedElement) return;
const afterElement = getDragAfterElement(e.clientY);
if (afterElement == null) {
paramList.appendChild(draggedElement);
} else {
paramList.insertBefore(draggedElement, afterElement);
}
});
function initDragAndDrop(element) {
element.draggable = true;
const noDragSelectors = 'input, button, .separator-label';
element.addEventListener('dragstart', (e) => {
if (e.target.closest(noDragSelectors)) {
e.preventDefault();
return;
}
draggedElement = element;
setTimeout(() => element.classList.add('dragging'), 0);
e.dataTransfer.effectAllowed = 'move';
});
element.addEventListener('dragend', (e) => {
if (draggedElement) draggedElement.classList.remove('dragging');
[...paramList.children].forEach(child => {
child.classList.remove('drag-over-top', 'drag-over-bottom');
});
draggedElement = null;
saveState();
});
}
// ---- 核心逻辑 ----
function 参数化(str) {
let obj = {}
try {
return JSON.parse(str)
} catch (e) { }
if (str.includes("=")) {
new URLSearchParams(str).forEach((v, k) => { obj[k] = v; })
return obj
}
return str;
}
function getMergedParams() {
let res;
document.querySelectorAll(".param-row input[type=checkbox]:checked").forEach(checkbox => {
const row = checkbox.closest(".param-row");
const input = row.querySelector("input[type=text]");
const param = 参数化(input.value);
if (typeof res === "object") {
if (Array.isArray(res)) {
if (Array.isArray(param)) res = [...res, ...param]
} else {
if (typeof param === "object" && !Array.isArray(param)) res = { ...res, ...param }
}
} else if (typeof res === "string") {
if (typeof param === "string") res += param
}
else if (typeof res === "undefined") {
res = param
}
});
return res;
}
async function sendRequest(url, method = "POST") {
const params = getMergedParams();
method = method === '*' ? 'POST' : method.toUpperCase();
console.log(`%c${method} ${url}\n`,"color:dodgerblue", params);
const options = {
method: method,
headers: { "Content-Type": "application/json" }
};
let finalUrl = url;
if (["GET", "HEAD"].includes(method)) {
const queryString = new URLSearchParams(params).toString();
if (queryString) {
finalUrl += (finalUrl.includes("?") ? "&" : "?") + queryString;
}
} else {
options.body = JSON.stringify(params);
}
try {
const response = await fetch(finalUrl, options);
const contentType = response.headers.get("content-type");
if (contentType && contentType.indexOf("application/json") !== -1) {
const data = await response.json();
console.log(`%c${response.status} ${response.statusText}\n`,`color: ${response.ok ? "lightgreen" : "red"};`, data);
} else {
const text = await response.text();
console.log(`%c${response.status} ${response.statusText}\n`,`color: ${response.ok ? "lightgreen" : "red"};`, text);
}
} catch (error) {
console.error(`%c[DataError]`, "color: red;", error);
}
}
// ---- 分隔线功能 ----
function createSeparator(label = "分隔线") {
const separator = document.createElement("div");
separator.className = "separator-row";
const labelContainer = document.createElement("div");
labelContainer.className = "separator-label";
const labelText = document.createElement("span");
labelText.textContent = label;
const deleteBtn = document.createElement("button");
deleteBtn.textContent = "删除";
deleteBtn.style.color = "#c62828";
labelContainer.appendChild(labelText);
labelContainer.appendChild(deleteBtn);
separator.appendChild(labelContainer);
labelText.addEventListener("dblclick", () => {
// 防止重复触发
if (labelContainer.querySelector('input')) return;
let isEditing = true; // 🔥 核心修复:状态锁
const originalValue = labelText.textContent;
const input = document.createElement("input");
input.type = "text";
input.value = originalValue;
labelContainer.replaceChild(input, labelText);
input.focus();
input.select();
const finishEdit = (saveChanges = true) => {
// 如果锁已关闭,则直接退出,防止重复执行
if (!isEditing) return;
// 立即关闭锁
isEditing = false;
labelText.textContent = saveChanges ? (input.value || "分隔线") : originalValue;
labelContainer.replaceChild(labelText, input);
if (saveChanges) saveState();
};
input.addEventListener("blur", () => finishEdit(true));
input.addEventListener("keydown", (e) => {
if (e.key === "Enter") finishEdit(true);
else if (e.key === "Escape") finishEdit(false);
});
});
deleteBtn.addEventListener("click", () => {
separator.remove();
saveState();
});
initDragAndDrop(separator);
return separator;
}
// ---- 页面交互与DOM操作 ----
document.addEventListener("DOMContentLoaded", loadState);
selectAllCheckbox.addEventListener("click", toggleSelectAll);
function createParamRow(value = "", isChecked = false) {
const row = document.createElement("div");
row.className = "param-row";
row.innerHTML = `
<input type="checkbox" ${isChecked ? "checked" : ""}>
<input type="text" value="">
<div class="btn-group">
<button class="btn-clone">克隆</button>
<button class="btn-delete">删除</button>
<button class="btn-split">分隔</button>
<span>≡</span>
</div>
`;
row.querySelector("input[type=text]").value = value;
const checkbox = row.querySelector("input[type=checkbox]");
const textInput = row.querySelector("input[type=text]");
checkbox.addEventListener("change", () => {
updateSelectAllCheckboxState();
saveState();
});
textInput.addEventListener("input", saveState);
textInput.addEventListener("keydown", (event) => {
if (event.key === 'Enter') {
event.preventDefault();
checkbox.checked = !checkbox.checked;
checkbox.dispatchEvent(new Event('change'));
}
});
// 核心功能回归:动态控制可拖拽属性,确保输入框内操作不受影响
textInput.addEventListener('mouseenter', () => row.draggable = false);
textInput.addEventListener('mouseleave', () => row.draggable = true);
textInput.addEventListener('focus', () => row.draggable = false);
textInput.addEventListener('blur', () => row.draggable = true);
row.querySelector(".btn-clone").addEventListener("click", () => cloneRow(row));
row.querySelector(".btn-delete").addEventListener("click", () => {
row.remove();
saveState();
});
row.querySelector(".btn-split").addEventListener("click", () => {
const separator = createSeparator();
row.parentNode.insertBefore(separator, row);
saveState();
});
initDragAndDrop(row);
return row;
}
function addParamRow() {
paramList.appendChild(createParamRow("", true));
saveState();
}
function cloneRow(rowToClone) {
const value = rowToClone.querySelector("input[type=text]").value;
const isChecked = rowToClone.querySelector("input[type=checkbox]").checked;
const newRow = createParamRow(value, isChecked);
rowToClone.after(newRow);
saveState();
}
function batchDelete() {
const rowsToDelete = document.querySelectorAll(".param-row input[type=checkbox]:checked");
if (rowsToDelete.length > 0) {
rowsToDelete.forEach(checkbox => checkbox.closest(".param-row").remove());
saveState();
}
}
function toggleSelectAll() {
const checkedCount = document.querySelectorAll('.param-row input[type="checkbox"]:checked').length;
const shouldCheckAll = checkedCount <= 1;
document.querySelectorAll('.param-row input[type="checkbox"]').forEach(cb => cb.checked = shouldCheckAll);
saveState();
}
function updateSelectAllCheckboxState() {
const allCheckboxes = document.querySelectorAll(".param-row input[type=checkbox]");
const checkedCount = document.querySelectorAll(".param-row input[type=checkbox]:checked").length;
const totalCount = allCheckboxes.length;
if (totalCount > 0 && checkedCount === totalCount) {
selectAllCheckbox.checked = true;
selectAllCheckbox.indeterminate = false;
} else if (checkedCount > 1) {
selectAllCheckbox.checked = false;
selectAllCheckbox.indeterminate = true;
} else {
selectAllCheckbox.checked = false;
selectAllCheckbox.indeterminate = false;
}
}
// ---- 数据持久化 ----
function saveState() {
const data = [];
Array.from(paramList.children).forEach(node => {
if (node.classList && node.classList.contains("param-row")) {
data.push({
type: "param",
value: node.querySelector("input[type=text]").value,
isChecked: node.querySelector("input[type=checkbox]").checked
});
} else if (node.classList && node.classList.contains("separator-row")) {
const labelText = node.querySelector(".separator-label span");
data.push({
type: "separator",
label: labelText ? labelText.textContent : "分隔线"
});
}
});
localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
updateSelectAllCheckboxState();
}
function initializeExample() {
if (paramList.children.length > 0 && !confirm("确定要初始化为示例参数吗?当前数据将被清空。")) {
return;
}
paramList.innerHTML = "";
paramList.appendChild(createParamRow("hi=Ghini&hello=world", true));
paramList.appendChild(createParamRow("hello world", false));
paramList.appendChild(createParamRow('{"example": true, "test": {"number": [1,["b"],{"fruit":"peach"}]}}', true));
paramList.appendChild(createParamRow(`[1,"2","three"]`, true));
paramList.appendChild(createSeparator("其他参数"));
saveState();
}
function loadState() {
const savedData = localStorage.getItem(STORAGE_KEY);
if (savedData && JSON.parse(savedData).length > 0) {
const data = JSON.parse(savedData);
paramList.innerHTML = '';
data.forEach(item => {
if (item.type === "separator") {
paramList.appendChild(createSeparator(item.label));
} else {
paramList.appendChild(createParamRow(item.value || "", item.isChecked));
}
});
} else {
initializeExample();
}
updateSelectAllCheckboxState();
}
</script>
</body>
</html>