/** * API Tester - 前端应用主逻辑 * @author huazm */ // ==================== 粒子背景 ==================== class ParticleBackground { constructor(canvas) { this.canvas = canvas; this.ctx = canvas.getContext('2d'); this.particles = []; this.particleCount = 80; this.resize(); this.init(); this.animate(); window.addEventListener('resize', () => this.resize()); } resize() { this.canvas.width = window.innerWidth; this.canvas.height = window.innerHeight; } init() { this.particles = []; for (let i = 0; i < this.particleCount; i++) { this.particles.push({ x: Math.random() * this.canvas.width, y: Math.random() * this.canvas.height, vx: (Math.random() - 0.5) * 0.5, vy: (Math.random() - 0.5) * 0.5, size: Math.random() * 2 + 1, opacity: Math.random() * 0.5 + 0.2 }); } } animate() { this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); // 获取当前主题色 const style = getComputedStyle(document.documentElement); const accentColor = style.getPropertyValue('--accent-primary').trim() || '#3b82f6'; this.particles.forEach((p, i) => { // 更新位置 p.x += p.vx; p.y += p.vy; // 边界检测 if (p.x < 0 || p.x > this.canvas.width) p.vx *= -1; if (p.y < 0 || p.y > this.canvas.height) p.vy *= -1; // 绘制粒子 this.ctx.beginPath(); this.ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2); this.ctx.fillStyle = accentColor.replace(')', `, ${p.opacity})`).replace('rgb', 'rgba'); this.ctx.fill(); // 连接附近粒子 for (let j = i + 1; j < this.particles.length; j++) { const p2 = this.particles[j]; const dx = p.x - p2.x; const dy = p.y - p2.y; const dist = Math.sqrt(dx * dx + dy * dy); if (dist < 150) { this.ctx.beginPath(); this.ctx.moveTo(p.x, p.y); this.ctx.lineTo(p2.x, p2.y); this.ctx.strokeStyle = accentColor.replace(')', `, ${0.1 * (1 - dist / 150)})`).replace('rgb', 'rgba'); this.ctx.stroke(); } } }); requestAnimationFrame(() => this.animate()); } } // ==================== 主题管理 ==================== const themes = ['default', 'nebula', 'matrix', 'cyber', 'frost']; let currentThemeIndex = 0; function toggleTheme() { currentThemeIndex = (currentThemeIndex + 1) % themes.length; const theme = themes[currentThemeIndex]; if (theme === 'default') { document.documentElement.removeAttribute('data-theme'); } else { document.documentElement.setAttribute('data-theme', theme); } localStorage.setItem('api-tester-theme', theme); } function loadTheme() { const saved = localStorage.getItem('api-tester-theme'); if (saved && themes.includes(saved)) { currentThemeIndex = themes.indexOf(saved); if (saved !== 'default') { document.documentElement.setAttribute('data-theme', saved); } } } // ==================== API 请求 ==================== async function sendRequest() { const sendBtn = document.getElementById('send-btn'); const method = document.getElementById('method-select').value; const url = document.getElementById('url-input').value.trim(); if (!url) { showToast('请输入 URL', 'error'); return; } // 收集请求数据 const headers = collectKeyValues('headers-editor'); const params = collectKeyValues('params-editor'); const bodyType = document.querySelector('input[name="bodyType"]:checked')?.value || 'none'; const body = bodyType !== 'none' ? document.getElementById('body-editor').value : null; const authType = document.getElementById('auth-type').value; const authConfig = collectAuthConfig(authType); // 显示加载状态 sendBtn.classList.add('loading'); try { const response = await fetch('/api/request', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ method, url, headers, params, body, bodyType: bodyType !== 'none' ? bodyType : null, authType: authType !== 'none' ? authType : null, authConfig }) }); const result = await response.json(); displayResponse(result); } catch (error) { displayResponse({ success: false, error: error.message }); } finally { sendBtn.classList.remove('loading'); } } // ==================== 辅助函数 ==================== function collectKeyValues(editorId) { const editor = document.getElementById(editorId); const rows = editor.querySelectorAll('.kv-row'); const result = {}; rows.forEach(row => { const key = row.querySelector('.kv-key')?.value.trim(); const value = row.querySelector('.kv-value')?.value.trim(); if (key) result[key] = value || ''; }); return Object.keys(result).length > 0 ? result : null; } function collectAuthConfig(authType) { if (authType === 'none') return null; const config = {}; const container = document.getElementById('auth-config'); const inputs = container.querySelectorAll('input'); inputs.forEach(input => { if (input.dataset.key) { config[input.dataset.key] = input.value; } }); return config; } function displayResponse(result) { const statusCode = document.getElementById('status-code'); const statusText = document.getElementById('status-text'); const responseTime = document.getElementById('response-time'); const responseSize = document.getElementById('response-size'); const responseBody = document.getElementById('response-body'); const responseHeaders = document.getElementById('response-headers-view'); if (result.success) { const code = result.statusCode; statusCode.textContent = code; statusCode.className = 'status-code ' + (code < 300 ? 'success' : code < 400 ? 'redirect' : 'error'); statusText.textContent = getStatusText(code); responseTime.textContent = `${result.duration}ms`; responseSize.textContent = formatBytes(result.size || 0); // 格式化响应体 let body = result.body; try { const parsed = JSON.parse(body); body = JSON.stringify(parsed, null, 2); } catch {} responseBody.querySelector('code').textContent = body; // 响应头 if (result.headers) { responseHeaders.querySelector('code').textContent = JSON.stringify(result.headers, null, 2); } } else { statusCode.textContent = 'ERR'; statusCode.className = 'status-code error'; statusText.textContent = result.error || '请求失败'; responseTime.textContent = result.duration ? `${result.duration}ms` : '--'; responseSize.textContent = '--'; responseBody.querySelector('code').textContent = result.error || '请求失败'; } } function getStatusText(code) { const statusTexts = { 200: 'OK', 201: 'Created', 204: 'No Content', 301: 'Moved Permanently', 302: 'Found', 304: 'Not Modified', 400: 'Bad Request', 401: 'Unauthorized', 403: 'Forbidden', 404: 'Not Found', 405: 'Method Not Allowed', 500: 'Internal Server Error', 502: 'Bad Gateway', 503: 'Service Unavailable' }; return statusTexts[code] || 'Unknown'; } 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]; } function showToast(message, type = 'info') { const toast = document.createElement('div'); toast.className = `toast toast-${type}`; toast.textContent = message; toast.style.cssText = ` position: fixed; top: 20px; right: 20px; padding: 12px 24px; background: var(--bg-tertiary); border: 1px solid var(--${type === 'error' ? 'error' : 'accent-primary'}); color: var(--text-primary); border-radius: 8px; z-index: 9999; animation: slideIn 0.3s ease; `; document.body.appendChild(toast); setTimeout(() => toast.remove(), 3000); } // ==================== 认证配置 UI ==================== function updateAuthUI(authType) { const container = document.getElementById('auth-config'); container.innerHTML = ''; const configs = { bearer: [{ key: 'token', label: 'Token', type: 'password' }], basic: [ { key: 'username', label: '用户名', type: 'text' }, { key: 'password', label: '密码', type: 'password' } ], apikey: [ { key: 'key', label: 'Key Name', type: 'text' }, { key: 'value', label: 'Key Value', type: 'password' }, { key: 'location', label: '位置 (header/query)', type: 'text', default: 'header' } ] }; const fields = configs[authType] || []; fields.forEach(field => { const group = document.createElement('div'); group.className = 'auth-input-group'; group.innerHTML = ` `; container.appendChild(group); }); } // ==================== Key-Value 编辑器 ==================== function addKVRow(editorId) { const editor = document.getElementById(editorId); const row = document.createElement('div'); row.className = 'kv-row'; row.innerHTML = ` `; editor.appendChild(row); row.querySelector('.kv-remove').addEventListener('click', () => row.remove()); } // ==================== 标签页切换 ==================== function setupTabs() { // 请求配置标签页 document.querySelectorAll('.request-tabs .tab-btn').forEach(btn => { btn.addEventListener('click', () => { document.querySelectorAll('.request-tabs .tab-btn').forEach(b => b.classList.remove('active')); document.querySelectorAll('.request-content .tab-content').forEach(c => c.classList.remove('active')); btn.classList.add('active'); document.getElementById(`${btn.dataset.tab}-content`).classList.add('active'); }); }); // 响应标签页 document.querySelectorAll('.response-tabs .tab-btn').forEach(btn => { btn.addEventListener('click', () => { document.querySelectorAll('.response-tabs .tab-btn').forEach(b => b.classList.remove('active')); btn.classList.add('active'); const isBody = btn.dataset.tab === 'response-body'; document.getElementById('response-body').classList.toggle('hidden', !isBody); document.getElementById('response-headers-view').classList.toggle('hidden', isBody); }); }); // 主导航标签页 document.querySelectorAll('.header-nav .nav-btn').forEach(btn => { btn.addEventListener('click', () => { document.querySelectorAll('.header-nav .nav-btn').forEach(b => b.classList.remove('active')); btn.classList.add('active'); // 切换面板显示 const tab = btn.dataset.tab; document.getElementById('request-panel').classList.toggle('hidden', tab !== 'request'); document.getElementById('response-panel').classList.toggle('hidden', tab !== 'request'); document.getElementById('history-panel').classList.toggle('hidden', tab !== 'history'); if (tab === 'history') loadHistory(); }); }); } // ==================== 历史记录 ==================== async function loadHistory() { const list = document.getElementById('history-list'); list.innerHTML = '
加载中...
'; try { const response = await fetch('/api/history?size=50'); const result = await response.json(); if (result.success && result.data.length > 0) { list.innerHTML = result.data.map(item => `
${item.method} ${item.url} ${item.statusCode || '--'} ${new Date(item.createdAt).toLocaleString()}
`).join(''); } else { list.innerHTML = '
暂无历史记录
'; } } catch (error) { list.innerHTML = '
加载失败
'; } } async function loadHistoryItem(id) { try { const response = await fetch(`/api/history?size=1000`); const result = await response.json(); const item = result.data.find(h => h.id === id); if (item) { document.getElementById('method-select').value = item.method; document.getElementById('url-input').value = item.url; // 切换回请求面板 document.querySelectorAll('.header-nav .nav-btn').forEach(b => b.classList.remove('active')); document.querySelector('.header-nav .nav-btn[data-tab="request"]').classList.add('active'); document.getElementById('request-panel').classList.remove('hidden'); document.getElementById('response-panel').classList.remove('hidden'); document.getElementById('history-panel').classList.add('hidden'); showToast('已加载历史记录', 'info'); } } catch (error) { showToast('加载失败', 'error'); } } // ==================== 语音输入 ==================== function setupVoiceInput() { const voiceBtn = document.getElementById('voice-input'); if (!('webkitSpeechRecognition' in window) && !('SpeechRecognition' in window)) { voiceBtn.style.display = 'none'; return; } const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; const recognition = new SpeechRecognition(); recognition.lang = 'zh-CN'; recognition.continuous = false; let isRecording = false; voiceBtn.addEventListener('click', () => { if (isRecording) { recognition.stop(); voiceBtn.classList.remove('recording'); isRecording = false; } else { recognition.start(); voiceBtn.classList.add('recording'); isRecording = true; } }); recognition.onresult = (event) => { const text = event.results[0][0].transcript; const urlInput = document.getElementById('url-input'); urlInput.value = text; voiceBtn.classList.remove('recording'); isRecording = false; showToast('语音识别成功', 'info'); }; recognition.onerror = () => { voiceBtn.classList.remove('recording'); isRecording = false; showToast('语音识别失败', 'error'); }; } // ==================== JSON 格式化 ==================== function setupFormatButton() { document.getElementById('format-json-btn').addEventListener('click', () => { const editor = document.getElementById('body-editor'); try { const parsed = JSON.parse(editor.value); editor.value = JSON.stringify(parsed, null, 2); showToast('格式化成功', 'info'); } catch { showToast('JSON 格式错误', 'error'); } }); } // ==================== 预设按钮 ==================== function setupPresets() { document.querySelectorAll('.preset-btn').forEach(btn => { btn.addEventListener('click', () => { const key = btn.dataset.key; const value = btn.dataset.value; // 添加到 headers 编辑器 const editor = document.getElementById('headers-editor'); const rows = editor.querySelectorAll('.kv-row'); // 检查是否已存在该 key let existingRow = null; for (const row of rows) { const keyInput = row.querySelector('.kv-key'); if (keyInput.value === key) { existingRow = row; break; } } if (existingRow) { // 更新现有行 const valueInput = existingRow.querySelector('.kv-value'); valueInput.value = value; valueInput.focus(); // 如果是鉴权类型,光标定位到值末尾方便输入 token if (btn.classList.contains('auth-preset')) { valueInput.setSelectionRange(value.length, value.length); } } else { // 添加新行 addKVRow('headers-editor'); const newRow = editor.lastElementChild; newRow.querySelector('.kv-key').value = key; const valueInput = newRow.querySelector('.kv-value'); valueInput.value = value; // 如果是鉴权或空值类型,自动聚焦到值输入框 if (btn.classList.contains('auth-preset') || value === '') { valueInput.focus(); valueInput.setSelectionRange(value.length, value.length); } } showToast(`已添加 ${key}`, 'info'); }); }); } // ==================== 初始化 ==================== document.addEventListener('DOMContentLoaded', () => { // 初始化粒子背景 new ParticleBackground(document.getElementById('particles-canvas')); // 加载主题 loadTheme(); // 设置事件监听 document.getElementById('theme-toggle').addEventListener('click', toggleTheme); document.getElementById('send-btn').addEventListener('click', sendRequest); document.getElementById('auth-type').addEventListener('change', (e) => updateAuthUI(e.target.value)); // 添加行按钮 document.querySelectorAll('.add-row-btn').forEach(btn => { btn.addEventListener('click', () => addKVRow(btn.dataset.target)); }); // 删除行按钮 document.querySelectorAll('.kv-remove').forEach(btn => { btn.addEventListener('click', () => btn.parentElement.remove()); }); // 清空历史 document.getElementById('clear-history')?.addEventListener('click', async () => { if (confirm('确定要清空所有历史记录吗?')) { await fetch('/api/history/clear', { method: 'DELETE' }); loadHistory(); } }); // 设置标签页 setupTabs(); setupVoiceInput(); setupFormatButton(); setupPresets(); // 键盘快捷键 document.addEventListener('keydown', (e) => { if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) { sendRequest(); } }); });