Files
CyberStrikeAI/web/static/js/api-docs.js
2026-01-29 19:32:33 +08:00

945 lines
37 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 = `
<div class="empty-state">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"/>
<line x1="15" y1="9" x2="9" y2="15"/>
<line x1="9" y1="9" x2="15" y2="15"/>
</svg>
<h3>加载失败</h3>
<p>${message}</p>
<div style="margin-top: 16px;">
<a href="/" style="color: var(--accent-color); text-decoration: none;">返回首页登录</a>
</div>
</div>
`;
}
// 渲染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 = '<p style="margin: 0; font-size: 0.8125rem; color: #ff9800;"><strong>⚠ 未检测到 Token</strong> - 请先在前端页面登录,然后刷新此页面。测试接口时需要在请求头中添加 Authorization: Bearer token</p>';
}
}
// 渲染侧边栏
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 = `<a href="#" class="api-group-link" data-group="${group}">${group}</a>`;
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 = '<div class="empty-state"><h3>暂无API</h3><p>该分组下没有API端点</p></div>';
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 => `<span class="api-tag">${tag}</span>`).join('');
card.innerHTML = `
<div class="api-endpoint-header">
<div class="api-endpoint-title">
<span class="api-method ${methodClass}">${endpoint.method.toUpperCase()}</span>
<span class="api-path">${endpoint.path}</span>
${tagHtml}
</div>
</div>
<div class="api-endpoint-body">
<div class="api-section">
<div class="api-section-title">描述</div>
${endpoint.summary ? `<div class="api-description" style="font-weight: 500; margin-bottom: 8px; color: var(--text-primary);">${escapeHtml(endpoint.summary)}</div>` : ''}
${endpoint.description ? `
<div class="api-description-toggle">
<button class="description-toggle-btn" onclick="toggleDescription(this)">
<svg class="description-toggle-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="6 9 12 15 18 9"/>
</svg>
<span>查看详细说明</span>
</button>
<div class="api-description-detail" style="display: none;">
${formatDescription(endpoint.description)}
</div>
</div>
` : endpoint.summary ? '' : '<div class="api-description">无描述</div>'}
</div>
${renderParameters(endpoint)}
${renderRequestBody(endpoint)}
${renderResponses(endpoint)}
${renderTestSection(endpoint)}
</div>
`;
return card;
}
// 渲染参数
function renderParameters(endpoint) {
const params = endpoint.parameters || [];
if (params.length === 0) return '';
const rows = params.map(param => {
const required = param.required ? '<span class="api-param-required">必需</span>' : '<span class="api-param-optional">可选</span>';
// 处理描述文本,将换行符转换为<br>
let descriptionHtml = '-';
if (param.description) {
const escapedDesc = escapeHtml(param.description);
descriptionHtml = escapedDesc.replace(/\n/g, '<br>');
}
return `
<tr>
<td><span class="api-param-name">${param.name}</span></td>
<td><span class="api-param-type">${param.schema?.type || 'string'}</span></td>
<td>${descriptionHtml}</td>
<td>${required}</td>
</tr>
`;
}).join('');
return `
<div class="api-section">
<div class="api-section-title">参数</div>
<div class="api-table-wrapper">
<table class="api-params-table">
<thead>
<tr>
<th>参数名</th>
<th>类型</th>
<th>描述</th>
<th>必需</th>
</tr>
</thead>
<tbody>
${rows}
</tbody>
</table>
</div>
</div>
`;
}
// 渲染请求体
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)
? '<span class="api-param-required">必需</span>'
: '<span class="api-param-optional">可选</span>';
// 处理嵌套类型
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(', ')})`;
}
// 处理描述文本,将换行符转换为<br>,但保持其他格式
let descriptionHtml = '-';
if (prop.description) {
// 转义HTML然后处理换行
const escapedDesc = escapeHtml(prop.description);
// 将 \n 转换为 <br>,但不要转换已经转义的换行
descriptionHtml = escapedDesc.replace(/\n/g, '<br>');
}
return `
<tr>
<td><span class="api-param-name">${escapeHtml(key)}</span></td>
<td><span class="api-param-type">${escapeHtml(typeDisplay)}</span></td>
<td>${descriptionHtml}</td>
<td>${required}</td>
<td>${prop.example !== undefined ? `<code>${escapeHtml(String(prop.example))}</code>` : '-'}</td>
</tr>
`;
}).join('');
if (rows) {
paramsTable = `
<div class="api-table-wrapper" style="margin-top: 12px;">
<table class="api-params-table">
<thead>
<tr>
<th>参数名</th>
<th>类型</th>
<th>描述</th>
<th>必需</th>
<th>示例</th>
</tr>
</thead>
<tbody>
${rows}
</tbody>
</table>
</div>
`;
}
}
// 生成示例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 `
<div class="api-section">
<div class="api-section-title">请求体</div>
${endpoint.requestBody.description ? `<div class="api-description">${endpoint.requestBody.description}</div>` : ''}
${paramsTable}
${example ? `
<div style="margin-top: 16px;">
<div style="font-weight: 500; margin-bottom: 8px; color: var(--text-primary);">示例JSON:</div>
<div class="api-response-example">
<pre>${escapeHtml(example)}</pre>
</div>
</div>
` : ''}
</div>
`;
}
// 渲染响应
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 `
<div style="margin-bottom: 16px;">
<strong style="color: ${status.startsWith('2') ? 'var(--success-color)' : status.startsWith('4') ? 'var(--error-color)' : 'var(--warning-color)'}">${status}</strong>
${response.description ? `<span style="color: var(--text-secondary); margin-left: 8px;">${response.description}</span>` : ''}
${example ? `
<div class="api-response-example" style="margin-top: 8px;">
<pre>${escapeHtml(example)}</pre>
</div>
` : ''}
</div>
`;
}).join('');
if (!responseItems) return '';
return `
<div class="api-section">
<div class="api-section-title">响应</div>
${responseItems}
</div>
`;
}
// 渲染测试区域
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 = `
<div class="api-test-input-group">
<label>请求体 (JSON)</label>
<textarea id="${bodyInputId}" class="test-body-input" placeholder='请输入JSON格式的请求体'>${defaultBody}</textarea>
</div>
`;
}
// 处理路径参数
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 `
<div class="api-test-input-group">
<label>${param.name} <span style="color: var(--error-color);">*</span></label>
<input type="text" id="${inputId}" placeholder="${param.description || param.name}" required>
</div>
`;
}).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 ? '<span style="color: var(--error-color);">*</span>' : '<span style="color: var(--text-muted);">可选</span>';
return `
<div class="api-test-input-group">
<label>${param.name} ${required}</label>
<input type="${param.schema?.type === 'number' || param.schema?.type === 'integer' ? 'number' : 'text'}"
id="${inputId}"
placeholder="${placeholder}"
value="${defaultValue}"
${param.required ? 'required' : ''}>
</div>
`;
}).join('');
}
return `
<div class="api-test-section">
<div class="api-section-title">测试接口</div>
<div class="api-test-form">
${pathParamsInput}
${queryParamsInput ? `<div style="margin-top: 16px;"><div style="font-weight: 500; margin-bottom: 8px; color: var(--text-primary);">查询参数:</div>${queryParamsInput}</div>` : ''}
${bodyInput}
<div class="api-test-buttons">
<button class="api-test-btn primary" onclick="testAPI('${method}', '${escapeHtml(path)}', '${endpoint.operationId || ''}')">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polygon points="5 3 19 12 5 21 5 3"/>
</svg>
发送请求
</button>
<button class="api-test-btn copy-curl" onclick="copyCurlCommand(event, '${method}', '${escapeHtml(path)}')" title="复制curl命令">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2" stroke="currentColor" stroke-width="2"/>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" stroke="currentColor" stroke-width="2"/>
</svg>
复制curl
</button>
<button class="api-test-btn clear-result" onclick="clearTestResult('${escapeId(path)}-${method}')" title="清除测试结果">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="3 6 5 6 21 6"/>
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
</svg>
清除结果
</button>
</div>
<div id="test-result-${escapeId(path)}-${method}" class="api-test-result" style="display: none;"></div>
</div>
</div>
`;
}
// 测试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 = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"/></svg>已复制';
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 = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"/></svg>已复制';
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);
}
}
// 格式化描述文本处理markdown格式
function formatDescription(text) {
if (!text) return '';
// 先提取代码块避免代码块内的markdown被处理
let formatted = text;
const codeBlocks = [];
let codeBlockIndex = 0;
// 提取代码块(支持语言标识符,如 ```json 或 ```javascript
formatted = formatted.replace(/```(\w+)?\s*\n?([\s\S]*?)```/g, (match, lang, code) => {
const placeholder = `__CODE_BLOCK_${codeBlockIndex}__`;
codeBlocks[codeBlockIndex] = {
lang: (lang && lang.trim()) || '',
code: code.trim()
};
codeBlockIndex++;
return placeholder;
});
// 提取行内代码避免行内代码内的markdown被处理
const inlineCodes = [];
let inlineCodeIndex = 0;
formatted = formatted.replace(/`([^`\n]+)`/g, (match, code) => {
const placeholder = `__INLINE_CODE_${inlineCodeIndex}__`;
inlineCodes[inlineCodeIndex] = code;
inlineCodeIndex++;
return placeholder;
});
// 转义HTML但保留占位符
formatted = escapeHtml(formatted);
// 恢复行内代码(需要转义,因为占位符已经被转义了)
inlineCodes.forEach((code, index) => {
formatted = formatted.replace(
`__INLINE_CODE_${index}__`,
`<code class="inline-code">${escapeHtml(code)}</code>`
);
});
// 恢复代码块(代码块内容已经转义过,直接使用)
codeBlocks.forEach((block, index) => {
const langLabel = block.lang ? `<span class="code-lang">${escapeHtml(block.lang)}</span>` : '';
// 代码块内容已经在提取时保存,不需要再次转义
formatted = formatted.replace(
`__CODE_BLOCK_${index}__`,
`<pre class="code-block">${langLabel}<code>${escapeHtml(block.code)}</code></pre>`
);
});
// 处理标题(### 标题)
formatted = formatted.replace(/^###\s+(.+)$/gm, '<h3 class="md-h3">$1</h3>');
formatted = formatted.replace(/^##\s+(.+)$/gm, '<h2 class="md-h2">$1</h2>');
formatted = formatted.replace(/^#\s+(.+)$/gm, '<h1 class="md-h1">$1</h1>');
// 处理加粗文本(**text** 或 __text__
formatted = formatted.replace(/\*\*([^*]+?)\*\*/g, '<strong>$1</strong>');
formatted = formatted.replace(/__([^_]+?)__/g, '<strong>$1</strong>');
// 处理斜体(*text* 或 _text_但不与加粗冲突
formatted = formatted.replace(/(?<!\*)\*([^*\n]+?)\*(?!\*)/g, '<em>$1</em>');
formatted = formatted.replace(/(?<!_)_([^_\n]+?)_(?!_)/g, '<em>$1</em>');
// 处理链接 [text](url)
formatted = formatted.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer" class="md-link">$1</a>');
// 处理列表项(有序和无序)
const lines = formatted.split('\n');
const result = [];
let inUnorderedList = false;
let inOrderedList = false;
let orderedListStart = 1;
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const unorderedMatch = line.match(/^[-*]\s+(.+)$/);
const orderedMatch = line.match(/^\d+\.\s+(.+)$/);
if (unorderedMatch) {
if (inOrderedList) {
result.push('</ol>');
inOrderedList = false;
}
if (!inUnorderedList) {
result.push('<ul class="md-list">');
inUnorderedList = true;
}
result.push(`<li class="md-list-item">${unorderedMatch[1]}</li>`);
} else if (orderedMatch) {
if (inUnorderedList) {
result.push('</ul>');
inUnorderedList = false;
}
if (!inOrderedList) {
result.push('<ol class="md-list">');
inOrderedList = true;
orderedListStart = parseInt(line.match(/^(\d+)\./)[1]) || 1;
}
result.push(`<li class="md-list-item">${orderedMatch[1]}</li>`);
} else {
if (inUnorderedList) {
result.push('</ul>');
inUnorderedList = false;
}
if (inOrderedList) {
result.push('</ol>');
inOrderedList = false;
}
if (line.trim()) {
result.push(line);
} else if (i < lines.length - 1) {
// 只在非最后一行时添加换行
result.push('<br>');
}
}
}
if (inUnorderedList) {
result.push('</ul>');
}
if (inOrderedList) {
result.push('</ol>');
}
formatted = result.join('\n');
// 处理段落(连续的空行分隔段落)
formatted = formatted.replace(/(<br>\s*){2,}/g, '</p><p class="md-paragraph">');
formatted = '<p class="md-paragraph">' + formatted + '</p>';
// 清理多余的<br>标签(在块级元素前后)
formatted = formatted.replace(/(<\/?(h[1-6]|ul|ol|li|pre|p)[^>]*>)\s*<br>/gi, '$1');
formatted = formatted.replace(/<br>\s*(<\/?(h[1-6]|ul|ol|li|pre|p)[^>]*>)/gi, '$1');
// 将剩余的单个换行符转换为<br>(但避免在块级元素内)
formatted = formatted.replace(/\n(?!<\/?(h[1-6]|ul|ol|li|pre|p|code))/g, '<br>');
return formatted;
}
// 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, '-');
}
// 切换描述显示/隐藏
function toggleDescription(button) {
const icon = button.querySelector('.description-toggle-icon');
const detail = button.parentElement.querySelector('.api-description-detail');
const span = button.querySelector('span');
if (detail.style.display === 'none') {
detail.style.display = 'block';
icon.style.transform = 'rotate(180deg)';
span.textContent = '隐藏详细说明';
} else {
detail.style.display = 'none';
icon.style.transform = 'rotate(0deg)';
span.textContent = '查看详细说明';
}
}