// API文档页面JavaScript let apiSpec = null; let currentToken = null; // 初始化 document.addEventListener('DOMContentLoaded', async () => { await loadToken(); await loadAPISpec(); if (apiSpec) { renderAPIDocs(); } }); // 加载token async function loadToken() { try { const authData = localStorage.getItem('cyberstrike-auth'); if (authData) { const parsed = JSON.parse(authData); if (parsed && parsed.token) { const expiry = parsed.expiresAt ? new Date(parsed.expiresAt) : null; if (!expiry || expiry.getTime() > Date.now()) { currentToken = parsed.token; return; } } } currentToken = localStorage.getItem('swagger_auth_token'); } catch (e) { console.error('加载token失败:', e); } } // 加载OpenAPI规范 async function loadAPISpec() { try { let url = '/api/openapi/spec'; if (currentToken) { url += '?token=' + encodeURIComponent(currentToken); } const response = await fetch(url); if (!response.ok) { if (response.status === 401) { showError('需要登录才能查看API文档。请先在前端页面登录,然后刷新此页面。'); return; } throw new Error('加载API规范失败: ' + response.status); } apiSpec = await response.json(); } catch (error) { console.error('加载API规范失败:', error); showError('加载API文档失败: ' + error.message); } } // 显示错误 function showError(message) { const main = document.getElementById('api-docs-main'); main.innerHTML = `

加载失败

${message}

返回首页登录
`; } // 渲染API文档 function renderAPIDocs() { if (!apiSpec || !apiSpec.paths) { showError('API规范格式错误'); return; } // 显示认证说明 renderAuthInfo(); // 渲染侧边栏分组 renderSidebar(); // 渲染API端点 renderEndpoints(); } // 渲染认证说明 function renderAuthInfo() { const authSection = document.getElementById('auth-info-section'); if (!authSection) return; // 显示认证说明部分 authSection.style.display = 'block'; // 检查是否有token const tokenStatus = document.getElementById('token-status'); if (currentToken && tokenStatus) { tokenStatus.style.display = 'block'; } else if (tokenStatus) { // 如果没有token,显示提示 tokenStatus.style.display = 'block'; tokenStatus.style.background = 'rgba(255, 152, 0, 0.1)'; tokenStatus.style.borderLeftColor = '#ff9800'; tokenStatus.innerHTML = '

⚠ 未检测到 Token - 请先在前端页面登录,然后刷新此页面。测试接口时需要在请求头中添加 Authorization: Bearer token

'; } } // 渲染侧边栏 function renderSidebar() { const groups = new Set(); Object.keys(apiSpec.paths).forEach(path => { Object.keys(apiSpec.paths[path]).forEach(method => { const endpoint = apiSpec.paths[path][method]; if (endpoint.tags && endpoint.tags.length > 0) { endpoint.tags.forEach(tag => groups.add(tag)); } }); }); const groupList = document.getElementById('api-group-list'); const allGroups = Array.from(groups).sort(); allGroups.forEach(group => { const li = document.createElement('li'); li.className = 'api-group-item'; li.innerHTML = `${group}`; groupList.appendChild(li); }); // 绑定点击事件 groupList.querySelectorAll('.api-group-link').forEach(link => { link.addEventListener('click', (e) => { e.preventDefault(); groupList.querySelectorAll('.api-group-link').forEach(l => l.classList.remove('active')); link.classList.add('active'); const group = link.dataset.group; renderEndpoints(group === 'all' ? null : group); }); }); } // 渲染API端点 function renderEndpoints(filterGroup = null) { const main = document.getElementById('api-docs-main'); main.innerHTML = ''; const endpoints = []; Object.keys(apiSpec.paths).forEach(path => { Object.keys(apiSpec.paths[path]).forEach(method => { const endpoint = apiSpec.paths[path][method]; const tags = endpoint.tags || []; if (!filterGroup || filterGroup === 'all' || tags.includes(filterGroup)) { endpoints.push({ path, method, ...endpoint }); } }); }); // 按分组排序 endpoints.sort((a, b) => { const tagA = a.tags && a.tags.length > 0 ? a.tags[0] : ''; const tagB = b.tags && b.tags.length > 0 ? b.tags[0] : ''; if (tagA !== tagB) return tagA.localeCompare(tagB); return a.path.localeCompare(b.path); }); if (endpoints.length === 0) { main.innerHTML = '

暂无API

该分组下没有API端点

'; return; } endpoints.forEach(endpoint => { main.appendChild(createEndpointCard(endpoint)); }); } // 创建API端点卡片 function createEndpointCard(endpoint) { const card = document.createElement('div'); card.className = 'api-endpoint'; const methodClass = endpoint.method.toLowerCase(); const tags = endpoint.tags || []; const tagHtml = tags.map(tag => `${tag}`).join(''); card.innerHTML = `
${endpoint.method.toUpperCase()} ${endpoint.path} ${tagHtml}
描述
${endpoint.summary || endpoint.description || '无描述'}
${renderParameters(endpoint)} ${renderRequestBody(endpoint)} ${renderResponses(endpoint)} ${renderTestSection(endpoint)}
`; return card; } // 渲染参数 function renderParameters(endpoint) { const params = endpoint.parameters || []; if (params.length === 0) return ''; const rows = params.map(param => { const required = param.required ? '必需' : '可选'; return ` ${param.name} ${param.schema?.type || 'string'} ${param.description || '-'} ${required} `; }).join(''); return `
参数
${rows}
参数名 类型 描述 必需
`; } // 渲染请求体 function renderRequestBody(endpoint) { if (!endpoint.requestBody) return ''; const content = endpoint.requestBody.content || {}; let schema = content['application/json']?.schema || {}; // 处理 $ref 引用 if (schema.$ref) { const refPath = schema.$ref.split('/'); const refName = refPath[refPath.length - 1]; if (apiSpec.components && apiSpec.components.schemas && apiSpec.components.schemas[refName]) { schema = apiSpec.components.schemas[refName]; } } // 渲染参数表格 let paramsTable = ''; if (schema.properties) { const requiredFields = schema.required || []; const rows = Object.keys(schema.properties).map(key => { const prop = schema.properties[key]; const required = requiredFields.includes(key) ? '必需' : '可选'; // 处理嵌套类型 let typeDisplay = prop.type || 'object'; if (prop.type === 'array' && prop.items) { typeDisplay = `array[${prop.items.type || 'object'}]`; } else if (prop.$ref) { const refPath = prop.$ref.split('/'); typeDisplay = refPath[refPath.length - 1]; } // 处理枚举 if (prop.enum) { typeDisplay += ` (${prop.enum.join(', ')})`; } return ` ${escapeHtml(key)} ${escapeHtml(typeDisplay)} ${prop.description ? escapeHtml(prop.description) : '-'} ${required} ${prop.example !== undefined ? `${escapeHtml(String(prop.example))}` : '-'} `; }).join(''); if (rows) { paramsTable = ` ${rows}
参数名 类型 描述 必需 示例
`; } } // 生成示例JSON let example = ''; if (schema.example) { example = JSON.stringify(schema.example, null, 2); } else if (schema.properties) { const exampleObj = {}; Object.keys(schema.properties).forEach(key => { const prop = schema.properties[key]; if (prop.example !== undefined) { exampleObj[key] = prop.example; } else { // 根据类型生成默认示例 if (prop.type === 'string') { exampleObj[key] = prop.description || 'string'; } else if (prop.type === 'number') { exampleObj[key] = 0; } else if (prop.type === 'boolean') { exampleObj[key] = false; } else if (prop.type === 'array') { exampleObj[key] = []; } else { exampleObj[key] = null; } } }); example = JSON.stringify(exampleObj, null, 2); } return `
请求体
${endpoint.requestBody.description ? `
${endpoint.requestBody.description}
` : ''} ${paramsTable} ${example ? `
示例JSON:
${escapeHtml(example)}
` : ''}
`; } // 渲染响应 function renderResponses(endpoint) { const responses = endpoint.responses || {}; const responseItems = Object.keys(responses).map(status => { const response = responses[status]; const schema = response.content?.['application/json']?.schema || {}; let example = ''; if (schema.example) { example = JSON.stringify(schema.example, null, 2); } return `
${status} ${response.description ? `${response.description}` : ''} ${example ? `
${escapeHtml(example)}
` : ''}
`; }).join(''); if (!responseItems) return ''; return `
响应
${responseItems}
`; } // 渲染测试区域 function renderTestSection(endpoint) { const method = endpoint.method.toUpperCase(); const path = endpoint.path; const hasBody = endpoint.requestBody && ['POST', 'PUT', 'PATCH'].includes(method); let bodyInput = ''; if (hasBody) { const schema = endpoint.requestBody.content?.['application/json']?.schema || {}; let defaultBody = ''; if (schema.example) { defaultBody = JSON.stringify(schema.example, null, 2); } else if (schema.properties) { const exampleObj = {}; Object.keys(schema.properties).forEach(key => { const prop = schema.properties[key]; exampleObj[key] = prop.example || (prop.type === 'string' ? '' : prop.type === 'number' ? 0 : prop.type === 'boolean' ? false : null); }); defaultBody = JSON.stringify(exampleObj, null, 2); } const bodyInputId = `test-body-${escapeId(path)}-${method}`; bodyInput = `
`; } // 处理路径参数 const pathParams = (endpoint.parameters || []).filter(p => p.in === 'path'); let pathParamsInput = ''; if (pathParams.length > 0) { pathParamsInput = pathParams.map(param => { const inputId = `test-param-${param.name}-${escapeId(path)}-${method}`; return `
`; }).join(''); } // 处理查询参数 const queryParams = (endpoint.parameters || []).filter(p => p.in === 'query'); let queryParamsInput = ''; if (queryParams.length > 0) { queryParamsInput = queryParams.map(param => { const inputId = `test-query-${param.name}-${escapeId(path)}-${method}`; const defaultValue = param.schema?.default !== undefined ? param.schema.default : ''; const placeholder = param.description || param.name; const required = param.required ? '*' : '可选'; return `
`; }).join(''); } return `
测试接口
${pathParamsInput} ${queryParamsInput ? `
查询参数:
${queryParamsInput}
` : ''} ${bodyInput}
`; } // 测试API async function testAPI(method, path, operationId) { const resultId = `test-result-${escapeId(path)}-${method}`; const resultDiv = document.getElementById(resultId); if (!resultDiv) return; resultDiv.style.display = 'block'; resultDiv.className = 'api-test-result loading'; resultDiv.textContent = '发送请求中...'; try { // 替换路径参数 let actualPath = path; const pathParams = path.match(/\{([^}]+)\}/g) || []; pathParams.forEach(param => { const paramName = param.slice(1, -1); const inputId = `test-param-${paramName}-${escapeId(path)}-${method}`; const input = document.getElementById(inputId); if (input && input.value) { actualPath = actualPath.replace(param, encodeURIComponent(input.value)); } else { throw new Error(`路径参数 ${paramName} 不能为空`); } }); // 确保路径以/api开头(如果OpenAPI规范中的路径不包含/api) if (!actualPath.startsWith('/api') && !actualPath.startsWith('http')) { actualPath = '/api' + actualPath; } // 构建查询参数 const queryParams = []; const endpointSpec = apiSpec.paths[path]?.[method.toLowerCase()]; if (endpointSpec && endpointSpec.parameters) { endpointSpec.parameters.filter(p => p.in === 'query').forEach(param => { const inputId = `test-query-${param.name}-${escapeId(path)}-${method}`; const input = document.getElementById(inputId); if (input && input.value !== '' && input.value !== null && input.value !== undefined) { queryParams.push(`${encodeURIComponent(param.name)}=${encodeURIComponent(input.value)}`); } else if (param.required) { throw new Error(`查询参数 ${param.name} 不能为空`); } }); } // 添加查询字符串 if (queryParams.length > 0) { actualPath += (actualPath.includes('?') ? '&' : '?') + queryParams.join('&'); } // 构建请求选项 const options = { method: method, headers: { 'Content-Type': 'application/json', } }; // 添加token if (currentToken) { options.headers['Authorization'] = 'Bearer ' + currentToken; } else { // 如果没有token,提示用户 throw new Error('未检测到 Token。请先在前端页面登录,然后刷新此页面。或者手动在请求头中添加 Authorization: Bearer your_token'); } // 添加请求体 if (['POST', 'PUT', 'PATCH'].includes(method)) { const bodyInputId = `test-body-${escapeId(path)}-${method}`; const bodyInput = document.getElementById(bodyInputId); if (bodyInput && bodyInput.value.trim()) { try { options.body = JSON.stringify(JSON.parse(bodyInput.value.trim())); } catch (e) { throw new Error('请求体JSON格式错误: ' + e.message); } } } // 发送请求 const response = await fetch(actualPath, options); const responseText = await response.text(); let responseData; try { responseData = JSON.parse(responseText); } catch { responseData = responseText; } // 显示结果 resultDiv.className = response.ok ? 'api-test-result success' : 'api-test-result error'; resultDiv.textContent = `状态码: ${response.status} ${response.statusText}\n\n${typeof responseData === 'string' ? responseData : JSON.stringify(responseData, null, 2)}`; } catch (error) { resultDiv.className = 'api-test-result error'; resultDiv.textContent = '请求失败: ' + error.message; } } // 清除测试结果 function clearTestResult(id) { const resultDiv = document.getElementById(`test-result-${id}`); if (resultDiv) { resultDiv.style.display = 'none'; resultDiv.textContent = ''; } } // 复制curl命令 function copyCurlCommand(event, method, path) { try { // 替换路径参数 let actualPath = path; const pathParams = path.match(/\{([^}]+)\}/g) || []; pathParams.forEach(param => { const paramName = param.slice(1, -1); const inputId = `test-param-${paramName}-${escapeId(path)}-${method}`; const input = document.getElementById(inputId); if (input && input.value) { actualPath = actualPath.replace(param, encodeURIComponent(input.value)); } }); // 确保路径以/api开头 if (!actualPath.startsWith('/api') && !actualPath.startsWith('http')) { actualPath = '/api' + actualPath; } // 构建查询参数 const queryParams = []; const endpointSpec = apiSpec.paths[path]?.[method.toLowerCase()]; if (endpointSpec && endpointSpec.parameters) { endpointSpec.parameters.filter(p => p.in === 'query').forEach(param => { const inputId = `test-query-${param.name}-${escapeId(path)}-${method}`; const input = document.getElementById(inputId); if (input && input.value !== '' && input.value !== null && input.value !== undefined) { queryParams.push(`${encodeURIComponent(param.name)}=${encodeURIComponent(input.value)}`); } }); } // 添加查询字符串 if (queryParams.length > 0) { actualPath += (actualPath.includes('?') ? '&' : '?') + queryParams.join('&'); } // 构建完整的URL const baseUrl = window.location.origin; const fullUrl = baseUrl + actualPath; // 构建curl命令 let curlCommand = `curl -X ${method.toUpperCase()} "${fullUrl}"`; // 添加请求头 curlCommand += ` \\\n -H "Content-Type: application/json"`; // 添加Authorization头 if (currentToken) { curlCommand += ` \\\n -H "Authorization: Bearer ${currentToken}"`; } else { curlCommand += ` \\\n -H "Authorization: Bearer YOUR_TOKEN_HERE"`; } // 添加请求体(如果有) if (['POST', 'PUT', 'PATCH'].includes(method.toUpperCase())) { const bodyInputId = `test-body-${escapeId(path)}-${method}`; const bodyInput = document.getElementById(bodyInputId); if (bodyInput && bodyInput.value.trim()) { try { // 验证JSON格式并格式化 const jsonBody = JSON.parse(bodyInput.value.trim()); const jsonString = JSON.stringify(jsonBody); // 在单引号内,只需要转义单引号本身 const escapedJson = jsonString.replace(/'/g, "'\\''"); curlCommand += ` \\\n -d '${escapedJson}'`; } catch (e) { // 如果不是有效JSON,直接使用原始值 const escapedBody = bodyInput.value.trim().replace(/'/g, "'\\''"); curlCommand += ` \\\n -d '${escapedBody}'`; } } } // 复制到剪贴板 const button = event ? event.target.closest('button') : null; navigator.clipboard.writeText(curlCommand).then(() => { // 显示成功提示 if (button) { const originalText = button.innerHTML; button.innerHTML = '已复制'; button.style.color = 'var(--success-color)'; setTimeout(() => { button.innerHTML = originalText; button.style.color = ''; }, 2000); } else { alert('curl命令已复制到剪贴板!'); } }).catch(err => { console.error('复制失败:', err); // 如果clipboard API失败,使用fallback方法 const textarea = document.createElement('textarea'); textarea.value = curlCommand; textarea.style.position = 'fixed'; textarea.style.opacity = '0'; document.body.appendChild(textarea); textarea.select(); try { document.execCommand('copy'); if (button) { const originalText = button.innerHTML; button.innerHTML = '已复制'; button.style.color = 'var(--success-color)'; setTimeout(() => { button.innerHTML = originalText; button.style.color = ''; }, 2000); } else { alert('curl命令已复制到剪贴板!'); } } catch (e) { alert('复制失败,请手动复制:\n\n' + curlCommand); } document.body.removeChild(textarea); }); } catch (error) { console.error('生成curl命令失败:', error); alert('生成curl命令失败: ' + error.message); } } // HTML转义 function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } // ID转义(用于HTML ID属性) function escapeId(text) { return text.replace(/[{}]/g, '').replace(/\//g, '-'); }