UNPKG

@ghini/kit

Version:

js practical tools to assist efficient development

654 lines (574 loc) 19.4 kB
<!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> <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>